Adding our own cache of currently installed apks in the database.
Previously the data was not stored anywhere, and each time we wanted to know about all installed apps, we built a ridiculously long SQL query. The query had essentially one "OR" clause for each installed app. To make matters worse, it also required one parameter for each of these, so we could bind the installed app name to a "?" in the query. SQL has a limit of (usually) 999 parameters which can be provided to a query, which meant it would fall over if the user had more than 1000 apps installed. This change introduces a new table called "fdroid_installedApps". It is initialized on first run, by iterating over the installed apps as given by the PackageManager. It is subsequenty kept up to date by a set of BroadcastReceivers, which listen for apps being uninstalled/installed/upgraded. It also includes tests to verify that queries of installed apps, when there are more than 1000 apps installed, don't break. Finally, tests are also now able to to insert into providers other than the one under test. This is due to the fact that the providers often join onto tables managed by other providers.
This commit is contained in:
parent
655f2bf7e3
commit
4e24050760
@ -61,6 +61,11 @@
|
||||
android:name="org.fdroid.fdroid.data.ApkProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<provider
|
||||
android:authorities="org.fdroid.fdroid.data.InstalledAppProvider"
|
||||
android:name="org.fdroid.fdroid.data.InstalledAppProvider"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".FDroid"
|
||||
android:launchMode="singleTop"
|
||||
@ -278,10 +283,22 @@
|
||||
<category android:name="android.intent.category.HOME" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".PackageReceiver" >
|
||||
<receiver android:name=".PackageAddedReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_ADDED" />
|
||||
<action android:name="android.intent.action.PACKAGE_UPGRADED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".PackageUpgradedReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_CHANGED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver android:name=".PackageRemovedReceiver" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.PACKAGE_REMOVED" />
|
||||
|
||||
<data android:scheme="package" />
|
||||
|
@ -136,7 +136,7 @@ public class AppDetails extends ListActivity {
|
||||
+ " " + apk.version
|
||||
+ (apk.vercode == app.suggestedVercode ? " ☆" : ""));
|
||||
|
||||
if (apk.vercode == app.getInstalledVerCode(getContext())
|
||||
if (apk.vercode == app.installedVersionCode
|
||||
&& mInstalledSigID != null && apk.sig != null
|
||||
&& apk.sig.equals(mInstalledSigID)) {
|
||||
holder.status.setText(getString(R.string.inst));
|
||||
@ -437,7 +437,7 @@ public class AppDetails extends ListActivity {
|
||||
// Get the signature of the installed package...
|
||||
mInstalledSignature = null;
|
||||
mInstalledSigID = null;
|
||||
if (app.getInstalledVersion(this) != null) {
|
||||
if (app.isInstalled()) {
|
||||
PackageManager pm = getBaseContext().getPackageManager();
|
||||
try {
|
||||
PackageInfo pi = pm.getPackageInfo(appid,
|
||||
@ -624,11 +624,11 @@ public class AppDetails extends ListActivity {
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
TextView tv = (TextView) findViewById(R.id.status);
|
||||
if (app.getInstalledVersion(this) == null)
|
||||
if (!app.isInstalled())
|
||||
tv.setText(getString(R.string.details_notinstalled));
|
||||
else
|
||||
tv.setText(getString(R.string.details_installed,
|
||||
app.getInstalledVersion(this)));
|
||||
app.installedVersionName));
|
||||
|
||||
tv = (TextView) infoView.findViewById(R.id.signature);
|
||||
if (pref_expert && mInstalledSignature != null) {
|
||||
@ -643,9 +643,9 @@ public class AppDetails extends ListActivity {
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
final Apk apk = adapter.getItem(position - l.getHeaderViewsCount());
|
||||
if (app.getInstalledVerCode(this) == apk.vercode)
|
||||
if (app.installedVersionCode == apk.vercode)
|
||||
removeApk(app.id);
|
||||
else if (app.getInstalledVerCode(this) > apk.vercode) {
|
||||
else if (app.installedVersionCode > apk.vercode) {
|
||||
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
|
||||
ask_alrt.setMessage(getString(R.string.installDowngrade));
|
||||
ask_alrt.setPositiveButton(getString(R.string.yes),
|
||||
@ -676,7 +676,7 @@ public class AppDetails extends ListActivity {
|
||||
menu.clear();
|
||||
if (app == null)
|
||||
return true;
|
||||
if (app.canAndWantToUpdate(this)) {
|
||||
if (app.canAndWantToUpdate()) {
|
||||
MenuItemCompat.setShowAsAction(menu.add(
|
||||
Menu.NONE, INSTALL, 0, R.string.menu_upgrade)
|
||||
.setIcon(R.drawable.ic_menu_refresh),
|
||||
@ -685,14 +685,14 @@ public class AppDetails extends ListActivity {
|
||||
}
|
||||
|
||||
// Check count > 0 due to incompatible apps resulting in an empty list.
|
||||
if (app.getInstalledVersion(this) == null && app.suggestedVercode > 0 &&
|
||||
if (!app.isInstalled() && app.suggestedVercode > 0 &&
|
||||
adapter.getCount() > 0) {
|
||||
MenuItemCompat.setShowAsAction(menu.add(
|
||||
Menu.NONE, INSTALL, 1, R.string.menu_install)
|
||||
.setIcon(android.R.drawable.ic_menu_add),
|
||||
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
|
||||
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
|
||||
} else if (app.getInstalledVersion(this) != null) {
|
||||
} else if (app.isInstalled()) {
|
||||
MenuItemCompat.setShowAsAction(menu.add(
|
||||
Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall)
|
||||
.setIcon(android.R.drawable.ic_menu_delete),
|
||||
@ -719,7 +719,7 @@ public class AppDetails extends ListActivity {
|
||||
.setCheckable(true)
|
||||
.setChecked(app.ignoreAllUpdates);
|
||||
|
||||
if (app.hasUpdates(this)) {
|
||||
if (app.hasUpdates()) {
|
||||
menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
|
||||
.setIcon(android.R.drawable.ic_menu_close_clear_cancel)
|
||||
.setCheckable(true)
|
||||
|
@ -18,42 +18,30 @@
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import java.io.File;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import com.nostra13.universalimageloader.cache.disc.impl.LimitedAgeDiscCache;
|
||||
import com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
|
||||
import com.nostra13.universalimageloader.utils.StorageUtils;
|
||||
|
||||
import de.duenndns.ssl.MemorizingTrustManager;
|
||||
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.compat.PRNGFixes;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
|
||||
import org.thoughtcrime.ssl.pinning.PinningTrustManager;
|
||||
import org.thoughtcrime.ssl.pinning.SystemKeyStore;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.io.File;
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
public class FDroidApp extends Application {
|
||||
|
||||
private static enum Theme {
|
||||
@ -91,9 +79,12 @@ public class FDroidApp extends Application {
|
||||
//Apply the Google PRNG fixes to properly seed SecureRandom
|
||||
PRNGFixes.apply();
|
||||
|
||||
// Set this up here, and the testing framework will override it when
|
||||
// it gets fired up.
|
||||
Utils.setupInstalledApkCache(new Utils.InstalledApkCache());
|
||||
// Check that the installed app cache hasn't gotten out of sync somehow.
|
||||
// e.g. if we crashed/ran out of battery half way through responding
|
||||
// to a package installed intent. It doesn't really matter where
|
||||
// we put this in the bootstrap process, because it runs on a different
|
||||
// thread. In fact, we may as well start early for this reason.
|
||||
InstalledAppCacheUpdater.updateInBackground(getApplicationContext());
|
||||
|
||||
// If the user changes the preference to do with filtering rooted apps,
|
||||
// it is easier to just notify a change in the app provider,
|
||||
|
44
src/org/fdroid/fdroid/PackageAddedReceiver.java
Normal file
44
src/org/fdroid/fdroid/PackageAddedReceiver.java
Normal file
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Peter Serwylo, peter@serwylo.com
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
|
||||
public class PackageAddedReceiver extends PackageReceiver {
|
||||
|
||||
@Override
|
||||
protected void handle(Context context, String appId) {
|
||||
PackageInfo info = getPackageInfo(context, appId);
|
||||
|
||||
Log.d("FDroid", "Inserting installed app info for '" + appId + "' (v" + info.versionCode + ")");
|
||||
|
||||
Uri uri = InstalledAppProvider.getContentUri();
|
||||
ContentValues values = new ContentValues(3);
|
||||
values.put(InstalledAppProvider.DataColumns.APP_ID, appId);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
|
||||
context.getContentResolver().insert(uri, values);
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2012 Ciaran Gultnieks, ciaran@ciarang.com
|
||||
* Copyright (C) 2014 Ciaran Gultnieks, ciaran@ciarang.com,
|
||||
* Peter Serwylo, peter@serwylo.com
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -21,17 +22,31 @@ package org.fdroid.fdroid;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
|
||||
public class PackageReceiver extends BroadcastReceiver {
|
||||
abstract class PackageReceiver extends BroadcastReceiver {
|
||||
|
||||
abstract protected void handle(Context context, String appId);
|
||||
|
||||
protected PackageInfo getPackageInfo(Context context, String appId) {
|
||||
for( PackageInfo info : context.getPackageManager().getInstalledPackages(0)) {
|
||||
if (info.packageName.equals(appId)) {
|
||||
return info;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
String appid = intent.getData().getSchemeSpecificPart();
|
||||
Log.d("FDroid", "PackageReceiver received "+appid);
|
||||
ctx.getContentResolver().notifyChange(AppProvider.getContentUri(appid), null);
|
||||
Utils.clearInstalledApksCache();
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d("FDroid", "PackageReceiver received [action = '" + intent.getAction() + "', data = '" + intent.getData() + "']");
|
||||
String appId = intent.getData().getSchemeSpecificPart();
|
||||
handle(context, appId);
|
||||
context.getContentResolver().notifyChange(AppProvider.getContentUri(appId), null);
|
||||
context.getContentResolver().notifyChange(ApkProvider.getAppUri(appId), null);
|
||||
}
|
||||
|
||||
}
|
||||
|
38
src/org/fdroid/fdroid/PackageRemovedReceiver.java
Normal file
38
src/org/fdroid/fdroid/PackageRemovedReceiver.java
Normal file
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Peter Serwylo, peter@serwylo.com
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
|
||||
public class PackageRemovedReceiver extends PackageReceiver {
|
||||
|
||||
@Override
|
||||
protected void handle(Context context, String appId) {
|
||||
|
||||
Log.d("FDroid", "Removing installed app info for '" + appId + "'");
|
||||
|
||||
Uri uri = InstalledAppProvider.getAppUri(appId);
|
||||
context.getContentResolver().delete(uri, null, null);
|
||||
}
|
||||
|
||||
}
|
49
src/org/fdroid/fdroid/PackageUpgradedReceiver.java
Normal file
49
src/org/fdroid/fdroid/PackageUpgradedReceiver.java
Normal file
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Peter Serwylo, peter@serwylo.com
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 3
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
|
||||
/**
|
||||
* For some reason, devices seem to be keen on sending a REMOVED and then an INSTALLED
|
||||
* intent, rather than an CHANGED intent. Therefore, this is probably not used on many
|
||||
* devices. Regardless, it is tested in the unit tests and should work on devices that
|
||||
* opt instead to send the PACKAGE_CHANGED intent.
|
||||
*/
|
||||
public class PackageUpgradedReceiver extends PackageReceiver {
|
||||
|
||||
@Override
|
||||
protected void handle(Context context, String appId) {
|
||||
PackageInfo info = getPackageInfo(context, appId);
|
||||
|
||||
Log.d("FDroid", "Updating installed app info for '" + appId + "' to v" + info.versionCode + " (" + info.versionName + ")");
|
||||
|
||||
Uri uri = InstalledAppProvider.getAppUri(appId);
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
|
||||
context.getContentResolver().update(uri, values, null, null);
|
||||
}
|
||||
|
||||
}
|
@ -405,7 +405,7 @@ public class UpdateService extends IntentService implements ProgressListener {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!ignored && app.hasUpdates(this)) {
|
||||
if (!ignored && app.hasUpdates()) {
|
||||
updateCount++;
|
||||
}
|
||||
}
|
||||
|
@ -195,14 +195,6 @@ public final class Utils {
|
||||
return apkCacheDir;
|
||||
}
|
||||
|
||||
public static Map<String, PackageInfo> getInstalledApps(Context context) {
|
||||
return installedApkCache.getApks(context);
|
||||
}
|
||||
|
||||
public static void clearInstalledApksCache() {
|
||||
installedApkCache.emptyCache();
|
||||
}
|
||||
|
||||
public static String calcFingerprint(String keyHexString) {
|
||||
if (TextUtils.isEmpty(keyHexString))
|
||||
return null;
|
||||
@ -296,44 +288,4 @@ public final class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
private static InstalledApkCache installedApkCache = null;
|
||||
|
||||
/**
|
||||
* We do a lot of querying of the installed app's. As a result, we like
|
||||
* to cache this information quite heavily (and flush the cache when new
|
||||
* apps are installed). The caching implementation needs to be setup like
|
||||
* this so that it is possible to mock for testing purposes.
|
||||
*/
|
||||
public static void setupInstalledApkCache(InstalledApkCache cache) {
|
||||
installedApkCache = cache;
|
||||
}
|
||||
|
||||
public static class InstalledApkCache {
|
||||
|
||||
protected Map<String, PackageInfo> installedApks = null;
|
||||
|
||||
protected Map<String, PackageInfo> buildAppList(Context context) {
|
||||
Map<String, PackageInfo> info = new HashMap<String, PackageInfo>();
|
||||
Log.d("FDroid", "Reading installed packages");
|
||||
List<PackageInfo> installedPackages = context.getPackageManager().getInstalledPackages(0);
|
||||
for (PackageInfo appInfo : installedPackages) {
|
||||
info.put(appInfo.packageName, appInfo);
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
public Map<String, PackageInfo> getApks(Context context) {
|
||||
if (installedApks == null) {
|
||||
installedApks = buildAppList(context);
|
||||
}
|
||||
return installedApks;
|
||||
}
|
||||
|
||||
public void emptyCache() {
|
||||
installedApks = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.BaseColumns;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.UpdateService;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
|
@ -79,6 +79,10 @@ public class App extends ValueObject implements Comparable<App> {
|
||||
|
||||
public String iconUrl;
|
||||
|
||||
public String installedVersionName;
|
||||
|
||||
public int installedVersionCode;
|
||||
|
||||
@Override
|
||||
public int compareTo(App app) {
|
||||
return name.compareToIgnoreCase(app.name);
|
||||
@ -148,6 +152,10 @@ public class App extends ValueObject implements Comparable<App> {
|
||||
ignoreThisUpdate = cursor.getInt(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.ICON_URL)) {
|
||||
iconUrl = cursor.getString(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.InstalledApp.VERSION_CODE)) {
|
||||
installedVersionCode = cursor.getInt(i);
|
||||
} else if (column.equals(AppProvider.DataColumns.InstalledApp.VERSION_NAME)) {
|
||||
installedVersionName = cursor.getString(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -186,53 +194,25 @@ public class App extends ValueObject implements Comparable<App> {
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version string for the currently installed version of this apk.
|
||||
* If not installed, returns null.
|
||||
*/
|
||||
public String getInstalledVersion(Context context) {
|
||||
PackageInfo info = getInstalledInfo(context);
|
||||
return info == null ? null : info.versionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Version code for the currently installed version of this apk.
|
||||
* If not installed, it returns -1.
|
||||
*/
|
||||
public int getInstalledVerCode(Context context) {
|
||||
PackageInfo info = getInstalledInfo(context);
|
||||
return info == null ? -1 : info.versionCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if installed by the user, false if a system apk or not installed.
|
||||
*/
|
||||
public boolean getUserInstalled(Context context) {
|
||||
PackageInfo info = getInstalledInfo(context);
|
||||
return info != null && ((info.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 1);
|
||||
}
|
||||
|
||||
public PackageInfo getInstalledInfo(Context context) {
|
||||
Map<String, PackageInfo> installed = Utils.getInstalledApps(context);
|
||||
return installed.containsKey(id) ? installed.get(id) : null;
|
||||
public boolean isInstalled() {
|
||||
return installedVersionCode > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if there are new versions (apks) available
|
||||
*/
|
||||
public boolean hasUpdates(Context context) {
|
||||
public boolean hasUpdates() {
|
||||
boolean updates = false;
|
||||
if (suggestedVercode > 0) {
|
||||
int installedVerCode = getInstalledVerCode(context);
|
||||
updates = (installedVerCode > 0 && installedVerCode < suggestedVercode);
|
||||
updates = (installedVersionCode > 0 && installedVersionCode < suggestedVercode);
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
// True if there are new versions (apks) available and the user wants
|
||||
// to be notified about them
|
||||
public boolean canAndWantToUpdate(Context context) {
|
||||
boolean canUpdate = hasUpdates(context);
|
||||
public boolean canAndWantToUpdate() {
|
||||
boolean canUpdate = hasUpdates();
|
||||
boolean wantsUpdate = !ignoreAllUpdates && ignoreThisUpdate < suggestedVercode;
|
||||
return canUpdate && wantsUpdate && !isFiltered();
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.*;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
@ -13,9 +12,6 @@ import java.util.*;
|
||||
|
||||
public class AppProvider extends FDroidProvider {
|
||||
|
||||
/**
|
||||
* @see org.fdroid.fdroid.data.ApkProvider.MAX_APKS_TO_QUERY
|
||||
*/
|
||||
public static final int MAX_APPS_TO_QUERY = 900;
|
||||
|
||||
public static final class Helper {
|
||||
@ -166,6 +162,11 @@ public class AppProvider extends FDroidProvider {
|
||||
public static final String VERSION = "suggestedApkVersion";
|
||||
}
|
||||
|
||||
public interface InstalledApp {
|
||||
public static final String VERSION_CODE = "installedVersionCode";
|
||||
public static final String VERSION_NAME = "installedVersionName";
|
||||
}
|
||||
|
||||
public static String[] ALL = {
|
||||
IS_COMPATIBLE, APP_ID, NAME, SUMMARY, ICON, DESCRIPTION,
|
||||
LICENSE, WEB_URL, TRACKER_URL, SOURCE_URL, DONATE_URL,
|
||||
@ -173,14 +174,76 @@ public class AppProvider extends FDroidProvider {
|
||||
UPSTREAM_VERSION, UPSTREAM_VERSION_CODE, ADDED, LAST_UPDATED,
|
||||
CATEGORIES, ANTI_FEATURES, REQUIREMENTS, IGNORE_ALLUPDATES,
|
||||
IGNORE_THISUPDATE, ICON_URL, SUGGESTED_VERSION_CODE,
|
||||
SuggestedApk.VERSION
|
||||
SuggestedApk.VERSION, InstalledApp.VERSION_CODE,
|
||||
InstalledApp.VERSION_NAME
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A QuerySelection which is aware of the option/need to join onto the
|
||||
* installed apps table. Not that the base classes
|
||||
* {@link org.fdroid.fdroid.data.QuerySelection#add(QuerySelection)} and
|
||||
* {@link org.fdroid.fdroid.data.QuerySelection#add(String, String[])} methods
|
||||
* will only return the base class {@link org.fdroid.fdroid.data.QuerySelection}
|
||||
* which is not aware of the installed app table.
|
||||
* However, the
|
||||
* {@link org.fdroid.fdroid.data.AppProvider.AppQuerySelection#add(org.fdroid.fdroid.data.AppProvider.AppQuerySelection)}
|
||||
* method from this class will return an instance of this class, that is aware of
|
||||
* the install apps table.
|
||||
*/
|
||||
private static class AppQuerySelection extends QuerySelection {
|
||||
|
||||
private boolean naturalJoinToInstalled = false;
|
||||
|
||||
public AppQuerySelection() {
|
||||
// The same as no selection, because "1" will always resolve to true when executing the SQL query.
|
||||
// e.g. "WHERE 1 AND ..." is the same as "WHERE ..."
|
||||
super("1");
|
||||
}
|
||||
|
||||
public AppQuerySelection(String selection) {
|
||||
super(selection);
|
||||
}
|
||||
|
||||
public AppQuerySelection(String selection, String[] args) {
|
||||
super(selection, args);
|
||||
}
|
||||
|
||||
public AppQuerySelection(String selection, List<String> args) {
|
||||
super(selection, args);
|
||||
}
|
||||
|
||||
public boolean naturalJoinToInstalled() {
|
||||
return naturalJoinToInstalled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells the query selection that it will need to join onto the installed apps table
|
||||
* when used. This should be called when your query makes use of fields from that table
|
||||
* (for example, list all installed, or list those which can be updated).
|
||||
* @return A reference to this object, to allow method chaining, for example
|
||||
* <code>return new AppQuerySelection(selection).requiresInstalledTable())</code>
|
||||
*/
|
||||
public AppQuerySelection requireNaturalInstalledTable() {
|
||||
naturalJoinToInstalled = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AppQuerySelection add(AppQuerySelection query) {
|
||||
QuerySelection both = super.add(query);
|
||||
AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs());
|
||||
if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) {
|
||||
bothWithJoin.requireNaturalInstalledTable();
|
||||
}
|
||||
return bothWithJoin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Query extends QueryBuilder {
|
||||
|
||||
private boolean isSuggestedApkTableAdded = false;
|
||||
|
||||
private boolean requiresInstalledTable = false;
|
||||
private boolean categoryFieldAdded = false;
|
||||
|
||||
@Override
|
||||
@ -193,10 +256,43 @@ public class AppProvider extends FDroidProvider {
|
||||
return fieldCount() == 1 && categoryFieldAdded;
|
||||
}
|
||||
|
||||
public void addSelection(AppQuerySelection selection) {
|
||||
addSelection(selection.getSelection());
|
||||
if (selection.naturalJoinToInstalled()) {
|
||||
naturalJoinToInstalledTable();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: What if the selection requires a natural join, but we first get a left join
|
||||
// because something causes leftJoin to be caused first? Maybe throw an exception?
|
||||
public void naturalJoinToInstalledTable() {
|
||||
if (!requiresInstalledTable) {
|
||||
join(
|
||||
DBHelper.TABLE_INSTALLED_APP,
|
||||
"installed",
|
||||
"installed." + InstalledAppProvider.DataColumns.APP_ID + " = " + DBHelper.TABLE_APP + ".id");
|
||||
requiresInstalledTable = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void leftJoinToInstalledTable() {
|
||||
if (!requiresInstalledTable) {
|
||||
leftJoin(
|
||||
DBHelper.TABLE_INSTALLED_APP,
|
||||
"installed",
|
||||
"installed." + InstalledAppProvider.DataColumns.APP_ID + " = " + DBHelper.TABLE_APP + ".id");
|
||||
requiresInstalledTable = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addField(String field) {
|
||||
if (field.equals(DataColumns.SuggestedApk.VERSION)) {
|
||||
addSuggestedApkVersionField();
|
||||
} else if (field.equals(DataColumns.InstalledApp.VERSION_NAME)) {
|
||||
addInstalledAppVersionName();
|
||||
} else if (field.equals(DataColumns.InstalledApp.VERSION_CODE)) {
|
||||
addInstalledAppVersionCode();
|
||||
} else if (field.equals(DataColumns._COUNT)) {
|
||||
appendCountField();
|
||||
} else {
|
||||
@ -227,6 +323,25 @@ public class AppProvider extends FDroidProvider {
|
||||
}
|
||||
appendField(fieldName, "suggestedApk", alias);
|
||||
}
|
||||
|
||||
private void addInstalledAppVersionName() {
|
||||
addInstalledAppField(
|
||||
InstalledAppProvider.DataColumns.VERSION_NAME,
|
||||
DataColumns.InstalledApp.VERSION_NAME
|
||||
);
|
||||
}
|
||||
|
||||
private void addInstalledAppVersionCode() {
|
||||
addInstalledAppField(
|
||||
InstalledAppProvider.DataColumns.VERSION_CODE,
|
||||
DataColumns.InstalledApp.VERSION_CODE
|
||||
);
|
||||
}
|
||||
|
||||
private void addInstalledAppField(String fieldName, String alias) {
|
||||
leftJoinToInstalledTable();
|
||||
appendField(fieldName, "installed", alias);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String PROVIDER_NAME = "AppProvider";
|
||||
@ -242,7 +357,6 @@ public class AppProvider extends FDroidProvider {
|
||||
private static final String PATH_NEWLY_ADDED = "newlyAdded";
|
||||
private static final String PATH_CATEGORY = "category";
|
||||
private static final String PATH_IGNORED = "ignored";
|
||||
|
||||
private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex";
|
||||
|
||||
private static final int CAN_UPDATE = CODE_SINGLE + 1;
|
||||
@ -254,7 +368,6 @@ public class AppProvider extends FDroidProvider {
|
||||
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1;
|
||||
private static final int CATEGORY = NEWLY_ADDED + 1;
|
||||
private static final int IGNORED = CATEGORY + 1;
|
||||
|
||||
private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1;
|
||||
|
||||
static {
|
||||
@ -359,49 +472,19 @@ public class AppProvider extends FDroidProvider {
|
||||
return matcher;
|
||||
}
|
||||
|
||||
private QuerySelection queryCanUpdate() {
|
||||
Map<String, PackageInfo> installedApps = Utils.getInstalledApps(getContext());
|
||||
|
||||
private AppQuerySelection queryCanUpdate() {
|
||||
String ignoreCurrent = " fdroid_app.ignoreThisUpdate != fdroid_app.suggestedVercode ";
|
||||
String ignoreAll = " fdroid_app.ignoreAllUpdates != 1 ";
|
||||
String ignore = " ( " + ignoreCurrent + " AND " + ignoreAll + " ) ";
|
||||
|
||||
StringBuilder where = new StringBuilder( ignore + " AND ( 0 ");
|
||||
String[] selectionArgs = new String[installedApps.size() * 2];
|
||||
int i = 0;
|
||||
for (PackageInfo info : installedApps.values() ) {
|
||||
where.append(" OR ( fdroid_app.")
|
||||
.append(DataColumns.APP_ID)
|
||||
.append(" = ? AND fdroid_app.")
|
||||
.append(DataColumns.SUGGESTED_VERSION_CODE)
|
||||
.append(" > ?) ");
|
||||
selectionArgs[ i * 2 ] = info.packageName;
|
||||
selectionArgs[ i * 2 + 1 ] = Integer.toString(info.versionCode);
|
||||
i ++;
|
||||
}
|
||||
where.append(") ");
|
||||
|
||||
return new QuerySelection(where.toString(), selectionArgs);
|
||||
String where = ignore + " AND fdroid_app." + DataColumns.SUGGESTED_VERSION_CODE + " > installed.versionCode";
|
||||
return new AppQuerySelection(where).requireNaturalInstalledTable();
|
||||
}
|
||||
|
||||
private QuerySelection queryInstalled() {
|
||||
Map<String, PackageInfo> installedApps = Utils.getInstalledApps(getContext());
|
||||
StringBuilder where = new StringBuilder( " ( 0 ");
|
||||
String[] selectionArgs = new String[installedApps.size()];
|
||||
int i = 0;
|
||||
for (Map.Entry<String, PackageInfo> entry : installedApps.entrySet() ) {
|
||||
where.append(" OR fdroid_app.")
|
||||
.append(AppProvider.DataColumns.APP_ID)
|
||||
.append(" = ? ");
|
||||
selectionArgs[i] = entry.getKey();
|
||||
i ++;
|
||||
}
|
||||
where.append(" ) ");
|
||||
|
||||
return new QuerySelection(where.toString(), selectionArgs);
|
||||
private AppQuerySelection queryInstalled() {
|
||||
return new AppQuerySelection().requireNaturalInstalledTable();
|
||||
}
|
||||
|
||||
private QuerySelection querySearch(String keywords) {
|
||||
private AppQuerySelection querySearch(String keywords) {
|
||||
keywords = "%" + keywords + "%";
|
||||
String selection =
|
||||
"fdroid_app.id like ? OR " +
|
||||
@ -409,34 +492,34 @@ public class AppProvider extends FDroidProvider {
|
||||
"fdroid_app.summary like ? OR " +
|
||||
"fdroid_app.description like ? ";
|
||||
String[] args = new String[] { keywords, keywords, keywords, keywords};
|
||||
return new QuerySelection(selection, args);
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection querySingle(String id) {
|
||||
private AppQuerySelection querySingle(String id) {
|
||||
String selection = "fdroid_app.id = ?";
|
||||
String[] args = { id };
|
||||
return new QuerySelection(selection, args);
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryIgnored() {
|
||||
private AppQuerySelection queryIgnored() {
|
||||
String selection = "fdroid_app.ignoreAllUpdates = 1 OR " +
|
||||
"fdroid_app.ignoreThisUpdate >= fdroid_app.suggestedVercode";
|
||||
return new QuerySelection(selection);
|
||||
return new AppQuerySelection(selection);
|
||||
}
|
||||
|
||||
private QuerySelection queryNewlyAdded() {
|
||||
private AppQuerySelection queryNewlyAdded() {
|
||||
String selection = "fdroid_app.added > ?";
|
||||
String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) };
|
||||
return new QuerySelection(selection, args);
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryRecentlyUpdated() {
|
||||
private AppQuerySelection queryRecentlyUpdated() {
|
||||
String selection = "fdroid_app.added != fdroid_app.lastUpdated AND fdroid_app.lastUpdated > ?";
|
||||
String[] args = { Utils.DATE_FORMAT.format(Preferences.get().calcMaxHistory()) };
|
||||
return new QuerySelection(selection, args);
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryCategory(String category) {
|
||||
private AppQuerySelection queryCategory(String category) {
|
||||
// TODO: In the future, add a new table for categories,
|
||||
// so we can join onto it.
|
||||
String selection =
|
||||
@ -450,67 +533,68 @@ public class AppProvider extends FDroidProvider {
|
||||
"%," + category,
|
||||
"%," + category + ",%",
|
||||
};
|
||||
return new QuerySelection(selection, args);
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
private QuerySelection queryNoApks() {
|
||||
private AppQuerySelection queryNoApks() {
|
||||
String selection = "(SELECT COUNT(*) FROM fdroid_apk WHERE fdroid_apk.id = fdroid_app.id) = 0";
|
||||
return new QuerySelection(selection);
|
||||
return new AppQuerySelection(selection);
|
||||
}
|
||||
|
||||
private QuerySelection queryApps(String appIds) {
|
||||
private AppQuerySelection queryApps(String appIds) {
|
||||
String[] args = appIds.split(",");
|
||||
String selection = "fdroid_app.id IN (" + generateQuestionMarksForInClause(args.length) + ")";
|
||||
return new QuerySelection(selection, args);
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
QuerySelection query = new QuerySelection(selection, selectionArgs);
|
||||
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
||||
Query query = new Query();
|
||||
AppQuerySelection selection = new AppQuerySelection(customSelection, selectionArgs);
|
||||
switch (matcher.match(uri)) {
|
||||
case CODE_LIST:
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
query = query.add(querySingle(uri.getLastPathSegment()));
|
||||
selection = selection.add(querySingle(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case CAN_UPDATE:
|
||||
query = query.add(queryCanUpdate());
|
||||
selection = selection.add(queryCanUpdate());
|
||||
break;
|
||||
|
||||
case INSTALLED:
|
||||
query = query.add(queryInstalled());
|
||||
selection = selection.add(queryInstalled());
|
||||
break;
|
||||
|
||||
case SEARCH:
|
||||
query = query.add(querySearch(uri.getLastPathSegment()));
|
||||
selection = selection.add(querySearch(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case NO_APKS:
|
||||
query = query.add(queryNoApks());
|
||||
selection = selection.add(queryNoApks());
|
||||
break;
|
||||
|
||||
case APPS:
|
||||
query = query.add(queryApps(uri.getLastPathSegment()));
|
||||
selection = selection.add(queryApps(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case IGNORED:
|
||||
query = query.add(queryIgnored());
|
||||
selection = selection.add(queryIgnored());
|
||||
break;
|
||||
|
||||
case CATEGORY:
|
||||
query = query.add(queryCategory(uri.getLastPathSegment()));
|
||||
selection = selection.add(queryCategory(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
case RECENTLY_UPDATED:
|
||||
sortOrder = " fdroid_app.lastUpdated DESC";
|
||||
query = query.add(queryRecentlyUpdated());
|
||||
selection = selection.add(queryRecentlyUpdated());
|
||||
break;
|
||||
|
||||
case NEWLY_ADDED:
|
||||
sortOrder = " fdroid_app.added DESC";
|
||||
query = query.add(queryNewlyAdded());
|
||||
selection = selection.add(queryNewlyAdded());
|
||||
break;
|
||||
|
||||
default:
|
||||
@ -522,12 +606,11 @@ public class AppProvider extends FDroidProvider {
|
||||
sortOrder = " lower( fdroid_app." + sortOrder + " ) ";
|
||||
}
|
||||
|
||||
Query q = new Query();
|
||||
q.addFields(projection);
|
||||
q.addSelection(query.getSelection());
|
||||
q.addOrderBy(sortOrder);
|
||||
query.addSelection(selection);
|
||||
query.addFields(projection); // TODO: Make the order of addFields/addSelection not dependent on each other...
|
||||
query.addOrderBy(sortOrder);
|
||||
|
||||
Cursor cursor = read().rawQuery(q.toString(), query.getArgs());
|
||||
Cursor cursor = read().rawQuery(query.toString(), selection.getArgs());
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
@ -2,18 +2,20 @@ package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.*;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DBHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.data.DBHelper";
|
||||
|
||||
public static final String DATABASE_NAME = "fdroid";
|
||||
|
||||
public static final String TABLE_REPO = "fdroid_repo";
|
||||
@ -23,7 +25,6 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
// This information is retrieved from the repositories.
|
||||
public static final String TABLE_APK = "fdroid_apk";
|
||||
|
||||
|
||||
private static final String CREATE_TABLE_REPO = "create table "
|
||||
+ TABLE_REPO + " (_id integer primary key, "
|
||||
+ "address text not null, "
|
||||
@ -87,7 +88,15 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
+ "iconUrl text, "
|
||||
+ "primary key(id));";
|
||||
|
||||
private static final int DB_VERSION = 42;
|
||||
public static final String TABLE_INSTALLED_APP = "fdroid_installedApp";
|
||||
private static final String CREATE_TABLE_INSTALLED_APP = "CREATE TABLE " + TABLE_INSTALLED_APP
|
||||
+ " ( "
|
||||
+ "appId TEXT NOT NULL PRIMARY KEY, "
|
||||
+ "versionCode INT NOT NULL, "
|
||||
+ "versionName TEXT NOT NULL "
|
||||
+ " );";
|
||||
|
||||
private static final int DB_VERSION = 43;
|
||||
|
||||
private Context context;
|
||||
|
||||
@ -177,6 +186,7 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
|
||||
createAppApk(db);
|
||||
createInstalledApp(db);
|
||||
db.execSQL(CREATE_TABLE_REPO);
|
||||
|
||||
insertRepo(
|
||||
@ -239,6 +249,8 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
addLastUpdatedToRepo(db, oldVersion);
|
||||
renameRepoId(db, oldVersion);
|
||||
populateRepoNames(db, oldVersion);
|
||||
|
||||
if (oldVersion < 43) createInstalledApp(db);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -381,6 +393,11 @@ public class DBHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("create index apk_id on " + TABLE_APK + " (id);");
|
||||
}
|
||||
|
||||
private void createInstalledApp(SQLiteDatabase db) {
|
||||
Log.d(TAG, "Creating 'installed app' database table.");
|
||||
db.execSQL(CREATE_TABLE_INSTALLED_APP);
|
||||
}
|
||||
|
||||
private static boolean columnExists(SQLiteDatabase db,
|
||||
String table, String column) {
|
||||
return (db.rawQuery( "select * from " + table + " limit 0,1", null )
|
||||
|
@ -26,6 +26,14 @@ public abstract class FDroidProvider extends ContentProvider {
|
||||
|
||||
abstract protected String getProviderName();
|
||||
|
||||
/**
|
||||
* Should always be the same as the provider:name in the AndroidManifest
|
||||
* @return
|
||||
*/
|
||||
public final String getName() {
|
||||
return AUTHORITY + "." + getProviderName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells us if we are in the middle of a batch of operations. Allows us to
|
||||
* decide not to notify the content resolver of changes,
|
||||
|
194
src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java
Normal file
194
src/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java
Normal file
@ -0,0 +1,194 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Compares what is in the fdroid_installedApp SQLite database table with the package
|
||||
* info that we can gleam from the {@link android.content.pm.PackageManager}. If there
|
||||
* is any updates/removals/insertions which need to take place, we will perform them.
|
||||
* TODO: The content providers are not thread safe, so it is possible we will be writing
|
||||
* to the database at the same time we respond to a broadcasted intent.
|
||||
*/
|
||||
public class InstalledAppCacheUpdater {
|
||||
|
||||
private static final String TAG = "org.fdroid.fdroid.data.InstalledAppCacheUpdater";
|
||||
|
||||
private Context context;
|
||||
|
||||
private List<PackageInfo> toInsert = new ArrayList<PackageInfo>();
|
||||
private List<PackageInfo> toUpdate = new ArrayList<PackageInfo>();
|
||||
private List<String> toDelete = new ArrayList<String>();
|
||||
|
||||
protected InstalledAppCacheUpdater(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure our database of installed apps is in sync with what the PackageManager tells us is installed.
|
||||
* Once completed, the relevant ContentProviders will be notified of any changes to installed statuses.
|
||||
* This method will block until completed, which could be in the order of a few seconds (depending on
|
||||
* how many apps are installed).
|
||||
*/
|
||||
public static void updateInForeground(Context context) {
|
||||
InstalledAppCacheUpdater updater = new InstalledAppCacheUpdater(context);
|
||||
if (updater.update()) {
|
||||
updater.notifyProviders();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure our database of installed apps is in sync with what the PackageManager tells us is installed.
|
||||
* Once completed, the relevant ContentProviders will be notified of any changes to installed statuses.
|
||||
* This method returns immediately, and will continue to work in an AsyncTask.
|
||||
*/
|
||||
public static void updateInBackground(Context context) {
|
||||
InstalledAppCacheUpdater updater = new InstalledAppCacheUpdater(context);
|
||||
updater.startBackgroundWorker();
|
||||
}
|
||||
|
||||
protected boolean update() {
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
compareCacheToPackageManager();
|
||||
updateCache();
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
Log.d(TAG, "Took " + duration + "ms to compare the installed app cache with PackageManager.");
|
||||
|
||||
return hasChanged();
|
||||
}
|
||||
|
||||
protected void notifyProviders() {
|
||||
Log.i(TAG, "Installed app cache has changed, notifying content providers (so they can update the relevant views).");
|
||||
context.getContentResolver().notifyChange(AppProvider.getContentUri(), null);
|
||||
context.getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
|
||||
}
|
||||
|
||||
protected void startBackgroundWorker() {
|
||||
new Worker().execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* If any of the cached app details have been removed, updated or inserted,
|
||||
* then the cache has changed.
|
||||
*/
|
||||
private boolean hasChanged() {
|
||||
return toInsert.size() > 0 || toUpdate.size() > 0 || toDelete.size() > 0;
|
||||
}
|
||||
|
||||
private void updateCache() {
|
||||
|
||||
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
|
||||
ops.addAll(deleteFromCache(toDelete));
|
||||
ops.addAll(updateCachedValues(toUpdate));
|
||||
ops.addAll(insertIntoCache(toInsert));
|
||||
|
||||
if (ops.size() > 0) {
|
||||
try {
|
||||
context.getContentResolver().applyBatch(InstalledAppProvider.getAuthority(), ops);
|
||||
Log.d(TAG, "Finished executing " + ops.size() + " CRUD operations on installed app cache.");
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Error updating installed app cache: " + e);
|
||||
} catch (OperationApplicationException e) {
|
||||
Log.e(TAG, "Error updating installed app cache: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void compareCacheToPackageManager() {
|
||||
|
||||
Map<String, Integer> cachedInfo = InstalledAppProvider.Helper.all(context);
|
||||
|
||||
List<PackageInfo> installedPackages = context.getPackageManager().getInstalledPackages(0);
|
||||
for (PackageInfo appInfo : installedPackages) {
|
||||
if (!cachedInfo.containsKey(appInfo.packageName)) {
|
||||
toInsert.add(appInfo);
|
||||
} else {
|
||||
if (cachedInfo.get(appInfo.packageName) < appInfo.versionCode) {
|
||||
toUpdate.add(appInfo);
|
||||
}
|
||||
cachedInfo.remove(appInfo.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
if (cachedInfo.size() > 0) {
|
||||
for (Map.Entry<String, Integer> entry : cachedInfo.entrySet() ) {
|
||||
toDelete.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<ContentProviderOperation> insertIntoCache(List<PackageInfo> appsToInsert) {
|
||||
List<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(appsToInsert.size());
|
||||
if (appsToInsert.size() > 0) {
|
||||
Log.d(TAG, "Preparing to cache installed info for " + appsToInsert.size() + " new apps.");
|
||||
Uri uri = InstalledAppProvider.getContentUri();
|
||||
for (PackageInfo info : appsToInsert) {
|
||||
ContentProviderOperation op = ContentProviderOperation.newInsert(uri)
|
||||
.withValue(InstalledAppProvider.DataColumns.APP_ID, info.packageName)
|
||||
.withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode)
|
||||
.withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName)
|
||||
.build();
|
||||
ops.add(op);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
private List<ContentProviderOperation> updateCachedValues(List<PackageInfo> appsToUpdate) {
|
||||
List<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(appsToUpdate.size());
|
||||
if (appsToUpdate.size() > 0) {
|
||||
Log.d(TAG, "Preparing to update installed app cache for " + appsToUpdate.size() + " apps.");
|
||||
for (PackageInfo info : appsToUpdate) {
|
||||
Uri uri = InstalledAppProvider.getAppUri(info.packageName);
|
||||
ContentProviderOperation op = ContentProviderOperation.newUpdate(uri)
|
||||
.withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode)
|
||||
.withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName)
|
||||
.build();
|
||||
ops.add(op);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
private List<ContentProviderOperation> deleteFromCache(List<String> appIds) {
|
||||
List<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(appIds.size());
|
||||
if (appIds.size() > 0) {
|
||||
Log.d(TAG, "Preparing to remove " + appIds.size() + " apps from the installed app cache.");
|
||||
for (String appId : appIds) {
|
||||
Uri uri = InstalledAppProvider.getAppUri(appId);
|
||||
ops.add(ContentProviderOperation.newDelete(uri).build());
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
private class Worker extends AsyncTask<Void, Void, Boolean> {
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
return update();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean changed) {
|
||||
if (changed) {
|
||||
notifyProviders();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
180
src/org/fdroid/fdroid/data/InstalledAppProvider.java
Normal file
180
src/org/fdroid/fdroid/data/InstalledAppProvider.java
Normal file
@ -0,0 +1,180 @@
|
||||
package org.fdroid.fdroid.data;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import org.fdroid.fdroid.R;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class InstalledAppProvider extends FDroidProvider {
|
||||
|
||||
public static class Helper {
|
||||
|
||||
/**
|
||||
* @return The keys are the app ids (package names), and their corresponding values are
|
||||
* the version code which is installed.
|
||||
*/
|
||||
public static Map<String, Integer> all(Context context) {
|
||||
|
||||
Map<String, Integer> cachedInfo = new HashMap<String, Integer>();
|
||||
|
||||
Uri uri = InstalledAppProvider.getContentUri();
|
||||
String[] projection = InstalledAppProvider.DataColumns.ALL;
|
||||
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
|
||||
if (cursor != null) {
|
||||
cursor.moveToFirst();
|
||||
while (!cursor.isAfterLast()) {
|
||||
cachedInfo.put(
|
||||
cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)),
|
||||
cursor.getInt(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_CODE))
|
||||
);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return cachedInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface DataColumns {
|
||||
|
||||
public static final String APP_ID = "appId";
|
||||
public static final String VERSION_CODE = "versionCode";
|
||||
public static final String VERSION_NAME = "versionName";
|
||||
|
||||
public static String[] ALL = { APP_ID, VERSION_CODE, VERSION_NAME };
|
||||
|
||||
}
|
||||
|
||||
private static final String PROVIDER_NAME = "InstalledAppProvider";
|
||||
|
||||
private static final UriMatcher matcher = new UriMatcher(-1);
|
||||
|
||||
static {
|
||||
matcher.addURI(getAuthority(), null, CODE_LIST);
|
||||
matcher.addURI(getAuthority(), "*", CODE_SINGLE);
|
||||
}
|
||||
|
||||
public static Uri getContentUri() {
|
||||
return Uri.parse("content://" + getAuthority());
|
||||
}
|
||||
|
||||
public static Uri getAppUri(String appId) {
|
||||
return Uri.withAppendedPath(getContentUri(), appId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTableName() {
|
||||
return DBHelper.TABLE_INSTALLED_APP;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getProviderName() {
|
||||
return "InstalledAppProvider";
|
||||
}
|
||||
|
||||
public static String getAuthority() {
|
||||
return AUTHORITY + "." + PROVIDER_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UriMatcher getMatcher() {
|
||||
return matcher;
|
||||
}
|
||||
|
||||
private QuerySelection queryApp(String appId) {
|
||||
return new QuerySelection("appId = ?", new String[] { appId } );
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String customSelection, String[] selectionArgs, String sortOrder) {
|
||||
QuerySelection selection = new QuerySelection(customSelection, selectionArgs);
|
||||
switch (matcher.match(uri)) {
|
||||
case CODE_LIST:
|
||||
break;
|
||||
|
||||
case CODE_SINGLE:
|
||||
selection = selection.add(queryApp(uri.getLastPathSegment()));
|
||||
break;
|
||||
|
||||
default:
|
||||
String message = "Invalid URI for installed app content provider: " + uri;
|
||||
Log.e("FDroid", message);
|
||||
throw new UnsupportedOperationException(message);
|
||||
}
|
||||
|
||||
Cursor cursor = read().query(getTableName(), projection, selection.getSelection(), selection.getArgs(), null, null, null);
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String where, String[] whereArgs) {
|
||||
|
||||
if (matcher.match(uri) != CODE_SINGLE) {
|
||||
throw new UnsupportedOperationException("Delete not supported for " + uri + ".");
|
||||
}
|
||||
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
query = query.add(queryApp(uri.getLastPathSegment()));
|
||||
|
||||
int count = write().delete(getTableName(), query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
|
||||
if (matcher.match(uri) != CODE_LIST) {
|
||||
throw new UnsupportedOperationException("Insert not supported for " + uri + ".");
|
||||
}
|
||||
|
||||
verifyVersionNameNotNull(values);
|
||||
write().insertOrThrow(getTableName(), null, values);
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return getAppUri(values.getAsString(DataColumns.APP_ID));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
|
||||
|
||||
if (matcher.match(uri) != CODE_SINGLE) {
|
||||
throw new UnsupportedOperationException("Update not supported for " + uri + ".");
|
||||
}
|
||||
|
||||
QuerySelection query = new QuerySelection(where, whereArgs);
|
||||
query = query.add(queryApp(uri.getLastPathSegment()));
|
||||
|
||||
verifyVersionNameNotNull(values);
|
||||
int count = write().update(getTableName(), values, query.getSelection(), query.getArgs());
|
||||
if (!isApplyingBatch()) {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* During development, I stumbled across one (out of over 300) installed apps which had a versionName
|
||||
* of null. As such, I figured we may as well store it as "Unknown". The alternative is to allow the
|
||||
* column to accept NULL values in the database, and then deal with the potential of a null everywhere
|
||||
* "versionName" is used.
|
||||
*/
|
||||
private void verifyVersionNameNotNull(ContentValues values) {
|
||||
if (values.containsKey(DataColumns.VERSION_NAME) && values.getAsString(DataColumns.VERSION_NAME) == null) {
|
||||
values.put(DataColumns.VERSION_NAME, getContext().getString(R.string.unknown));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -62,17 +62,27 @@ abstract class QueryBuilder {
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
protected final void leftJoin(String table, String alias,
|
||||
String condition) {
|
||||
tables.append(" LEFT JOIN ");
|
||||
tables.append(table);
|
||||
protected final void leftJoin(String table, String alias, String condition) {
|
||||
joinWithType("LEFT", table, alias, condition);
|
||||
}
|
||||
|
||||
protected final void join(String table, String alias, String condition) {
|
||||
joinWithType("", table, alias, condition);
|
||||
}
|
||||
|
||||
private void joinWithType(String type, String table, String alias, String condition) {
|
||||
tables.append(' ')
|
||||
.append(type)
|
||||
.append(" JOIN ")
|
||||
.append(table);
|
||||
|
||||
if (alias != null) {
|
||||
tables.append(" AS ");
|
||||
tables.append(alias);
|
||||
tables.append(" AS ").append(alias);
|
||||
}
|
||||
tables.append(" ON (");
|
||||
tables.append(condition);
|
||||
tables.append(")");
|
||||
|
||||
tables.append(" ON (")
|
||||
.append(condition)
|
||||
.append(')');
|
||||
}
|
||||
|
||||
private String fieldsSql() {
|
||||
|
@ -126,16 +126,14 @@ abstract public class AppListAdapter extends CursorAdapter {
|
||||
return null;
|
||||
}
|
||||
|
||||
PackageInfo installedInfo = app.getInstalledInfo(mContext);
|
||||
|
||||
if (installedInfo == null) {
|
||||
if (!app.isInstalled()) {
|
||||
return app.getSuggestedVersion();
|
||||
}
|
||||
|
||||
String installedVersionString = installedInfo.versionName;
|
||||
int installedVersionCode = installedInfo.versionCode;
|
||||
String installedVersionString = app.installedVersionName;
|
||||
int installedVersionCode = app.installedVersionCode;
|
||||
|
||||
if (app.canAndWantToUpdate(mContext) && showStatusUpdate()) {
|
||||
if (app.canAndWantToUpdate() && showStatusUpdate()) {
|
||||
return installedVersionString + " → " + app.getSuggestedVersion();
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,8 @@ abstract public class AppListFragment extends ListFragment implements
|
||||
AppProvider.DataColumns.LICENSE,
|
||||
AppProvider.DataColumns.ICON,
|
||||
AppProvider.DataColumns.ICON_URL,
|
||||
AppProvider.DataColumns.InstalledApp.VERSION_CODE,
|
||||
AppProvider.DataColumns.InstalledApp.VERSION_NAME,
|
||||
AppProvider.DataColumns.SuggestedApk.VERSION,
|
||||
AppProvider.DataColumns.SUGGESTED_VERSION_CODE,
|
||||
AppProvider.DataColumns.IGNORE_ALLUPDATES,
|
||||
|
@ -4,6 +4,7 @@ import android.content.pm.PackageInfo;
|
||||
import android.test.mock.MockPackageManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class MockInstallablePackageManager extends MockPackageManager {
|
||||
@ -16,11 +17,36 @@ public class MockInstallablePackageManager extends MockPackageManager {
|
||||
}
|
||||
|
||||
public void install(String id, int version, String versionName) {
|
||||
PackageInfo p = new PackageInfo();
|
||||
p.packageName = id;
|
||||
p.versionCode = version;
|
||||
p.versionName = versionName;
|
||||
info.add(p);
|
||||
PackageInfo existing = getPackageInfo(id);
|
||||
if (existing != null) {
|
||||
existing.versionCode = version;
|
||||
existing.versionName = versionName;
|
||||
} else {
|
||||
PackageInfo p = new PackageInfo();
|
||||
p.packageName = id;
|
||||
p.versionCode = version;
|
||||
p.versionName = versionName;
|
||||
info.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
public PackageInfo getPackageInfo(String id) {
|
||||
for (PackageInfo i : info) {
|
||||
if (i.packageName.equals(id)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void remove(String id) {
|
||||
for (Iterator<PackageInfo> it = info.iterator(); it.hasNext();) {
|
||||
PackageInfo info = it.next();
|
||||
if (info.packageName.equals(id)) {
|
||||
it.remove();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,14 +4,13 @@ import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
|
||||
import mock.MockCategoryResources;
|
||||
import mock.MockContextSwappableComponents;
|
||||
import mock.MockInstallablePackageManager;
|
||||
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.App;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -41,6 +40,42 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Although this doesn't directly relate to the AppProvider, it is here because
|
||||
* the AppProvider used to stumble across this bug when asking for installed apps,
|
||||
* and the device had over 1000 apps installed.
|
||||
*/
|
||||
public void testMaxSqliteParams() {
|
||||
|
||||
MockInstallablePackageManager pm = new MockInstallablePackageManager();
|
||||
getSwappableContext().setPackageManager(pm);
|
||||
|
||||
insertApp("com.example.app1", "App 1");
|
||||
insertApp("com.example.app100", "App 100");
|
||||
insertApp("com.example.app1000", "App 1000");
|
||||
|
||||
for (int i = 0; i < 50; i ++) {
|
||||
pm.install("com.example.app" + i, 1, "v" + 1);
|
||||
}
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(1, AppProvider.getInstalledUri());
|
||||
|
||||
for (int i = 50; i < 500; i ++) {
|
||||
pm.install("com.example.app" + i, 1, "v" + 1);
|
||||
}
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(2, AppProvider.getInstalledUri());
|
||||
|
||||
for (int i = 500; i < 1100; i ++) {
|
||||
pm.install("com.example.app" + i, 1, "v" + 1);
|
||||
}
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(3, AppProvider.getInstalledUri());
|
||||
}
|
||||
|
||||
public void testCantFindApp() {
|
||||
assertNull(AppProvider.Helper.findById(getMockContentResolver(), "com.example.doesnt-exist"));
|
||||
}
|
||||
@ -87,7 +122,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode);
|
||||
insertApp(id, "App: " + id, values);
|
||||
|
||||
packageManager.install(id, installedVercode, "v" + installedVercode);
|
||||
TestUtils.installAndBroadcast(getMockContext(), packageManager, id, installedVercode, "v" + installedVercode);
|
||||
}
|
||||
|
||||
public void testCanUpdate() {
|
||||
@ -112,7 +147,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
|
||||
// Can't "update", although can "install"...
|
||||
App notInstalled = AppProvider.Helper.findById(r, "not installed");
|
||||
assertFalse(notInstalled.canAndWantToUpdate(c));
|
||||
assertFalse(notInstalled.canAndWantToUpdate());
|
||||
|
||||
App installedOnlyOneVersionAvailable = AppProvider.Helper.findById(r, "installed, only one version available");
|
||||
App installedAlreadyLatestNoIgnore = AppProvider.Helper.findById(r, "installed, already latest, no ignore");
|
||||
@ -120,21 +155,36 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
App installedAlreadyLatestIgnoreLatest = AppProvider.Helper.findById(r, "installed, already latest, ignore latest");
|
||||
App installedAlreadyLatestIgnoreOld = AppProvider.Helper.findById(r, "installed, already latest, ignore old");
|
||||
|
||||
assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate(c));
|
||||
assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate(c));
|
||||
assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate(c));
|
||||
assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate(c));
|
||||
assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate(c));
|
||||
assertFalse(installedOnlyOneVersionAvailable.canAndWantToUpdate());
|
||||
assertFalse(installedAlreadyLatestNoIgnore.canAndWantToUpdate());
|
||||
assertFalse(installedAlreadyLatestIgnoreAll.canAndWantToUpdate());
|
||||
assertFalse(installedAlreadyLatestIgnoreLatest.canAndWantToUpdate());
|
||||
assertFalse(installedAlreadyLatestIgnoreOld.canAndWantToUpdate());
|
||||
|
||||
App installedOldNoIgnore = AppProvider.Helper.findById(r, "installed, old version, no ignore");
|
||||
App installedOldIgnoreAll = AppProvider.Helper.findById(r, "installed, old version, ignore all");
|
||||
App installedOldIgnoreLatest = AppProvider.Helper.findById(r, "installed, old version, ignore latest");
|
||||
App installedOldIgnoreNewerNotLatest = AppProvider.Helper.findById(r, "installed, old version, ignore newer, but not latest");
|
||||
|
||||
assertTrue(installedOldNoIgnore.canAndWantToUpdate(c));
|
||||
assertFalse(installedOldIgnoreAll.canAndWantToUpdate(c));
|
||||
assertFalse(installedOldIgnoreLatest.canAndWantToUpdate(c));
|
||||
assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate(c));
|
||||
assertTrue(installedOldNoIgnore.canAndWantToUpdate());
|
||||
assertFalse(installedOldIgnoreAll.canAndWantToUpdate());
|
||||
assertFalse(installedOldIgnoreLatest.canAndWantToUpdate());
|
||||
assertTrue(installedOldIgnoreNewerNotLatest.canAndWantToUpdate());
|
||||
|
||||
Cursor canUpdateCursor = r.query(AppProvider.getCanUpdateUri(), AppProvider.DataColumns.ALL, null, null, null);
|
||||
canUpdateCursor.moveToFirst();
|
||||
List<String> canUpdateIds = new ArrayList<String>(canUpdateCursor.getCount());
|
||||
while (!canUpdateCursor.isAfterLast()) {
|
||||
canUpdateIds.add(new App(canUpdateCursor).id);
|
||||
canUpdateCursor.moveToNext();
|
||||
}
|
||||
|
||||
String[] expectedUpdateableIds = {
|
||||
"installed, old version, no ignore",
|
||||
"installed, old version, ignore newer, but not latest",
|
||||
};
|
||||
|
||||
TestUtils.assertContainsOnly(expectedUpdateableIds, canUpdateIds);
|
||||
}
|
||||
|
||||
public void testIgnored() {
|
||||
@ -182,9 +232,6 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
}
|
||||
|
||||
public void testInstalled() {
|
||||
|
||||
Utils.clearInstalledApksCache();
|
||||
|
||||
MockInstallablePackageManager pm = new MockInstallablePackageManager();
|
||||
getSwappableContext().setPackageManager(pm);
|
||||
|
||||
@ -194,7 +241,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
assertResultCount(0, AppProvider.getInstalledUri());
|
||||
|
||||
for (int i = 10; i < 20; i ++) {
|
||||
pm.install("com.example.test." + i, i, "v1");
|
||||
TestUtils.installAndBroadcast(getMockContext(), pm, "com.example.test." + i, i, "v1");
|
||||
}
|
||||
|
||||
assertResultCount(10, AppProvider.getInstalledUri());
|
||||
@ -237,6 +284,13 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
return getMockContentResolver().query(AppProvider.getContentUri(), getMinimalProjection(), null, null, null);
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// "Categories"
|
||||
// (at this point) not an additional table, but we treat them sort of
|
||||
// like they are. That means that if we change the implementation to
|
||||
// use a separate table in the future, these should still pass.
|
||||
// ========================================================================
|
||||
|
||||
public void testCategoriesSingle() {
|
||||
insertAppWithCategory("com.dog", "Dog", "Animal");
|
||||
insertAppWithCategory("com.rock", "Rock", "Mineral");
|
||||
@ -300,12 +354,16 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
|
||||
TestUtils.assertContainsOnly(categoriesLonger, expectedLonger);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// Misc helper functions
|
||||
// (to be used by any tests in this suite)
|
||||
// =======================================================================
|
||||
|
||||
private void insertApp(String id, String name) {
|
||||
insertApp(id, name, new ContentValues());
|
||||
}
|
||||
|
||||
private void insertAppWithCategory(String id, String name,
|
||||
String categories) {
|
||||
private void insertAppWithCategory(String id, String name, String categories) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(AppProvider.DataColumns.CATEGORIES, categories);
|
||||
insertApp(id, name, values);
|
||||
|
@ -9,17 +9,22 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.ContactsContract;
|
||||
import android.test.ProviderTestCase2MockContext;
|
||||
import mock.MockCategoryResources;
|
||||
import mock.MockContextEmptyComponents;
|
||||
import mock.MockContextSwappableComponents;
|
||||
import mock.MockFDroidResources;
|
||||
import org.fdroid.fdroid.data.FDroidProvider;
|
||||
import org.fdroid.fdroid.mock.MockInstalledApkCache;
|
||||
import org.fdroid.fdroid.data.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class FDroidProviderTest<T extends FDroidProvider> extends ProviderTestCase2MockContext<T> {
|
||||
|
||||
private FDroidProvider[] allProviders = {
|
||||
new AppProvider(),
|
||||
new RepoProvider(),
|
||||
new ApkProvider(),
|
||||
new InstalledAppProvider(),
|
||||
};
|
||||
|
||||
private MockContextSwappableComponents swappableContext;
|
||||
|
||||
public FDroidProviderTest(Class<T> providerClass, String providerAuthority) {
|
||||
@ -33,7 +38,18 @@ public abstract class FDroidProviderTest<T extends FDroidProvider> extends Provi
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
Utils.setupInstalledApkCache(new MockInstalledApkCache());
|
||||
|
||||
// Instantiate all providers other than the one which was already created by the base class.
|
||||
// This is because F-Droid providers tend to perform joins onto tables managed by other
|
||||
// providers, and so we need to be able to insert into those other providers for these
|
||||
// joins to be tested correctly.
|
||||
for (FDroidProvider provider : allProviders) {
|
||||
if (!provider.getName().equals(getProvider().getName())) {
|
||||
provider.attachInfo(getMockContext(), null);
|
||||
getMockContentResolver().addProvider(provider.getName(), provider);
|
||||
}
|
||||
}
|
||||
|
||||
getSwappableContext().setResources(getMockResources());
|
||||
|
||||
// The *Provider.Helper.* functions tend to take a Context as their
|
||||
@ -127,4 +143,26 @@ public abstract class FDroidProviderTest<T extends FDroidProvider> extends Provi
|
||||
assertNotNull(result);
|
||||
assertEquals(expectedCount, result.getCount());
|
||||
}
|
||||
|
||||
protected void assertIsInstalledVersionInDb(String appId, int versionCode, String versionName) {
|
||||
Uri uri = InstalledAppProvider.getAppUri(appId);
|
||||
|
||||
String[] projection = {
|
||||
InstalledAppProvider.DataColumns.APP_ID,
|
||||
InstalledAppProvider.DataColumns.VERSION_CODE,
|
||||
InstalledAppProvider.DataColumns.VERSION_NAME,
|
||||
};
|
||||
|
||||
Cursor cursor = getMockContentResolver().query(uri, projection, null, null, null);
|
||||
|
||||
assertNotNull(cursor);
|
||||
assertEquals("App \"" + appId + "\" not installed", 1, cursor.getCount());
|
||||
|
||||
cursor.moveToFirst();
|
||||
|
||||
assertEquals(appId, cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID)));
|
||||
assertEquals(versionCode, cursor.getInt(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_CODE)));
|
||||
assertEquals(versionName, cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_NAME)));
|
||||
}
|
||||
|
||||
}
|
||||
|
179
test/src/org/fdroid/fdroid/InstalledAppCacheTest.java
Normal file
179
test/src/org/fdroid/fdroid/InstalledAppCacheTest.java
Normal file
@ -0,0 +1,179 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import mock.MockInstallablePackageManager;
|
||||
import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
|
||||
/**
|
||||
* Tests the ability of the {@link org.fdroid.fdroid.data.InstalledAppCacheUpdater} to stay in sync with
|
||||
* the {@link android.content.pm.PackageManager}.
|
||||
* For practical reasons, it extends FDroidProviderTest<InstalledAppProvider>, although there is also a
|
||||
* separate test for the InstalledAppProvider which tests the CRUD operations in more detail.
|
||||
*/
|
||||
public class InstalledAppCacheTest extends FDroidProviderTest<InstalledAppProvider> {
|
||||
|
||||
private MockInstallablePackageManager packageManager;
|
||||
|
||||
public InstalledAppCacheTest() {
|
||||
super(InstalledAppProvider.class, InstalledAppProvider.getAuthority());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
packageManager = new MockInstallablePackageManager();
|
||||
getSwappableContext().setPackageManager(packageManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getMinimalProjection() {
|
||||
return new String[] {
|
||||
InstalledAppProvider.DataColumns.APP_ID
|
||||
};
|
||||
}
|
||||
|
||||
public void install(String appId, int versionCode, String versionName) {
|
||||
packageManager.install(appId, versionCode, versionName);
|
||||
}
|
||||
|
||||
public void remove(String appId) {
|
||||
packageManager.remove(appId);
|
||||
}
|
||||
|
||||
public void testFromEmptyCache() {
|
||||
assertResultCount(0, InstalledAppProvider.getContentUri());
|
||||
for (int i = 1; i <= 15; i ++) {
|
||||
install("com.example.app" + i, 200, "2.0");
|
||||
}
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
String[] expectedInstalledIds = {
|
||||
"com.example.app1",
|
||||
"com.example.app2",
|
||||
"com.example.app3",
|
||||
"com.example.app4",
|
||||
"com.example.app5",
|
||||
"com.example.app6",
|
||||
"com.example.app7",
|
||||
"com.example.app8",
|
||||
"com.example.app9",
|
||||
"com.example.app10",
|
||||
"com.example.app11",
|
||||
"com.example.app12",
|
||||
"com.example.app13",
|
||||
"com.example.app14",
|
||||
"com.example.app15",
|
||||
};
|
||||
|
||||
TestUtils.assertContainsOnly(getInstalledAppIdsFromProvider(), expectedInstalledIds);
|
||||
}
|
||||
|
||||
private String[] getInstalledAppIdsFromProvider() {
|
||||
Uri uri = InstalledAppProvider.getContentUri();
|
||||
String[] projection = { InstalledAppProvider.DataColumns.APP_ID };
|
||||
Cursor result = getMockContext().getContentResolver().query(uri, projection, null, null, null);
|
||||
if (result == null) {
|
||||
return new String[0];
|
||||
}
|
||||
|
||||
String[] installedAppIds = new String[result.getCount()];
|
||||
result.moveToFirst();
|
||||
int i = 0;
|
||||
while (!result.isAfterLast()) {
|
||||
installedAppIds[i] = result.getString(result.getColumnIndex(InstalledAppProvider.DataColumns.APP_ID));
|
||||
result.moveToNext();
|
||||
i ++;
|
||||
}
|
||||
result.close();
|
||||
return installedAppIds;
|
||||
}
|
||||
|
||||
public void testAppsAdded() {
|
||||
assertResultCount(0, InstalledAppProvider.getContentUri());
|
||||
|
||||
install("com.example.app1", 1, "v1");
|
||||
install("com.example.app2", 1, "v1");
|
||||
install("com.example.app3", 1, "v1");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(3, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app1", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app2", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app3", 1, "v1");
|
||||
|
||||
install("com.example.app10", 1, "v1");
|
||||
install("com.example.app11", 1, "v1");
|
||||
install("com.example.app12", 1, "v1");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(6, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app10", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app11", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app12", 1, "v1");
|
||||
}
|
||||
|
||||
public void testAppsRemoved() {
|
||||
install("com.example.app1", 1, "v1");
|
||||
install("com.example.app2", 1, "v1");
|
||||
install("com.example.app3", 1, "v1");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(3, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app1", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app2", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app3", 1, "v1");
|
||||
|
||||
remove("com.example.app2");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app1", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app3", 1, "v1");
|
||||
}
|
||||
|
||||
public void testAppsUpdated() {
|
||||
install("com.example.app1", 1, "v1");
|
||||
install("com.example.app2", 1, "v1");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app1", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app2", 1, "v1");
|
||||
|
||||
install("com.example.app2", 20, "v2.0");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app1", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app2", 20, "v2.0");
|
||||
}
|
||||
|
||||
public void testAppsAddedRemovedAndUpdated() {
|
||||
install("com.example.app1", 1, "v1");
|
||||
install("com.example.app2", 1, "v1");
|
||||
install("com.example.app3", 1, "v1");
|
||||
install("com.example.app4", 1, "v1");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(4, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app1", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app2", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app3", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app4", 1, "v1");
|
||||
|
||||
install("com.example.app1", 13, "v1.3");
|
||||
remove("com.example.app2");
|
||||
remove("com.example.app3");
|
||||
install("com.example.app10", 1, "v1");
|
||||
InstalledAppCacheUpdater.updateInForeground(getMockContext());
|
||||
|
||||
assertResultCount(3, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app1", 13, "v1.3");
|
||||
assertIsInstalledVersionInDb("com.example.app4", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.app10", 1, "v1");
|
||||
|
||||
}
|
||||
|
||||
}
|
168
test/src/org/fdroid/fdroid/InstalledAppProviderTest.java
Normal file
168
test/src/org/fdroid/fdroid/InstalledAppProviderTest.java
Normal file
@ -0,0 +1,168 @@
|
||||
package org.fdroid.fdroid;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import mock.MockInstallablePackageManager;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
import org.fdroid.fdroid.data.InstalledAppProvider;
|
||||
import org.fdroid.fdroid.data.RepoProvider;
|
||||
|
||||
public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppProvider> {
|
||||
|
||||
private MockInstallablePackageManager packageManager;
|
||||
|
||||
public InstalledAppProviderTest() {
|
||||
super(InstalledAppProvider.class, InstalledAppProvider.getAuthority());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
packageManager = new MockInstallablePackageManager();
|
||||
getSwappableContext().setPackageManager(packageManager);
|
||||
}
|
||||
|
||||
protected MockInstallablePackageManager getPackageManager() {
|
||||
return packageManager;
|
||||
}
|
||||
|
||||
public void testUris() {
|
||||
assertInvalidUri(InstalledAppProvider.getAuthority());
|
||||
assertInvalidUri(RepoProvider.getContentUri());
|
||||
assertInvalidUri(AppProvider.getContentUri());
|
||||
assertInvalidUri(ApkProvider.getContentUri());
|
||||
assertInvalidUri("blah");
|
||||
|
||||
assertValidUri(InstalledAppProvider.getContentUri());
|
||||
assertValidUri(InstalledAppProvider.getAppUri("com.example.com"));
|
||||
assertValidUri(InstalledAppProvider.getAppUri("blah"));
|
||||
}
|
||||
|
||||
public void testInsert() {
|
||||
|
||||
assertResultCount(0, InstalledAppProvider.getContentUri());
|
||||
|
||||
insertInstalledApp("com.example.com1", 1, "v1");
|
||||
insertInstalledApp("com.example.com2", 2, "v2");
|
||||
insertInstalledApp("com.example.com3", 3, "v3");
|
||||
|
||||
assertResultCount(3, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.com1", 1, "v1");
|
||||
assertIsInstalledVersionInDb("com.example.com2", 2, "v2");
|
||||
assertIsInstalledVersionInDb("com.example.com3", 3, "v3");
|
||||
}
|
||||
|
||||
public void testUpdate() {
|
||||
|
||||
insertInstalledApp("com.example.app1", 10, "1.0");
|
||||
insertInstalledApp("com.example.app2", 10, "1.0");
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app2", 10, "1.0");
|
||||
|
||||
getMockContentResolver().update(
|
||||
InstalledAppProvider.getAppUri("com.example.app2"),
|
||||
createContentValues(11, "1.1"),
|
||||
null, null
|
||||
);
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app2", 11, "1.1");
|
||||
|
||||
}
|
||||
|
||||
public void testDelete() {
|
||||
|
||||
insertInstalledApp("com.example.app1", 10, "1.0");
|
||||
insertInstalledApp("com.example.app2", 10, "1.0");
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
|
||||
getMockContentResolver().delete(InstalledAppProvider.getAppUri("com.example.app1"), null, null);
|
||||
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.app2", 10, "1.0");
|
||||
|
||||
}
|
||||
|
||||
public void testInsertWithBroadcast() {
|
||||
|
||||
installAndBroadcast("com.example.broadcasted1", 10, "v1.0");
|
||||
installAndBroadcast("com.example.broadcasted2", 105, "v1.05");
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.broadcasted1", 10, "v1.0");
|
||||
assertIsInstalledVersionInDb("com.example.broadcasted2", 105, "v1.05");
|
||||
}
|
||||
|
||||
public void testUpdateWithBroadcast() {
|
||||
|
||||
installAndBroadcast("com.example.toUpgrade", 1, "v0.1");
|
||||
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toUpgrade", 1, "v0.1");
|
||||
|
||||
upgradeAndBroadcast("com.example.toUpgrade", 2, "v0.2");
|
||||
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toUpgrade", 2, "v0.2");
|
||||
|
||||
}
|
||||
|
||||
public void testDeleteWithBroadcast() {
|
||||
|
||||
installAndBroadcast("com.example.toKeep", 1, "v0.1");
|
||||
installAndBroadcast("com.example.toDelete", 1, "v0.1");
|
||||
|
||||
assertResultCount(2, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1");
|
||||
assertIsInstalledVersionInDb("com.example.toDelete", 1, "v0.1");
|
||||
|
||||
removeAndBroadcast("com.example.toDelete");
|
||||
|
||||
assertResultCount(1, InstalledAppProvider.getContentUri());
|
||||
assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getMinimalProjection() {
|
||||
return new String[] {
|
||||
InstalledAppProvider.DataColumns.APP_ID,
|
||||
InstalledAppProvider.DataColumns.VERSION_CODE,
|
||||
InstalledAppProvider.DataColumns.VERSION_NAME,
|
||||
};
|
||||
}
|
||||
|
||||
private ContentValues createContentValues(int versionCode, String versionNumber) {
|
||||
return createContentValues(null, versionCode, versionNumber);
|
||||
}
|
||||
|
||||
private ContentValues createContentValues(String appId, int versionCode, String versionNumber) {
|
||||
ContentValues values = new ContentValues(3);
|
||||
if (appId != null) {
|
||||
values.put(InstalledAppProvider.DataColumns.APP_ID, appId);
|
||||
}
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, versionCode);
|
||||
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, versionNumber);
|
||||
return values;
|
||||
}
|
||||
|
||||
private void insertInstalledApp(String appId, int versionCode, String versionNumber) {
|
||||
ContentValues values = createContentValues(appId, versionCode, versionNumber);
|
||||
getMockContentResolver().insert(InstalledAppProvider.getContentUri(), values);
|
||||
}
|
||||
|
||||
private void removeAndBroadcast(String appId) {
|
||||
TestUtils.removeAndBroadcast(getMockContext(), getPackageManager(), appId);
|
||||
}
|
||||
|
||||
private void upgradeAndBroadcast(String appId, int versionCode, String versionName) {
|
||||
TestUtils.upgradeAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName);
|
||||
}
|
||||
|
||||
private void installAndBroadcast(String appId, int versionCode, String versionName) {
|
||||
TestUtils.installAndBroadcast(getMockContext(), getPackageManager(), appId, versionCode, versionName);
|
||||
}
|
||||
|
||||
}
|
@ -3,6 +3,7 @@ package org.fdroid.fdroid;
|
||||
import android.content.*;
|
||||
import android.net.Uri;
|
||||
import junit.framework.AssertionFailedError;
|
||||
import mock.MockInstallablePackageManager;
|
||||
import org.fdroid.fdroid.data.ApkProvider;
|
||||
import org.fdroid.fdroid.data.AppProvider;
|
||||
|
||||
@ -12,10 +13,22 @@ import java.util.List;
|
||||
|
||||
public class TestUtils {
|
||||
|
||||
public static <T extends Comparable> void assertContainsOnly(List<T> actualList, T[] expectedContains) {
|
||||
List<T> containsList = new ArrayList<T>(expectedContains.length);
|
||||
Collections.addAll(containsList, expectedContains);
|
||||
assertContainsOnly(actualList, containsList);
|
||||
public static <T extends Comparable> void assertContainsOnly(List<T> actualList, T[] expectedArray) {
|
||||
List<T> expectedList = new ArrayList<T>(expectedArray.length);
|
||||
Collections.addAll(expectedList, expectedArray);
|
||||
assertContainsOnly(actualList, expectedList);
|
||||
}
|
||||
|
||||
public static <T extends Comparable> void assertContainsOnly(T[] actualArray, List<T> expectedList) {
|
||||
List<T> actualList = new ArrayList<T>(actualArray.length);
|
||||
Collections.addAll(actualList, actualArray);
|
||||
assertContainsOnly(actualList, expectedList);
|
||||
}
|
||||
|
||||
public static <T extends Comparable> void assertContainsOnly(T[] actualArray, T[] expectedArray) {
|
||||
List<T> expectedList = new ArrayList<T>(expectedArray.length);
|
||||
Collections.addAll(expectedList, expectedArray);
|
||||
assertContainsOnly(actualArray, expectedList);
|
||||
}
|
||||
|
||||
public static <T> String listToString(List<T> list) {
|
||||
@ -60,6 +73,10 @@ public class TestUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void insertApp(ContentResolver resolver, String appId, String name) {
|
||||
insertApp(resolver, appId, name, new ContentValues());
|
||||
}
|
||||
|
||||
public static void insertApp(ContentResolver resolver, String id, String name, ContentValues additionalValues) {
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
@ -106,4 +123,56 @@ public class TestUtils {
|
||||
|
||||
return providerTest.getMockContentResolver().insert(uri, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will tell {@code pm} that we are installing {@code appId}, and then alert the
|
||||
* {@link org.fdroid.fdroid.PackageAddedReceiver}. This will in turn update the
|
||||
* "installed apps" table in the database.
|
||||
*
|
||||
* Note: in order for this to work, the {@link AppProviderTest#getSwappableContext()}
|
||||
* will need to be aware of the package manager that we have passed in. Therefore,
|
||||
* you will have to have called
|
||||
* {@link mock.MockContextSwappableComponents#setPackageManager(android.content.pm.PackageManager)}
|
||||
* on the {@link AppProviderTest#getSwappableContext()} before invoking this method.
|
||||
*/
|
||||
public static void installAndBroadcast(
|
||||
Context context, MockInstallablePackageManager pm,
|
||||
String appId, int versionCode, String versionName) {
|
||||
|
||||
pm.install(appId, versionCode, versionName);
|
||||
Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED);
|
||||
installIntent.setData(Uri.parse("package:" + appId));
|
||||
new PackageAddedReceiver().onReceive(context, installIntent);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String)
|
||||
*/
|
||||
public static void upgradeAndBroadcast(
|
||||
Context context, MockInstallablePackageManager pm,
|
||||
String appId, int versionCode, String versionName) {
|
||||
/*
|
||||
removeAndBroadcast(context, pm, appId);
|
||||
installAndBroadcast(context, pm, appId, versionCode, versionName);
|
||||
*/
|
||||
pm.install(appId, versionCode, versionName);
|
||||
Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
|
||||
installIntent.setData(Uri.parse("package:" + appId));
|
||||
new PackageUpgradedReceiver().onReceive(context, installIntent);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @see org.fdroid.fdroid.TestUtils#installAndBroadcast(android.content.Context context, mock.MockInstallablePackageManager, String, int, String)
|
||||
*/
|
||||
public static void removeAndBroadcast(Context context, MockInstallablePackageManager pm, String appId) {
|
||||
|
||||
pm.remove(appId);
|
||||
Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
|
||||
installIntent.setData(Uri.parse("package:" + appId));
|
||||
new PackageRemovedReceiver().onReceive(context, installIntent);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,16 +0,0 @@
|
||||
package org.fdroid.fdroid.mock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class MockInstalledApkCache extends Utils.InstalledApkCache {
|
||||
|
||||
@Override
|
||||
public Map<String, PackageInfo> getApks(Context context) {
|
||||
return buildAppList(context);
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user