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.
This commit is contained in:
		
							parent
							
								
									67e66a7b0c
								
							
						
					
					
						commit
						08988f2369
					
				| @ -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<String, Apk> ACTIVE_APKS = new HashMap<String, Apk>(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<String, BroadcastReceiver[]> receivers = new HashMap<String, BroadcastReceiver[]>(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. <code>packageName != null</code>) 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 | ||||
|  | ||||
| @ -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. <code>packageName != null</code>) 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 { | ||||
|      * <p/> | ||||
|      * 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"); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Hans-Christoph Steiner
						Hans-Christoph Steiner