chromium/components/browser_ui/notifications/android/java/src/org/chromium/components/browser_ui/notifications/ThrottlingNotificationScheduler.java

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.browser_ui.notifications;

import android.os.Handler;
import android.os.Looper;
import android.util.Pair;

import org.chromium.base.ContextUtils;

import java.util.Iterator;
import java.util.PriorityQueue;

/**
 * On Android, notification updates will be dropped if there are too many updates within a second.
 * This class throttles all the notifications that will be sent to the NotificationManager. One
 * notification update will be sent on each |UPDATE_DELAY_MILLIS| interval. Notification tasks with
 * high priorities will be sent first, and arriving order matters too. To use this class, create a
 * {@link PendingNotificationTask} that handles notification creation and posting, and call the
 * addPendingNotificationTask() method. Before canceling a notification, call
 * cancelPendingNotificationTask() to ensure all the pending tasks are removed.
 * TODO(qinmin): convert all notification code to use this class instead of directly sending
 * notifications to the NotificationManager.
 */
public class ThrottlingNotificationScheduler {
    // To avoid notification updates being throttled by Android, using 350 ms as the interval
    // so that no more than 3 updates are posted per second.
    public static final long UPDATE_DELAY_MILLIS = 350;

    // Priority queue hold all the pending notifications.
    private final PriorityQueue<PendingNotificationTask> mPendingNotificationTasks =
            new PriorityQueue<PendingNotificationTask>(5, PendingNotificationTask::compare);

    private final Handler mHandler;
    private boolean mScheduled;

    // Initialization on demand holder idiom
    private static class LazyHolder {
        private static final ThrottlingNotificationScheduler INSTANCE =
                new ThrottlingNotificationScheduler();
    }

    /**
     * Get the singleton instance of ThrottlingNotificationScheduler.
     * @return the instance of ThrottlingNotificationScheduler
     */
    public static ThrottlingNotificationScheduler getInstance() {
        return LazyHolder.INSTANCE;
    }

    ThrottlingNotificationScheduler() {
        mHandler = new Handler(Looper.getMainLooper());
    }

    /**
     * Add a new pending notification task to be throttled.
     * @param task Task to run when throttler finishes all tasks before it.
     */
    public void addPendingNotificationTask(PendingNotificationTask task) {
        PendingNotificationTask oldTask = removePendingNotificationTask(task.taskId);
        // Use the old timestamp, so the pending task won't starve.
        if (oldTask != null) task.timestamp = oldTask.timestamp;
        if (mScheduled) {
            mPendingNotificationTasks.add(task);
        } else {
            mScheduled = true;
            task.notificationTask.run();
            mHandler.postDelayed(this::pumpQueue, UPDATE_DELAY_MILLIS);
        }
    }

    /**
     * Convenient method, use this if nothing else needs to be handled when notification
     * is actually posted.
     * @param notificationWrapper Weapper containing the notification to be posted.
     */
    public void addPendingNotification(NotificationWrapper notificationWrapper) {
        Pair<String, Integer> taskId =
                Pair.create(
                        notificationWrapper.getMetadata().tag,
                        Integer.valueOf(notificationWrapper.getMetadata().id));
        PendingNotificationTask task =
                new PendingNotificationTask(
                        taskId,
                        PendingNotificationTask.Priority.HIGH,
                        () -> {
                            BaseNotificationManagerProxyFactory.create(
                                            ContextUtils.getApplicationContext())
                                    .notify(notificationWrapper);
                        });
        addPendingNotificationTask(task);
    }

    /**
     * Removes a pending task from the throttler.
     * @param taskId ID of the task.
     */
    public void cancelPendingNotificationTask(Object taskId) {
        removePendingNotificationTask(taskId);
    }

    /**
     * Convenient method, removes a pending task from the throttler and cancels the notification.
     * @param tag Tag of the notification.
     * @param id ID of the notification.
     */
    public void cancelPendingNotification(String tag, int id) {
        Pair<String, Integer> taskId = Pair.create(tag, Integer.valueOf(id));
        removePendingNotificationTask(taskId);
        BaseNotificationManagerProxyFactory.create(ContextUtils.getApplicationContext())
                .cancel(tag, id);
    }

    /** Clear the pending task queue. */
    public void clear() {
        mPendingNotificationTasks.clear();
        mHandler.removeCallbacksAndMessages(null);
        mScheduled = false;
    }

    /**
     * Removes a pending task from the task queue and return it.
     * @param taskId ID of the task.
     */
    private PendingNotificationTask removePendingNotificationTask(Object taskId) {
        Iterator<PendingNotificationTask> iter = mPendingNotificationTasks.iterator();
        while (iter.hasNext()) {
            PendingNotificationTask task = iter.next();
            if (task.taskId.equals(taskId)) {
                iter.remove();
                return task;
            }
        }
        return null;
    }

    /** Get the next task from the queue, and schedule another task |UPDATE_DELAY_MILLIS| later. */
    private void pumpQueue() {
        PendingNotificationTask task = mPendingNotificationTasks.poll();
        if (task != null) {
            task.notificationTask.run();
            mHandler.postDelayed(this::pumpQueue, UPDATE_DELAY_MILLIS);
        } else {
            mScheduled = false;
        }
    }
}