From 08988f2369efc9352648c079d3a177b480c6fc03 Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Fri, 6 May 2016 12:48:26 +0200 Subject: [PATCH] move all downloading notifications to InstallManagerService This keeps DownloaderService tightly focused on downloading, and makes it a lot easier to manage Notifications since InstallManagerService's lifecycle lasts as long as the Notifications, unlike DownloaderService. --- .../installer/InstallManagerService.java | 165 +++++++++++++++++- .../fdroid/fdroid/net/DownloaderService.java | 117 +------------ 2 files changed, 167 insertions(+), 115 deletions(-) diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index 9e651b52b..7c3a725b8 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -1,15 +1,27 @@ package org.fdroid.fdroid.installer; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.IBinder; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; +import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.FDroid; +import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; @@ -45,12 +57,19 @@ public class InstallManagerService extends Service { public static final String TAG = "InstallManagerService"; private static final String ACTION_INSTALL = "org.fdroid.fdroid.InstallManagerService.action.INSTALL"; + private static final int NOTIFY_DOWNLOADING = 0x2344; /** * The collection of APKs that are actively going through this whole process. */ private static final HashMap ACTIVE_APKS = new HashMap(3); + /** + * The array of active {@link BroadcastReceiver}s for each active APK. The key is the + * download URL, as in {@link Apk#getUrl()} or {@code urlString}. + */ + private final HashMap receivers = new HashMap(3); + private LocalBroadcastManager localBroadcastManager; /** @@ -73,11 +92,17 @@ public class InstallManagerService extends Service { Utils.debugLog(TAG, "onStartCommand " + intent); String urlString = intent.getDataString(); Apk apk = ACTIVE_APKS.get(urlString); + + Notification notification = createNotification(intent.getDataString(), apk.packageName).build(); + startForeground(NOTIFY_DOWNLOADING, notification); + + registerDownloaderReceivers(urlString); + File apkFilePath = Utils.getApkDownloadPath(this, intent.getData()); long apkFileSize = apkFilePath.length(); if (!apkFilePath.exists() || apkFileSize < apk.size) { Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath); - DownloaderService.queue(this, apk.packageName, urlString); + DownloaderService.queue(this, urlString); } else if (apkFileSize == apk.size) { Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); @@ -85,7 +110,7 @@ public class InstallManagerService extends Service { } else { Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath); apkFilePath.delete(); - DownloaderService.queue(this, apk.packageName, urlString); + DownloaderService.queue(this, urlString); } return START_REDELIVER_INTENT; // if killed before completion, retry Intent } @@ -97,6 +122,142 @@ public class InstallManagerService extends Service { localBroadcastManager.sendBroadcast(intent); } + private void unregisterDownloaderReceivers(String urlString) { + for (BroadcastReceiver receiver : receivers.get(urlString)) { + localBroadcastManager.unregisterReceiver(receiver); + } + } + + private void registerDownloaderReceivers(String urlString) { + BroadcastReceiver startedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + } + }; + BroadcastReceiver progressReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String urlString = intent.getDataString(); + Apk apk = ACTIVE_APKS.get(urlString); + int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0); + int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + Notification notification = createNotification(urlString, apk.packageName) + .setProgress(totalBytes, bytesRead, false) + .build(); + nm.notify(NOTIFY_DOWNLOADING, notification); + } + }; + BroadcastReceiver completeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String urlString = intent.getDataString(); + Apk apk = ACTIVE_APKS.remove(urlString); + notifyDownloadComplete(apk.packageName, intent.getDataString()); + unregisterDownloaderReceivers(urlString); + } + }; + BroadcastReceiver interruptedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String urlString = intent.getDataString(); + ACTIVE_APKS.remove(urlString); + unregisterDownloaderReceivers(urlString); + } + }; + localBroadcastManager.registerReceiver(startedReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_STARTED)); + localBroadcastManager.registerReceiver(progressReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_PROGRESS)); + localBroadcastManager.registerReceiver(completeReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_COMPLETE)); + localBroadcastManager.registerReceiver(interruptedReceiver, + DownloaderService.getIntentFilter(urlString, Downloader.ACTION_INTERRUPTED)); + receivers.put(urlString, new BroadcastReceiver[]{ + startedReceiver, progressReceiver, completeReceiver, interruptedReceiver, + }); + } + + private NotificationCompat.Builder createNotification(String urlString, @Nullable String packageName) { + return new NotificationCompat.Builder(this) + .setAutoCancel(true) + .setContentIntent(createAppDetailsIntent(0, packageName)) + .setContentTitle(getNotificationTitle(packageName)) + .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel), + DownloaderService.createCancelDownloadIntent(this, 0, urlString)) + .setSmallIcon(android.R.drawable.stat_sys_download) + .setContentText(urlString) + .setProgress(100, 0, true); + } + + /** + * If downloading an apk (i.e. packageName != null) then the title will indicate + * the name of the app which the apk belongs to. Otherwise, it will be a generic "Downloading..." + * message. + */ + private String getNotificationTitle(@Nullable String packageName) { + String title; + if (packageName != null) { + App app = AppProvider.Helper.findByPackageName( + getContentResolver(), packageName, new String[]{AppProvider.DataColumns.NAME}); + title = getString(R.string.downloading_apk, app.name); + } else { + title = getString(R.string.downloading); + } + return title; + } + + private PendingIntent createAppDetailsIntent(int requestCode, String packageName) { + TaskStackBuilder stackBuilder; + if (packageName != null) { + Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class) + .putExtra(AppDetails.EXTRA_APPID, packageName); + + stackBuilder = TaskStackBuilder + .create(getApplicationContext()) + .addParentStack(AppDetails.class) + .addNextIntent(notifyIntent); + } else { + Intent notifyIntent = new Intent(getApplicationContext(), FDroid.class); + stackBuilder = TaskStackBuilder + .create(getApplicationContext()) + .addParentStack(FDroid.class) + .addNextIntent(notifyIntent); + } + + return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT); + } + + /** + * Post a notification about a completed download. {@code packageName} must be a valid + * and currently in the app index database. + */ + private void notifyDownloadComplete(String packageName, String urlString) { + String title; + try { + PackageManager pm = getPackageManager(); + title = String.format(getString(R.string.tap_to_update_format), + pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0))); + } catch (PackageManager.NameNotFoundException e) { + App app = AppProvider.Helper.findByPackageName(getContentResolver(), packageName, + new String[]{ + AppProvider.DataColumns.NAME, + }); + title = String.format(getString(R.string.tap_to_install_format), app.name); + } + + int downloadUrlId = urlString.hashCode(); + NotificationCompat.Builder builder = + new NotificationCompat.Builder(this) + .setAutoCancel(true) + .setContentTitle(title) + .setSmallIcon(android.R.drawable.stat_sys_download_done) + .setContentIntent(createAppDetailsIntent(downloadUrlId, packageName)) + .setContentText(getString(R.string.tap_to_install)); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.notify(downloadUrlId, builder.build()); + } + /** * Install an APK, checking the cache and downloading if necessary before starting the process. * All notifications are sent as an {@link Intent} via local broadcasts to be received by diff --git a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java index e5685ac49..7a5a0f37e 100644 --- a/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java +++ b/app/src/main/java/org/fdroid/fdroid/net/DownloaderService.java @@ -17,14 +17,11 @@ package org.fdroid.fdroid.net; -import android.app.Notification; -import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.Handler; import android.os.HandlerThread; @@ -34,19 +31,11 @@ import android.os.Message; import android.os.PatternMatcher; import android.os.Process; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.TaskStackBuilder; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Log; -import org.fdroid.fdroid.AppDetails; -import org.fdroid.fdroid.FDroid; -import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; -import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.SanitizedFile; import java.io.File; @@ -85,13 +74,9 @@ import java.net.URL; public class DownloaderService extends Service { private static final String TAG = "DownloaderService"; - private static final String EXTRA_PACKAGE_NAME = "org.fdroid.fdroid.net.DownloaderService.extra.PACKAGE_NAME"; - private static final String ACTION_QUEUE = "org.fdroid.fdroid.net.DownloaderService.action.QUEUE"; private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL"; - private static final int NOTIFY_DOWNLOADING = 0x2344; - private volatile Looper serviceLooper; private static volatile ServiceHandler serviceHandler; private static volatile Downloader downloader; @@ -153,55 +138,6 @@ public class DownloaderService extends Service { } } - private NotificationCompat.Builder createNotification(String urlString, @Nullable String packageName) { - return new NotificationCompat.Builder(this) - .setAutoCancel(true) - .setContentIntent(createAppDetailsIntent(0, packageName)) - .setContentTitle(getNotificationTitle(packageName)) - .addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel), - createCancelDownloadIntent(this, 0, urlString)) - .setSmallIcon(android.R.drawable.stat_sys_download) - .setContentText(urlString) - .setProgress(100, 0, true); - } - - /** - * If downloading an apk (i.e. packageName != null) then the title will indicate - * the name of the app which the apk belongs to. Otherwise, it will be a generic "Downloading..." - * message. - */ - private String getNotificationTitle(@Nullable String packageName) { - if (packageName != null) { - final App app = AppProvider.Helper.findByPackageName( - getContentResolver(), packageName, new String[]{AppProvider.DataColumns.NAME}); - if (app != null) { - return getString(R.string.downloading_apk, app.name); - } - } - return getString(R.string.downloading); - } - - private PendingIntent createAppDetailsIntent(int requestCode, String packageName) { - TaskStackBuilder stackBuilder; - if (packageName != null) { - Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class) - .putExtra(AppDetails.EXTRA_APPID, packageName); - - stackBuilder = TaskStackBuilder - .create(getApplicationContext()) - .addParentStack(AppDetails.class) - .addNextIntent(notifyIntent); - } else { - Intent notifyIntent = new Intent(getApplicationContext(), FDroid.class); - stackBuilder = TaskStackBuilder - .create(getApplicationContext()) - .addParentStack(FDroid.class) - .addNextIntent(notifyIntent); - } - - return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_UPDATE_CURRENT); - } - public static PendingIntent createCancelDownloadIntent(@NonNull Context context, int requestCode, @NonNull String urlString) { Intent cancelIntent = new Intent(context.getApplicationContext(), DownloaderService.class) @@ -254,12 +190,8 @@ public class DownloaderService extends Service { protected void handleIntent(Intent intent) { final Uri uri = intent.getData(); final SanitizedFile localFile = Utils.getApkDownloadPath(this, uri); - final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); sendBroadcast(uri, Downloader.ACTION_STARTED, localFile); - Notification notification = createNotification(intent.getDataString(), packageName).build(); - startForeground(NOTIFY_DOWNLOADING, notification); - try { downloader = DownloaderFactory.create(this, uri, localFile); downloader.setListener(new Downloader.DownloaderProgressListener() { @@ -270,17 +202,10 @@ public class DownloaderService extends Service { intent.putExtra(Downloader.EXTRA_BYTES_READ, bytesRead); intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes); localBroadcastManager.sendBroadcast(intent); - - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - Notification notification = createNotification(uri.toString(), packageName) - .setProgress(totalBytes, bytesRead, false) - .build(); - nm.notify(NOTIFY_DOWNLOADING, notification); } }); downloader.download(); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); - notifyDownloadComplete(packageName, intent.getDataString()); } catch (InterruptedException e) { sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile); } catch (IOException e) { @@ -296,36 +221,6 @@ public class DownloaderService extends Service { downloader = null; } - /** - * Post a notification about a completed download. {@code packageName} must be a valid - * and currently in the app index database. - */ - private void notifyDownloadComplete(String packageName, String urlString) { - String title; - try { - PackageManager pm = getPackageManager(); - title = String.format(getString(R.string.tap_to_update_format), - pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0))); - } catch (PackageManager.NameNotFoundException e) { - App app = AppProvider.Helper.findByPackageName(getContentResolver(), packageName, - new String[]{ - AppProvider.DataColumns.NAME, - }); - title = String.format(getString(R.string.tap_to_install_format), app.name); - } - - int downloadUrlId = urlString.hashCode(); - NotificationCompat.Builder builder = - new NotificationCompat.Builder(this) - .setAutoCancel(true) - .setContentTitle(title) - .setSmallIcon(android.R.drawable.stat_sys_download_done) - .setContentIntent(createAppDetailsIntent(downloadUrlId, packageName)) - .setContentText(getString(R.string.tap_to_install)); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.notify(downloadUrlId, builder.build()); - } - private void sendBroadcast(Uri uri, String action, File file) { sendBroadcast(uri, action, file, null); } @@ -345,19 +240,15 @@ public class DownloaderService extends Service { *

* All notifications are sent as an {@link Intent} via local broadcasts to be received by * - * @param context this app's {@link Context} - * @param packageName The packageName of the app being downloaded - * @param urlString The URL to add to the download queue + * @param context this app's {@link Context} + * @param urlString The URL to add to the download queue * @see #cancel(Context, String) */ - public static void queue(Context context, String packageName, String urlString) { + public static void queue(Context context, String urlString) { Utils.debugLog(TAG, "Preparing " + urlString + " to go into the download queue"); Intent intent = new Intent(context, DownloaderService.class); intent.setAction(ACTION_QUEUE); intent.setData(Uri.parse(urlString)); - if (!TextUtils.isEmpty(packageName)) { - intent.putExtra(EXTRA_PACKAGE_NAME, packageName); - } context.startService(intent); } @@ -368,7 +259,7 @@ public class DownloaderService extends Service { * * @param context this app's {@link Context} * @param urlString The URL to remove from the download queue - * @see #queue(Context, String, String) + * @see #queue(Context, String) */ public static void cancel(Context context, String urlString) { Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download");