From 3ec206b15285ff90745f2790dc12a26cf4fcdb3f Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Thu, 12 May 2016 21:11:41 +0200 Subject: [PATCH 01/10] simplify local repo XML writing and remove dead code This has a couple of things stuck in it that aren't really used at all, like maxage. --- .../fdroid/localrepo/LocalRepoManager.java | 39 +++++-------------- 1 file changed, 9 insertions(+), 30 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 9b49f0ba1..ab7044f16 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -1,7 +1,6 @@ package org.fdroid.fdroid.localrepo; import android.content.Context; -import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; @@ -11,7 +10,6 @@ import android.graphics.Bitmap.Config; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; @@ -65,9 +63,6 @@ import java.util.jar.JarOutputStream; public final class LocalRepoManager { private static final String TAG = "LocalRepoManager"; - // For ref, official F-droid repo presently uses a maxage of 14 days - private static final String DEFAULT_REPO_MAX_AGE_DAYS = "14"; - private final Context context; private final PackageManager pm; private final AssetManager assetManager; @@ -354,43 +349,27 @@ public final class LocalRepoManager { Writer output = new FileWriter(file); serializer.setOutput(output); serializer.startDocument(null, null); - tagFdroid(); - serializer.endDocument(); - output.close(); - } - - private void tagFdroid() throws IOException, LocalRepoKeyStore.InitException { serializer.startTag("", "fdroid"); - tagRepo(); - for (Map.Entry entry : apps.entrySet()) { - tagApplication(entry.getValue()); - } - serializer.endTag("", "fdroid"); - } - - private void tagRepo() throws IOException, LocalRepoKeyStore.InitException { - - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - - // max age is an EditTextPreference, which is always a String - // TODO: This pref is probably never being set. Also, why - // are we mixing floats and ints? - int repoMaxAge = Float.valueOf(prefs.getString("max_repo_age_days", DEFAULT_REPO_MAX_AGE_DAYS)).intValue(); // NOPMD + // block serializer.startTag("", "repo"); - serializer.attribute("", "icon", "blah.png"); - serializer.attribute("", "maxage", String.valueOf(repoMaxAge)); serializer.attribute("", "name", Preferences.get().getLocalRepoName() + " on " + FDroidApp.ipAddressString); serializer.attribute("", "pubkey", Hasher.hex(LocalRepoKeyStore.get(context).getCertificate())); long timestamp = System.currentTimeMillis() / 1000L; serializer.attribute("", "timestamp", String.valueOf(timestamp)); serializer.attribute("", "version", "10"); - tag("description", "A local FDroid repo generated from apps installed on " + Preferences.get().getLocalRepoName()); - serializer.endTag("", "repo"); + // blocks + for (Map.Entry entry : apps.entrySet()) { + tagApplication(entry.getValue()); + } + + serializer.endTag("", "fdroid"); + serializer.endDocument(); + output.close(); } /** From 944d355e293b2005e615be263957fd6e1bdceaa8 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 May 2016 16:15:41 +0200 Subject: [PATCH 02/10] swap: skip writing index.xml, output straight to index.jar This makes the index generation noticeably faster. This also converts IndexXmlBuilder to a singleton, since that's how it is used. --- .../fdroid/localrepo/LocalRepoManager.java | 68 ++++++------------- 1 file changed, 20 insertions(+), 48 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 ab7044f16..32978983b 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -14,12 +14,10 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; -import android.widget.Toast; import org.fdroid.fdroid.FDroidApp; import org.fdroid.fdroid.Hasher; import org.fdroid.fdroid.Preferences; -import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; @@ -28,20 +26,16 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; -import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.io.Writer; import java.security.cert.CertificateEncodingException; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -69,14 +63,13 @@ public final class LocalRepoManager { private final String fdroidPackageName; private static final String[] WEB_ROOT_ASSET_FILES = { - "swap-icon.png", - "swap-tick-done.png", - "swap-tick-not-done.png", + "swap-icon.png", + "swap-tick-done.png", + "swap-tick-not-done.png", }; private final Map apps = new HashMap<>(); - private final SanitizedFile xmlIndex; private final SanitizedFile xmlIndexJar; private final SanitizedFile xmlIndexJarUnsigned; private final SanitizedFile webRoot; @@ -110,7 +103,6 @@ public final class LocalRepoManager { repoDir = new SanitizedFile(fdroidDir, "repo"); repoDirCaps = new SanitizedFile(fdroidDirCaps, "REPO"); iconsDir = new SanitizedFile(repoDir, "icons"); - xmlIndex = new SanitizedFile(repoDir, "index.xml"); xmlIndexJar = new SanitizedFile(repoDir, "index.jar"); xmlIndexJarUnsigned = new SanitizedFile(repoDir, "index.unsigned.jar"); @@ -322,32 +314,30 @@ public final class LocalRepoManager { /** * Helper class to aid in constructing index.xml file. - * It uses the PullParser API, because the DOM api is only able to be serialized from - * API 8 upwards, but we support 7 at time of implementation. */ - public static class IndexXmlBuilder { + 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; - @NonNull - private final Map apps; - - @NonNull - private final Context context; - @NonNull private final DateFormat dateToStr = new SimpleDateFormat("yyyy-MM-dd", Locale.US); - public IndexXmlBuilder(@NonNull Context context, @NonNull Map apps) throws XmlPullParserException, IOException { - this.context = context; - this.apps = apps; + private IndexXmlBuilder() throws XmlPullParserException { serializer = XmlPullParserFactory.newInstance().newSerializer(); } - public void build(File file) throws IOException, LocalRepoKeyStore.InitException { - Writer output = new FileWriter(file); - serializer.setOutput(output); + public void build(Context context, Map apps, OutputStream output) throws IOException, LocalRepoKeyStore.InitException { + serializer.setOutput(output, "UTF-8"); serializer.startDocument(null, null); serializer.startTag("", "fdroid"); @@ -383,6 +373,7 @@ public final class LocalRepoManager { /** * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)} * That accepts a number instead of string. + * * @see IndexXmlBuilder#tag(String, String) */ private void tag(String name, long number) throws IOException { @@ -392,6 +383,7 @@ public final class LocalRepoManager { /** * Alias for {@link org.fdroid.fdroid.localrepo.LocalRepoManager.IndexXmlBuilder#tag(String, String)} * that accepts a date instead of a string. + * * @see IndexXmlBuilder#tag(String, String) */ private void tag(String name, Date date) throws IOException { @@ -487,32 +479,12 @@ public final class LocalRepoManager { } } - public void writeIndexJar() throws IOException { - - try { - new IndexXmlBuilder(context, apps).build(xmlIndex); - } catch (Exception e) { - Log.e(TAG, "Could not write index jar", e); - Toast.makeText(context, R.string.failed_to_create_index, Toast.LENGTH_LONG).show(); - return; - } - + public void writeIndexJar() throws IOException, XmlPullParserException, LocalRepoKeyStore.InitException { BufferedOutputStream bo = new BufferedOutputStream(new FileOutputStream(xmlIndexJarUnsigned)); JarOutputStream jo = new JarOutputStream(bo); - - BufferedInputStream bi = new BufferedInputStream(new FileInputStream(xmlIndex)); - JarEntry je = new JarEntry("index.xml"); jo.putNextEntry(je); - - byte[] buf = new byte[1024]; - int bytesRead; - - while ((bytesRead = bi.read(buf)) != -1) { - jo.write(buf, 0, bytesRead); - } - - bi.close(); + IndexXmlBuilder.get().build(context, apps, jo); jo.close(); bo.close(); From 335be87cf8646645b25cf76e050d5ed0f76fc054 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 27 May 2016 23:02:54 +0200 Subject: [PATCH 03/10] new CacheSwapAppsService for caching parsed apps to be swapped Since it takes a chunk of time to generate and write the app index.jar when swapping apps, this service starts running in the background immediately when SwapService starts. It first indexes the installed apps that were not cached, then caches apps based PACKAGE_ADDED broadcasts. It does not index system apps, since there are many and they are rarely swapped. --- app/src/main/AndroidManifest.xml | 3 + .../main/java/org/fdroid/fdroid/data/App.java | 2 + .../localrepo/CacheSwapAppsService.java | 84 +++++++++++++++++++ .../fdroid/localrepo/LocalRepoManager.java | 5 +- .../fdroid/fdroid/localrepo/SwapService.java | 34 ++++---- 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9fb2fe575..c1c25f546 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -465,6 +465,9 @@ + diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 6dc0f496b..5e5356cd5 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -309,6 +309,8 @@ public class App extends ValueObject implements Comparable { this.name = (String) appInfo.loadLabel(pm); this.icon = getIconName(packageName, packageInfo.versionCode); + this.installedVersionName = packageInfo.versionName; + this.installedVersionCode = packageInfo.versionCode; this.compatible = true; } diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java new file mode 100644 index 000000000..a3674f9d9 --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java @@ -0,0 +1,84 @@ +package org.fdroid.fdroid.localrepo; + +import android.app.IntentService; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; + +import org.apache.commons.io.FileUtils; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.App; + +import java.io.File; +import java.io.IOException; +import java.security.cert.CertificateEncodingException; + +/** + * An {@link IntentService} subclass for generating cached info about the installed APKs + * which are available for swapping. It does not cache system apps, since those are + * rarely swapped. This is meant to start running when {@link SwapService} starts. + *

+ * This could probably be replaced by {@link org.fdroid.fdroid.data.InstalledAppProvider} + * if that contained all of the info to generate complete {@link App} and + * {@link org.fdroid.fdroid.data.Apk} instances. + */ +public class CacheSwapAppsService extends IntentService { + public static final String TAG = "CacheSwapAppsService"; + + private static final String ACTION_PARSE_APP = "org.fdroid.fdroid.localrepo.action.PARSE_APP"; + + public CacheSwapAppsService() { + super("CacheSwapAppsService"); + } + + /** + * 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) { + intent.setClass(context, CacheSwapAppsService.class); + intent.setAction(ACTION_PARSE_APP); + context.startService(intent); + } + + /** + * Parse all of the locally installed APKs into a memory cache, starting + * with the currently selected apps. APKs that are already parsed in the + * {@code index.jar} file will be read from that file. + */ + public static void startCaching(Context context) { + File indexJarFile = LocalRepoManager.get(context).getIndexJar(); + PackageManager pm = context.getPackageManager(); + for (ApplicationInfo applicationInfo : pm.getInstalledApplications(0)) { + if (applicationInfo.publicSourceDir.startsWith(FDroidApp.SYSTEM_DIR_NAME)) { + continue; + } + if (!indexJarFile.exists() + || FileUtils.isFileNewer(new File(applicationInfo.sourceDir), indexJarFile)) { + Intent intent = new Intent(); + intent.setData(Uri.parse("package:" + applicationInfo.packageName)); + parseApp(context, intent); + } + } + } + + @Override + protected void onHandleIntent(Intent intent) { + android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_LOWEST); + if (intent == null || !ACTION_PARSE_APP.equals(intent.getAction())) { + Utils.debugLog(TAG, "received bad Intent: " + intent); + return; + } + + try { + PackageManager pm = getPackageManager(); + String packageName = intent.getData().getSchemeSpecificPart(); + App app = new App(this, pm, packageName); + SwapService.putAppInCache(packageName, app); + } catch (CertificateEncodingException | IOException | PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + } +} 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 32978983b..3cf9b79b3 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/LocalRepoManager.java @@ -257,7 +257,10 @@ public final class LocalRepoManager { public void addApp(Context context, String packageName) { App app; try { - app = new App(context.getApplicationContext(), pm, packageName); + app = SwapService.getAppFromCache(packageName); + if (app == null) { + app = new App(context.getApplicationContext(), pm, packageName); + } if (!app.isValid()) { return; } diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index fcadd5176..8414b05d8 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -31,6 +31,7 @@ import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.UpdateService; import org.fdroid.fdroid.Utils; +import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Repo; import org.fdroid.fdroid.data.RepoProvider; import org.fdroid.fdroid.localrepo.peers.Peer; @@ -52,6 +53,7 @@ import java.util.List; import java.util.Set; import java.util.Timer; import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; import rx.Observable; import rx.Subscription; @@ -61,23 +63,6 @@ import rx.schedulers.Schedulers; /** * Central service which manages all of the different moving parts of swap which are required * to enable p2p swapping of apps. - * - * The following UI elements don't do anything: - * + TODO: Be notified of changes to wifi state correctly, particularly from the WiFi AP (https://github.com/mvdan/accesspoint/issues/5) - * + TODO: The "?" button in the top right of the swap start screen doesn't do anything - * (This has been commented out for now, but it is still preferable to have a working help mechanism) - * - * TODO: Show "Waiting for other device to finish setting up swap" when only F-Droid shown in swap - * TODO: Handle "not connected to wifi" more gracefully. For example, Bonjour discovery falls over. - * TODO: When unable to reach the swap repo, but viewing apps to swap, show relevant feedback when attempting to download and install. - * TODO: Remove peers from list of peers when no longer "visible". - * TODO: Feedback for "Setting up (wifi|bluetooth)" in start swap view is not as immediate as I had hoped. - * TODO: Turn off bluetooth after cancelling/timing out if we turned it on. - * TODO: Disable the Scan QR button unless visible via something. Could equally show relevant feedback. - * - * TODO: Starting wifi after cancelling swap and beginning again doesn't work properly - * TODO: Scan QR hangs when updating repoo. Swapper was 2.3.3 and Swappee was 5.0 - * TODO: Showing the progress bar during install doesn't work when the view is inflated again, or when the adapter is scrolled off screen and back again. */ public class SwapService extends Service { @@ -90,6 +75,19 @@ public class SwapService extends Service { @NonNull private final Set appsToSwap = new HashSet<>(); + /** + * A cache of parsed APKs from the file system. + */ + private static final ConcurrentHashMap INSTALLED_APPS = new ConcurrentHashMap<>(); + + static App getAppFromCache(String packageName) { + return INSTALLED_APPS.get(packageName); + } + + static void putAppInCache(String packageName, App app) { + INSTALLED_APPS.put(packageName, app); + } + /** * Where relevant, the state of the swap process will be saved to disk using preferences. * Note that this is not always useful, for example saving the "current wifi network" is @@ -516,6 +514,8 @@ public class SwapService extends Service { Utils.debugLog(TAG, "Creating swap service."); + CacheSwapAppsService.startCaching(this); + SharedPreferences preferences = getSharedPreferences(SHARED_PREFERENCES, Context.MODE_PRIVATE); appsToSwap.addAll(deserializePackages(preferences.getString(KEY_APPS_TO_SWAP, ""))); From ae3ea853550ae20604b749cdb63de13a4268a73a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 18 May 2016 17:04:09 +0200 Subject: [PATCH 04/10] SwapService should be running as long as anything swap is active The SwapService is the central container for all things swap. If anything at all related to swap is active, then SwapService needs to be running. That also means that stopping SwapService should stop all things related to swapping, including any screens or notifications. fixes #258 https://gitlab.com/fdroid/fdroidclient/issues/258 --- .../fdroid/fdroid/localrepo/SwapService.java | 97 ++++++------------- .../views/swap/SwapWorkflowActivity.java | 6 +- 2 files changed, 31 insertions(+), 72 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java index 8414b05d8..6f1eb4b47 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/SwapService.java @@ -80,6 +80,11 @@ public class SwapService extends Service { */ private static final ConcurrentHashMap INSTALLED_APPS = new ConcurrentHashMap<>(); + public static void stop(Context context) { + Intent intent = new Intent(context, SwapService.class); + context.stopService(intent); + } + static App getAppFromCache(String packageName) { return INSTALLED_APPS.get(packageName); } @@ -384,14 +389,17 @@ public class SwapService extends Service { // Remember which swap technologies a user used in the past // ============================================================= - private void persistPreferredSwapTypes() { - Utils.debugLog(TAG, "Remembering that Bluetooth swap " + (bluetoothSwap.isConnected() ? "IS" : "is NOT") + - " connected and WiFi swap " + (wifiSwap.isConnected() ? "IS" : "is NOT") + " connected."); - persistence().edit() - .putBoolean(KEY_BLUETOOTH_ENABLED, bluetoothSwap.isConnected()) - .putBoolean(KEY_WIFI_ENABLED, wifiSwap.isConnected()) - .commit(); - } + private final BroadcastReceiver receiveSwapStatusChanged = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + Utils.debugLog(TAG, "Remembering that Bluetooth swap " + (bluetoothSwap.isConnected() ? "IS" : "is NOT") + + " connected and WiFi swap " + (wifiSwap.isConnected() ? "IS" : "is NOT") + " connected."); + persistence().edit() + .putBoolean(KEY_BLUETOOTH_ENABLED, bluetoothSwap.isConnected()) + .putBoolean(KEY_WIFI_ENABLED, wifiSwap.isConnected()) + .commit(); + } + }; /* private boolean wasBluetoothEnabled() { @@ -403,32 +411,6 @@ public class SwapService extends Service { return persistence().getBoolean(KEY_WIFI_ENABLED, false); } - // ========================================== - // Local repo stop/start/restart handling - // ========================================== - - /** - * Moves the service to the forground and [re]starts the timeout timer. - */ - private void attachService() { - Utils.debugLog(TAG, "Moving SwapService to foreground so that it hangs around even when F-Droid is closed (may already be foregrounded)."); - startForeground(NOTIFICATION, createNotification()); - - // Regardless of whether it was previously enabled, start the timer again. This ensures that - // if, e.g. a person views the swap activity again, it will attempt to enable swapping if - // appropriate, and thus restart this timer. - initTimer(); - } - - private void detachService() { - if (timer != null) { - timer.cancel(); - } - - Utils.debugLog(TAG, "Moving SwapService to background so that it can be GC'ed if required."); - stopForeground(true); - } - /** * Handles checking if the {@link SwapService} is running, and only restarts it if it was running. */ @@ -513,6 +495,7 @@ public class SwapService extends Service { super.onCreate(); Utils.debugLog(TAG, "Creating swap service."); + startForeground(NOTIFICATION, createNotification()); CacheSwapAppsService.startCaching(this); @@ -545,55 +528,33 @@ public class SwapService extends Service { } } - /** - * Responsible for moving the service into the foreground or the background, depending on - * whether or not there are any swap services (i.e. bluetooth or wifi) running or not. - */ - private final BroadcastReceiver receiveSwapStatusChanged = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (intent.hasExtra(EXTRA_STARTED)) { - if (getWifiSwap().isConnected() || getBluetoothSwap().isConnected()) { - attachService(); - } - } else if (intent.hasExtra(EXTRA_STOPPED)) { - if (!getWifiSwap().isConnected() && !getBluetoothSwap().isConnected()) { - detachService(); - } - } - persistPreferredSwapTypes(); - } - }; - @Override public int onStartCommand(Intent intent, int flags, int startId) { - return START_STICKY; } @Override public IBinder onBind(Intent intent) { + // reset the timer on each new connect, the user has come back + initTimer(); return binder; } - public void disableAllSwapping() { - Log.i(TAG, "Asked to stop swapping, will stop bluetooth, wifi, and move service to BG for GC."); - //getBluetoothSwap().stopInBackground(); - getWifiSwap().stopInBackground(); - - // Ensure the user is sent back go the first screen when returning if we have just forceably - // cancelled all swapping. - setStep(STEP_INTRO); - detachService(); - } - @Override public void onDestroy() { Utils.debugLog(TAG, "Destroying service, will disable swapping if required, and unregister listeners."); - disableAllSwapping(); Preferences.get().unregisterLocalRepoHttpsListeners(httpsEnabledListener); LocalBroadcastManager.getInstance(this).unregisterReceiver(onWifiChange); LocalBroadcastManager.getInstance(this).unregisterReceiver(receiveSwapStatusChanged); + + //TODO getBluetoothSwap().stopInBackground(); + getWifiSwap().stopInBackground(); + + if (timer != null) { + timer.cancel(); + } + stopForeground(true); + super.onDestroy(); } @@ -621,7 +582,7 @@ public class SwapService extends Service { @Override public void run() { Utils.debugLog(TAG, "Disabling swap because " + TIMEOUT + "ms passed."); - disableAllSwapping(); + stop(SwapService.this); } }, TIMEOUT); } diff --git a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java index 72a8eca54..fc760bda7 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java +++ b/app/src/main/java/org/fdroid/fdroid/views/swap/SwapWorkflowActivity.java @@ -151,9 +151,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { @Override public void onBackPressed() { if (currentView.getStep() == SwapService.STEP_INTRO) { - if (service != null) { - service.disableAllSwapping(); - } + SwapService.stop(this); finish(); } else { int nextStep = currentView.getPreviousStep(); @@ -359,7 +357,7 @@ public class SwapWorkflowActivity extends AppCompatActivity { } private void onToolbarCancel() { - getService().disableAllSwapping(); + SwapService.stop(this); finish(); } From 677db72bb3dc9d91f0f960509d848b66255238c4 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 25 May 2016 13:00:00 +0200 Subject: [PATCH 05/10] Utils.getPackageUri() for creating Uris from packageNames Since this is done a lot, might as well have a reusable method. --- app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java | 6 +++--- app/src/main/java/org/fdroid/fdroid/Utils.java | 7 +++++++ .../org/fdroid/fdroid/localrepo/CacheSwapAppsService.java | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java b/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java index 21a49518c..ab171366e 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/TestUtils.java @@ -158,7 +158,7 @@ public class TestUtils { context.setPackageManager(pm); pm.install(appId, versionCode, versionName); Intent installIntent = new Intent(Intent.ACTION_PACKAGE_ADDED); - installIntent.setData(Uri.parse("package:" + appId)); + installIntent.setData(Utils.getPackageUri(appId)); new PackageAddedReceiver().onReceive(context, installIntent); } @@ -176,7 +176,7 @@ public class TestUtils { context.setPackageManager(pm); pm.install(appId, versionCode, versionName); Intent installIntent = new Intent(Intent.ACTION_PACKAGE_CHANGED); - installIntent.setData(Uri.parse("package:" + appId)); + installIntent.setData(Utils.getPackageUri(appId)); new PackageUpgradedReceiver().onReceive(context, installIntent); } @@ -189,7 +189,7 @@ public class TestUtils { context.setPackageManager(pm); pm.remove(appId); Intent installIntent = new Intent(Intent.ACTION_PACKAGE_REMOVED); - installIntent.setData(Uri.parse("package:" + appId)); + installIntent.setData(Utils.getPackageUri(appId)); new PackageRemovedReceiver().onReceive(context, installIntent); } diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java index 73b81d805..619e63bbc 100644 --- a/app/src/main/java/org/fdroid/fdroid/Utils.java +++ b/app/src/main/java/org/fdroid/fdroid/Utils.java @@ -267,6 +267,13 @@ public final class Utils { return b.build(); } + /** + * Create a standard {@link PackageManager} {@link Uri} for pointing to an app. + */ + public static Uri getPackageUri(String packageName) { + return Uri.parse("package:" + packageName); + } + /** * This location is only for caching, do not install directly from this location * because if the file is on the External Storage, any other app could swap out 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 a3674f9d9..63a1fa1c0 100644 --- a/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java +++ b/app/src/main/java/org/fdroid/fdroid/localrepo/CacheSwapAppsService.java @@ -58,7 +58,7 @@ public class CacheSwapAppsService extends IntentService { if (!indexJarFile.exists() || FileUtils.isFileNewer(new File(applicationInfo.sourceDir), indexJarFile)) { Intent intent = new Intent(); - intent.setData(Uri.parse("package:" + applicationInfo.packageName)); + intent.setData(Utils.getPackageUri(applicationInfo.packageName)); parseApp(context, intent); } } From d734e584f661e5074c32111a7d9a124ac906992e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 27 May 2016 16:51:35 +0200 Subject: [PATCH 06/10] 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); - } - -} From 906a26414ae808d1c32d31c728efffa2610bb50e Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 27 May 2016 16:33:49 +0200 Subject: [PATCH 07/10] rate limit InstallApp install/delete notifications to 1000ms InstallAppProviderService now processes install and delete events one at a time, where InstalledAppCacheUpdater made a batch of changes which it ran all at once. This means that InstallAppProviderService will send out a flood of notifications when first initializing, since it will index every single installed app and send a notification for each one. This makes the GUI lock up. This commit puts a rate limit on those notifications if they start coming fast. They are limited to one per second. --- .../data/InstalledAppProviderService.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 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 241356eee..175a78976 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -14,6 +14,9 @@ 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; /** * Handles all updates to {@link InstalledAppProvider}, whether checking the contents @@ -36,6 +39,9 @@ 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; + public InstalledAppProviderService() { super("InstalledAppProviderService"); } @@ -125,10 +131,7 @@ public class InstalledAppProviderService extends IntentService { } 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); + notifyChange(); } } @@ -160,4 +163,32 @@ 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 90467bf8bf2f8e4a46cb1db563154df4035bf746 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 27 May 2016 16:52:54 +0200 Subject: [PATCH 08/10] InstalledAppProvider: store APK hash and last update time The APK hash is useful for comparing whether something is exactly the same file as something else. For example, to compare whether the installed APK matches something that f-droid.org hosts. The "last update time" is a fast way to check whether the information is current. --- .../mock/MockInstallablePackageManager.java | 1 + .../fdroid/data/InstalledAppProviderTest.java | 38 +++++++++++++++++++ .../java/org/fdroid/fdroid/data/DBHelper.java | 32 ++++++++-------- .../fdroid/data/InstalledAppProvider.java | 8 +++- .../data/InstalledAppProviderService.java | 6 +++ 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/app/src/androidTest/java/mock/MockInstallablePackageManager.java b/app/src/androidTest/java/mock/MockInstallablePackageManager.java index 4b95dcf60..1fcef1e86 100644 --- a/app/src/androidTest/java/mock/MockInstallablePackageManager.java +++ b/app/src/androidTest/java/mock/MockInstallablePackageManager.java @@ -38,6 +38,7 @@ public class MockInstallablePackageManager extends MockPackageManager { p.versionCode = version; p.versionName = versionName; p.applicationInfo = new MockApplicationInfo(p); + p.lastUpdateTime = System.currentTimeMillis(); info.add(p); } } 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 e06f7531d..7c96e66e2 100644 --- a/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java +++ b/app/src/androidTest/java/org/fdroid/fdroid/data/InstalledAppProviderTest.java @@ -2,6 +2,8 @@ package org.fdroid.fdroid.data; import android.content.ContentValues; import android.content.pm.PackageInfo; +import android.database.Cursor; +import android.net.Uri; import mock.MockContextSwappableComponents; import mock.MockInstallablePackageManager; @@ -81,6 +83,39 @@ public class InstalledAppProviderTest extends FDroidProviderTest 0); + assertTrue(lastUpdateTime < System.currentTimeMillis()); + cursor.close(); + + insertInstalledApp(packageName, 11, "1.1"); + cursor = getMockContentResolver().query(uri, projection, null, null, null); + assertNotNull(cursor); + assertEquals("App \"" + packageName + "\" not installed", 1, cursor.getCount()); + cursor.moveToFirst(); + assertTrue(lastUpdateTime < cursor.getLong(cursor.getColumnIndex(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME))); + cursor.close(); + } + public void testDelete() { insertInstalledApp("com.example.app1", 10, "1.0"); @@ -156,6 +191,9 @@ public class InstalledAppProviderTest extends FDroidProviderTest= 51) { + /** + * If any column was added or removed, just drop the table, create it again + * and let the cache be filled from scratch by {@link InstalledAppProviderService} + * For DB versions older than 43, this will create the {@link InstalledAppProvider} + * table for the first time. + */ + private void recreateInstalledAppTable(SQLiteDatabase db, int oldVersion) { + if (oldVersion >= 57) { return; } + Utils.debugLog(TAG, "(re)creating 'installed app' database table."); db.execSQL(DROP_TABLE_INSTALLED_APP); - createInstalledApp(db); + db.execSQL(CREATE_TABLE_INSTALLED_APP); } private static boolean columnExists(SQLiteDatabase db, 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 414e528f5..9b3d01877 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -64,10 +64,13 @@ public class InstalledAppProvider extends FDroidProvider { String VERSION_NAME = "versionName"; String APPLICATION_LABEL = "applicationLabel"; String SIGNATURE = "sig"; + String LAST_UPDATE_TIME = "lastUpdateTime"; + String HASH_TYPE = "hashType"; + String HASH = "hash"; String[] ALL = { _ID, PACKAGE_NAME, VERSION_CODE, VERSION_NAME, APPLICATION_LABEL, - SIGNATURE, + SIGNATURE, LAST_UPDATE_TIME, HASH_TYPE, HASH, }; } @@ -89,6 +92,9 @@ public class InstalledAppProvider extends FDroidProvider { return Uri.parse("content://" + getAuthority()); } + /** + * @return the {@link Uri} that points to a specific installed app + */ public static Uri getAppUri(String packageName) { return Uri.withAppendedPath(getContentUri(), packageName); } 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 175a78976..acd5a8d45 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -155,6 +155,12 @@ public class InstalledAppProviderService extends IntentService { InstalledAppProvider.getApplicationLabel(context, packageInfo.packageName)); contentValues.put(InstalledAppProvider.DataColumns.SIGNATURE, InstalledAppProvider.getPackageSig(packageInfo)); + contentValues.put(InstalledAppProvider.DataColumns.LAST_UPDATE_TIME, packageInfo.lastUpdateTime); + + String hashType = "sha256"; + String hash = Utils.getBinaryHash(new File(packageInfo.applicationInfo.publicSourceDir), hashType); + contentValues.put(InstalledAppProvider.DataColumns.HASH_TYPE, hashType); + contentValues.put(InstalledAppProvider.DataColumns.HASH, hash); context.getContentResolver().insert(uri, contentValues); } From 748352e5a19f023e5289dcb9e5238a0cbf7689ab Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Mon, 30 May 2016 23:01:50 +0200 Subject: [PATCH 09/10] do not update InstalledAppProvider if already current This adds a check of whether the database has the current APK in it, based on PackageInfo's lastUpdateTime field. This avoids recalculating the hash of the whole APK, which is quite time and resource intensive. --- .../fdroid/fdroid/data/InstalledAppProvider.java | 15 ++++++++++----- .../fdroid/data/InstalledAppProviderService.java | 13 ++++++++----- 2 files changed, 18 insertions(+), 10 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 9b3d01877..166d838f0 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProvider.java @@ -27,12 +27,12 @@ public class InstalledAppProvider extends FDroidProvider { public static class Helper { /** - * @return The keys are the app ids (package names), and their corresponding values are - * the version code which is installed. + * @return The keys are the package names, and their corresponding values are + * the {@link PackageInfo#lastUpdateTime last update time} in milliseconds. */ - public static Map all(Context context) { + public static Map all(Context context) { - Map cachedInfo = new HashMap<>(); + Map cachedInfo = new HashMap<>(); final Uri uri = InstalledAppProvider.getContentUri(); final String[] projection = InstalledAppProvider.DataColumns.ALL; @@ -43,7 +43,7 @@ public class InstalledAppProvider extends FDroidProvider { while (!cursor.isAfterLast()) { cachedInfo.put( cursor.getString(cursor.getColumnIndex(InstalledAppProvider.DataColumns.PACKAGE_NAME)), - cursor.getInt(cursor.getColumnIndex(InstalledAppProvider.DataColumns.VERSION_CODE)) + cursor.getLong(cursor.getColumnIndex(DataColumns.LAST_UPDATE_TIME)) ); cursor.moveToNext(); } @@ -223,6 +223,11 @@ public class InstalledAppProvider extends FDroidProvider { return getAppUri(values.getAsString(DataColumns.PACKAGE_NAME)); } + /** + * Update is not supported for {@code InstalledAppProvider}. Instead, use + * {@link #insert(Uri, ContentValues)}, and it will overwrite the relevant + * row, if one exists. This just throws {@link UnsupportedOperationException} + */ @Override public int update(Uri uri, ContentValues values, String where, String[] whereArgs) { throw new UnsupportedOperationException("\"Update' not supported for installed appp provider. Instead, you should insert, and it will overwrite the relevant rows if one exists."); 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 acd5a8d45..3dc103bac 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java +++ b/app/src/main/java/org/fdroid/fdroid/data/InstalledAppProviderService.java @@ -23,7 +23,7 @@ import java.util.concurrent.TimeUnit; * 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, @@ -94,7 +94,7 @@ public class InstalledAppProviderService extends IntentService { * 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 @@ -103,15 +103,18 @@ public class InstalledAppProviderService extends IntentService { * {@link Process#THREAD_PRIORITY_LOWEST}. */ public static void compareToPackageManager(Context context) { - Map cachedInfo = InstalledAppProvider.Helper.all(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)) { + if (packageInfo.lastUpdateTime > cachedInfo.get(packageInfo.packageName)) { + insert(context, packageInfo); + } cachedInfo.remove(packageInfo.packageName); + } else { + insert(context, packageInfo); } } From cf4fedbe13cc99131ddd9587e24478033981a83a Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Wed, 1 Jun 2016 22:44:03 +0200 Subject: [PATCH 10/10] fix crash if an APK has a short embedded description caught java.lang.StringIndexOutOfBoundsException: at java.lang.String.substring(String.java:1651) at java.lang.String.subSequence(String.java:2040) at org.fdroid.fdroid.data.App.setFromPackageInfo(App.java:298) at org.fdroid.fdroid.data.App.(App.java:268) at org.fdroid.fdroid.localrepo.CacheSwapAppsService.onHandleIntent(CacheSwapAppsService.java:78) at android.app.IntentService$ServiceHandler.handleMessage(IntentService.java:59) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:130) at android.os.HandlerThread.run(HandlerThread.java:60) --- app/src/main/java/org/fdroid/fdroid/data/App.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java index 5e5356cd5..b20203b89 100644 --- a/app/src/main/java/org/fdroid/fdroid/data/App.java +++ b/app/src/main/java/org/fdroid/fdroid/data/App.java @@ -294,8 +294,10 @@ public class App extends ValueObject implements Comparable { final CharSequence appDescription = appInfo.loadDescription(pm); if (TextUtils.isEmpty(appDescription)) { this.summary = "(installed by " + installerPackageLabel + ")"; - } else { + } else if (appDescription.length() > 40) { this.summary = (String) appDescription.subSequence(0, 40); + } else { + this.summary = (String) appDescription; } this.added = new Date(packageInfo.firstInstallTime); this.lastUpdated = new Date(packageInfo.lastUpdateTime);