From d734e584f661e5074c32111a7d9a124ac906992e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 27 May 2016 16:51:35 +0200 Subject: [PATCH] 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. --- .../test/ProviderTestCase2MockContext.java | 5 + .../java/mock/MockApplicationInfo.java | 8 + .../mock/MockInstallablePackageManager.java | 1 + .../java/org/fdroid/fdroid/TestUtils.java | 55 ----- .../fdroid/fdroid/data/AppProviderTest.java | 26 ++- .../fdroid/data/InstalledAppProviderTest.java | 53 +++-- app/src/main/AndroidManifest.xml | 19 +- .../java/org/fdroid/fdroid/FDroidApp.java | 4 +- .../fdroid/data/InstalledAppCacheUpdater.java | 191 ------------------ .../data/InstalledAppProviderService.java | 163 +++++++++++++++ .../fdroid/receiver/PackageAddedReceiver.java | 65 ------ .../receiver/PackageManagerReceiver.java | 35 ++++ .../fdroid/receiver/PackageReceiver.java | 62 ------ .../receiver/PackageRemovedReceiver.java | 50 ----- .../receiver/PackageUpgradedReceiver.java | 67 ------ 15 files changed, 270 insertions(+), 534 deletions(-) delete mode 100644 app/src/main/java/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java create mode 100644 app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/receiver/PackageAddedReceiver.java create mode 100644 app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/receiver/PackageReceiver.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/receiver/PackageRemovedReceiver.java delete mode 100644 app/src/main/java/org/fdroid/fdroid/receiver/PackageUpgradedReceiver.java diff --git a/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java b/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java index 659fba2f3..405902649 100644 --- a/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java +++ b/app/src/androidTest/java/android/test/ProviderTestCase2MockContext.java @@ -98,6 +98,11 @@ public abstract class ProviderTestCase2MockContext ex public Context getApplicationContext() { return this; } + + @Override + public String getPackageName() { + return "org.fdroid.fdroid"; + } } /** diff --git a/app/src/androidTest/java/mock/MockApplicationInfo.java b/app/src/androidTest/java/mock/MockApplicationInfo.java index 4d88d3f9b..1ab4592a4 100644 --- a/app/src/androidTest/java/mock/MockApplicationInfo.java +++ b/app/src/androidTest/java/mock/MockApplicationInfo.java @@ -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 diff --git a/app/src/androidTest/java/mock/MockInstallablePackageManager.java b/app/src/androidTest/java/mock/MockInstallablePackageManager.java index b7056711f..4b95dcf60 100644 --- a/app/src/androidTest/java/mock/MockInstallablePackageManager.java +++ b/app/src/androidTest/java/mock/MockInstallablePackageManager.java @@ -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); } } diff --git a/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java b/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java index ab171366e..8b01eb630 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java @@ -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; diff --git a/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java b/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java index b19c1d819..454759c0e 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/data/AppProviderTest.java @@ -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 { 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 { 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 { 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()); diff --git a/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java b/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java index f36fcfed3..e06f7531d 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java @@ -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 - + - - - - - - - - - - - - - + @@ -468,6 +456,9 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 95e0ea708..41eda63e0 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -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, diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java deleted file mode 100644 index 1e86bc8c9..000000000 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppCacheUpdater.java +++ /dev/null @@ -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 toInsert = new ArrayList<>(); - private final List 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 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 cachedInfo = InstalledAppProvider.Helper.all(context); - - List 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 entry : cachedInfo.entrySet()) { - toDelete.add(entry.getKey()); - } - } - } - - private List insertIntoCache(List appsToInsert) { - List 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 deleteFromCache(List packageNames) { - List 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 { - - @Override - protected Boolean doInBackground(Void... params) { - try { - Thread.sleep(10000); - } catch (InterruptedException ignored) { } - return update(); - } - - @Override - protected void onPostExecute(Boolean changed) { - if (changed) { - notifyProviders(); - } - } - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java new file mode 100644 index 000000000..241356eee --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -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} + *

+ * 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. + *

+ * 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 cachedInfo = InstalledAppProvider.Helper.all(context); + + List 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageAddedReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageAddedReceiver.java deleted file mode 100644 index 849a2c1ed..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageAddedReceiver.java +++ /dev/null @@ -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); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java new file mode 100644 index 000000000..d9c44b19c --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/receiver/PackageManagerReceiver.java @@ -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); + } + } + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageReceiver.java deleted file mode 100644 index b8d368845..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageReceiver.java +++ /dev/null @@ -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); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageRemovedReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageRemovedReceiver.java deleted file mode 100644 index ab0c3827c..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageRemovedReceiver.java +++ /dev/null @@ -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); - } - -} diff --git a/app/src/main/java/org/fdroid/fdroid/receiver/PackageUpgradedReceiver.java b/app/src/main/java/org/fdroid/fdroid/receiver/PackageUpgradedReceiver.java deleted file mode 100644 index dcf914f84..000000000 --- a/app/src/main/java/org/fdroid/fdroid/receiver/PackageUpgradedReceiver.java +++ /dev/null @@ -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); - } - -}