/* * Copyright (C) 2015 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.content.Context; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; import com.achep.base.Build; import com.achep.base.Device; import com.achep.base.utils.Operator; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import static com.achep.acdisplay.notifications.NotificationPresenter.FLAG_IMMEDIATELY; /** * Processes the notification flow, and re-transfers it through the * {@link Looper#getMainLooper() main thread}. * * @author Artem Chepurnoy */ class NotificationPrProxy { private static final String TAG = "NotificationPrProxy"; private static final long DELAY = 400; // 0.4 sec. private static final long MAX_DELAY = DELAY * 5; // 2 sec. @NonNull private final Object mMonitor = new Object(); @NonNull private final NotificationPresenter mPresenter; @NonNull private final Handler mHandler; @NonNull private final List<NotificationPrTask> mTasks; private final Runnable mProcessRunnable = new Runnable() { @Override public void run() { synchronized (mMonitor) { mStartTime = 0; mProcessing = true; optimizePrTasks(mTasks); sendPrTasks(mTasks); mTasks.clear(); mProcessing = false; } } }; private volatile long mStartTime; private volatile boolean mProcessing; public NotificationPrProxy( @NonNull NotificationPresenter presenter, @NonNull Looper looper) { mPresenter = presenter; mHandler = new Handler(looper); mTasks = new ArrayList<>(Device.hasLollipopApi() ? 15 /* multiply notifications levels have introduced a much larger flow of notifications */ : 4); } /** * Called on {@link NotificationPresenter#postNotification(Context, OpenNotification, int)} * (direct posting a notification). * * @see #onRemoved(OpenNotification) * @see #onClear() */ void onPosted(@NonNull OpenNotification n) { onRemoveDuplicates(n); } public void postNotification( @NonNull Context context, @NonNull OpenNotification n, int flags) { synchronized (mMonitor) { boolean immediately = Operator.bitAnd(flags, FLAG_IMMEDIATELY); addTask(context, n, flags, true, immediately); } } /** * Called on {@link NotificationPresenter#removeNotification(OpenNotification, int)} * (direct removing a notification). * * @see #onPosted(OpenNotification) (OpenNotification) * @see #onClear() */ void onRemoved(@NonNull OpenNotification n) { onRemoveDuplicates(n); } public void removeNotification(@NonNull OpenNotification n, int flags) { synchronized (mMonitor) { boolean immediately = Operator.bitAnd(flags, FLAG_IMMEDIATELY); addTask(null, n, flags, false, immediately); } } /** * Called on {@link NotificationPresenter#clear(boolean)} * (direct clean-up). * * @see #onPosted(OpenNotification) * @see #onRemoved(OpenNotification) */ void onClear() { synchronized (mMonitor) { mStartTime = 0; mTasks.clear(); mHandler.removeCallbacks(mProcessRunnable); } } private void addTask(@Nullable Context context, @NonNull OpenNotification notification, int flags, boolean posts, boolean immediately) { mTasks.add(new NotificationPrTask(context, notification, posts, flags)); // Do not allow an infinitive loop here. final long now = SystemClock.elapsedRealtime(); if (mStartTime == 0) mStartTime = now; final long delta = now - mStartTime; // Delay the processing. mHandler.removeCallbacks(mProcessRunnable); mHandler.postDelayed(mProcessRunnable, immediately || delta > MAX_DELAY ? 0 : DELAY); } private void onRemoveDuplicates(@NonNull OpenNotification n) { synchronized (mMonitor) { if (!mProcessing) removeOverridingTasks(n); } } private void removeOverridingTasks(@NonNull OpenNotification n) { Iterator<NotificationPrTask> iterator = mTasks.iterator(); while (iterator.hasNext()) { if (NotificationUtils.hasIdenticalIds( iterator.next().notification, n)) iterator.remove(); } } /** * Optimize the {@link NotificationPrTask post/remove tasks} list by removing redundant * tasks and sorting families. */ public void optimizePrTasks(@NonNull List<NotificationPrTask> list) { if (Build.DEBUG) Log.d(TAG, "Optimizing post/remove tasks... " + list.toString()); int size = list.size(); //noinspection ConstantConditions NotificationPrTask empty = new NotificationPrTask(null, null, false, 0); // 1. Remove overriding tasks. for (int i = size - 1; i >= 0; i--) { NotificationPrTask task = list.get(i); if (task == empty) continue; for (int j = i - 1; j >= 0; j--) { NotificationPrTask sub = list.get(j); if (sub == empty) continue; if (NotificationUtils.hasIdenticalIds( task.notification, sub.notification)) { Log.i(TAG, "Removed overridden task on pre-processing tasks list. "); list.set(j, empty); } } } // 2. Remove empty objects. Iterator<NotificationPrTask> iterator = list.iterator(); while (iterator.hasNext()) { NotificationPrTask task = iterator.next(); if (task == empty) iterator.remove(); } size = list.size(); // 3. Sort families. // FIXME: Check if it works correctly. for (int i = 0; i < size; i++) { NotificationPrTask task = list.get(i); if (task == empty || !task.notification.isGroupChild()) continue; for (int j = i + 1; j < size; j++) { NotificationPrTask sub = list.get(j); if (sub == empty || !sub.notification.isGroupSummary()) continue; String subGroupKey = sub.notification.getGroupKey(); String taskGroupKey = task.notification.getGroupKey(); assert taskGroupKey != null; if (taskGroupKey.equals(subGroupKey)) { Log.d(TAG, "Swapped two tasks on pre-processing tasks list."); // Swap two tasks. list.set(j, task); list.set(i, sub); break; } } } // 4. Anything else? if (Build.DEBUG) Log.d(TAG, "Done optimizing post/remove tasks... " + list.toString()); } /** * Materializes the tasks by {@link #postNotification(Context, OpenNotification, int) posting} * or {@link #removeNotification(OpenNotification, int) removing} appropriate notifications. */ public void sendPrTasks(@NonNull List<NotificationPrTask> list) { for (NotificationPrTask task : list) { if (task.posts) { assert task.context != null; mPresenter.postNotification(task.context, task.notification, task.flags); } else { mPresenter.removeNotification(task.notification, task.flags); } } } }