Create notification helper class

This commit is contained in:
mvp76 2016-11-28 17:30:58 +01:00
parent 9e1458f41e
commit 10cbf1ef7a
7 changed files with 577 additions and 115 deletions

View File

@ -220,6 +220,8 @@ public class FDroidApp extends Application {
curTheme = Preferences.get().getTheme();
Preferences.get().configureProxy();
NotificationHelper.init(getApplicationContext());
InstalledAppProviderService.compareToPackageManager(this);
// If the user changes the preference to do with filtering rooted apps,

View File

@ -0,0 +1,522 @@
package org.fdroid.fdroid;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.util.LongSparseArray;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.StyleSpan;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageSize;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class NotificationHelper {
private static final String BROADCAST_NOTIFICATIONS_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.updates.cleared";
private static final String BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED = "org.fdroid.fdroid.installer.notifications.installed.cleared";
private static final String BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED = "org.fdroid.fdroid.installer.notifications.deleted";
private static final int NOTIFY_ID_UPDATES = 4711;
private static final int NOTIFY_ID_INSTALLED = 4712;
private static final int MAX_UPDATES_TO_SHOW = 5;
private static final int MAX_INSTALLED_TO_SHOW = 10;
private static final String EXTRA_NOTIFICATION_TAG = "tag";
private static final String GROUP_UPDATES = "updates";
private static final String GROUP_INSTALLED = "installed";
public enum Status {
UpdateAvailable,
Downloading,
ReadyToInstall,
Installing,
Installed,
Error
}
private static NotificationHelper instance;
public static void init(Context context) {
instance = new NotificationHelper(context);
}
private static NotificationHelper getInstance() {
return instance;
}
private class AppEntry {
App app;
Apk apk;
Status status;
PendingIntent intent;
int progressCurrent;
int progressMax;
AppEntry(App app, Apk apk, Status status, PendingIntent intent) {
this.app = app;
this.apk = apk;
this.status = status;
this.intent = intent;
}
String getTag() {
return apk.getUrl();
}
int getId() {
return getTag().hashCode();
}
}
private final Context context;
private final NotificationManagerCompat notificationManager;
private HashMap<String, AppEntry> appMapping;
private boolean isBatchUpdating;
private ArrayList<AppEntry> updates;
private ArrayList<AppEntry> installed;
private NotificationHelper(Context context) {
this.context = context;
notificationManager = NotificationManagerCompat.from(context);
// We need to listen to when notifications are cleared, so that we "forget" all that we currently know about updates
// and installs.
IntentFilter filter = new IntentFilter();
filter.addAction(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED);
filter.addAction(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
filter.addAction(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED);
BroadcastReceiver mReceiverNotificationsCleared = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED:
clearAllInstalledInternal();
break;
case BROADCAST_NOTIFICATIONS_UPDATES_CLEARED:
clearAllUpdatesInternal();
break;
case BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED:
String id = intent.getStringExtra(EXTRA_NOTIFICATION_TAG);
// TODO
break;
}
}
};
context.registerReceiver(mReceiverNotificationsCleared, filter);
appMapping = new HashMap<>();
updates = new ArrayList<>();
installed = new ArrayList<>();
}
private void setApkInternal(Apk apk, Status status, PendingIntent intent) {
if (apk == null) {
return;
}
AppEntry entry = appMapping.get(apk.getUrl());
if (status == null) {
// Remove
if (entry != null) {
appMapping.remove(apk.getUrl());
notificationManager.cancel(entry.getTag(), entry.getId());
}
} else if (entry != null) {
// Update
boolean isStatusUpdate = (entry.status != status);
entry.status = status;
entry.intent = intent;
createNotificationForAppEntry(entry);
if (isStatusUpdate) {
updateSummaryNotifications();
}
} else {
// Add
ContentResolver resolver = context.getContentResolver();
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
entry = new AppEntry(app, apk, status, intent);
appMapping.put(apk.getUrl(), entry);
createNotificationForAppEntry(entry);
updateSummaryNotifications();
}
}
private void setApkProgressInternal(Apk apk, int max, int current) {
if (appMapping.get(apk.getUrl()) != null) {
AppEntry entry = appMapping.get(apk.getUrl());
entry.progressMax = max;
entry.progressCurrent = current;
createNotificationForAppEntry(entry);
}
}
private void clearAllUpdatesInternal() {
for(Iterator<Map.Entry<String, AppEntry>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, AppEntry> entry = it.next();
if(entry.getValue().status != Status.Installed) {
it.remove();
}
}
}
private void clearAllInstalledInternal() {
for(Iterator<Map.Entry<String, AppEntry>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, AppEntry> entry = it.next();
if(entry.getValue().status == Status.Installed) {
it.remove();
}
}
}
private void updateSummaryNotifications() {
if (!isBatchUpdating) {
// Get the list of updates and installed available
updates.clear();
installed.clear();
for (Iterator<Map.Entry<String, AppEntry>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, AppEntry> entry = it.next();
if (entry.getValue().status != Status.Installed) {
updates.add(entry.getValue());
} else {
installed.add(entry.getValue());
}
}
NotificationCompat.Builder builder;
if (updates.size() == 0) {
// No updates, remove summary
notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES);
} else {
builder = createUpdateSummaryNotification(updates);
notificationManager.notify(GROUP_UPDATES, NOTIFY_ID_UPDATES, builder.build());
}
if (installed.size() == 0) {
// No installed, remove summary
notificationManager.cancel(GROUP_INSTALLED, NOTIFY_ID_INSTALLED);
} else {
builder = createInstalledSummaryNotification(installed);
notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build());
}
}
}
private void createNotificationForAppEntry(AppEntry entry) {
NotificationCompat.Builder builder;
if (entry.status == Status.Installed) {
builder = createInstalledNotification(entry);
} else {
builder = createUpdateNotification(entry);
}
notificationManager.notify(entry.getTag(), entry.getId(), builder.build());
}
/**
* Add an Apk to the notifications manager.
* @param apk The apk to add.
* @param status The current status of the app
* @param pendingIntent Action when notification is clicked. Can be null for default action(s)
*/
public static void setApk(Apk apk, Status status, PendingIntent pendingIntent) {
getInstance().setApkInternal(apk, status, pendingIntent);
}
public static void removeApk(Apk apk) {
getInstance().setApkInternal(apk, null, null);
}
public static void setApkProgress(Apk apk, int max, int current) {
getInstance().setApkProgressInternal(apk, max, current);
}
public static void startBatchUpdates() {
getInstance().isBatchUpdating = true;
}
public static void endBatchUpdates() {
getInstance().isBatchUpdating = false;
getInstance().updateSummaryNotifications();
}
public static void clearAllUpdates() {
getInstance().clearAllUpdatesInternal();
}
public static void clearAllInstalled() {
getInstance().clearAllInstalledInternal();
}
private NotificationCompat.Action getAction(AppEntry entry) {
if (entry.status == Status.UpdateAvailable) {
// Make sure we have an intent to install the app. If not set, we create an intent
// to open up the app details page for the app. From there, the user can hit "install"
PendingIntent intent = entry.intent;
if (intent == null) {
intent = getAppDetailsIntent(0, entry.apk);
}
return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, "Update", intent);
} else if (entry.status == Status.Downloading || entry.status == Status.Installing) {
PendingIntent intent = entry.intent;
if (intent != null) {
return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, "Cancel", intent);
}
} else if (entry.status == Status.ReadyToInstall) {
// Make sure we have an intent to install the app. If not set, we create an intent
// to open up the app details page for the app. From there, the user can hit "install"
PendingIntent intent = entry.intent;
if (intent == null) {
intent = getAppDetailsIntent(0, entry.apk);
}
return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, "Install", intent);
}
return null;
}
private String getSingleItemTitleString(App app, Status status) {
switch (status) {
case UpdateAvailable:
return "Update Available";
case Downloading:
return app.name;
case ReadyToInstall:
return "Update ready to install"; // TODO - "Update"? Should just be "ready to install"?
case Installing:
return app.name;
case Installed:
return app.name;
}
return "";
}
private String getSingleItemContentString(App app, Status status) {
switch (status) {
case UpdateAvailable:
return app.name;
case Downloading:
return String.format("Downloading update for \"%s\"...", app.name);
case ReadyToInstall:
return app.name;
case Installing:
return String.format("Installing \"%s\"...", app.name);
case Installed:
return "Successfully installed";
}
return "";
}
private String getMultiItemContentString(App app, Status status) {
switch (status) {
case UpdateAvailable:
return "Update available";
case Downloading:
return "Downloading update...";
case ReadyToInstall:
return "Ready to install";
case Installing:
return "Installing";
case Installed:
return "Successfully installed";
}
return "";
}
/**
* Get a {@link PendingIntent} for a {@link Notification} to send when it
* is clicked. {@link AppDetails} handles {@code Intent}s that are missing
* or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked
* here.
*/
private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) {
Intent notifyIntent = new Intent(context, AppDetails.class)
.putExtra(AppDetails.EXTRA_APPID, apk.packageName);
return TaskStackBuilder.create(context)
.addParentStack(AppDetails.class)
.addNextIntent(notifyIntent)
.getPendingIntent(requestCode, 0);
}
private NotificationCompat.Builder createUpdateNotification(AppEntry entry) {
App app = entry.app;
Status status = entry.status;
// TODO - async image loading
int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
Bitmap iconLarge = ImageLoader.getInstance().loadImageSync(app.iconUrl, new ImageSize(largeIconSize, largeIconSize));
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setAutoCancel(true)
.setLargeIcon(iconLarge)
.setSmallIcon(R.drawable.ic_stat_notify_updates)
.setContentTitle(getSingleItemTitleString(app, status))
.setContentText(getSingleItemContentString(app, status))
.setGroup(GROUP_UPDATES);
// Handle actions
//
NotificationCompat.Action action = getAction(entry);
if (action != null) {
builder.addAction(action);
// TODO - also click on whole item?
builder.setContentIntent(action.getActionIntent());
} else if (entry.intent != null) {
builder.setContentIntent(entry.intent);
}
// Handle progress bar (for some states)
//
if (status == Status.Downloading) {
if (entry.progressMax == 0)
builder.setProgress(100, 0, true);
else
builder.setProgress(entry.progressMax, entry.progressCurrent, false);
} else if (status == Status.Installing) {
builder.setProgress(100, 0, true); // indeterminate bar
}
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED);
intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId());
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
builder.setDeleteIntent(piDeleted);
return builder;
}
private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList<AppEntry> updates) {
String title = String.format("%d Updates", updates.size());
StringBuilder text = new StringBuilder();
NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle();
inboxStyle.setBigContentTitle(title);
for (int i = 0; i < MAX_UPDATES_TO_SHOW && i < updates.size(); i++) {
AppEntry entry = updates.get(i);
App app = entry.app;
Status status = entry.status;
String content = getMultiItemContentString(app, status);
SpannableStringBuilder sb = new SpannableStringBuilder(app.name);
sb.setSpan(new StyleSpan(Typeface.BOLD), 0, sb.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
sb.append(" ");
sb.append(content);
inboxStyle.addLine(sb);
if (text.length() > 0)
text.append(", ");
text.append(app.name);
}
//if (updates.size() > MAX_UPDATES_TO_SHOW) {
// int diff = updates.size() - MAX_UPDATES_TO_SHOW;
// inboxStyle.setSummaryText(context.getString(R.string.update_notification_more, diff));
//}
inboxStyle.setSummaryText(title);
// Intent to open main app list
Intent intentObject = new Intent(context, FDroid.class);
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(piAction)
.setStyle(inboxStyle)
.setGroup(GROUP_UPDATES)
.setGroupSummary(true);
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
builder.setDeleteIntent(piDeleted);
return builder;
}
private NotificationCompat.Builder createInstalledNotification(AppEntry entry) {
App app = entry.app;
int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
Bitmap iconLarge = ImageLoader.getInstance().loadImageSync(app.iconUrl, new ImageSize(largeIconSize, largeIconSize));
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setAutoCancel(true)
.setLargeIcon(iconLarge)
.setSmallIcon(R.drawable.ic_stat_notify_updates)
.setContentTitle(app.name)
.setContentText("Successfully Installed")
.setGroup(GROUP_INSTALLED);
PackageManager pm = context.getPackageManager();
Intent intentObject = pm.getLaunchIntentForPackage(app.packageName);
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
builder.setContentIntent(piAction);
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED);
intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId());
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
builder.setDeleteIntent(piDeleted);
return builder;
}
private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList<AppEntry> installed) {
String title = String.format("%d Apps Installed", installed.size());
StringBuilder text = new StringBuilder();
NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle();
bigTextStyle.setBigContentTitle(title);
for (int i = 0; i < MAX_INSTALLED_TO_SHOW && i < installed.size(); i++) {
AppEntry entry = installed.get(i);
App app = entry.app;
if (text.length() > 0)
text.append(", ");
text.append(app.name);
}
bigTextStyle.bigText(text);
if (installed.size() > MAX_INSTALLED_TO_SHOW) {
int diff = installed.size() - MAX_INSTALLED_TO_SHOW;
bigTextStyle.setSummaryText(context.getString(R.string.update_notification_more, diff));
}
// Intent to open main app list
Intent intentObject = new Intent(context, FDroid.class);
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)
.setAutoCancel(true)
.setSmallIcon(R.drawable.ic_launcher)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(piAction)
.setGroup(GROUP_INSTALLED)
.setGroupSummary(true);
Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED);
PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0);
builder.setDeleteIntent(piDeleted);
return builder;
}
}

