From fa7f57a18addfd58e1cdb5d6c09793fdaeac3ff1 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Thu, 2 Jun 2016 14:44:23 +1000 Subject: [PATCH 1/6] Remove unused test code. Since refactoring the installed app cache stuff, these methods are no longer required for testing purposes. This is because the tests directly ask the content provider to insert relevant apps, rather than testing the broadcast receiving functionality. --- .../fdroid/data/InstalledAppProviderTest.java | 70 ------------------- 1 file changed, 70 deletions(-) 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 7c96e66e2..7da03fcd0 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java @@ -11,23 +11,10 @@ import mock.MockInstallablePackageManager; @SuppressWarnings("PMD") // TODO port this to JUnit 4 semantics 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()); @@ -130,45 +117,6 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro } - public void testInsertWithBroadcast() { - 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"); - assertIsInstalledVersionInDb("com.example.broadcasted2", 105, "v1.05"); - } - - public void testUpdateWithBroadcast() { - - install("com.example.toUpgrade", 1, "v0.1"); - - assertResultCount(1, InstalledAppProvider.getContentUri()); - assertIsInstalledVersionInDb("com.example.toUpgrade", 1, "v0.1"); - - install("com.example.toUpgrade", 2, "v0.2"); - - assertResultCount(1, InstalledAppProvider.getContentUri()); - assertIsInstalledVersionInDb("com.example.toUpgrade", 2, "v0.2"); - - } - - public void testDeleteWithBroadcast() { - - 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"); - - remove("com.example.toDelete"); - - assertResultCount(1, InstalledAppProvider.getContentUri()); - assertIsInstalledVersionInDb("com.example.toKeep", 1, "v0.1"); - - } - @Override protected String[] getMinimalProjection() { return new String[]{ @@ -202,14 +150,6 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro getMockContentResolver().insert(InstalledAppProvider.getContentUri(), values); } - private void remove(String packageName) { - remove(getSwappableContext(), getPackageManager(), packageName); - } - - private void install(String appId, int versionCode, String versionName) { - install(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. @@ -224,14 +164,4 @@ public class InstalledAppProviderTest extends FDroidProviderTest<InstalledAppPro 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); - } - } From 371312ef650c652c3c522e5cb5ccd17a7a88ab97 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Thu, 2 Jun 2016 14:24:04 +1000 Subject: [PATCH 2/6] Replace rate limiting code with RX. Now that we have RX as a dependency, it can be used as a nice concise way to achieve certain tasks. Rate limiting is one thing it does well - via the `debounce` mechanism: http://reactivex.io/documentation/operators/debounce.html The semantics of this code is the same as before, limiting content change notifications to one per second. --- .../data/InstalledAppProviderService.java | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index 3dc103bac..1a9ff48b8 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -14,10 +14,12 @@ import org.fdroid.fdroid.Utils; import java.io.File; import java.util.List; import java.util.Map; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import rx.functions.Action1; +import rx.schedulers.Schedulers; +import rx.subjects.PublishSubject; + /** * Handles all updates to {@link InstalledAppProvider}, whether checking the contents * versus what Android says is installed, or processing {@link Intent}s that come @@ -39,13 +41,34 @@ public class InstalledAppProviderService extends IntentService { private static final String EXTRA_PACKAGE_INFO = "org.fdroid.fdroid.data.extra.PACKAGE_INFO"; - private ScheduledExecutorService worker; - private boolean notifyChangeNeedsSending; + /** + * This is for notifing the users of this {@link android.content.ContentProvider} + * that the contents has changed. Since {@link Intent}s can come in slow + * or fast, and this can trigger a lot of UI updates, the actual + * notifications are rate limited to one per second. + */ + private PublishSubject<Void> notifyEvents; public InstalledAppProviderService() { super("InstalledAppProviderService"); } + @Override + public void onCreate() { + super.onCreate(); + notifyEvents = PublishSubject.create(); + notifyEvents.debounce(1, TimeUnit.SECONDS) + .subscribeOn(Schedulers.newThread()) + .subscribe(new Action1<Void>() { + @Override + public void call(Void voidArg) { + Utils.debugLog(TAG, "Notifying content providers (so they can update the relevant views)."); + getContentResolver().notifyChange(AppProvider.getContentUri(), null); + getContentResolver().notifyChange(ApkProvider.getContentUri(), null); + } + }); + } + /** * 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 @@ -134,7 +157,7 @@ public class InstalledAppProviderService extends IntentService { } else if (ACTION_DELETE.equals(action)) { deleteAppFromDb(this, packageName); } - notifyChange(); + notifyEvents.onNext(null); } } @@ -172,32 +195,4 @@ public class InstalledAppProviderService extends IntentService { Uri uri = InstalledAppProvider.getAppUri(packageName); context.getContentResolver().delete(uri, null, null); } - - /** - * This notifies the users of this {@link android.content.ContentProvider} - * that the contents has changed. Since {@link Intent}s can come in slow - * or fast, and this can trigger a lot of UI updates, the actual - * notifications are rate limited to one per second. - */ - private void notifyChange() { - notifyChangeNeedsSending = true; - Runnable task = new Runnable() { - @Override - public void run() { - if (notifyChangeNeedsSending) { - Utils.debugLog(TAG, "Notifying content providers (so they can update the relevant views)."); - getContentResolver().notifyChange(AppProvider.getContentUri(), null); - getContentResolver().notifyChange(ApkProvider.getContentUri(), null); - notifyChangeNeedsSending = false; - } else { - worker.shutdown(); - worker = null; - } - } - }; - if (worker == null || worker.isShutdown()) { - worker = Executors.newSingleThreadScheduledExecutor(); - worker.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS); - } - } } \ No newline at end of file From a74e951cdf2e3f79f6dcb67b2488bc23ae25216c Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Thu, 2 Jun 2016 14:46:58 +1000 Subject: [PATCH 3/6] Simplify code by creating the object when required rather than using singleton. This should not be a particularly expensive opperation,. Also, at time of writing it is only used in a background thread, and only used once in that thread (i.e. not in a loop or anything like that). --- .../fdroid/fdroid/localrepo/LocalRepoManager.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java index 3cf9b79b3..1e18438f7 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -319,16 +319,6 @@ public final class LocalRepoManager { * Helper class to aid in constructing index.xml file. */ public static final class IndexXmlBuilder { - - private static IndexXmlBuilder indexXmlBuilder; - - public static IndexXmlBuilder get() throws XmlPullParserException { - if (indexXmlBuilder == null) { - indexXmlBuilder = new IndexXmlBuilder(); - } - return indexXmlBuilder; - } - @NonNull private final XmlSerializer serializer; @@ -487,7 +477,7 @@ public final class LocalRepoManager { JarOutputStream jo = new JarOutputStream(bo); JarEntry je = new JarEntry("index.xml"); jo.putNextEntry(je); - IndexXmlBuilder.get().build(context, apps, jo); + new IndexXmlBuilder().build(context, apps, jo); jo.close(); bo.close(); From 7076bb767d69bc57fdc824df725592fa864334c9 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Thu, 2 Jun 2016 14:54:27 +1000 Subject: [PATCH 4/6] Clarify what needs to be passed into `parseApp` and make it private. The `parseApp` method was previously accepting an `Intent`, which could have been anything. Given it was only used once, this now pushed the creation of that `Intent` into the `parseApp` method, and also reduced the visibility of the method as it is only used once at time of writing. --- .../org/fdroid/fdroid/localrepo/CacheSwapAppsService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java index 63a1fa1c0..31d3c880c 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java @@ -37,7 +37,9 @@ public class CacheSwapAppsService extends IntentService { * Parse the locally installed APK for {@code packageName} and save its XML * to the APK XML cache. */ - public static void parseApp(Context context, Intent intent) { + private static void parseApp(Context context, String packageName) { + Intent intent = new Intent(); + intent.setData(Utils.getPackageUri(packageName)); intent.setClass(context, CacheSwapAppsService.class); intent.setAction(ACTION_PARSE_APP); context.startService(intent); @@ -57,9 +59,7 @@ public class CacheSwapAppsService extends IntentService { } if (!indexJarFile.exists() || FileUtils.isFileNewer(new File(applicationInfo.sourceDir), indexJarFile)) { - Intent intent = new Intent(); - intent.setData(Utils.getPackageUri(applicationInfo.packageName)); - parseApp(context, intent); + parseApp(context, applicationInfo.packageName); } } } From 2d90a484df1097c7d088b28d221429148f438530 Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Thu, 2 Jun 2016 20:41:00 +1000 Subject: [PATCH 5/6] Move method only used by `InstalledAppProviderService`. The method was only used here, so lets move the method here. May as well make it private too until somebody else comes up with a use case for it. --- .../fdroid/data/InstalledAppProvider.java | 18 --------------- .../data/InstalledAppProviderService.java | 22 +++++++++++++++++-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java index 166d838f0..17d9a86d5 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -6,17 +6,14 @@ import android.content.UriMatcher; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; -import android.content.pm.Signature; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.util.Log; -import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.Map; @@ -118,21 +115,6 @@ public class InstalledAppProvider extends FDroidProvider { return packageName; // all else fails, return packageName } - public static String getPackageSig(PackageInfo info) { - if (info == null || info.signatures == null || info.signatures.length < 1) { - return ""; - } - Signature sig = info.signatures[0]; - String sigHash = ""; - try { - Hasher hash = new Hasher("MD5", sig.toCharsString().getBytes()); - sigHash = hash.getHash(); - } catch (NoSuchAlgorithmException e) { - // ignore - } - return sigHash; - } - @Override protected String getTableName() { return DBHelper.TABLE_INSTALLED_APP; diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java index 1a9ff48b8..f1959596f 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -6,12 +6,15 @@ import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.pm.Signature; import android.net.Uri; import android.os.Process; +import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Utils; import java.io.File; +import java.security.NoSuchAlgorithmException; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -179,8 +182,7 @@ public class InstalledAppProviderService extends IntentService { 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)); + contentValues.put(InstalledAppProvider.DataColumns.SIGNATURE, getPackageSig(packageInfo)); contentValues.put(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME, packageInfo.lastUpdateTime); String hashType = "sha256"; @@ -195,4 +197,20 @@ public class InstalledAppProviderService extends IntentService { Uri uri = InstalledAppProvider.getAppUri(packageName); context.getContentResolver().delete(uri, null, null); } + + private static String getPackageSig(PackageInfo info) { + if (info == null || info.signatures == null || info.signatures.length < 1) { + return ""; + } + Signature sig = info.signatures[0]; + String sigHash = ""; + try { + Hasher hash = new Hasher("MD5", sig.toCharsString().getBytes()); + sigHash = hash.getHash(); + } catch (NoSuchAlgorithmException e) { + // ignore + } + return sigHash; + } + } \ No newline at end of file From 7c8ea5c5af29d1994506bfacb085d8130a34509f Mon Sep 17 00:00:00 2001 From: Peter Serwylo <peter@serwylo.com> Date: Thu, 2 Jun 2016 20:44:41 +1000 Subject: [PATCH 6/6] Prevent `InstalledAppProvider` from notifying about changes. Historically the providers were responsible for notifying about inserts/deletes for this table. However this is no longer the case with the new service responsible for throttling the rate with which these notifications occur. --- .../org/fdroid/fdroid/data/InstalledAppProvider.java | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java index 17d9a86d5..ffeb36f59 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -183,11 +183,7 @@ public class InstalledAppProvider extends FDroidProvider { QuerySelection query = new QuerySelection(where, whereArgs); query = query.add(queryApp(uri.getLastPathSegment())); - int count = db().delete(getTableName(), query.getSelection(), query.getArgs()); - if (!isApplyingBatch()) { - getContext().getContentResolver().notifyChange(uri, null); - } - return count; + return db().delete(getTableName(), query.getSelection(), query.getArgs()); } @Override @@ -199,9 +195,6 @@ public class InstalledAppProvider extends FDroidProvider { verifyVersionNameNotNull(values); db().replaceOrThrow(getTableName(), null, values); - if (!isApplyingBatch()) { - getContext().getContentResolver().notifyChange(uri, null); - } return getAppUri(values.getAsString(DataColumns.PACKAGE_NAME)); }