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:
Hans-Christoph Steiner 2016-05-06 12:48:26 +02:00
parent 67e66a7b0c
commit 08988f2369
2 changed files with 167 additions and 115 deletions

View File

@ -1,15 +1,27 @@
package org.fdroid.fdroid.installer; package org.fdroid.fdroid.installer;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.IBinder; 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 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.Utils;
import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService; import org.fdroid.fdroid.net.DownloaderService;
@ -45,12 +57,19 @@ public class InstallManagerService extends Service {
public static final String TAG = "InstallManagerService"; public static final String TAG = "InstallManagerService";
private static final String ACTION_INSTALL = "org.fdroid.fdroid.InstallManagerService.action.INSTALL"; 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. * 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); 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; private LocalBroadcastManager localBroadcastManager;
/** /**
@ -73,11 +92,17 @@ public class InstallManagerService extends Service {
Utils.debugLog(TAG, "onStartCommand " + intent); Utils.debugLog(TAG, "onStartCommand " + intent);
String urlString = intent.getDataString(); String urlString = intent.getDataString();
Apk apk = ACTIVE_APKS.get(urlString); 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()); File apkFilePath = Utils.getApkDownloadPath(this, intent.getData());
long apkFileSize = apkFilePath.length(); long apkFileSize = apkFilePath.length();
if (!apkFilePath.exists() || apkFileSize < apk.size) { if (!apkFilePath.exists() || apkFileSize < apk.size) {
Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "download " + urlString + " " + apkFilePath);
DownloaderService.queue(this, apk.packageName, urlString); DownloaderService.queue(this, urlString);
} else if (apkFileSize == apk.size) { } else if (apkFileSize == apk.size) {
Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath); Utils.debugLog(TAG, "skip download, we have it, straight to install " + urlString + " " + apkFilePath);
sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath); sendBroadcast(intent.getData(), Downloader.ACTION_STARTED, apkFilePath);
@ -85,7 +110,7 @@ public class InstallManagerService extends Service {
} else { } else {
Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath); Utils.debugLog(TAG, " delete and download again " + urlString + " " + apkFilePath);
apkFilePath.delete(); apkFilePath.delete();
DownloaderService.queue(this, apk.packageName, urlString); DownloaderService.queue(this, urlString);
} }
return START_REDELIVER_INTENT; // if killed before completion, retry Intent return START_REDELIVER_INTENT; // if killed before completion, retry Intent
} }
@ -97,6 +122,142 @@ public class InstallManagerService extends Service {
localBroadcastManager.sendBroadcast(intent); 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. * 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 * All notifications are sent as an {@link Intent} via local broadcasts to be received by

View File

@ -17,14 +17,11 @@
package org.fdroid.fdroid.net; package org.fdroid.fdroid.net;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.IntentFilter; import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
@ -34,19 +31,11 @@ import android.os.Message;
import android.os.PatternMatcher; import android.os.PatternMatcher;
import android.os.Process; import android.os.Process;
import android.support.annotation.NonNull; 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.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Log; 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.Utils;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.SanitizedFile; import org.fdroid.fdroid.data.SanitizedFile;
import java.io.File; import java.io.File;
@ -85,13 +74,9 @@ import java.net.URL;
public class DownloaderService extends Service { public class DownloaderService extends Service {
private static final String TAG = "DownloaderService"; 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_QUEUE = "org.fdroid.fdroid.net.DownloaderService.action.QUEUE";
private static final String ACTION_CANCEL = "org.fdroid.fdroid.net.DownloaderService.action.CANCEL"; 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 volatile Looper serviceLooper;
private static volatile ServiceHandler serviceHandler; private static volatile ServiceHandler serviceHandler;
private static volatile Downloader downloader; 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 public static PendingIntent createCancelDownloadIntent(@NonNull Context context, int
requestCode, @NonNull String urlString) { requestCode, @NonNull String urlString) {
Intent cancelIntent = new Intent(context.getApplicationContext(), DownloaderService.class) Intent cancelIntent = new Intent(context.getApplicationContext(), DownloaderService.class)
@ -254,12 +190,8 @@ public class DownloaderService extends Service {
protected void handleIntent(Intent intent) { protected void handleIntent(Intent intent) {
final Uri uri = intent.getData(); final Uri uri = intent.getData();
final SanitizedFile localFile = Utils.getApkDownloadPath(this, uri); final SanitizedFile localFile = Utils.getApkDownloadPath(this, uri);
final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
sendBroadcast(uri, Downloader.ACTION_STARTED, localFile); sendBroadcast(uri, Downloader.ACTION_STARTED, localFile);
Notification notification = createNotification(intent.getDataString(), packageName).build();
startForeground(NOTIFY_DOWNLOADING, notification);
try { try {
downloader = DownloaderFactory.create(this, uri, localFile); downloader = DownloaderFactory.create(this, uri, localFile);
downloader.setListener(new Downloader.DownloaderProgressListener() { 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_BYTES_READ, bytesRead);
intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes); intent.putExtra(Downloader.EXTRA_TOTAL_BYTES, totalBytes);
localBroadcastManager.sendBroadcast(intent); 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(); downloader.download();
sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile); sendBroadcast(uri, Downloader.ACTION_COMPLETE, localFile);
notifyDownloadComplete(packageName, intent.getDataString());
} catch (InterruptedException e) { } catch (InterruptedException e) {
sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile); sendBroadcast(uri, Downloader.ACTION_INTERRUPTED, localFile);
} catch (IOException e) { } catch (IOException e) {
@ -296,36 +221,6 @@ public class DownloaderService extends Service {
downloader = null; 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) { private void sendBroadcast(Uri uri, String action, File file) {
sendBroadcast(uri, action, file, null); sendBroadcast(uri, action, file, null);
} }
@ -346,18 +241,14 @@ public class DownloaderService extends Service {
* All notifications are sent as an {@link Intent} via local broadcasts to be received by * All notifications are sent as an {@link Intent} via local broadcasts to be received by
* *
* @param context this app's {@link Context} * @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 urlString The URL to add to the download queue
* @see #cancel(Context, String) * @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"); Utils.debugLog(TAG, "Preparing " + urlString + " to go into the download queue");
Intent intent = new Intent(context, DownloaderService.class); Intent intent = new Intent(context, DownloaderService.class);
intent.setAction(ACTION_QUEUE); intent.setAction(ACTION_QUEUE);
intent.setData(Uri.parse(urlString)); intent.setData(Uri.parse(urlString));
if (!TextUtils.isEmpty(packageName)) {
intent.putExtra(EXTRA_PACKAGE_NAME, packageName);
}
context.startService(intent); context.startService(intent);
} }
@ -368,7 +259,7 @@ public class DownloaderService extends Service {
* *
* @param context this app's {@link Context} * @param context this app's {@link Context}
* @param urlString The URL to remove from the download queue * @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) { public static void cancel(Context context, String urlString) {
Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download"); Utils.debugLog(TAG, "Preparing cancellation of " + urlString + " download");