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<String, AppUpdateStatus> 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<AppUpdateStatus> 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<Map.Entry<String, AppUpdateStatus>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry<String, AppUpdateStatus> entry = it.next();
+                if (entry.getValue().status != Status.Installed) {
+                    it.remove();
+                }
+            }
+            notifyChange();
+        }
+    }
+
+    void clearAllInstalled() {
+        synchronized (appMapping) {
+            for (Iterator<Map.Entry<String, AppUpdateStatus>> it = appMapping.entrySet().iterator(); it.hasNext(); ) {
+                Map.Entry<String, AppUpdateStatus> 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<String, AppEntry> appMapping;
-    private boolean isBatchUpdating;
-    private ArrayList<AppEntry> updates;
-    private ArrayList<AppEntry> 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<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 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<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());
-                }
-            }
+        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<AppUpdateStatusManager.AppUpdateStatus> updates = new ArrayList<>();
+        ArrayList<AppUpdateStatusManager.AppUpdateStatus> 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<AppEntry> updates) {
+    private NotificationCompat.Builder createUpdateSummaryNotification(ArrayList<AppUpdateStatusManager.AppUpdateStatus> 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<AppEntry> installed) {
+    private NotificationCompat.Builder createInstalledSummaryNotification(ArrayList<AppUpdateStatusManager.AppUpdateStatus> 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<String, Apk> ACTIVE_APKS = new HashMap<>(3);
+    //private static final HashMap<String, Apk> 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<String, App> ACTIVE_APPS = new HashMap<>(3);
+    //private static final HashMap<String, App> 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<String, Apk> 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<String> 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<String> getActivePackageNames() {
-        return ACTIVE_APPS.keySet();
-    }
 }