Merge branch 'test-multi-repos' into 'master'

Added tests for multiple repositories providing same apks

Right now, multi repo support works, but is kinda funky. While fixing #324, I accidentally broke this support without realising it. So in the interests of making my approach to #324 more test driven, I've written some tests for multi repo support.

Initially I wrote tests for the actual correct, desirable behaviour. Then when it became apparant that we dont' do this, I commented those tests out (but left them there for hopefully future multi-repo work) and then added tests for the current behaviour to make sure we don't introduce regressions.

Android unit testing framework is nice for testing content providers. It is nice for testing file handling. However I really struggled to get it working with both. Had to do some interesting things with instrumentation and contexts in order to get it to work.

I'm sure Android has nice `Service` testing capabilities too. But given the trouble with instrumentation/contexts/files/providers/etc, it was easier for me to refactor the parts of `UpdateService` that I needed to test into a separate, testable class. 

See merge request !163
This commit is contained in:
Daniel Martí 2015-11-07 11:45:47 +00:00
commit 94b30a54a6
10 changed files with 890 additions and 338 deletions

View File

@ -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.
* <p/>
* NOTE: In the future, these attributes will be moved to a join table, so that the app table
* is essentially completely transient, and can be nuked at any time.
*/
private static final String[] APP_FIELDS_TO_IGNORE = {
AppProvider.DataColumns.IGNORE_ALLUPDATES,
AppProvider.DataColumns.IGNORE_THISUPDATE,
};
@NonNull
private final Context context;
private Map<String, App> appsToUpdate = new HashMap<>();
private List<Apk> apksToUpdate = new ArrayList<>();
private List<Repo> repos = new ArrayList<>();
public RepoPersister(@NonNull Context context) {
this.context = context;
}
public RepoPersister queueUpdater(RepoUpdater updater) {
queueApps(updater.getApps());
queueApks(updater.getApks());
repos.add(updater.repo);
return this;
}
private void queueApps(List<App> apps) {
for (final App app : apps) {
appsToUpdate.put(app.id, app);
}
}
private void queueApks(List<Apk> apks) {
apksToUpdate.addAll(apks);
}
public void save(List<Repo> disabledRepos) {
List<App> listOfAppsToUpdate = new ArrayList<>();
listOfAppsToUpdate.addAll(appsToUpdate.values());
calcApkCompatibilityFlags(apksToUpdate);
// Need to do this BEFORE updating the apks, otherwise when it continually
// calls "get existing apks for repo X" then it will be getting the newly
// created apks, rather than those from the fresh, juicy index we just processed.
removeApksNoLongerInRepo(apksToUpdate, repos);
int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size();
updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0);
updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size());
removeApksFromRepos(disabledRepos);
removeAppsWithoutApks();
// This will sort out the icon urls, compatibility flags. and suggested version
// for each app. It used to happen here in Java code, but was moved to SQL when
// it became apparant we don't always have enough info (depending on which repos
// were updated).
AppProvider.Helper.calcDetailsFromIndex(context);
}
/**
* This cannot be offloaded to the database (as we did with the query which
* updates apps, depending on whether their apks are compatible or not).
* The reason is that we need to interact with the CompatibilityChecker
* in order to see if, and why an apk is not compatible.
*/
private void calcApkCompatibilityFlags(List<Apk> apks) {
final CompatibilityChecker checker = new CompatibilityChecker(context);
for (final Apk apk : apks) {
final List<String> reasons = checker.getIncompatibleReasons(apk);
if (reasons.size() > 0) {
apk.compatible = false;
apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons);
} else {
apk.compatible = true;
apk.incompatibleReasons = null;
}
}
}
/**
* If a repo was updated (i.e. it is in use, and the index has changed
* since last time we did an update), then we want to remove any apks that
* belong to the repo which are not in the current list of apks that were
* retrieved.
*/
private void removeApksNoLongerInRepo(List<Apk> apksToUpdate, List<Repo> updatedRepos) {
long startTime = System.currentTimeMillis();
List<Apk> toRemove = new ArrayList<>();
final String[] fields = {
ApkProvider.DataColumns.APK_ID,
ApkProvider.DataColumns.VERSION_CODE,
ApkProvider.DataColumns.VERSION,
};
for (final Repo repo : updatedRepos) {
final List<Apk> existingApks = ApkProvider.Helper.findByRepo(context, repo, fields);
for (final Apk existingApk : existingApks) {
if (!isApkToBeUpdated(existingApk, apksToUpdate)) {
toRemove.add(existingApk);
}
}
}
long duration = System.currentTimeMillis() - startTime;
Utils.debugLog(TAG, "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)");
if (toRemove.size() > 0) {
ApkProvider.Helper.deleteApks(context, toRemove);
}
}
private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) {
List<ContentProviderOperation> operations = new ArrayList<>();
List<String> knownAppIds = getKnownAppIds(appsToUpdate);
for (final App app : appsToUpdate) {
if (knownAppIds.contains(app.id)) {
operations.add(updateExistingApp(app));
} else {
operations.add(insertNewApp(app));
}
}
Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apps.");
try {
executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Could not update or insert apps", e);
}
}
private void executeBatchWithStatus(String providerAuthority,
List<ContentProviderOperation> operations,
int currentCount,
int totalUpdateCount)
throws RemoteException, OperationApplicationException {
int i = 0;
while (i < operations.size()) {
int count = Math.min(operations.size() - i, 100);
ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count));
UpdateService.sendStatus(context, UpdateService.STATUS_INFO, context.getString(
R.string.status_inserting,
(int) ((double) (currentCount + i) / totalUpdateCount * 100)));
context.getContentResolver().applyBatch(providerAuthority, o);
i += 100;
}
}
/**
* Return list of apps from the "apks" argument which are already in the database.
*/
private List<Apk> getKnownApks(List<Apk> apks) {
final String[] fields = {
ApkProvider.DataColumns.APK_ID,
ApkProvider.DataColumns.VERSION,
ApkProvider.DataColumns.VERSION_CODE,
};
return ApkProvider.Helper.knownApks(context, apks, fields);
}
private void updateOrInsertApks(List<Apk> apksToUpdate, int totalApksAppsCount, int currentCount) {
List<ContentProviderOperation> operations = new ArrayList<>();
List<Apk> knownApks = getKnownApks(apksToUpdate);
for (final Apk apk : apksToUpdate) {
boolean known = false;
for (final Apk knownApk : knownApks) {
if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) {
known = true;
break;
}
}
if (known) {
operations.add(updateExistingApk(apk));
} else {
operations.add(insertNewApk(apk));
knownApks.add(apk); // In case another repo has the same version/id combo for this apk.
}
}
Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apks.");
try {
executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Could not update/insert apps", e);
}
}
private ContentProviderOperation updateExistingApk(final Apk apk) {
Uri uri = ApkProvider.getContentUri(apk);
ContentValues values = apk.toContentValues();
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
}
private ContentProviderOperation insertNewApk(final Apk apk) {
ContentValues values = apk.toContentValues();
Uri uri = ApkProvider.getContentUri();
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
private ContentProviderOperation updateExistingApp(App app) {
Uri uri = AppProvider.getContentUri(app);
ContentValues values = app.toContentValues();
for (final String toIgnore : APP_FIELDS_TO_IGNORE) {
if (values.containsKey(toIgnore)) {
values.remove(toIgnore);
}
}
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
}
private ContentProviderOperation insertNewApp(App app) {
ContentValues values = app.toContentValues();
Uri uri = AppProvider.getContentUri();
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
private static boolean isApkToBeUpdated(Apk existingApk, List<Apk> apksToUpdate) {
for (final Apk apkToUpdate : apksToUpdate) {
if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) {
return true;
}
}
return false;
}
private void removeApksFromRepos(List<Repo> repos) {
for (final Repo repo : repos) {
Uri uri = ApkProvider.getRepoUri(repo.getId());
int numDeleted = context.getContentResolver().delete(uri, null, null);
Utils.debugLog(TAG, "Removing " + numDeleted + " apks from repo " + repo.address);
}
}
private void removeAppsWithoutApks() {
int numDeleted = context.getContentResolver().delete(AppProvider.getNoApksUri(), null, null);
Utils.debugLog(TAG, "Removing " + numDeleted + " apks that don't have any apks");
}
private List<String> getKnownAppIds(List<App> apps) {
List<String> knownAppIds = new ArrayList<>();
if (apps.isEmpty()) {
return knownAppIds;
}
if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) {
int middle = apps.size() / 2;
List<App> apps1 = apps.subList(0, middle);
List<App> apps2 = apps.subList(middle, apps.size());
knownAppIds.addAll(getKnownAppIds(apps1));
knownAppIds.addAll(getKnownAppIds(apps2));
} else {
knownAppIds.addAll(getKnownAppIdsFromProvider(apps));
}
return knownAppIds;
}
/**
* Looks in the database to see which apps we already know about. Only
* returns ids of apps that are in the database if they are in the "apps"
* array.
*/
private List<String> getKnownAppIdsFromProvider(List<App> apps) {
final Uri uri = AppProvider.getContentUri(apps);
final String[] fields = {AppProvider.DataColumns.APP_ID};
Cursor cursor = context.getContentResolver().query(uri, fields, null, null, null);
int knownIdCount = cursor != null ? cursor.getCount() : 0;
List<String> knownIds = new ArrayList<>(knownIdCount);
if (cursor != null) {
if (knownIdCount > 0) {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
knownIds.add(cursor.getString(0));
cursor.moveToNext();
}
}
cursor.close();
}
return knownIds;
}
}

