From 55c7a21c90385f145e323fdffde8b962954c11c4 Mon Sep 17 00:00:00 2001 From: mvp76 Date: Mon, 13 Feb 2017 15:46:37 +0100 Subject: [PATCH] Move app status handling to new AppUpdateStatusManager --- .../fdroid/fdroid/AppUpdateStatusManager.java | 310 ++++++++++++ .../java/org/fdroid/fdroid/FDroidApp.java | 3 +- .../org/fdroid/fdroid/NotificationHelper.java | 475 ++++++++---------- .../java/org/fdroid/fdroid/UpdateService.java | 65 +-- .../installer/InstallManagerService.java | 200 ++------ 5 files changed, 553 insertions(+), 500 deletions(-) create mode 100644 app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java diff --git a/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java new file mode 100644 index 000000000..6ccdea17f --- /dev/null +++ b/app/src/main/java/org/fdroid/fdroid/AppUpdateStatusManager.java @@ -0,0 +1,310 @@ +package org.fdroid.fdroid; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.support.v4.app.TaskStackBuilder; +import android.support.v4.content.LocalBroadcastManager; + +import org.fdroid.fdroid.data.Apk; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.installer.ErrorDialogActivity; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +public class AppUpdateStatusManager { + + static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange"; + static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add"; + static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change"; + static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove"; + static final String EXTRA_APK_URL = "urlstring"; + static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate"; + + private static final String LOGTAG = "AppUpdateStatusManager"; + + public enum Status { + Unknown, + UpdateAvailable, + Downloading, + ReadyToInstall, + Installing, + Installed, + InstallError + } + + public static AppUpdateStatusManager getInstance(Context context) { + return new AppUpdateStatusManager(context); + } + + class AppUpdateStatus { + final App app; + final Apk apk; + Status status; + PendingIntent intent; + int progressCurrent; + int progressMax; + String errorText; + + AppUpdateStatus(App app, Apk apk, Status status, PendingIntent intent) { + this.app = app; + this.apk = apk; + this.status = status; + this.intent = intent; + } + + String getUniqueKey() { + return apk.getUrl(); + } + } + + private final Context context; + private final LocalBroadcastManager localBroadcastManager; + private static final HashMap appMapping = new HashMap<>(); + private boolean isBatchUpdating; + + private AppUpdateStatusManager(Context context) { + this.context = context; + localBroadcastManager = LocalBroadcastManager.getInstance(context.getApplicationContext()); + } + + public AppUpdateStatus get(String key) { + synchronized (appMapping) { + return appMapping.get(key); + } + } + + public Collection getAll() { + synchronized (appMapping) { + return appMapping.values(); + } + } + + private void setApkInternal(Apk apk, Status status, PendingIntent intent) { + if (apk == null) { + return; + } + + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(apk.getUrl()); + if (status == null) { + // Remove + Utils.debugLog(LOGTAG, "Remove APK " + apk.apkName); + if (entry != null) { + appMapping.remove(apk.getUrl()); + notifyRemove(entry); + } + } else if (entry != null) { + // Update + Utils.debugLog(LOGTAG, "Update APK " + apk.apkName + " state to " + status.name()); + boolean isStatusUpdate = (entry.status != status); + entry.status = status; + entry.intent = intent; + // If intent not set, see if we need to create a default intent + if (entry.intent == null) { + entry.intent = getContentIntent(entry); + } + notifyChange(entry, isStatusUpdate); + } else { + // Add + Utils.debugLog(LOGTAG, "Add APK " + apk.apkName + " with state " + status.name()); + entry = createAppEntry(apk, status, intent); + // If intent not set, see if we need to create a default intent + if (entry.intent == null) { + entry.intent = getContentIntent(entry); + } + appMapping.put(entry.getUniqueKey(), entry); + notifyAdd(entry); + } + } + } + + private void notifyChange() { + if (!isBatchUpdating) { + localBroadcastManager.sendBroadcast(new Intent(BROADCAST_APPSTATUS_LIST_CHANGED)); + } + } + + private void notifyAdd(AppUpdateStatus entry) { + if (!isBatchUpdating) { + Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_ADDED); + broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey()); + localBroadcastManager.sendBroadcast(broadcastIntent); + } + } + + private void notifyChange(AppUpdateStatus entry, boolean isStatusUpdate) { + if (!isBatchUpdating) { + Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_CHANGED); + broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey()); + broadcastIntent.putExtra(EXTRA_IS_STATUS_UPDATE, isStatusUpdate); + localBroadcastManager.sendBroadcast(broadcastIntent); + } + } + + private void notifyRemove(AppUpdateStatus entry) { + if (!isBatchUpdating) { + Intent broadcastIntent = new Intent(BROADCAST_APPSTATUS_REMOVED); + broadcastIntent.putExtra(EXTRA_APK_URL, entry.getUniqueKey()); + localBroadcastManager.sendBroadcast(broadcastIntent); + } + } + + private AppUpdateStatus createAppEntry(Apk apk, Status status, PendingIntent intent) { + synchronized (appMapping) { + ContentResolver resolver = context.getContentResolver(); + App app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo); + AppUpdateStatus ret = new AppUpdateStatus(app, apk, status, intent); + appMapping.put(apk.getUrl(), ret); + return ret; + } + } + + /** + * Add an Apk to the AppUpdateStatusManager 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 void addApk(Apk apk, Status status, PendingIntent pendingIntent) { + setApkInternal(apk, status, pendingIntent); + } + + public void updateApk(String key, Status status, PendingIntent pendingIntent) { + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(key); + if (entry != null) { + setApkInternal(entry.apk, status, pendingIntent); + } + } + } + + public Apk getApk(String key) { + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(key); + if (entry != null) { + return entry.apk; + } + return null; + } + } + + public void removeApk(String key) { + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(key); + if (entry != null) { + setApkInternal(entry.apk, null, null); // remove + } + } + } + + public void updateApkProgress(String key, int max, int current) { + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(key); + if (entry != null) { + entry.progressMax = max; + entry.progressCurrent = current; + notifyChange(entry, false); + } + } + } + + public void setApkError(Apk apk, String errorText) { + synchronized (appMapping) { + AppUpdateStatus entry = appMapping.get(apk.getUrl()); + if (entry == null) { + entry = createAppEntry(apk, Status.InstallError, null); + } + entry.status = Status.InstallError; + entry.errorText = errorText; + entry.intent = getAppErrorIntent(entry); + notifyChange(entry, false); + } + } + + void startBatchUpdates() { + synchronized (appMapping) { + isBatchUpdating = true; + } + } + + void endBatchUpdates() { + synchronized (appMapping) { + isBatchUpdating = false; + notifyChange(); + } + } + + void clearAllUpdates() { + synchronized (appMapping) { + for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (entry.getValue().status != Status.Installed) { + it.remove(); + } + } + notifyChange(); + } + } + + void clearAllInstalled() { + synchronized (appMapping) { + for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { + Map.Entry entry = it.next(); + if (entry.getValue().status == Status.Installed) { + it.remove(); + } + } + notifyChange(); + } + } + + private PendingIntent getContentIntent(AppUpdateStatus 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" + return getAppDetailsIntent(entry.apk); + } else if (entry.status == Status.ReadyToInstall) { + return getAppDetailsIntent(entry.apk); + } else if (entry.status == Status.InstallError) { + return getAppErrorIntent(entry); + } + return null; + } + + /** + * 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(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(0, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private PendingIntent getAppErrorIntent(AppUpdateStatus entry) { + String title = String.format(context.getString(R.string.install_error_notify_title), entry.app.name); + + Intent errorDialogIntent = new Intent(context, ErrorDialogActivity.class); + errorDialogIntent.putExtra( + ErrorDialogActivity.EXTRA_TITLE, title); + errorDialogIntent.putExtra( + ErrorDialogActivity.EXTRA_MESSAGE, entry.errorText); + return PendingIntent.getActivity( + context, + 0, + errorDialogIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + } +} diff --git a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java index 2507c46d2..ec57e4d45 100644 --- a/app/src/main/java/org/fdroid/fdroid/FDroidApp.java +++ b/app/src/main/java/org/fdroid/fdroid/FDroidApp.java @@ -220,8 +220,6 @@ 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, @@ -264,6 +262,7 @@ public class FDroidApp extends Application { CleanCacheService.schedule(this); + NotificationHelper.create(getApplicationContext()); UpdateService.schedule(getApplicationContext()); bluetoothAdapter = getBluetoothAdapter(); diff --git a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java index 18c8190f5..2b7bcae46 100644 --- a/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java +++ b/app/src/main/java/org/fdroid/fdroid/NotificationHelper.java @@ -1,20 +1,17 @@ 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.os.Build; 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.support.v4.content.LocalBroadcastManager; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.StyleSpan; @@ -22,278 +19,143 @@ 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 { +class NotificationHelper { - private static final String BROADCAST_NOTIFICATIONS_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.updates.cleared"; + private static final String BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED = "org.fdroid.fdroid.installer.notifications.allupdates.cleared"; + private static final String BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED = "org.fdroid.fdroid.installer.notifications.allinstalled.cleared"; + private static final String BROADCAST_NOTIFICATIONS_UPDATE_CLEARED = "org.fdroid.fdroid.installer.notifications.update.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 NOTIFY_ID_UPDATES = 1; + private static final int NOTIFY_ID_INSTALLED = 2; 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 EXTRA_NOTIFICATION_KEY = "key"; 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 final String LOGTAG = "NotificationHelper"; private static NotificationHelper instance; - public static void init(Context context) { - instance = new NotificationHelper(context); - } - - private static NotificationHelper getInstance() { + public static NotificationHelper create(Context context) { + if (instance == null) { + instance = new NotificationHelper(context.getApplicationContext()); + } 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 appMapping; - private boolean isBatchUpdating; - private ArrayList updates; - private ArrayList installed; + private final AppUpdateStatusManager appUpdateStatusMananger; private NotificationHelper(Context context) { this.context = context; + appUpdateStatusMananger = AppUpdateStatusManager.getInstance(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_ALL_UPDATES_CLEARED); + filter.addAction(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED); + filter.addAction(BROADCAST_NOTIFICATIONS_UPDATE_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<>(); + context.registerReceiver(receiverNotificationsCleared, filter); + filter = new IntentFilter(); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED); + filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED); + LocalBroadcastManager.getInstance(context).registerReceiver(receiverAppStatusChanges, filter); } - 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> it = appMapping.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if(entry.getValue().status != Status.Installed) { - it.remove(); - } - } - } - - private void clearAllInstalledInternal() { - for(Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if(entry.getValue().status == Status.Installed) { - it.remove(); - } - } + private boolean useStackedNotifications() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; } private void updateSummaryNotifications() { - if (!isBatchUpdating) { - // Get the list of updates and installed available - updates.clear(); - installed.clear(); - for (Iterator> it = appMapping.entrySet().iterator(); it.hasNext(); ) { - Map.Entry entry = it.next(); - if (entry.getValue().status != Status.Installed) { - updates.add(entry.getValue()); - } else { - installed.add(entry.getValue()); - } - } + if (!notificationManager.areNotificationsEnabled()) { + return; + } - NotificationCompat.Builder builder; - if (updates.size() == 0) { - // No updates, remove summary - notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES); + // Get the list of updates and installed available + ArrayList updates = new ArrayList<>(); + ArrayList installed = new ArrayList<>(); + for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusMananger.getAll()) { + if (entry.status == AppUpdateStatusManager.Status.Unknown) { + continue; + } else if (entry.status != AppUpdateStatusManager.Status.Installed) { + updates.add(entry); } 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()); + installed.add(entry); } } + + NotificationCompat.Builder builder; + if (updates.size() == 0) { + // No updates, remove summary + notificationManager.cancel(GROUP_UPDATES, NOTIFY_ID_UPDATES); + } else if (updates.size() == 1 && !useStackedNotifications()) { + // If we use stacked notifications we have already created one. + doCreateNotification(updates.get(0)); + } 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 if (installed.size() == 1 && !useStackedNotifications()) { + // If we use stacked notifications we have already created one. + doCreateNotification(installed.get(0)); + } else { + builder = createInstalledSummaryNotification(installed); + notificationManager.notify(GROUP_INSTALLED, NOTIFY_ID_INSTALLED, builder.build()); + } } - private void createNotificationForAppEntry(AppEntry entry) { + private void createNotification(AppUpdateStatusManager.AppUpdateStatus entry) { + if (useStackedNotifications() && notificationManager.areNotificationsEnabled() && entry.status != AppUpdateStatusManager.Status.Unknown) { + doCreateNotification(entry); + } + } + + private void doCreateNotification(AppUpdateStatusManager.AppUpdateStatus entry) { NotificationCompat.Builder builder; - if (entry.status == Status.Installed) { + int id; + if (entry.status == AppUpdateStatusManager.Status.Installed) { builder = createInstalledNotification(entry); + id = NOTIFY_ID_INSTALLED; + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_UPDATES); } else { builder = createUpdateNotification(entry); + id = NOTIFY_ID_UPDATES; + notificationManager.cancel(entry.getUniqueKey(), NOTIFY_ID_INSTALLED); } - notificationManager.notify(entry.getTag(), entry.getId(), builder.build()); + notificationManager.notify(entry.getUniqueKey(), id, 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); + private NotificationCompat.Action getAction(AppUpdateStatusManager.AppUpdateStatus entry) { + if (entry.intent != null) { + if (entry.status == AppUpdateStatusManager.Status.UpdateAvailable) { + return new NotificationCompat.Action(R.drawable.ic_notify_update_24dp, "Update", entry.intent); + } else if (entry.status == AppUpdateStatusManager.Status.Downloading || entry.status == AppUpdateStatusManager.Status.Installing) { + return new NotificationCompat.Action(R.drawable.ic_notify_cancel_24dp, "Cancel", entry.intent); + } else if (entry.status == AppUpdateStatusManager.Status.ReadyToInstall) { + return new NotificationCompat.Action(R.drawable.ic_notify_install_24dp, "Install", entry.intent); } - 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) { + private String getSingleItemTitleString(App app, AppUpdateStatusManager.Status status) { switch (status) { case UpdateAvailable: return "Update Available"; @@ -305,11 +167,13 @@ public class NotificationHelper { return app.name; case Installed: return app.name; + case InstallError: + return "Install Failed"; } return ""; } - private String getSingleItemContentString(App app, Status status) { + private String getSingleItemContentString(App app, AppUpdateStatusManager.Status status) { switch (status) { case UpdateAvailable: return app.name; @@ -321,11 +185,13 @@ public class NotificationHelper { return String.format("Installing \"%s\"...", app.name); case Installed: return "Successfully installed"; + case InstallError: + return "Install Failed"; } return ""; } - private String getMultiItemContentString(App app, Status status) { + private String getMultiItemContentString(App app, AppUpdateStatusManager.Status status) { switch (status) { case UpdateAvailable: return "Update available"; @@ -337,72 +203,64 @@ public class NotificationHelper { return "Installing"; case Installed: return "Successfully installed"; + case InstallError: + return "Install Failed"; } 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) { + private NotificationCompat.Builder createUpdateNotification(AppUpdateStatusManager.AppUpdateStatus entry) { App app = entry.app; - Status status = entry.status; + AppUpdateStatusManager.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)); + // TODO - why? + final int icon = Build.VERSION.SDK_INT >= 11 ? R.drawable.ic_stat_notify_updates : R.drawable.ic_launcher; + NotificationCompat.Builder builder = new NotificationCompat.Builder(context) - .setAutoCancel(true) + .setAutoCancel(false) .setLargeIcon(iconLarge) - .setSmallIcon(R.drawable.ic_stat_notify_updates) + .setSmallIcon(icon) .setContentTitle(getSingleItemTitleString(app, status)) .setContentText(getSingleItemContentString(app, status)) .setGroup(GROUP_UPDATES); + // Handle intents + // + if (entry.intent != null) { + builder.setContentIntent(entry.intent); + } + // 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 (status == AppUpdateStatusManager.Status.Downloading) { if (entry.progressMax == 0) builder.setProgress(100, 0, true); else builder.setProgress(entry.progressMax, entry.progressCurrent, false); - } else if (status == Status.Installing) { + } else if (status == AppUpdateStatusManager.Status.Installing) { builder.setProgress(100, 0, true); // indeterminate bar } - Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_NOTIFICATION_DELETED); - intentDeleted.putExtra(EXTRA_NOTIFICATION_TAG, entry.getId()); + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATE_CLEARED); + intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); return builder; } - private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList updates) { + private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList updates) { String title = String.format("%d Updates", updates.size()); StringBuilder text = new StringBuilder(); @@ -410,9 +268,9 @@ public class NotificationHelper { inboxStyle.setBigContentTitle(title); for (int i = 0; i < MAX_UPDATES_TO_SHOW && i < updates.size(); i++) { - AppEntry entry = updates.get(i); + AppUpdateStatusManager.AppUpdateStatus entry = updates.get(i); App app = entry.app; - Status status = entry.status; + AppUpdateStatusManager.Status status = entry.status; String content = getMultiItemContentString(app, status); SpannableStringBuilder sb = new SpannableStringBuilder(app.name); @@ -425,14 +283,10 @@ public class NotificationHelper { 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); - + 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)); + } // Intent to open main app list Intent intentObject = new Intent(context, FDroid.class); @@ -445,16 +299,21 @@ public class NotificationHelper { .setContentTitle(title) .setContentText(text) .setContentIntent(piAction) - .setStyle(inboxStyle) - .setGroup(GROUP_UPDATES) - .setGroupSummary(true); - Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_UPDATES_CLEARED); + .setStyle(inboxStyle); + if (BuildConfig.DEBUG) { + builder.setPriority(NotificationCompat.PRIORITY_LOW); // To make not at top of list! + } + if (useStackedNotifications()) { + builder.setGroup(GROUP_UPDATES) + .setGroupSummary(true); + } + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); return builder; } - private NotificationCompat.Builder createInstalledNotification(AppEntry entry) { + private NotificationCompat.Builder createInstalledNotification(AppUpdateStatusManager.AppUpdateStatus entry) { App app = entry.app; int largeIconSize = context.getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); @@ -474,14 +333,14 @@ public class NotificationHelper { 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()); + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); + intentDeleted.putExtra(EXTRA_NOTIFICATION_KEY, entry.getUniqueKey()); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); return builder; } - private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList installed) { + private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList installed) { String title = String.format("%d Apps Installed", installed.size()); StringBuilder text = new StringBuilder(); @@ -489,7 +348,7 @@ public class NotificationHelper { bigTextStyle.setBigContentTitle(title); for (int i = 0; i < MAX_INSTALLED_TO_SHOW && i < installed.size(); i++) { - AppEntry entry = installed.get(i); + AppUpdateStatusManager.AppUpdateStatus entry = installed.get(i); App app = entry.app; if (text.length() > 0) text.append(", "); @@ -511,12 +370,76 @@ public class NotificationHelper { .setSmallIcon(R.drawable.ic_launcher) .setContentTitle(title) .setContentText(text) - .setContentIntent(piAction) - .setGroup(GROUP_INSTALLED) - .setGroupSummary(true); - Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED); + .setContentIntent(piAction); + if (useStackedNotifications()) { + builder.setGroup(GROUP_INSTALLED) + .setGroupSummary(true); + } + Intent intentDeleted = new Intent(BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED); PendingIntent piDeleted = PendingIntent.getBroadcast(context, 0, intentDeleted, 0); builder.setDeleteIntent(piDeleted); return builder; } + + private BroadcastReceiver receiverNotificationsCleared = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case BROADCAST_NOTIFICATIONS_ALL_UPDATES_CLEARED: + appUpdateStatusMananger.clearAllUpdates(); + break; + case BROADCAST_NOTIFICATIONS_ALL_INSTALLED_CLEARED: + appUpdateStatusMananger.clearAllInstalled(); + break; + case BROADCAST_NOTIFICATIONS_UPDATE_CLEARED: + break; + case BROADCAST_NOTIFICATIONS_INSTALLED_CLEARED: + String key = intent.getStringExtra(EXTRA_NOTIFICATION_KEY); + appUpdateStatusMananger.removeApk(key); + break; + } + } + }; + + private BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + switch (intent.getAction()) { + case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED: + notificationManager.cancelAll(); + for (AppUpdateStatusManager.AppUpdateStatus entry : appUpdateStatusMananger.getAll()) { + createNotification(entry); + } + updateSummaryNotifications(); + break; + case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED: { + String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusMananger.get(url); + if (entry != null) { + createNotification(entry); + } + updateSummaryNotifications(); + break; + } + case AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED: { + String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + AppUpdateStatusManager.AppUpdateStatus entry = appUpdateStatusMananger.get(url); + if (entry != null) { + createNotification(entry); + } + if (intent.getBooleanExtra(AppUpdateStatusManager.EXTRA_IS_STATUS_UPDATE, false)) { + updateSummaryNotifications(); + } + break; + } + case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED: { + String url = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL); + notificationManager.cancel(url, NOTIFY_ID_INSTALLED); + notificationManager.cancel(url, NOTIFY_ID_UPDATES); + updateSummaryNotifications(); + break; + } + } + } + }; } diff --git a/app/src/main/java/org/fdroid/fdroid/UpdateService.java b/app/src/main/java/org/fdroid/fdroid/UpdateService.java index ce83afce8..11a11cf4f 100644 --- a/app/src/main/java/org/fdroid/fdroid/UpdateService.java +++ b/app/src/main/java/org/fdroid/fdroid/UpdateService.java @@ -37,7 +37,6 @@ import android.os.Process; import android.os.SystemClock; import android.preference.PreferenceManager; 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; @@ -79,7 +78,6 @@ public class UpdateService extends IntentService { private static final String STATE_LAST_UPDATED = "lastUpdateCheck"; private static final int NOTIFY_ID_UPDATING = 0; - private static final int NOTIFY_ID_UPDATES_AVAILABLE = 1; private static final int FLAG_NET_UNAVAILABLE = 0; private static final int FLAG_NET_METERED = 1; @@ -89,6 +87,7 @@ public class UpdateService extends IntentService { private NotificationManager notificationManager; private NotificationCompat.Builder notificationBuilder; + private AppUpdateStatusManager appUpdateStatusManager; public UpdateService() { super("UpdateService"); @@ -147,6 +146,7 @@ public class UpdateService extends IntentService { .setOngoing(true) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setContentTitle(getString(R.string.update_notification_title)); + appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); // Android docs are a little sketchy, however it seems that Gingerbread is the last // sdk that made a content intent mandatory: @@ -469,39 +469,6 @@ public class UpdateService extends IntentService { } } - private PendingIntent createNotificationIntent() { - Intent notifyIntent = new Intent(this, FDroid.class).putExtra(FDroid.EXTRA_TAB_UPDATE, true); - TaskStackBuilder stackBuilder = TaskStackBuilder - .create(this).addParentStack(FDroid.class) - .addNextIntent(notifyIntent); - return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); - } - - private static final int MAX_UPDATES_TO_SHOW = 5; - - private NotificationCompat.Style createNotificationBigStyle(Cursor hasUpdates) { - - final String contentText = hasUpdates.getCount() > 1 - ? getString(R.string.many_updates_available, hasUpdates.getCount()) - : getString(R.string.one_update_available); - - NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - inboxStyle.setBigContentTitle(contentText); - hasUpdates.moveToFirst(); - for (int i = 0; i < Math.min(hasUpdates.getCount(), MAX_UPDATES_TO_SHOW); i++) { - App app = new App(hasUpdates); - hasUpdates.moveToNext(); - inboxStyle.addLine(app.name + " (" + app.installedVersionName + " → " + app.getSuggestedVersionName() + ")"); - } - - if (hasUpdates.getCount() > MAX_UPDATES_TO_SHOW) { - int diff = hasUpdates.getCount() - MAX_UPDATES_TO_SHOW; - inboxStyle.setSummaryText(getString(R.string.update_notification_more, diff)); - } - - return inboxStyle; - } - private void autoDownloadUpdates() { Cursor cursor = getContentResolver().query( AppProvider.getCanUpdateUri(), @@ -520,37 +487,17 @@ 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++) { + appUpdateStatusManager.startBatchUpdates(); + for (int i = 0; i < 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); + appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.UpdateAvailable, null); } - NotificationHelper.endBatchUpdates(); + appUpdateStatusManager.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; - - final String contentText = hasUpdates.getCount() > 1 - ? getString(R.string.many_updates_available, hasUpdates.getCount()) - : getString(R.string.one_update_available); - - NotificationCompat.Builder builder = - new NotificationCompat.Builder(this) - .setAutoCancel(true) - .setContentTitle(getString(R.string.fdroid_updates_available)) - .setSmallIcon(icon) - .setContentIntent(createNotificationIntent()) - .setContentText(contentText) - .setStyle(createNotificationBigStyle(hasUpdates)); - - notificationManager.notify(NOTIFY_ID_UPDATES_AVAILABLE, builder.build()); } /** diff --git a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java index e50d2ccec..528c9bc7b 100644 --- a/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java +++ b/app/src/main/java/org/fdroid/fdroid/installer/InstallManagerService.java @@ -1,44 +1,31 @@ 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.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.net.Uri; import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.support.v4.app.TaskStackBuilder; -import android.support.v4.content.IntentCompat; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import org.apache.commons.io.FileUtils; import org.apache.commons.io.filefilter.WildcardFileFilter; -import org.fdroid.fdroid.AppDetails; +import org.fdroid.fdroid.AppUpdateStatusManager; 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; import org.fdroid.fdroid.data.Apk; import org.fdroid.fdroid.data.App; -import org.fdroid.fdroid.data.AppProvider; -import org.fdroid.fdroid.data.Schema; import org.fdroid.fdroid.net.Downloader; import org.fdroid.fdroid.net.DownloaderService; import java.io.File; import java.io.FileFilter; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; /** * Manages the whole process when a background update triggers an install or the user @@ -89,16 +76,17 @@ public class InstallManagerService extends Service { * matching the {@link App}s in {@code ACTIVE_APPS}. The key is the download URL, as * in {@link Apk#getUrl()} or {@code urlString}. */ - private static final HashMap ACTIVE_APKS = new HashMap<>(3); + //private static final HashMap ACTIVE_APKS = new HashMap<>(3); /** * The collection of {@link App}s that are actively going through this whole process, * matching the {@link Apk}s in {@code ACTIVE_APKS}. The key is the * {@code packageName} of the app. */ - private static final HashMap ACTIVE_APPS = new HashMap<>(3); + //private static final HashMap ACTIVE_APPS = new HashMap<>(3); private LocalBroadcastManager localBroadcastManager; + private AppUpdateStatusManager appUpdateStatusManager; /** * This service does not use binding, so no need to implement this method @@ -113,18 +101,14 @@ public class InstallManagerService extends Service { super.onCreate(); Utils.debugLog(TAG, "creating Service"); localBroadcastManager = LocalBroadcastManager.getInstance(this); + appUpdateStatusManager = AppUpdateStatusManager.getInstance(this); BroadcastReceiver br = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String packageName = intent.getData().getSchemeSpecificPart(); - for (Map.Entry entry : ACTIVE_APKS.entrySet()) { - if (TextUtils.equals(packageName, entry.getValue().packageName)) { - String urlString = entry.getKey(); - NotificationHelper.removeApk(getApkFromActive(urlString)); - break; - } - } + //TODO: do we need to mark as installed, or is this handled by other code already? + //appUpdateStatusManager.removeApk(packageName); } }; IntentFilter intentFilter = new IntentFilter(); @@ -146,10 +130,12 @@ public class InstallManagerService extends Service { String action = intent.getAction(); if (ACTION_CANCEL.equals(action)) { DownloaderService.cancel(this, urlString); - Apk apk = getApkFromActive(urlString); - DownloaderService.cancel(this, apk.getPatchObbUrl()); - DownloaderService.cancel(this, apk.getMainObbUrl()); - NotificationHelper.removeApk(apk); + Apk apk = appUpdateStatusManager.getApk(urlString); + if (apk != null) { + DownloaderService.cancel(this, apk.getPatchObbUrl()); + DownloaderService.cancel(this, apk.getMainObbUrl()); + } + appUpdateStatusManager.removeApk(urlString); return START_NOT_STICKY; } else if (!ACTION_INSTALL.equals(action)) { Utils.debugLog(TAG, "Ignoring " + intent + " as it is not an " + ACTION_INSTALL + " intent"); @@ -165,7 +151,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."); - NotificationHelper.removeApk(getApkFromActive(urlString)); + appUpdateStatusManager.removeApk(urlString); return START_NOT_STICKY; } @@ -175,7 +161,7 @@ public class InstallManagerService extends Service { Utils.debugLog(TAG, "Intent had null EXTRA_APP and/or EXTRA_APK: " + intent); return START_NOT_STICKY; } - addToActive(urlString, app, apk); + appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.Unknown, null); registerApkDownloaderReceivers(urlString); getObb(urlString, apk.getMainObbUrl(), apk.getMainObbFile(), apk.obbMainFileSha256); @@ -282,32 +268,31 @@ public class InstallManagerService extends Service { intentObject.setAction(ACTION_CANCEL); intentObject.setData(downloadUri); PendingIntent action = PendingIntent.getService(context, 0, intentObject, 0); - NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.Downloading, action); + appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.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); - NotificationHelper.setApkProgress(getApkFromActive(urlString), totalBytes, bytesRead); + appUpdateStatusManager.updateApkProgress(urlString, totalBytes, bytesRead); break; case Downloader.ACTION_COMPLETE: File localFile = new File(intent.getStringExtra(Downloader.EXTRA_DOWNLOAD_PATH)); Uri localApkUri = Uri.fromFile(localFile); Utils.debugLog(TAG, "download completed of " + urlString + " to " + localApkUri); - - NotificationHelper.setApk(getApkFromActive(urlString), NotificationHelper.Status.ReadyToInstall, null); + appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.ReadyToInstall, null); localBroadcastManager.unregisterReceiver(this); registerInstallerReceivers(downloadUri); - Apk apk = ACTIVE_APKS.get(urlString); - InstallerService.install(context, localApkUri, downloadUri, apk); + Apk apk = appUpdateStatusManager.getApk(urlString); + if (apk != null) { + InstallerService.install(context, localApkUri, downloadUri, apk); + } break; case Downloader.ACTION_INTERRUPTED: - NotificationHelper.removeApk(getApkFromActive(urlString)); - - removeFromActive(urlString); + appUpdateStatusManager.updateApk(urlString, AppUpdateStatusManager.Status.UpdateAvailable, null); localBroadcastManager.unregisterReceiver(this); break; default: @@ -329,18 +314,18 @@ public class InstallManagerService extends Service { Apk apk; switch (intent.getAction()) { case Installer.ACTION_INSTALL_STARTED: - NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.Installing, null); + appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.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); - + appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.Installed, null); + Apk apkComplete = appUpdateStatusManager.getApk(downloadUrl); + if (apkComplete != null) { + PackageManagerCompat.setInstaller(getPackageManager(), apkComplete.packageName); + } localBroadcastManager.unregisterReceiver(this); break; case Installer.ACTION_INSTALL_INTERRUPTED: - NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, null); + appUpdateStatusManager.updateApk(downloadUrl, AppUpdateStatusManager.Status.ReadyToInstall, null); apk = intent.getParcelableExtra(Installer.EXTRA_APK); String errorMessage = @@ -348,21 +333,21 @@ public class InstallManagerService extends Service { // show notification if app details is not visible if (!TextUtils.isEmpty(errorMessage)) { - App app = getAppFromActive(downloadUrl); - if (app == null) { - ContentResolver resolver = context.getContentResolver(); - app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo); - } + appUpdateStatusManager.setApkError(apk, errorMessage); +// App app = getAppFromActive(downloadUrl); +// if (app == null) { +// ContentResolver resolver = context.getContentResolver(); +// app = AppProvider.Helper.findSpecificApp(resolver, apk.packageName, apk.repo); +// } // TODO - show error } - removeFromActive(downloadUrl); localBroadcastManager.unregisterReceiver(this); break; case Installer.ACTION_INSTALL_USER_INTERACTION: apk = intent.getParcelableExtra(Installer.EXTRA_APK); PendingIntent installPendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI); - NotificationHelper.setApk(getApkFromActive(downloadUrl), NotificationHelper.Status.ReadyToInstall, installPendingIntent); + appUpdateStatusManager.addApk(apk, AppUpdateStatusManager.Status.ReadyToInstall, installPendingIntent); break; default: throw new RuntimeException("intent action not handled!"); @@ -374,98 +359,6 @@ public class InstallManagerService extends Service { Installer.getInstallIntentFilter(downloadUri)); } - private String getAppName(Apk apk) { - return ACTIVE_APPS.get(apk.packageName).name; - } - - private void notifyError(String urlString, App app, String text) { - int downloadUrlId = urlString.hashCode(); - - String name; - if (app == null) { - // if we have nothing else, show the APK filename - String path = Uri.parse(urlString).getPath(); - name = path.substring(path.lastIndexOf('/'), path.length()); - } else { - name = app.name; - } - String title = String.format(getString(R.string.install_error_notify_title), name); - - Intent errorDialogIntent = new Intent(this, ErrorDialogActivity.class); - errorDialogIntent.putExtra( - ErrorDialogActivity.EXTRA_TITLE, title); - errorDialogIntent.putExtra( - ErrorDialogActivity.EXTRA_MESSAGE, text); - PendingIntent errorDialogPendingIntent = PendingIntent.getActivity( - getApplicationContext(), - downloadUrlId, - errorDialogIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - - NotificationCompat.Builder builder = - new NotificationCompat.Builder(this) - .setAutoCancel(true) - .setContentTitle(title) - .setContentIntent(errorDialogPendingIntent) - .setSmallIcon(R.drawable.ic_issues) - .setContentText(text); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.notify(downloadUrlId, builder.build()); - } - - private static void addToActive(String urlString, App app, Apk apk) { - ACTIVE_APKS.put(urlString, apk); - ACTIVE_APPS.put(app.packageName, app); - } - - /** - * Always returns an {@link Apk} instance to avoid annoying null guards. - */ - private static Apk getApkFromActive(String urlString) { - Apk apk = ACTIVE_APKS.get(urlString); - if (apk == null) { - return new Apk(); - } else { - return apk; - } - } - - /** - * Remove the {@link App} and {@Apk} instances that are associated with - * {@code urlString} from the {@link Map} of active apps. This can be - * called after this service has been destroyed and recreated based on the - * {@link BroadcastReceiver}s, in which case {@code urlString} would not - * find anything in the active maps. - */ - private static App getAppFromActive(String urlString) { - return ACTIVE_APPS.get(getApkFromActive(urlString).packageName); - } - - /** - * Remove the URL from this service, and return the {@link Apk}. This returns - * an empty {@code Apk} instance if we get a null one so the code doesn't need - * lots of null guards. - */ - private static Apk removeFromActive(String urlString) { - Apk apk = ACTIVE_APKS.remove(urlString); - if (apk == null) { - return new Apk(); - } - ACTIVE_APPS.remove(apk.packageName); - return apk; - } - - private PendingIntent getCancelPendingIntent(String urlString) { - Intent intent = new Intent(this, InstallManagerService.class) - .setData(Uri.parse(urlString)) - .setAction(ACTION_CANCEL) - .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | IntentCompat.FLAG_ACTIVITY_CLEAR_TASK); - return PendingIntent.getService(this, - urlString.hashCode(), - intent, - PendingIntent.FLAG_UPDATE_CURRENT); - } - /** * 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 @@ -492,23 +385,4 @@ public class InstallManagerService extends Service { intent.setData(Uri.parse(urlString)); context.startService(intent); } - - /** - * Returns a {@link Set} of the {@code urlString}s that are currently active. - * {@code urlString}s are used as unique IDs throughout the - * {@code InstallManagerService} process, either as a {@code String} or as an - * {@code int} from {@link String#hashCode()}. - */ - public static Set getActiveDownloadUrls() { - return ACTIVE_APKS.keySet(); - } - - /** - * Returns a {@link Set} of the {@code packageName}s that are currently active. - * {@code packageName}s are used as unique IDs for apps throughout all of - * Android, F-Droid, and other apps stores. - */ - public static Set getActivePackageNames() { - return ACTIVE_APPS.keySet(); - } }