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;
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

View File

@ -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");