/* * Copyright (C) 2014 AChep@xda <artemchep@gmail.com> * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package com.achep.acdisplay.notifications; import android.annotation.SuppressLint; import android.app.NotificationManager; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.service.notification.StatusBarNotification; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import com.achep.acdisplay.App; import com.achep.acdisplay.Config; import com.achep.acdisplay.blacklist.AppConfig; import com.achep.acdisplay.blacklist.Blacklist; import com.achep.base.AppHeap; import com.achep.base.Device; import com.achep.base.content.ConfigBase; import com.achep.base.interfaces.IOnLowMemory; import com.achep.base.interfaces.ISubscriptable; import com.achep.base.tests.Check; import com.achep.base.utils.Operator; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import static com.achep.base.Build.DEBUG; /** * Created by Artem on 27.12.13. */ public class NotificationPresenter implements NotificationList.OnNotificationListChangedListener, ISubscriptable<NotificationPresenter.OnNotificationListChangedListener>, IOnLowMemory { private static final String TAG = "NotificationPresenter"; private static final String WAKE_LOCK_TAG = "Notification pool post/remove lock."; /** * {@code true} to filter the noisy flow of same notifications, * {@code false} to handle all notifications' updates normally. */ private static final boolean FILTER_NOISY_NOTIFICATIONS = true; private static final int FRESH_NOTIFICATION_EXPIRY_TIME = 4000; // 4 sec. public static final int FLAG_SILENCE = 1; public static final int FLAG_IMMEDIATELY = 1 << 1; public static final int EVENT_BATH = 0; public static final int EVENT_POSTED = 1; public static final int EVENT_CHANGED = 2; public static final int EVENT_CHANGED_SPAM = 3; public static final int EVENT_REMOVED = 4; @NonNull public static String getEventName(int event) { switch (event) { case EVENT_POSTED: return "EVENT_POSTED"; case EVENT_CHANGED: return "EVENT_CHANGED"; case EVENT_CHANGED_SPAM: return "EVENT_CHANGED_SPAM"; case EVENT_REMOVED: return "EVENT_REMOVED"; case EVENT_BATH: return "EVENT_BATH"; default: return "UNKNOWN_VALUE"; } } private static final int RESULT_SUCCESS = 1; private static final int RESULT_SPAM = -1; private static NotificationPresenter sNotificationPresenter; private final NotificationList mGList; private final NotificationList mLList; private final Set<String> mGroupsWithSummaries; private volatile OnNotificationPostedListener mMainListener; private final ArrayList<WeakReference<OnNotificationListChangedListener>> mListenersRefs; private final ArrayList<NotificationListChange> mFrozenEvents; private volatile int mFreezeLevel; // Threading private final Handler mHandler; private final NotificationPrProxy mProxy; private final NotificationSpamFilter mFilter; final Object monitor = new Object(); //-- HANDLING CONFIG & BLACKLIST ------------------------------------------ private final Config mConfig; private final Blacklist mBlacklist; // Do not make local! private final ConfigListener mConfigListener; private final BlacklistListener mBlacklistListener; /** * Listens to config to update notification list when needed. */ private class ConfigListener implements ConfigBase.OnConfigChangedListener { private volatile int mMinPriority; private volatile int mMaxPriority; public ConfigListener(@NonNull Config config) { mMinPriority = config.getNotifyMinPriority(); mMaxPriority = config.getNotifyMaxPriority(); } @Override public void onConfigChanged(@NonNull ConfigBase configBase, @NonNull String key, @NonNull Object value) { synchronized (monitor) { Check.getInstance().isInMainThread(); onConfigChangedSynced(key, value); } } public void onConfigChangedSynced(@NonNull String key, @NonNull Object value) { boolean enabled; int v; switch (key) { case Config.KEY_ENABLED: rebuildLocalList(); break; case Config.KEY_NOTIFY_MIN_PRIORITY: v = (int) value; handleNotifyPriorityChanged(v, mMinPriority); mMinPriority = v; break; case Config.KEY_NOTIFY_MAX_PRIORITY: v = (int) value; handleNotifyPriorityChanged(v, mMaxPriority); mMaxPriority = v; break; case Config.KEY_UI_DYNAMIC_BACKGROUND_MODE: enabled = Operator.bitAnd((int) value, Config.DYNAMIC_BG_NOTIFICATION_MASK); for (OpenNotification notification : mLList) { if (enabled) { notification.loadBackgroundAsync(); } else { notification.clearBackground(); } } break; case Config.KEY_UI_EMOTICONS: boolean b = (boolean) value; for (OpenNotification n : mGList) { n.setEmoticonsEnabled(b); } break; } } private void handleNotifyPriorityChanged(int a, int b) { if (a > b) { int k = a; a = b; b = k; // FIXME: This swapping method doesn't work on Java, but does work on C++ // a -= b += a -= b *= -1; } final int lower = a, higher = b; rebuildLocalList(new RebuildConfirmatory() { @Override public boolean needsRebuild(@NonNull OpenNotification n) { int priority = n.getNotification().priority; return priority >= lower && priority <= higher; } }); } } private class BlacklistListener extends Blacklist.OnBlacklistChangedListener { @Override public void onBlacklistChanged( @NonNull AppConfig configNew, @NonNull AppConfig configOld, int diff) { boolean hiddenNew = configNew.isHidden(); boolean hiddenOld = configOld.isHidden(); boolean nonClearableEnabledNew = configNew.isNonClearableEnabled(); boolean nonClearableEnabledOld = configOld.isNonClearableEnabled(); // Check if something important has changed. if (hiddenNew != hiddenOld || nonClearableEnabledNew != nonClearableEnabledOld) { synchronized (monitor) { // TODO: Check.getInstance().isInMainThread(); handlePackageVisibilityChanged(configNew.packageName); } } } private void handlePackageVisibilityChanged(@NonNull final String packageName) { rebuildLocalList(new RebuildConfirmatory() { @Override public boolean needsRebuild(@NonNull OpenNotification n) { return n.getPackageName().equals(packageName); } }); } } private interface RebuildConfirmatory { boolean needsRebuild(@NonNull OpenNotification n); } private void rebuildLocalList(@NonNull RebuildConfirmatory rebuildConfirmatory) { for (OpenNotification n : mGList) { if (rebuildConfirmatory.needsRebuild(n)) { rebuildLocalList(); break; } } } //-- LISTENERS ------------------------------------------------------------ public interface OnNotificationListChangedListener { /** * Callback that the list of notifications has changed. * * @param n an instance of notification (must be non-null, if the * event is not a {@link #EVENT_BATH, {@code null} otherwise}) * @param event event type: * {@link #EVENT_POSTED}, {@link #EVENT_REMOVED}, * {@link #EVENT_CHANGED}, {@link #EVENT_CHANGED_SPAM}, * {@link #EVENT_BATH} * @param isLastEventInSequence {@code true} if this is last of bath changes, {@code false} * otherwise. */ void onNotificationListChanged(@NonNull NotificationPresenter np, OpenNotification n, int event, boolean isLastEventInSequence); } /** * {@inheritDoc} */ @Override public void registerListener(@NonNull OnNotificationListChangedListener listener) { // Make sure to register listener only once. for (WeakReference<OnNotificationListChangedListener> ref : mListenersRefs) { if (ref.get() == listener) { Log.w(TAG, "Tried to register already registered listener!"); return; } } cleanDeadListeners(); mListenersRefs.add(new WeakReference<>(listener)); } /** * {@inheritDoc} */ @Override public void unregisterListener(@NonNull OnNotificationListChangedListener listener) { for (WeakReference<OnNotificationListChangedListener> ref : mListenersRefs) { if (ref.get() == listener) { mListenersRefs.remove(ref); return; } } Log.w(TAG, "Tried to unregister non-existent listener!"); } /* Ideally this method and the whole weakness thing should not be needed. */ private void cleanDeadListeners() { Iterator<WeakReference<OnNotificationListChangedListener>> i = mListenersRefs.iterator(); while (i.hasNext()) { WeakReference wr = i.next(); if (wr.get() == null) { Log.w(TAG, "Removing the dead listener."); i.remove(); } } } /** * @author Artem Chepurnoy */ public interface OnNotificationPostedListener { /** * @see #postNotificationFromMain(android.content.Context, OpenNotification, int) * @see #postNotification(android.content.Context, OpenNotification, int) */ void onNotificationPosted(@NonNull Context context, @NonNull OpenNotification n, int flags); } /** * @see #registerListener(OnNotificationListChangedListener) * @see #unregisterListener(OnNotificationListChangedListener) */ public void setOnNotificationPostedListener(@Nullable OnNotificationPostedListener listener) { mMainListener = listener; } //-- MAIN ----------------------------------------------------------------- private NotificationPresenter() { mFrozenEvents = new ArrayList<>(); mListenersRefs = new ArrayList<>(); mGList = new NotificationList(new NotificationList.OnNotificationListChangedListener() { @Override public int onNotificationAdded(@NonNull OpenNotification n) { return NotificationList.RESULT_DEFAULT; } @Override public int onNotificationChanged( @NonNull OpenNotification n, @NonNull OpenNotification old) { if (n.isGroupSummary() && old.isGroupSummary()) { // Copy-paste all children from old notification to the // new one. List<OpenNotification> children = n.getGroupNotifications(); List<OpenNotification> aged = old.getGroupNotifications(); assert children != null; assert aged != null; Check.getInstance().isTrue(children.isEmpty()); children.addAll(aged); } return NotificationList.RESULT_DEFAULT; } @Override public int onNotificationRemoved(@NonNull OpenNotification n) { return NotificationList.RESULT_DEFAULT; } }); mLList = new NotificationList(this); mGroupsWithSummaries = new HashSet<>(); mHandler = new Handler(Looper.getMainLooper()); mProxy = new NotificationPrProxy(this, Looper.getMainLooper()); mFilter = new NotificationSpamFilter(); if (!Device.hasJellyBeanMR2Api()) { // pre 4.3 version mGList.setMaximumSize(5); mLList.setMaximumSize(5); } mConfig = Config.getInstance(); mConfigListener = new ConfigListener(mConfig); // because of weak listeners mConfig.registerListener(mConfigListener); mBlacklistListener = new BlacklistListener(); mBlacklist = Blacklist.getInstance(); mBlacklist.registerListener(mBlacklistListener); } @NonNull public synchronized static NotificationPresenter getInstance() { if (sNotificationPresenter == null) { sNotificationPresenter = new NotificationPresenter(); } return sNotificationPresenter; } /** * {@inheritDoc} */ @Override public void onLowMemory() { mGList.onLowMemory(); // It does cover all local list's notifications } public void postNotificationFromMain( @NonNull final Context context, @NonNull final OpenNotification n, final int flags) { if (!mFilter.postNotification(n).isValid(n)) { // TODO: Implement a basic spam filter. return; } mProxy.postNotification(context, n, flags); } /** * Posts notification to global list, notifies every follower * about this change, and tries to launch * {@link com.achep.acdisplay.ui.activities.AcDisplayActivity}. * <p><i> * To create {@link OpenNotification}, use * {@link OpenNotification#newInstance(StatusBarNotification)} or * {@link OpenNotification#newInstance(android.app.Notification)} * method. * </i></p> * * @see #FLAG_SILENCE */ void postNotification( @NonNull Context context, @NonNull OpenNotification n, int flags) { synchronized (monitor) { Check.getInstance().isInMainThread(); mProxy.onPosted(n); // Check for the test notification. if (isInitNotification(context, n)) { NotificationUtils.dismissNotification(n); // Try with another way, just to be sure. String name = Context.NOTIFICATION_SERVICE; NotificationManager nm = (NotificationManager) context.getSystemService(name); nm.cancel(App.ID_NOTIFY_INIT); return; } freezeListeners(); boolean globalValid = isValidForGlobal(n); boolean localValid = false; boolean groupChild = false; // If notification will not be added to the // list there's no point of loading its data. if (globalValid) { n.load(context); if (n.isGroupSummary()) { String groupKey = n.getGroupKey(); assert groupKey != null; //noinspection StatementWithEmptyBody if (mGroupsWithSummaries.contains(groupKey)) { // Put all group's children to its summary // notification. for (int i = mGList.size() - 1; i >= 0; i--) { OpenNotification n2 = mGList.get(i); if (groupKey.equals(n2.getGroupKey())) { if (n2.isGroupChild()) { assert n.getGroupNotifications() != null; n.getGroupNotifications().add(n2); // Remove this notification from the global list. mGList.removeNotification(i); mLList.removeNotification(n2); } else { // That's odd. Ideally this will never happen. Log.w(TAG, ""); removeNotification(n, 0); } } } } else { // Put all group's children to its summary // notification. for (int i = mGList.size() - 1; i >= 0; i--) { OpenNotification n2 = mGList.get(i); if (groupKey.equals(n2.getGroupKey())) { if (n2.isGroupChild()) { assert n.getGroupNotifications() != null; n.getGroupNotifications().add(n2); // Remove this notification from the global list. mGList.removeNotification(i); mLList.removeNotification(n2); } else { // That's odd. Ideally this will never happen. removeNotification(n, 0); } } } mGroupsWithSummaries.add(groupKey); } } else if (n.isGroupChild() && mGroupsWithSummaries.contains(n.getGroupKey())) { // Artem Chepurnoy: Not sure if this may happen. if (DEBUG) Log.d(TAG, "Adding a notification to an existent group."); String groupKey = n.getGroupKey(); assert groupKey != null; for (OpenNotification n2 : mGList) { if (groupKey.equals(n2.getGroupKey()) && n2.isGroupSummary()) { groupChild = true; assert n2.getGroupNotifications() != null; ((NotificationList) n2.getGroupNotifications()).pushNotification(n); notifyListeners(n2, EVENT_CHANGED); break; } } if (!groupChild) { // Failed to find the summary of this group, although the // set is indicating its presence. This is possible to happen due to // optimization list of pending events. mGroupsWithSummaries.remove(groupKey); if (DEBUG) Log.d(TAG, "Removed lost group from the set: group=" + groupKey); } } Config config = Config.getInstance(); n.setEmoticonsEnabled(config.isEmoticonsEnabled()); if (groupChild) { globalValid = false; // I assume that 'localValid' if // 'False' here. } else { localValid = isValidForLocal(n); if (!Device.hasJellyBeanMR2Api()) globalValid = localValid; } } mGList.pushOrRemoveNotification(n, globalValid); int result = mLList.pushOrRemoveNotification(n, localValid); if (localValid && result == RESULT_SUCCESS && mMainListener != null) { if (DEBUG) Log.d(TAG, "Notification posted: notifying the main listener."); mMainListener.onNotificationPosted(context, n, flags); } // Release listeners and send all pending // events. if (Operator.bitAnd(flags, FLAG_SILENCE)) mFrozenEvents.clear(); meltListeners(); } } public void removeNotificationFromMain(final @NonNull OpenNotification n, final int flags) { mProxy.removeNotification(n, flags); } /** * Removes notification from the presenter and sends * this event to followers. Calling his method will not * remove notification from system! */ public void removeNotification(@NonNull OpenNotification n, final int flags) { synchronized (monitor) { Check.getInstance().isInMainThread(); mProxy.onRemoved(n); // Update the summary set, group notifications etc. handleNotificationRemoval(n); NotificationList list = mGList; int i = list.indexOfNotification(n); if (i != -1) { n.recycle(); list.remove(i); mLList.removeNotification(n); // Watch for the memory leaks AppHeap.getRefWatcher().watch(n); } } } private void handleNotificationRemoval(@NonNull OpenNotification n) { if (n.isGroupSummary()) { String groupKey = n.getGroupKey(); assert groupKey != null; mGroupsWithSummaries.remove(groupKey); } else if (n.isGroupChild() && mGroupsWithSummaries.contains(n.getGroupKey())) { String groupKey = n.getGroupKey(); assert groupKey != null; for (OpenNotification n2 : mGList) { if (groupKey.equals(n2.getGroupKey())) { Check.getInstance().isTrue(n2.isGroupSummary()); assert n2.getGroupNotifications() != null; NotificationList list = (NotificationList) n2.getGroupNotifications(); int i = list.indexOfNotification(n); if (i != -1) { n.recycle(); list.remove(i); // Watch for the memory leaks AppHeap.getRefWatcher().watch(n); } return; } } // Failed to find the summary of this group, although the // set is indicating its presence. This is possible to happen due to // optimization list of pending events. mGroupsWithSummaries.remove(groupKey); if (DEBUG) Log.d(TAG, "Removed[2] lost group from the set: group=" + groupKey); } } /** * Re-validates all notifications from {@link #mGList global list} * and sends {@link #EVENT_BATH bath} event after. * * @see #isValidForLocal(OpenNotification) * @see #isValidForGlobal(OpenNotification) */ // Must be synced on monitor private void rebuildLocalList() { freezeListeners(); // Remove not valid notifications // from local list. for (int i = mLList.size() - 1; i >= 0; i--) { OpenNotification n = mLList.get(i); if (!isValidForLocal(n)) mLList.removeNotification(i); } // Add newly valid notifications to local list. for (OpenNotification n : mGList) { if (isValidForLocal(n)) mLList.pushNotification(n, false); } meltListeners(); } @Nullable public OpenNotification getFreshNotification() { synchronized (monitor) { for (OpenNotification n : getList()) { long delta = Math.max(n.getNotification().priority, 1) * FRESH_NOTIFICATION_EXPIRY_TIME; long past = SystemClock.elapsedRealtime() - delta; if (!n.isRead() && n.getLoadTimestamp() > past) return n; } return null; } } @NonNull public ArrayList<OpenNotification> getList() { synchronized (monitor) { return mLList; } } /** * @return the number of notifications in {@link #getList() local list}. * @see #isEmpty() */ public int size() { synchronized (monitor) { return mLList.size(); } } /** * @return {@code true} if the {@link #getList() local list} contains no notifications, * {@code false} otherwise. * @see #size() */ public boolean isEmpty() { synchronized (monitor) { return mLList.isEmpty(); } } //-- LOCAL LIST'S EVENTS -------------------------------------------------- /** * {@inheritDoc} */ @Override // Not an enter point, should not be synchronized. public int onNotificationAdded(@NonNull OpenNotification n) { Check.getInstance().isFalse(n.isRecycled()); loadNotificationBackground(n); notifyListeners(n, EVENT_POSTED); return RESULT_SUCCESS; } /** * {@inheritDoc} */ @Override // Not an enter point, should not be synchronized. public int onNotificationChanged(@NonNull OpenNotification n, @NonNull OpenNotification old) { Check.getInstance().isFalse(n.isRecycled()); loadNotificationBackground(n); old.clearBackground(); // Prevent god damn notification spam by // checking texts' equality. // An example of notification spammer is a well-known // DownloadProvider (seriously, Google?) if (n.getNumber() == old.getNumber() && TextUtils.equals(n.titleText, old.titleText) && TextUtils.equals(n.titleBigText, old.titleBigText) && TextUtils.equals(n.messageText, old.messageText) && TextUtils.equals(n.infoText, old.infoText) && !n.isMine() /* i'm not dumb */) { // Technically notification was changed, but it was a fault // of dumb developer. Mark notification as read, if old one was. n.setRead(old.isRead()); notifyListeners(n, EVENT_CHANGED_SPAM); return RESULT_SPAM; // Don't wake up. } notifyListeners(n, EVENT_CHANGED); return RESULT_SUCCESS; } /** * {@inheritDoc} */ @Override // Not an enter point, should not be synchronized. public int onNotificationRemoved(@NonNull OpenNotification n) { notifyListeners(n, EVENT_REMOVED); // You don't have to recycle the notification here, cause // it should be recycled on removing from the global list. Otherwise you // may get unexpected behaviour when this notification will be // added back to the local list. if (!n.isRecycled()) { n.clearBackground(); } if (isEmpty()) { // Clean-up static cache if (DEBUG) Log.d(TAG, "Cleaning the ref-cache..."); NotificationUiHelper.sAppIconCache.clear(); } return RESULT_SUCCESS; } private void loadNotificationBackground(@NonNull OpenNotification notification) { Config config = Config.getInstance(); // Selective load exactly what we need and nothing more. // This will reduce RAM consumption for a bit (1% or so.) if (Operator.bitAnd( config.getDynamicBackgroundMode(), Config.DYNAMIC_BG_NOTIFICATION_MASK)) notification.loadBackgroundAsync(); } //-- NOTIFICATION UTILS --------------------------------------------------- @SuppressLint("NewApi") public boolean isTestNotification(@NonNull Context context, @NonNull OpenNotification n) { StatusBarNotification sbn = n.getStatusBarNotification(); return n.isMine() && sbn != null && sbn.getId() == App.ID_NOTIFY_TEST; } @SuppressLint("NewApi") public boolean isInitNotification(@NonNull Context context, @NonNull OpenNotification n) { StatusBarNotification sbn = n.getStatusBarNotification(); return n.isMine() && sbn != null && sbn.getId() == App.ID_NOTIFY_INIT; } /** * Freezes the listeners notification process and * stores all events to list. * * @see #meltListeners() */ private void freezeListeners() { mFreezeLevel++; } /** * Unfreezes all events and sends them. * * @see #freezeListeners() */ private void meltListeners() { Check.getInstance().isTrue(mFreezeLevel > 0); if (--mFreezeLevel == 0) { notifyListeners(mFrozenEvents); mFrozenEvents.clear(); } } private void notifyListeners(@Nullable OpenNotification n, int event) { notifyListeners(n, event, true); } private void notifyListeners(@Nullable OpenNotification n, int event, boolean isLastEventInSequence) { Check.getInstance().isInMainThread(); if (mFreezeLevel > 0) { if (mFrozenEvents.size() >= 1 && mFrozenEvents.get(0).event == EVENT_BATH) return; if (event == EVENT_BATH) mFrozenEvents.clear(); mFrozenEvents.add(new NotificationListChange(event, n)); return; } for (int i = mListenersRefs.size() - 1; i >= 0; i--) { WeakReference<OnNotificationListChangedListener> ref = mListenersRefs.get(i); OnNotificationListChangedListener l = ref.get(); if (l == null) { // There were no links to this listener except // our class. Log.w(TAG, "Deleting an unused listener!"); mListenersRefs.remove(i); } else { l.onNotificationListChanged(this, n, event, isLastEventInSequence); } } } private void notifyListeners(@NonNull ArrayList<NotificationListChange> changes) { int size = changes.size(); for (int i = 0; i < size; i++) { NotificationListChange change = changes.get(i); notifyListeners(change.notification, change.event, i + 1 == size); } } /** * @return {@code true} if notification may be shown to user, * {@code false} otherwise. */ private boolean isValidForLocal(@NonNull OpenNotification notification) { if (!mConfig.isEnabled()) { return false; } AppConfig config = mBlacklist.getAppConfig(notification.getPackageName()); if (config.isHidden()) { // Do not display any notifications from this app. return false; } if (!notification.isClearable() && !config.isNonClearableEnabled()) { // Do not display non-clearable notification. return false; } if (notification.getNotification().priority < mConfig.getNotifyMinPriority()) { // Do not display too low-priority notification. return false; } if (notification.getNotification().priority > mConfig.getNotifyMaxPriority()) { // Do not display too high-priority notification. return false; } // Do not allow notifications with no content. return !(TextUtils.isEmpty(notification.titleText) && TextUtils.isEmpty(notification.titleBigText) && TextUtils.isEmpty(notification.messageText) && TextUtils.isEmpty(notification.messageBigText) && notification.messageTextLines == null); } // Here we filter completely wrong // notifications. @SuppressLint("NewApi") private boolean isValidForGlobal(@NonNull OpenNotification notification) { return true; } //-- INITIALIZING --------------------------------------------------------- void init(final @NonNull Context context, final @NonNull StatusBarNotification[] activeNotifications) { mHandler.post(new Runnable() { @SuppressLint("NewApi") @Override public void run() { clear(false); if (DEBUG) Log.d(TAG, "Initializing the notifications list..."); // Initialize the notifications list through the proxy to // optimize the process. This is completely not useful on // pre-Lollipop devices due to lack of children notifications. List<NotificationPrTask> list = new ArrayList<>(activeNotifications.length); for (StatusBarNotification sbn : activeNotifications) { OpenNotification n = OpenNotification.newInstance(sbn); list.add(new NotificationPrTask(context, n, true /* post */, 0)); } if (Device.hasLollipopApi()) mProxy.optimizePrTasks(list); mProxy.sendPrTasks(list); list.clear(); // This is probably not needed. } }); } void clearFromMain(final boolean notifyListeners) { mHandler.post(new Runnable() { @SuppressLint("NewApi") @Override public void run() { clear(notifyListeners); } }); } void clear(final boolean notifyListeners) { synchronized (monitor) { Check.getInstance().isInMainThread(); if (DEBUG) Log.d(TAG, "Clearing the notifications list... notify_listeners=" + notifyListeners); mProxy.onClear(); mGroupsWithSummaries.clear(); mGList.clear(); mLList.clear(); if (notifyListeners) notifyListeners(null, EVENT_BATH); } } }