chromium/chrome/android/java/src/org/chromium/chrome/browser/download/SystemDownloadNotifier.java

// Copyright 2015 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.chrome.browser.download;

import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;

import org.chromium.components.browser_ui.notifications.PendingNotificationTask;
import org.chromium.components.browser_ui.notifications.ThrottlingNotificationScheduler;
import org.chromium.components.offline_items_collection.ContentId;
import org.chromium.components.offline_items_collection.PendingState;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * DownloadNotifier implementation that creates and updates download notifications.
 * This class creates the {@link DownloadNotificationService} when needed, and binds
 * to the latter to issue calls to show and update notifications.
 */
public class SystemDownloadNotifier implements DownloadNotifier {
    private DownloadNotificationService mDownloadNotificationService;

    /**
     * Notification type for constructing the notification later on.
     * TODO(qinmin): this is very ugly and it doesn't scale if we want a more general notification
     * frame work. A better solution is to pass a notification builder or a notification into the
     * queue, so we don't need the switch statement in updateNotification().
     */
    @IntDef({
        NotificationType.PROGRESS,
        NotificationType.PAUSED,
        NotificationType.SUCCEEDED,
        NotificationType.FAILED,
        NotificationType.INTERRUPTED
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface NotificationType {
        int PROGRESS = 0;
        int PAUSED = 1;
        int SUCCEEDED = 2;
        int FAILED = 3;
        int INTERRUPTED = 4;
    }

    /** Information related to a notification. */
    private static final class NotificationInfo {
        final @NotificationType int mType;
        final DownloadInfo mInfo;
        final @PendingNotificationTask.Priority int mPriority;
        long mStartTime;
        long mSystemDownloadId;
        boolean mCanResolve;
        boolean mIsSupportedMimeType;
        boolean mCanDownloadWhileMetered;
        boolean mIsAutoResumable;
        @PendingState int mPendingState;

        NotificationInfo(
                @NotificationType int type,
                DownloadInfo info,
                @PendingNotificationTask.Priority int priority) {
            mType = type;
            mInfo = info;
            mPriority = priority;
        }
    }

    /** Constructor. */
    public SystemDownloadNotifier() {}

    DownloadNotificationService getDownloadNotificationService() {
        if (mDownloadNotificationService == null) {
            mDownloadNotificationService = DownloadNotificationService.getInstance();
        }
        return mDownloadNotificationService;
    }

    @VisibleForTesting
    void setDownloadNotificationService(DownloadNotificationService downloadNotificationService) {
        mDownloadNotificationService = downloadNotificationService;
    }

    @Override
    public void notifyDownloadCanceled(ContentId id) {
        ThrottlingNotificationScheduler.getInstance().cancelPendingNotificationTask(id);
        getDownloadNotificationService().notifyDownloadCanceled(id, false);
    }

    @Override
    public void notifyDownloadSuccessful(
            DownloadInfo info,
            long systemDownloadId,
            boolean canResolve,
            boolean isSupportedMimeType) {
        NotificationInfo notificationInfo =
                new NotificationInfo(
                        NotificationType.SUCCEEDED, info, PendingNotificationTask.Priority.HIGH);
        notificationInfo.mSystemDownloadId = systemDownloadId;
        notificationInfo.mCanResolve = canResolve;
        notificationInfo.mIsSupportedMimeType = isSupportedMimeType;
        addPendingNotification(notificationInfo);
    }

    @Override
    public void notifyDownloadFailed(DownloadInfo info) {
        NotificationInfo notificationInfo =
                new NotificationInfo(
                        NotificationType.FAILED, info, PendingNotificationTask.Priority.HIGH);
        addPendingNotification(notificationInfo);
    }

    @Override
    public void notifyDownloadProgress(
            DownloadInfo info, long startTime, boolean canDownloadWhileMetered) {
        NotificationInfo notificationInfo =
                new NotificationInfo(
                        NotificationType.PROGRESS, info, PendingNotificationTask.Priority.LOW);
        notificationInfo.mStartTime = startTime;
        notificationInfo.mCanDownloadWhileMetered = canDownloadWhileMetered;
        addPendingNotification(notificationInfo);
    }

    @Override
    public void notifyDownloadPaused(DownloadInfo info) {
        NotificationInfo notificationInfo =
                new NotificationInfo(
                        NotificationType.PAUSED, info, PendingNotificationTask.Priority.HIGH);
        addPendingNotification(notificationInfo);
    }

    @Override
    public void notifyDownloadInterrupted(
            DownloadInfo info, boolean isAutoResumable, @PendingState int pendingState) {
        NotificationInfo notificationInfo =
                new NotificationInfo(
                        NotificationType.INTERRUPTED, info, PendingNotificationTask.Priority.HIGH);
        notificationInfo.mIsAutoResumable = isAutoResumable;
        notificationInfo.mPendingState = pendingState;
        addPendingNotification(notificationInfo);
    }

    @Override
    public void removeDownloadNotification(int notificationId, DownloadInfo info) {
        ThrottlingNotificationScheduler.getInstance()
                .cancelPendingNotificationTask(info.getContentId());
        getDownloadNotificationService().cancelNotification(notificationId, info.getContentId());
    }

    /**
     * Add a new notification to be handled. If there is currently a posted task to handle pending
     * notifications, adding the new notification to the pending queue. Otherwise, process the
     * notification immediately and post a task to handle incoming ones.
     * @param notificationInfo Notification to be displayed.
     */
    void addPendingNotification(NotificationInfo notificationInfo) {
        ThrottlingNotificationScheduler.getInstance()
                .addPendingNotificationTask(
                        new PendingNotificationTask(
                                notificationInfo.mInfo.getContentId(),
                                notificationInfo.mPriority,
                                () -> {
                                    updateNotification(notificationInfo);
                                }));
    }

    /**
     * Sends a notification update to Android NotificationManager.
     * @param notificationInfo Information about the notification to be updated.
     */
    private void updateNotification(NotificationInfo notificationInfo) {
        DownloadInfo info = notificationInfo.mInfo;
        switch (notificationInfo.mType) {
            case NotificationType.PROGRESS:
                getDownloadNotificationService()
                        .notifyDownloadProgress(
                                info.getContentId(),
                                info.getFileName(),
                                info.getProgress(),
                                info.getBytesReceived(),
                                info.getTimeRemainingInMillis(),
                                notificationInfo.mStartTime,
                                info.getOTRProfileId(),
                                notificationInfo.mCanDownloadWhileMetered,
                                info.getIsTransient(),
                                info.getIcon(),
                                info.getOriginalUrl(),
                                info.getShouldPromoteOrigin());
                break;
            case NotificationType.PAUSED:
                getDownloadNotificationService()
                        .notifyDownloadPaused(
                                info.getContentId(),
                                info.getFileName(),
                                true,
                                false,
                                info.getOTRProfileId(),
                                info.getIsTransient(),
                                info.getIcon(),
                                info.getOriginalUrl(),
                                info.getShouldPromoteOrigin(),
                                false,
                                true,
                                info.getPendingState());
                break;
            case NotificationType.SUCCEEDED:
                final int notificationId =
                        getDownloadNotificationService()
                                .notifyDownloadSuccessful(
                                        info.getContentId(),
                                        info.getFilePath(),
                                        info.getFileName(),
                                        notificationInfo.mSystemDownloadId,
                                        info.getOTRProfileId(),
                                        notificationInfo.mIsSupportedMimeType,
                                        info.getIsOpenable(),
                                        info.getIcon(),
                                        info.getOriginalUrl(),
                                        info.getShouldPromoteOrigin(),
                                        info.getReferrer(),
                                        info.getBytesTotalSize());

                if (info.getIsOpenable()) {
                    DownloadManagerService.getDownloadManagerService()
                            .onSuccessNotificationShown(
                                    info,
                                    notificationInfo.mCanResolve,
                                    notificationId,
                                    notificationInfo.mSystemDownloadId);
                }
                break;
            case NotificationType.FAILED:
                getDownloadNotificationService()
                        .notifyDownloadFailed(
                                info.getContentId(),
                                info.getFileName(),
                                info.getIcon(),
                                info.getOriginalUrl(),
                                info.getShouldPromoteOrigin(),
                                info.getOTRProfileId(),
                                info.getFailState());
                break;
            case NotificationType.INTERRUPTED:
                getDownloadNotificationService()
                        .notifyDownloadPaused(
                                info.getContentId(),
                                info.getFileName(),
                                info.isResumable(),
                                notificationInfo.mIsAutoResumable,
                                info.getOTRProfileId(),
                                info.getIsTransient(),
                                info.getIcon(),
                                info.getOriginalUrl(),
                                info.getShouldPromoteOrigin(),
                                false,
                                false,
                                notificationInfo.mPendingState);
                break;
        }
    }
}