content: /*
   * 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 androidx.annotation.NonNull;
  import androidx.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);
          }
      }
  
  }