InstalledAppProviderService to replace InstalledAppCacheUpdater

InstalledAppCacheUpdater was a custom Service-like thing with some
threading issues.  InstalledAppProviderService is an IntentService that
relies on the built-in queue and threading of the IntentService to make
sure that things are processed nicely in the background and one at a time.

This changes the announcing so that each app added/changed/deleted triggers
a new annoucement.  This keeps the UI more updated, and makes the Installed
tab show something as soon as possible, rather than waiting for the all of
the install apps to be processed.  This becomes more important as more
stuff is added to InstalledAppProvider, like the hash of the APK.

This also strips down and simplifies the related BroadcastReceivers.
BroadcastReceivers work on the UI thread, so they should do as little work
as possible. PackageManagerReceiver just rebadges the incoming Intent and
sends it off to InstalledAppProviderService for processing.
This commit is contained in:
Hans-Christoph Steiner 2016-05-27 16:51:35 +02:00
parent 677db72bb3
commit d734e584f6
15 changed files with 270 additions and 534 deletions

View File

@ -98,6 +98,11 @@ public abstract class ProviderTestCase2MockContext<T extends ContentProvider> ex
public Context getApplicationContext() {
return this;
}
@Override
public String getPackageName() {
return "org.fdroid.fdroid";
}
}
/**

View File

@ -5,6 +5,9 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import java.io.File;
import java.io.IOException;
@SuppressLint("ParcelCreator")
public class MockApplicationInfo extends ApplicationInfo {
@ -12,6 +15,11 @@ public class MockApplicationInfo extends ApplicationInfo {
public MockApplicationInfo(PackageInfo info) {
this.info = info;
try {
this.publicSourceDir = File.createTempFile(info.packageName, "apk").getAbsolutePath();
} catch (IOException e) {
this.publicSourceDir = "/data/app/" + info.packageName + "-4.apk";
}
}
@Override

View File

@ -37,6 +37,7 @@ public class MockInstallablePackageManager extends MockPackageManager {
p.packageName = id;
p.versionCode = version;
p.versionName = versionName;
p.applicationInfo = new MockApplicationInfo(p);
info.add(p);
}
}

View File

@ -4,7 +4,6 @@ import android.app.Instrumentation;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.support.annotation.Nullable;
@ -15,9 +14,6 @@ import junit.framework.AssertionFailedError;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.FDroidProviderTest;
import org.fdroid.fdroid.receiver.PackageAddedReceiver;
import org.fdroid.fdroid.receiver.PackageRemovedReceiver;
import org.fdroid.fdroid.receiver.PackageUpgradedReceiver;
import java.io.File;
import java.io.FileOutputStream;
@ -28,9 +24,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import mock.MockContextSwappableComponents;
import mock.MockInstallablePackageManager;
public class TestUtils {
private static final String TAG = "TestUtils";
@ -146,54 +139,6 @@ 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.receiver.PackageAddedReceiver}. This will in turn update the
* "installed apps" table in the database.
*/
public static void installAndBroadcast(MockContextSwappableComponents context,
MockInstallablePackageManager pm, String appId,
int versionCode, String versionName) {
context.setPackageManager(pm);
pm.install(appId, versionCode, versionName);
Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED);
installIntent.setData(Utils.getPackageUri(appId));
new PackageAddedReceiver().onReceive(context, installIntent);
}
/**
* @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) {
/*
removeAndBroadcast(context, pm, appId);
installAndBroadcast(context, pm, appId, versionCode, versionName);
*/
context.setPackageManager(pm);
pm.install(appId, versionCode, versionName);
Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED);
installIntent.setData(Utils.getPackageUri(appId));
new PackageUpgradedReceiver().onReceive(context, installIntent);
}
/**
* @see org.fdroid.fdroid.TestUtils#installAndBroadcast(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String)
*/
public static void removeAndBroadcast(MockContextSwappableComponents context, MockInstallablePackageManager pm, String appId) {
context.setPackageManager(pm);
pm.remove(appId);
Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED);
installIntent.setData(Utils.getPackageUri(appId));
new PackageRemovedReceiver().onReceive(context, installIntent);
}
@Nullable
public static File copyAssetToDir(Context context, String assetName, File directory) {
File tempFile;

View File

@ -2,6 +2,7 @@ package org.fdroid.fdroid.data;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.pm.PackageInfo;
import android.content.res.Resources;
import android.database.Cursor;
@ -56,24 +57,27 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
insertApp("com.example.app1000", "App 1000");
for (int i = 0; i < 50; i++) {
pm.install("com.example.app" + i, 1, "v" + 1);
String packageName = "com.example.app" + i;
pm.install(packageName, 1, "v" + 1);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo);
}
InstalledAppCacheUpdater.updateInForeground(getMockContext());
assertResultCount(1, AppProvider.getInstalledUri());
for (int i = 50; i < 500; i++) {
pm.install("com.example.app" + i, 1, "v" + 1);
String packageName = "com.example.app" + i;
pm.install(packageName, 1, "v" + 1);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo);
}
InstalledAppCacheUpdater.updateInForeground(getMockContext());
assertResultCount(2, AppProvider.getInstalledUri());
for (int i = 500; i < 1100; i++) {
pm.install("com.example.app" + i, 1, "v" + 1);
String packageName = "com.example.app" + i;
pm.install(packageName, 1, "v" + 1);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(getSwappableContext(), packageName, packageInfo);
}
InstalledAppCacheUpdater.updateInForeground(getMockContext());
assertResultCount(3, AppProvider.getInstalledUri());
}
@ -127,7 +131,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVercode);
insertApp(id, "App: " + id, values);
TestUtils.installAndBroadcast(getSwappableContext(), packageManager, id, installedVercode, "v" + installedVercode);
InstalledAppProviderTest.install(getSwappableContext(), packageManager, id, installedVercode, "v" + installedVercode);
}
public void testCanUpdate() {
@ -247,7 +251,7 @@ public class AppProviderTest extends FDroidProviderTest<AppProvider> {
assertResultCount(0, AppProvider.getInstalledUri());
for (int i = 10; i < 20; i++) {
TestUtils.installAndBroadcast(getSwappableContext(), pm, "com.example.test." + i, i, "v1");
InstalledAppProviderTest.install(getSwappableContext(), pm, "com.example.test." + i, i, "v1");
}
assertResultCount(10, AppProvider.getInstalledUri());

View File

@ -1,9 +1,9 @@
package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.content.pm.PackageInfo;
import org.fdroid.fdroid.TestUtils;
import mock.MockContextSwappableComponents;
import mock.MockInstallablePackageManager;
@SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics
@ -96,9 +96,8 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
}
public void testInsertWithBroadcast() {
installAndBroadcast("com.example.broadcasted1", 10, "v1.0");
installAndBroadcast("com.example.broadcasted2", 105, "v1.05");
install("com.example.broadcasted1", 10, "v1.0");
install("com.example.broadcasted2", 105, "v1.05");
assertResultCount(2, InstalledAppProvider.getContentUri());
assertIsInstalledVersionInDb("com.example.broadcasted1", 10, "v1.0");
@ -107,12 +106,12 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
public void testUpdateWithBroadcast() {
installAndBroadcast("com.example.toUpgrade", 1, "v0.1");
install("com.example.toUpgrade", 1, "v0.1");
assertResultCount(1, InstalledAppProvider.getContentUri());
assertIsInstalledVersionInDb("com.example.toUpgrade", 1, "v0.1");
upgradeAndBroadcast("com.example.toUpgrade", 2, "v0.2");
install("com.example.toUpgrade", 2, "v0.2");
assertResultCount(1, InstalledAppProvider.getContentUri());
assertIsInstalledVersionInDb("com.example.toUpgrade", 2, "v0.2");
@ -121,14 +120,14 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
public void testDeleteWithBroadcast() {
installAndBroadcast("com.example.toKeep", 1, "v0.1");
installAndBroadcast("com.example.toDelete", 1, "v0.1");
install("com.example.toKeep", 1, "v0.1");
install("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");
remove("com.example.toDelete");
assertResultCount(1, InstalledAppProvider.getContentUri());
assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1");
@ -165,16 +164,36 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro
getMockContentResolver().insert(InstalledAppProvider.getContentUri(), values);
}
private void removeAndBroadcast(String appId) {
TestUtils.removeAndBroadcast(getSwappableContext(), getPackageManager(), appId);
private void remove(String packageName) {
remove(getSwappableContext(), getPackageManager(), packageName);
}
private void upgradeAndBroadcast(String appId, int versionCode, String versionName) {
TestUtils.upgradeAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
private void install(String appId, int versionCode, String versionName) {
install(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
}
private void installAndBroadcast(String appId, int versionCode, String versionName) {
TestUtils.installAndBroadcast(getSwappableContext(), getPackageManager(), appId, versionCode, versionName);
/**
* Will tell {@code pm} that we are installing {@code packageName}, and then update the
* "installed apps" table in the database.
*/
public static void install(MockContextSwappableComponents context,
MockInstallablePackageManager pm, String packageName,
int versionCode, String versionName) {
context.setPackageManager(pm);
pm.install(packageName, versionCode, versionName);
PackageInfo packageInfo = pm.getPackageInfo(packageName, 0);
InstalledAppProviderService.insertAppIntoDb(context, packageName, packageInfo);
}
/**
* @see #install(mock.MockContextSwappableComponents, mock.MockInstallablePackageManager, String, int, String)
*/
public static void remove(MockContextSwappableComponents context, MockInstallablePackageManager pm, String packageName) {
context.setPackageManager(pm);
pm.remove(packageName);
InstalledAppProviderService.deleteAppFromDb(context, packageName);
}
}

View File

@ -423,22 +423,10 @@
<category android:name="android.intent.category.HOME" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.PackageAddedReceiver" >
<receiver android:name=".receiver.PackageManagerReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.PackageUpgradedReceiver" >
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.PackageRemovedReceiver" >
<intent-filter>
<action android:name="android.intent.action.PACKAGE_CHANGED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
@ -468,6 +456,9 @@
<service
android:name=".localrepo.CacheSwapAppsService"
android:exported="false" />
<service
android:name=".data.InstalledAppProviderService"
android:exported="false" />
</application>
</manifest>

View File

@ -55,7 +55,7 @@ import org.fdroid.fdroid.Preferences.ChangeListener;
import org.fdroid.fdroid.Preferences.Theme;
import org.fdroid.fdroid.compat.PRNGFixes;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledAppCacheUpdater;
import org.fdroid.fdroid.data.InstalledAppProviderService;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.net.IconDownloader;
import org.fdroid.fdroid.net.WifiStateChangeService;
@ -224,7 +224,7 @@ public class FDroidApp extends Application {
curTheme = Preferences.get().getTheme();
Preferences.get().configureProxy();
InstalledAppCacheUpdater.updateInBackground(getApplicationContext());
InstalledAppProviderService.compareToPackageManager(this);
// If the user changes the preference to do with filtering rooted apps,
// it is easier to just notify a change in the app provider,

View File

@ -1,191 +0,0 @@
package org.fdroid.fdroid.data;
import android.content.ContentProviderOperation;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.RemoteException;
import android.util.Log;
import org.fdroid.fdroid.Utils;
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 final class InstalledAppCacheUpdater {
private static final String TAG = "InstalledAppCache";
private final Context context;
private final List<PackageInfo> toInsert = new ArrayList<>();
private final List<String> toDelete = new ArrayList<>();
private 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.
* 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 {@link android.content.Intent}. Once completed,
* the relevant {@link android.content.ContentProvider}s will be notified of any changes to installed
* statuses. This method returns immediately, and will continue to work in an AsyncTask. It doesn't
* really matter where we put this in the bootstrap process, because it runs on a different thread,
* which will be delayed by some seconds to avoid an error where the database is locked due to the
* database updater.
*/
public static void updateInBackground(Context context) {
InstalledAppCacheUpdater updater = new InstalledAppCacheUpdater(context);
updater.startBackgroundWorker();
}
private boolean update() {
long startTime = System.currentTimeMillis();
compareCacheToPackageManager();
updateCache();
long duration = System.currentTimeMillis() - startTime;
Utils.debugLog(TAG, "Took " + duration + "ms to compare the installed app cache with PackageManager.");
return hasChanged();
}
private void notifyProviders() {
Utils.debugLog(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);
}
private void startBackgroundWorker() {
new PostponedWorker().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 || toDelete.size() > 0;
}
private void updateCache() {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
ops.addAll(deleteFromCache(toDelete));
ops.addAll(insertIntoCache(toInsert));
if (ops.size() > 0) {
try {
context.getContentResolver().applyBatch(InstalledAppProvider.getAuthority(), ops);
Utils.debugLog(TAG, "Finished executing " + ops.size() + " CRUD operations on installed app cache.");
} catch (RemoteException | 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(PackageManager.GET_SIGNATURES);
for (PackageInfo appInfo : installedPackages) {
toInsert.add(appInfo);
if (cachedInfo.containsKey(appInfo.packageName)) {
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<>(appsToInsert.size());
if (appsToInsert.size() > 0) {
Utils.debugLog(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.PACKAGE_NAME, info.packageName)
.withValue(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode)
.withValue(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName)
.withValue(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, info.packageName))
.withValue(InstalledAppProvider.DataColumns.SIGNATURE,
InstalledAppProvider.getPackageSig(info))
.build();
ops.add(op);
}
}
return ops;
}
private List<ContentProviderOperation> deleteFromCache(List<String> packageNames) {
List<ContentProviderOperation> ops = new ArrayList<>(packageNames.size());
if (packageNames.size() > 0) {
Utils.debugLog(TAG, "Preparing to remove " + packageNames.size() + " apps from the installed app cache.");
for (final String packageName : packageNames) {
Uri uri = InstalledAppProvider.getAppUri(packageName);
ops.add(ContentProviderOperation.newDelete(uri).build());
}
}
return ops;
}
/**
* Waits 5 seconds before beginning to update cache of installed apps.
* This is due to a bug where the database was locked as F-Droid was starting,
* which caused a crash.
*/
private class PostponedWorker extends AsyncTask<Void, Void, Boolean> {
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.sleep(10000);
} catch (InterruptedException ignored) { }
return update();
}
@Override
protected void onPostExecute(Boolean changed) {
if (changed) {
notifyProviders();
}
}
}
}

View File

@ -0,0 +1,163 @@
package org.fdroid.fdroid.data;
import android.app.IntentService;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Process;
import org.fdroid.fdroid.Utils;
import java.io.File;
import java.util.List;
import java.util.Map;
/**
* Handles all updates to {@link InstalledAppProvider}, whether checking the contents
* versus what Android says is installed, or processing {@link Intent}s that come
* from {@link android.content.BroadcastReceiver}s for {@link Intent#ACTION_PACKAGE_ADDED}
* and {@link Intent#ACTION_PACKAGE_REMOVED}
* <p>
* Since {@link android.content.ContentProvider#insert(Uri, ContentValues)} does not check
* for duplicate records, it is entirely the job of this service to ensure that it is not
* inserting duplicate versions of the same installed APK. On that note,
* {@link #insertAppIntoDb(Context, String, PackageInfo)} and
* {@link #deleteAppFromDb(Context, String)} are both static methods to enable easy testing
* of this stuff.
*/
public class InstalledAppProviderService extends IntentService {
private static final String TAG = "InstalledAppProviderSer";
private static final String ACTION_INSERT = "org.fdroid.fdroid.data.action.INSERT";
private static final String ACTION_DELETE = "org.fdroid.fdroid.data.action.DELETE";
private static final String EXTRA_PACKAGE_INFO = "org.fdroid.fdroid.data.extra.PACKAGE_INFO";
public InstalledAppProviderService() {
super("InstalledAppProviderService");
}
/**
* Inserts an app into {@link InstalledAppProvider} based on a {@code package:} {@link Uri}.
* This has no checks for whether it is inserting an exact duplicate, whatever is provided
* will be inserted.
*/
public static void insert(Context context, PackageInfo packageInfo) {
insert(context, Utils.getPackageUri(packageInfo.packageName), packageInfo);
}
/**
* Inserts an app into {@link InstalledAppProvider} based on a {@code package:} {@link Uri}.
* This has no checks for whether it is inserting an exact duplicate, whatever is provided
* will be inserted.
*/
public static void insert(Context context, Uri uri) {
insert(context, uri, null);
}
private static void insert(Context context, Uri uri, PackageInfo packageInfo) {
Intent intent = new Intent(context, InstalledAppProviderService.class);
intent.setAction(ACTION_INSERT);
intent.setData(uri);
intent.putExtra(EXTRA_PACKAGE_INFO, packageInfo);
context.startService(intent);
}
/**
* Deletes an app from {@link InstalledAppProvider} based on a {@code package:} {@link Uri}
*/
public static void delete(Context context, String packageName) {
delete(context, Utils.getPackageUri(packageName));
}
/**
* Deletes an app from {@link InstalledAppProvider} based on a {@code package:} {@link Uri}
*/
public static void delete(Context context, Uri uri) {
Intent intent = new Intent(context, InstalledAppProviderService.class);
intent.setAction(ACTION_DELETE);
intent.setData(uri);
context.startService(intent);
}
/**
* Make sure that {@link InstalledAppProvider}, our database of installed apps,
* is in sync with what the {@link PackageManager} tells us is installed. Once
* completed, the relevant {@link android.content.ContentProvider}s will be
* notified of any changes to installed statuses.
* <p>
* The installed app cache could get out of sync, e.g. if F-Droid crashed/ or
* ran out of battery half way through responding to {@link Intent#ACTION_PACKAGE_ADDED}.
* This method returns immediately, and will continue to work in an
* {@link IntentService}. It doesn't really matter where we put this in the
* bootstrap process, because it runs in its own thread, at the lowest priority:
* {@link Process#THREAD_PRIORITY_LOWEST}.
*/
public static void compareToPackageManager(Context context) {
Map<String, Integer> cachedInfo = InstalledAppProvider.Helper.all(context);
List<PackageInfo> packageInfoList = context.getPackageManager()
.getInstalledPackages(PackageManager.GET_SIGNATURES);
// TODO check packageInfo.lastUpdateTime for freshness
for (PackageInfo packageInfo : packageInfoList) {
insert(context, packageInfo);
if (cachedInfo.containsKey(packageInfo.packageName)) {
cachedInfo.remove(packageInfo.packageName);
}
}
for (String packageName : cachedInfo.keySet()) {
delete(context, packageName);
}
}
@Override
protected void onHandleIntent(Intent intent) {
Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST);
if (intent != null) {
String packageName = intent.getData().getSchemeSpecificPart();
final String action = intent.getAction();
if (ACTION_INSERT.equals(action)) {
insertAppIntoDb(this, packageName, (PackageInfo) intent.getParcelableExtra(EXTRA_PACKAGE_INFO));
} else if (ACTION_DELETE.equals(action)) {
deleteAppFromDb(this, packageName);
}
Utils.debugLog(TAG, "Notifying content providers (so they can update the relevant views).");
getContentResolver().notifyChange(AppProvider.getContentUri(), null);
getContentResolver().notifyChange(ApkProvider.getContentUri(), null);
}
}
static void insertAppIntoDb(Context context, String packageName, PackageInfo packageInfo) {
if (packageInfo == null) {
try {
packageInfo = context.getPackageManager().getPackageInfo(packageName,
PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return;
}
}
Uri uri = InstalledAppProvider.getContentUri();
ContentValues contentValues = new ContentValues();
contentValues.put(InstalledAppProvider.DataColumns.PACKAGE_NAME, packageInfo.packageName);
contentValues.put(InstalledAppProvider.DataColumns.VERSION_CODE, packageInfo.versionCode);
contentValues.put(InstalledAppProvider.DataColumns.VERSION_NAME, packageInfo.versionName);
contentValues.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, packageInfo.packageName));
contentValues.put(InstalledAppProvider.DataColumns.SIGNATURE,
InstalledAppProvider.getPackageSig(packageInfo));
context.getContentResolver().insert(uri, contentValues);
}
static void deleteAppFromDb(Context context, String packageName) {
Uri uri = InstalledAppProvider.getAppUri(packageName);
context.getContentResolver().delete(uri, null, null);
}
}

View File

@ -1,65 +0,0 @@
/*
* 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.receiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.net.Uri;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.InstalledAppProvider;
public class PackageAddedReceiver extends PackageReceiver {
private static final String TAG = "PackageAddedReceiver";
@Override
protected boolean toDiscard(Intent intent) {
if (intent.hasExtra(Intent.EXTRA_REPLACING)) {
Utils.debugLog(TAG, "Discarding since this PACKAGE_ADDED is just a PACKAGE_REPLACED");
return true;
}
return false;
}
@Override
protected void handle(Context context, String packageName) {
PackageInfo info = getPackageInfo(context, packageName);
if (info == null) {
Utils.debugLog(TAG, "Could not get package info on '" + packageName + "' - skipping.");
return;
}
Utils.debugLog(TAG, "Inserting installed app info for '" + packageName + "' (v" + info.versionCode + ")");
Uri uri = InstalledAppProvider.getContentUri();
ContentValues values = new ContentValues(4);
values.put(InstalledAppProvider.DataColumns.PACKAGE_NAME, packageName);
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, packageName));
values.put(InstalledAppProvider.DataColumns.SIGNATURE,
InstalledAppProvider.getPackageSig(info));
context.getContentResolver().insert(uri, values);
}
}

View File

@ -0,0 +1,35 @@
package org.fdroid.fdroid.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.InstalledAppProviderService;
/**
* Receive {@link Intent#ACTION_PACKAGE_ADDED} and {@link Intent#ACTION_PACKAGE_REMOVED}
* events from {@link android.content.pm.PackageManager} to keep
* {@link org.fdroid.fdroid.data.InstalledAppProvider} updated. This ignores
* {@link Intent#EXTRA_REPLACING} and instead handles updates by just deleting then
* inserting the app being updated in direct response to the {@code Intent}s from
* the system. This is also necessary because there are no other checks to prevent
* multiple copies of the same app being inserted into {@Link InstalledAppProvider}.
*/
public class PackageManagerReceiver extends BroadcastReceiver {
private static final String TAG = "PackageManagerReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null) {
String action = intent.getAction();
if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
InstalledAppProviderService.insert(context, intent.getData());
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
InstalledAppProviderService.delete(context, intent.getData());
} else {
Utils.debugLog(TAG, "unsupported action: " + action + " " + intent);
}
}
}
}

View File

@ -1,62 +0,0 @@
/*
* 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
* 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.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.AppProvider;
abstract class PackageReceiver extends BroadcastReceiver {
private static final String TAG = "PackageReceiver";
protected abstract boolean toDiscard(Intent intent);
protected abstract void handle(Context context, String packageName);
protected PackageInfo getPackageInfo(Context context, String packageName) {
PackageInfo info = null;
try {
info = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
} catch (PackageManager.NameNotFoundException e) {
// ignore
}
return info;
}
@Override
public void onReceive(Context context, Intent intent) {
Utils.debugLog(TAG, "PackageReceiver received [action = '" + intent.getAction() + "', data = '" + intent.getData() + "']");
if (toDiscard(intent)) {
return;
}
String packageName = intent.getData().getSchemeSpecificPart();
handle(context, packageName);
context.getContentResolver().notifyChange(AppProvider.getContentUri(packageName), null);
context.getContentResolver().notifyChange(ApkProvider.getAppUri(packageName), null);
}
}

View File

@ -1,50 +0,0 @@
/*
* 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.receiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.InstalledAppProvider;
public class PackageRemovedReceiver extends PackageReceiver {
private static final String TAG = "PackageRemovedReceiver";
@Override
protected boolean toDiscard(Intent intent) {
if (intent.hasExtra(Intent.EXTRA_REPLACING)) {
Utils.debugLog(TAG, "Discarding since this PACKAGE_REMOVED is just a PACKAGE_REPLACED");
return true;
}
return false;
}
@Override
protected void handle(Context context, String packageName) {
Utils.debugLog(TAG, "Removing installed app info for '" + packageName + "'");
Uri uri = InstalledAppProvider.getAppUri(packageName);
context.getContentResolver().delete(uri, null, null);
}
}

View File

@ -1,67 +0,0 @@
/*
* 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.receiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.net.Uri;
import org.fdroid.fdroid.Utils;
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 {
private static final String TAG = "PackageUpgradedReceiver";
@Override
protected boolean toDiscard(Intent intent) {
return false;
}
@Override
protected void handle(Context context, String packageName) {
PackageInfo info = getPackageInfo(context, packageName);
if (info == null) {
Utils.debugLog(TAG, "Could not get package info on '" + packageName + "' - skipping.");
return;
}
Utils.debugLog(TAG, "Updating installed app info for '" + packageName + "' to v" + info.versionCode + " (" + info.versionName + ")");
Uri uri = InstalledAppProvider.getContentUri();
ContentValues values = new ContentValues(4);
values.put(InstalledAppProvider.DataColumns.PACKAGE_NAME, packageName);
values.put(InstalledAppProvider.DataColumns.VERSION_CODE, info.versionCode);
values.put(InstalledAppProvider.DataColumns.VERSION_NAME, info.versionName);
values.put(InstalledAppProvider.DataColumns.APPLICATION_LABEL,
InstalledAppProvider.getApplicationLabel(context, packageName));
values.put(InstalledAppProvider.DataColumns.SIGNATURE,
InstalledAppProvider.getPackageSig(info));
context.getContentResolver().insert(uri, values);
}
}