View File

@ -23,19 +23,14 @@ import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
@ -45,7 +40,6 @@ import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
@ -54,9 +48,7 @@ import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.net.Downloader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class UpdateService extends IntentService implements ProgressListener {
@ -89,20 +81,6 @@ public class UpdateService extends IntentService implements ProgressListener {
super("UpdateService");
}
/**
* When an app already exists in the db, and we are updating it on the off chance that some
* values changed in the index, some fields should not be updated. Rather, they should be
* ignored, because they were explicitly set by the user, and hence can't be automatically
* overridden by the index.
*
* NOTE: In the future, these attributes will be moved to a join table, so that the app table
* is essentially completely transient, and can be nuked at any time.
*/
private static final String[] APP_FIELDS_TO_IGNORE = {
AppProvider.DataColumns.IGNORE_ALLUPDATES,
AppProvider.DataColumns.IGNORE_THISUPDATE,
};
public static void updateNow(Context context) {
updateRepoNow(null, context);
}
@ -178,16 +156,16 @@ public class UpdateService extends IntentService implements ProgressListener {
localBroadcastManager.unregisterReceiver(updateStatusReceiver);
}
protected void sendStatus(int statusCode) {
sendStatus(statusCode, null);
protected static void sendStatus(Context context, int statusCode) {
sendStatus(context, statusCode, null);
}
protected void sendStatus(int statusCode, String message) {
protected static void sendStatus(Context context, int statusCode, String message) {
Intent intent = new Intent(LOCAL_ACTION_STATUS);
intent.putExtra(EXTRA_STATUS_CODE, statusCode);
if (!TextUtils.isEmpty(message))
intent.putExtra(EXTRA_MESSAGE, message);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
protected void sendRepoErrorStatus(int statusCode, ArrayList<CharSequence> repoErrors) {
@ -219,7 +197,7 @@ public class UpdateService extends IntentService implements ProgressListener {
String totalSizeFriendly = Utils.getFriendlySize(totalSize);
message = getString(R.string.status_download, repoAddress, downloadedSizeFriendly, totalSizeFriendly, percent);
}
sendStatus(STATUS_INFO, message);
sendStatus(context, STATUS_INFO, message);
}
};
@ -354,8 +332,8 @@ public class UpdateService extends IntentService implements ProgressListener {
List<Repo> repos = RepoProvider.Helper.all(this);
// Process each repo...
Map<String, App> appsToUpdate = new HashMap<>();
List<Apk> apksToUpdate = new ArrayList<>();
RepoPersister appSaver = new RepoPersister(this);
//List<Repo> swapRepos = new ArrayList<>();
List<Repo> unchangedRepos = new ArrayList<>();
List<Repo> updatedRepos = new ArrayList<>();
@ -380,16 +358,13 @@ public class UpdateService extends IntentService implements ProgressListener {
continue;
}
sendStatus(STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
sendStatus(this, STATUS_INFO, getString(R.string.status_connecting_to_repo, repo.address));
RepoUpdater updater = new RepoUpdater(getBaseContext(), repo);
updater.setProgressListener(this);
try {
updater.update();
if (updater.hasChanged()) {
for (final App app : updater.getApps()) {
appsToUpdate.put(app.id, app);
}
apksToUpdate.addAll(updater.getApks());
appSaver.queueUpdater(updater);
updatedRepos.add(repo);
changes = true;
repoUpdateRememberers.add(updater.getRememberer());
@ -406,29 +381,9 @@ public class UpdateService extends IntentService implements ProgressListener {
if (!changes) {
Utils.debugLog(TAG, "Not checking app details or compatibility, because all repos were up to date.");
} else {
sendStatus(STATUS_INFO, getString(R.string.status_checking_compatibility));
sendStatus(this, STATUS_INFO, getString(R.string.status_checking_compatibility));
List<App> listOfAppsToUpdate = new ArrayList<>();
listOfAppsToUpdate.addAll(appsToUpdate.values());
calcApkCompatibilityFlags(this, apksToUpdate);
// Need to do this BEFORE updating the apks, otherwise when it continually
// calls "get existing apks for repo X" then it will be getting the newly
// created apks, rather than those from the fresh, juicy index we just processed.
removeApksNoLongerInRepo(apksToUpdate, updatedRepos);
int totalInsertsUpdates = listOfAppsToUpdate.size() + apksToUpdate.size();
updateOrInsertApps(listOfAppsToUpdate, totalInsertsUpdates, 0);
updateOrInsertApks(apksToUpdate, totalInsertsUpdates, listOfAppsToUpdate.size());
removeApksFromRepos(disabledRepos);
removeAppsWithoutApks();
// This will sort out the icon urls, compatibility flags. and suggested version
// for each app. It used to happen here in Java code, but was moved to SQL when
// it became apparant we don't always have enough info (depending on which repos
// were updated).
AppProvider.Helper.calcDetailsFromIndex(this);
appSaver.save(disabledRepos);
notifyContentProviders();
@ -448,9 +403,9 @@ public class UpdateService extends IntentService implements ProgressListener {
if (errorRepos.isEmpty()) {
if (changes) {
sendStatus(STATUS_COMPLETE_WITH_CHANGES);
sendStatus(this, STATUS_COMPLETE_WITH_CHANGES);
} else {
sendStatus(STATUS_COMPLETE_AND_SAME);
sendStatus(this, STATUS_COMPLETE_AND_SAME);
}
} else {
if (updatedRepos.size() + unchangedRepos.size() == 0) {
@ -461,7 +416,7 @@ public class UpdateService extends IntentService implements ProgressListener {
}
} catch (Exception e) {
Log.e(TAG, "Exception during update processing", e);
sendStatus(STATUS_ERROR_GLOBAL, e.getMessage());
sendStatus(this, STATUS_ERROR_GLOBAL, e.getMessage());
}
}
@ -470,26 +425,6 @@ public class UpdateService extends IntentService implements ProgressListener {
getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
}
/**
* This cannot be offloaded to the database (as we did with the query which
* updates apps, depending on whether their apks are compatible or not).
* The reason is that we need to interact with the CompatibilityChecker
* in order to see if, and why an apk is not compatible.
*/
private static void calcApkCompatibilityFlags(Context context, List<Apk> apks) {
final CompatibilityChecker checker = new CompatibilityChecker(context);
for (final Apk apk : apks) {
final List<String> reasons = checker.getIncompatibleReasons(apk);
if (reasons.size() > 0) {
apk.compatible = false;
apk.incompatibleReasons = Utils.CommaSeparatedList.make(reasons);
} else {
apk.compatible = true;
apk.incompatibleReasons = null;
}
}
}
private void performUpdateNotification() {
Cursor cursor = getContentResolver().query(
AppProvider.getCanUpdateUri(),
@ -557,214 +492,6 @@ public class UpdateService extends IntentService implements ProgressListener {
notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build());
}
private List<String> getKnownAppIds(List<App> apps) {
List<String> knownAppIds = new ArrayList<>();
if (apps.isEmpty()) {
return knownAppIds;
}
if (apps.size() > AppProvider.MAX_APPS_TO_QUERY) {
int middle = apps.size() / 2;
List<App> apps1 = apps.subList(0, middle);
List<App> apps2 = apps.subList(middle, apps.size());
knownAppIds.addAll(getKnownAppIds(apps1));
knownAppIds.addAll(getKnownAppIds(apps2));
} else {
knownAppIds.addAll(getKnownAppIdsFromProvider(apps));
}
return knownAppIds;
}
/**
* Looks in the database to see which apps we already know about. Only
* returns ids of apps that are in the database if they are in the "apps"
* array.
*/
private List<String> getKnownAppIdsFromProvider(List<App> apps) {
final Uri uri = AppProvider.getContentUri(apps);
final String[] fields = {AppProvider.DataColumns.APP_ID};
Cursor cursor = getContentResolver().query(uri, fields, null, null, null);
int knownIdCount = cursor != null ? cursor.getCount() : 0;
List<String> knownIds = new ArrayList<>(knownIdCount);
if (cursor != null) {
if (knownIdCount > 0) {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
knownIds.add(cursor.getString(0));
cursor.moveToNext();
}
}
cursor.close();
}
return knownIds;
}
private void updateOrInsertApps(List<App> appsToUpdate, int totalUpdateCount, int currentCount) {
List<ContentProviderOperation> operations = new ArrayList<>();
List<String> knownAppIds = getKnownAppIds(appsToUpdate);
for (final App app : appsToUpdate) {
if (knownAppIds.contains(app.id)) {
operations.add(updateExistingApp(app));
} else {
operations.add(insertNewApp(app));
}
}
Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apps.");
try {
executeBatchWithStatus(AppProvider.getAuthority(), operations, currentCount, totalUpdateCount);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Could not update or insert apps", e);
}
}
private void executeBatchWithStatus(String providerAuthority,
List<ContentProviderOperation> operations,
int currentCount,
int totalUpdateCount)
throws RemoteException, OperationApplicationException {
int i = 0;
while (i < operations.size()) {
int count = Math.min(operations.size() - i, 100);
ArrayList<ContentProviderOperation> o = new ArrayList<>(operations.subList(i, i + count));
sendStatus(STATUS_INFO, getString(
R.string.status_inserting,
(int) ((double) (currentCount + i) / totalUpdateCount * 100)));
getContentResolver().applyBatch(providerAuthority, o);
i += 100;
}
}
/**
* Return list of apps from the "apks" argument which are already in the database.
*/
private List<Apk> getKnownApks(List<Apk> apks) {
final String[] fields = {
ApkProvider.DataColumns.APK_ID,
ApkProvider.DataColumns.VERSION,
ApkProvider.DataColumns.VERSION_CODE,
};
return ApkProvider.Helper.knownApks(this, apks, fields);
}
private void updateOrInsertApks(List<Apk> apksToUpdate, int totalApksAppsCount, int currentCount) {
List<ContentProviderOperation> operations = new ArrayList<>();
List<Apk> knownApks = getKnownApks(apksToUpdate);
for (final Apk apk : apksToUpdate) {
boolean known = false;
for (final Apk knownApk : knownApks) {
if (knownApk.id.equals(apk.id) && knownApk.vercode == apk.vercode) {
known = true;
break;
}
}
if (known) {
operations.add(updateExistingApk(apk));
} else {
operations.add(insertNewApk(apk));
knownApks.add(apk); // In case another repo has the same version/id combo for this apk.
}
}
Utils.debugLog(TAG, "Updating/inserting " + operations.size() + " apks.");
try {
executeBatchWithStatus(ApkProvider.getAuthority(), operations, currentCount, totalApksAppsCount);
} catch (RemoteException | OperationApplicationException e) {
Log.e(TAG, "Could not update/insert apps", e);
}
}
private ContentProviderOperation updateExistingApk(final Apk apk) {
Uri uri = ApkProvider.getContentUri(apk);
ContentValues values = apk.toContentValues();
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
}
private ContentProviderOperation insertNewApk(final Apk apk) {
ContentValues values = apk.toContentValues();
Uri uri = ApkProvider.getContentUri();
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
private ContentProviderOperation updateExistingApp(App app) {
Uri uri = AppProvider.getContentUri(app);
ContentValues values = app.toContentValues();
for (final String toIgnore : APP_FIELDS_TO_IGNORE) {
if (values.containsKey(toIgnore)) {
values.remove(toIgnore);
}
}
return ContentProviderOperation.newUpdate(uri).withValues(values).build();
}
private ContentProviderOperation insertNewApp(App app) {
ContentValues values = app.toContentValues();
Uri uri = AppProvider.getContentUri();
return ContentProviderOperation.newInsert(uri).withValues(values).build();
}
/**
* If a repo was updated (i.e. it is in use, and the index has changed
* since last time we did an update), then we want to remove any apks that
* belong to the repo which are not in the current list of apks that were
* retrieved.
*/
private void removeApksNoLongerInRepo(List<Apk> apksToUpdate, List<Repo> updatedRepos) {
long startTime = System.currentTimeMillis();
List<Apk> toRemove = new ArrayList<>();
final String[] fields = {
ApkProvider.DataColumns.APK_ID,
ApkProvider.DataColumns.VERSION_CODE,
ApkProvider.DataColumns.VERSION,
};
for (final Repo repo : updatedRepos) {
final List<Apk> existingApks = ApkProvider.Helper.findByRepo(this, repo, fields);
for (final Apk existingApk : existingApks) {
if (!isApkToBeUpdated(existingApk, apksToUpdate)) {
toRemove.add(existingApk);
}
}
}
long duration = System.currentTimeMillis() - startTime;
Utils.debugLog(TAG, "Found " + toRemove.size() + " apks no longer in the updated repos (took " + duration + "ms)");
if (toRemove.size() > 0) {
ApkProvider.Helper.deleteApks(this, toRemove);
}
}
private static boolean isApkToBeUpdated(Apk existingApk, List<Apk> apksToUpdate) {
for (final Apk apkToUpdate : apksToUpdate) {
if (apkToUpdate.vercode == existingApk.vercode && apkToUpdate.id.equals(existingApk.id)) {
return true;
}
}
return false;
}
private void removeApksFromRepos(List<Repo> repos) {
for (final Repo repo : repos) {
Uri uri = ApkProvider.getRepoUri(repo.getId());
int numDeleted = getContentResolver().delete(uri, null, null);
Utils.debugLog(TAG, "Removing " + numDeleted + " apks from repo " + repo.address);
}
}
private void removeAppsWithoutApks() {
int numDeleted = getContentResolver().delete(AppProvider.getNoApksUri(), null, null);
Utils.debugLog(TAG, "Removing " + numDeleted + " apks that don't have any apks");
}
/**
* Received progress event from the RepoXMLHandler. It could be progress
* downloading from the repo, or perhaps processing the info from the repo.
@ -783,6 +510,6 @@ public class UpdateService extends IntentService implements ProgressListener {
message = getString(R.string.status_processing_xml_percent, repoAddress, downloadedSize, totalSize, percent);
break;
}
sendStatus(STATUS_INFO, message);
sendStatus(this, STATUS_INFO, message);
}
}

View File

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

View File

@ -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)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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<Repo> 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<Repo> repos = RepoProvider.Helper.all(context);
assertEquals("Repos", 3, repos.size());
assertApp2048();
assertAppAdaway();
assertAppAdbWireless();
assertAppIcsImport();
}
private void assertApp(String packageName, int[] versionCodes) {
List<Apk> apks = ApkProvider.Helper.findByApp(context, packageName, ApkProvider.DataColumns.ALL);
assertApksExist(apks, packageName, versionCodes);
}
private void assertApp2048() {
assertApp("com.uberspot.a2048", new int[]{19, 18});
}
private void assertAppAdaway() {
assertApp("org.adaway", new int[]{54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 42, 40, 38, 37, 36, 35});
}
private void assertAppAdbWireless() {
assertApp("siir.es.adbWireless", new int[]{12});
}
private void assertAppIcsImport() {
assertApp("org.dgtale.icsimport", new int[]{3, 2});
}
/**
* + 2048 (com.uberspot.a2048)
* - Version 1.96 (19)
* - Version 1.95 (18)
* + AdAway (org.adaway)
* - Version 3.0.2 (54)
* - Version 3.0.1 (53)
* - Version 3.0 (52)
* + adbWireless (siir.es.adbWireless)
* - Version 1.5.4 (12)
*/
private void assertMainRepo(List<Repo> allRepos) {
Repo repo = findRepo(REPO_MAIN, allRepos);
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
assertEquals("Apks for main repo", apks.size(), 6);
assertApksExist(apks, "com.uberspot.a2048", new int[]{18, 19});
assertApksExist(apks, "org.adaway", new int[]{52, 53, 54});
assertApksExist(apks, "siir.es.adbWireless", new int[]{12});
}
/**
* + AdAway (org.adaway)
* - Version 2.9.2 (51)
* - Version 2.9.1 (50)
* - Version 2.9 (49)
* - Version 2.8.1 (48)
* - Version 2.8 (47)
* - Version 2.7 (46)
* - Version 2.6 (45)
* - Version 2.3 (42)
* - Version 2.1 (40)
* - Version 1.37 (38)
* - Version 1.36 (37)
* - Version 1.35 (36)
* - Version 1.34 (35)
*/
private void assertMainArchiveRepo(List<Repo> allRepos) {
Repo repo = findRepo(REPO_ARCHIVE, allRepos);
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
assertEquals("Apks for main archive repo", 13, apks.size());
assertApksExist(apks, "org.adaway", new int[]{35, 36, 37, 38, 40, 42, 45, 46, 47, 48, 49, 50, 51});
}
/**
* + AdAway (org.adaway)
* - Version 3.0.1 (53) *
* - Version 3.0 (52) *
* - Version 2.9.2 (51) *
* - Version 2.2.1 (50) *
* + Add to calendar (org.dgtale.icsimport)
* - Version 1.2 (3)
* - Version 1.1 (2)
*/
private void assertConflictingRepo(List<Repo> allRepos) {
Repo repo = findRepo(REPO_CONFLICTING, allRepos);
List<Apk> apks = ApkProvider.Helper.findByRepo(context, repo, ApkProvider.DataColumns.ALL);
assertEquals("Apks for main repo", 6, apks.size());
assertApksExist(apks, "org.adaway", new int[]{50, 51, 52, 53});
assertApksExist(apks, "org.dgtale.icsimport", new int[]{2, 3});
}
@NonNull
private Repo findRepo(@NonNull String name, List<Repo> allRepos) {
Repo repo = null;
for (Repo r : allRepos) {
if (TextUtils.equals(name, r.getName())) {
repo = r;
break;
}
}
assertNotNull("Repo " + allRepos, repo);
return repo;
}
/**
* Checks that each version of appId as specified in versionCodes is present in apksToCheck.
*/
private void assertApksExist(List<Apk> apksToCheck, String appId, int[] versionCodes) {
for (int versionCode : versionCodes) {
boolean found = false;
for (Apk apk : apksToCheck) {
if (apk.vercode == versionCode && apk.id.equals(appId)) {
found = true;
break;
}
}
assertTrue("Found app " + appId + ", v" + versionCode, found);
}
}
private void assertEmpty() {
assertEquals("No apps present", 0, AppProvider.Helper.all(context.getContentResolver()).size());
String[] packages = {
"com.uberspot.a2048",
"org.adaway",
"siir.es.adbWireless",
};
for (String id : packages) {
assertEquals("No apks for " + id, 0, ApkProvider.Helper.findByApp(context, id).size());
}
}
private void persistData() {
persister.save(new ArrayList<Repo>(0));
}
/* At time fo writing, the following tests did not pass. This is because the multi-repo support
in F-Droid was not sufficient. When working on proper multi repo support than this should be
ucommented and all these tests should pass:
public void testCorrectConflictingThenMainThenArchive() throws UpdateException {
assertEmpty();
if (updateConflicting() && updateMain() && updateArchive()) {
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;
}
}

View File

@ -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";

View File

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