View File

@ -520,6 +520,19 @@ public class UpdateService extends IntentService {
}
private void showAppUpdatesNotification(Cursor hasUpdates) {
if (hasUpdates != null) {
hasUpdates.moveToFirst();
NotificationHelper.startBatchUpdates();
for (int i = 0; i < Math.min(MAX_UPDATES_TO_SHOW, hasUpdates.getCount()); i++) {
App app = new App(hasUpdates);
hasUpdates.moveToNext();
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
NotificationHelper.setApk(apk, NotificationHelper.Status.UpdateAvailable, null);
}
NotificationHelper.endBatchUpdates();
}
Utils.debugLog(TAG, "Notifying " + hasUpdates.getCount() + " updates.");
final int icon = Build.VERSION.SDK_INT >= 11 ? R.drawable.ic_stat_notify_updates : R.drawable.ic_launcher;

View File

@ -22,6 +22,7 @@ import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.WildcardFileFilter;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.Hasher;
import org.fdroid.fdroid.NotificationHelper;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.PackageManagerCompat;
@ -98,7 +99,6 @@ public class InstallManagerService extends Service {
private static final HashMap<String, App> ACTIVE_APPS = new HashMap<>(3);
private LocalBroadcastManager localBroadcastManager;
private NotificationManager notificationManager;
/**
* This service does not use binding, so no need to implement this method
@ -113,7 +113,6 @@ public class InstallManagerService extends Service {
super.onCreate();
Utils.debugLog(TAG, "creating Service");
localBroadcastManager = LocalBroadcastManager.getInstance(this);
notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
BroadcastReceiver br = new BroadcastReceiver() {
@Override
@ -122,7 +121,7 @@ public class InstallManagerService extends Service {
for (Map.Entry<String, Apk> entry : ACTIVE_APKS.entrySet()) {
if (TextUtils.equals(packageName, entry.getValue().packageName)) {
String urlString = entry.getKey();
cancelNotification(urlString);
NotificationHelper.removeApk(getApkFromActive(urlString));
break;
}
}
@ -150,7 +149,7 @@ public class InstallManagerService extends Service {
Apk apk = getApkFromActive(urlString);
DownloaderService.cancel(this, apk.getPatchObbUrl());
DownloaderService.cancel(this, apk.getMainObbUrl());
cancelNotification(urlString);
NotificationHelper.removeApk(apk);
return START_NOT_STICKY;
} else if (!ACTION_INSTALL.equals(action)) {
Utils.debugLog(TAG, "Ignoring " + intent + " as it is not an " + ACTION_INSTALL + " intent");
@ -166,7 +165,7 @@ public class InstallManagerService extends Service {
&& !DownloaderService.isQueuedOrActive(urlString)) {
// TODO is there a case where we should allow an active urlString to pass through?
Utils.debugLog(TAG, urlString + " finished downloading while InstallManagerService was killed.");
cancelNotification(urlString);
NotificationHelper.removeApk(getApkFromActive(urlString));
return START_NOT_STICKY;
}
@ -178,12 +177,9 @@ public class InstallManagerService extends Service {
}
addToActive(urlString, app, apk);
NotificationCompat.Builder builder = createNotificationBuilder(urlString, apk);
notificationManager.notify(urlString.hashCode(), builder.build());
registerApkDownloaderReceivers(urlString, builder);
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256, builder);
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256, builder);
registerApkDownloaderReceivers(urlString);
getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256);
getObb(urlString, apk.getPatchObbUrl(), apk.getPatchObbFile(), apk.obbPatchFileSha256);
File apkFilePath = ApkCache.getApkDownloadPath(this, intent.getData());
long apkFileSize = apkFilePath.length();
@ -217,8 +213,7 @@ public class InstallManagerService extends Service {
* @see <a href="https://developer.android.com/google/play/expansion-files.html">APK Expansion Files</a>
*/
private void getObb(final String urlString, String obbUrlString,
final File obbDestFile, final String sha256,
final NotificationCompat.Builder builder) {
final File obbDestFile, final String sha256) {
if (obbDestFile == null || obbDestFile.exists() || TextUtils.isEmpty(obbUrlString)) {
return;
}
@ -232,8 +227,7 @@ public class InstallManagerService extends Service {
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
builder.setProgress(totalBytes, bytesRead, false);
notificationManager.notify(urlString.hashCode(), builder.build());
// TODO - handle obb notifications?
} else if (Downloader.ACTION_COMPLETE.equals(action)) {
localBroadcastManager.unregisterReceiver(this);
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
@ -274,7 +268,7 @@ public class InstallManagerService extends Service {
DownloaderService.getIntentFilter(obbUrlString));
}
private void registerApkDownloaderReceivers(String urlString, final NotificationCompat.Builder builder) {
private void registerApkDownloaderReceivers(String urlString) {
BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override
@ -284,13 +278,17 @@ public class InstallManagerService extends Service {
switch (intent.getAction()) {
case Downloader.ACTION_STARTED:
Intent intentObject = new Intent(context, InstallManagerService.class);
intentObject.setAction(ACTION_CANCEL);
intentObject.setData(downloadUri);
PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0);
NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.Downloading, action);
// nothing to do
break;
case Downloader.ACTION_PROGRESS:
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
builder.setProgress(totalBytes, bytesRead, false);
notificationManager.notify(urlString.hashCode(), builder.build());
NotificationHelper.setApkProgress(getApkFromActive(urlString), totalBytes, bytesRead);
break;
case Downloader.ACTION_COMPLETE:
File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH));
@ -298,17 +296,19 @@ public class InstallManagerService extends Service {
Utils.debugLog(TAG, "download completed of " + urlString + " to " + localApkUri);
NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.ReadyToInstall, null);
localBroadcastManager.unregisterReceiver(this);
registerInstallerReceivers(downloadUri);
Apk apk = ACTIVE_APKS.get(urlString);
InstallerService.install(context, localApkUri, downloadUri, apk);
break;
case Downloader.ACTION_INTERRUPTED:
NotificationHelper.removeApk(getApkFromActive(urlString));
removeFromActive(urlString);
localBroadcastManager.unregisterReceiver(this);
cancelNotification(urlString);
break;
default:
throw new RuntimeException("intent action not handled!");
@ -329,9 +329,10 @@ public class InstallManagerService extends Service {
Apk apk;
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
// nothing to do
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installing, null);
break;
case Installer.ACTION_INSTALL_COMPLETE:
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installed, null);
Apk apkComplete = removeFromActive(downloadUrl);
PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName);
@ -339,6 +340,8 @@ public class InstallManagerService extends Service {
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, null);
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
@ -350,12 +353,7 @@ public class InstallManagerService extends Service {
ContentResolver resolver = context.getContentResolver();
app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo);
}
// show notification if app details is not visible
if (app != null && AppDetails.isAppVisible(app.packageName)) {
cancelNotification(downloadUrl);
} else {
notifyError(downloadUrl, app, errorMessage);
}
// TODO - show error
}
removeFromActive(downloadUrl);
localBroadcastManager.unregisterReceiver(this);
@ -364,14 +362,7 @@ public class InstallManagerService extends Service {
apk = intent.getParcelableExtra(Installer.EXTRA_APK);
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
// show notification if app details is not visible
if (AppDetails.isAppVisible(apk.packageName)) {
cancelNotification(downloadUrl);
} else {
notifyDownloadComplete(apk, downloadUrl, installPendingIntent);
}
NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, installPendingIntent);
break;
default:
throw new RuntimeException("intent action not handled!");
@ -383,81 +374,10 @@ public class InstallManagerService extends Service {
Installer.getInstallIntentFilter(downloadUri));
}
private NotificationCompat.Builder createNotificationBuilder(String urlString, Apk apk) {
int downloadUrlId = urlString.hashCode();
return new NotificationCompat.Builder(this)
.setAutoCancel(false)
.setOngoing(true)
.setContentIntent(getAppDetailsIntent(downloadUrlId, apk))
.setContentTitle(getString(R.string.downloading_apk, getAppName(apk)))
.addAction(R.drawable.ic_cancel_black_24dp, getString(R.string.cancel),
getCancelPendingIntent(urlString))
.setSmallIcon(android.R.drawable.stat_sys_download)
.setContentText(urlString)
.setProgress(100, 0, true);
}
private String getAppName(Apk apk) {
return ACTIVE_APPS.get(apk.packageName).name;
}
/**
* Get a {@link PendingIntent} for a {@link Notification} to send when it
* is clicked. {@link AppDetails} handles {@code Intent}s that are missing
* or bad {@link AppDetails#EXTRA_APPID}, so it does not need to be checked
* here.
*/
private PendingIntent getAppDetailsIntent(int requestCode, Apk apk) {
Intent notifyIntent = new Intent(getApplicationContext(), AppDetails.class)
.putExtra(AppDetails.EXTRA_APPID, apk.packageName);
return TaskStackBuilder.create(getApplicationContext())
.addParentStack(AppDetails.class)
.addNextIntent(notifyIntent)
.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. This must create a new {@code Builder}
* instance otherwise the progress/cancel stuff does not go away.
*
* @see <a href=https://code.google.com/p/android/issues/detail?id=47809> Issue 47809:
* Removing the progress bar from a notification should cause the notification's content
* text to return to normal size</a>
*/
private void notifyDownloadComplete(Apk apk, String urlString, PendingIntent installPendingIntent) {
String title;
try {
PackageManager pm = getPackageManager();
title = String.format(getString(R.string.tap_to_update_format),
pm.getApplicationLabel(pm.getApplicationInfo(apk.packageName, 0)));
} catch (PackageManager.NameNotFoundException e) {
String name = getAppName(apk);
if (TextUtils.isEmpty(name) || name.equals(new App().name)) {
ContentResolver resolver = getContentResolver();
App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo,
new String[]{Schema.AppMetadataTable.Cols.NAME});
if (app == null || TextUtils.isEmpty(app.name)) {
return; // do not have a name to display, so leave notification as is
}
name = app.name;
}
title = String.format(getString(R.string.tap_to_install_format), name);
}
int downloadUrlId = urlString.hashCode();
notificationManager.cancel(downloadUrlId);
Notification notification = new NotificationCompat.Builder(this)
.setAutoCancel(true)
.setOngoing(false)
.setContentTitle(title)
.setContentIntent(installPendingIntent)
.setSmallIcon(android.R.drawable.stat_sys_download_done)
.setContentText(getString(R.string.tap_to_install))
.build();
notificationManager.notify(downloadUrlId, notification);
}
private void notifyError(String urlString, App app, String text) {
int downloadUrlId = urlString.hashCode();
@ -493,15 +413,6 @@ public class InstallManagerService extends Service {
nm.notify(downloadUrlId, builder.build());
}
/**
* Cancel the {@link Notification} tied to {@code urlString}, which is the
* unique ID used to represent a given APK file. {@link String#hashCode()}
* converts {@code urlString} to the required {@code int}.
*/
private void cancelNotification(String urlString) {
notificationManager.cancel(urlString.hashCode());
}
private static void addToActive(String urlString, App app, Apk apk) {
ACTIVE_APKS.put(urlString, apk);
ACTIVE_APPS.put(app.packageName, app);

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
<path android:fillColor="#30000000" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
<group android:pivotX="12" android:pivotY="12" android:rotation="-90">
<path android:fillColor="#30000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</group>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="32dp" android:viewportHeight="24.0" android:viewportWidth="24.0" android:width="32dp">
<path android:fillColor="#30000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>