chromium/chrome/android/java/src/org/chromium/chrome/browser/notifications/NotificationSuspender.java

// Copyright 2019 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.notifications;

import android.app.Notification;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;

import androidx.annotation.RequiresApi;

import org.jni_zero.JniType;
import org.jni_zero.NativeMethods;

import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.components.browser_ui.notifications.BaseNotificationManagerProxy;
import org.chromium.components.browser_ui.notifications.BaseNotificationManagerProxyFactory;
import org.chromium.components.browser_ui.notifications.NotificationManagerProxy;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
import org.chromium.components.browser_ui.notifications.NotificationWrapper;
import org.chromium.components.embedder_support.util.UrlConstants;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Class that suspends and revives notifications.
 *
 * All calls must be made on the UI thread, and the full browser must be started before using this
 * class.
 */
public class NotificationSuspender {
    private final Profile mProfile;
    private final Context mContext;
    private final BaseNotificationManagerProxy mNotificationManager;

    public NotificationSuspender(Profile profile) {
        this(
                profile,
                ContextUtils.getApplicationContext(),
                BaseNotificationManagerProxyFactory.create(ContextUtils.getApplicationContext()));
    }

    public NotificationSuspender(
            Profile profile, Context context, BaseNotificationManagerProxy notificationManager) {
        mProfile = profile;
        mContext = context;
        mNotificationManager = notificationManager;
    }

    /**
     * Suspends notifications from the given domains.
     *
     * <p>Suspending means storing the notification resources and canceling the Android
     * notifications themselves, so that they can be re-displayed later.
     *
     * @param fqdns The list of domain strings to suspend notifications from.
     */
    public void suspendNotificationsFromDomains(List<String> fqdns) {
        getActiveNotificationsForOrigins(
                getOriginsForDomains(fqdns),
                (activeNotifications) -> {
                    cancelNotificationsWithIds(storeNotificationResources(activeNotifications));
                });
    }

    /**
     * Stores resources for all notifications from a given origin back into the native
     * NotificationDatabase.
     *
     * <p>This allows re-displaying these notification later.
     *
     * @param notifications The origins for which all notification resources to store.
     * @param callback The origins for which all notification resources to store.
     */
    public void storeNotificationResourcesFromOrigins(
            List<Uri> origins, Callback<List<String>> callback) {
        getActiveNotificationsForOrigins(
                origins,
                (activeNotifications) ->
                        callback.onResult(storeNotificationResources(activeNotifications)));
    }

    /**
     * Stores resources for the given notifications back into the native NotificationDatabase.
     *
     * @param notifications The list of notifications whose resources to store.
     */
    public List<String> storeNotificationResources(List<NotificationWrapper> notifications) {
        if (notifications.isEmpty()) {
            return new ArrayList<String>();
        }

        String[] notificationIds = new String[notifications.size()];
        String[] origins = new String[notifications.size()];
        Bitmap[] resources = new Bitmap[notifications.size() * 3];

        for (int i = 0; i < notifications.size(); ++i) {
            Notification notification = notifications.get(i).getNotification();
            String tag = notifications.get(i).getMetadata().tag;
            // Chromium's `notificationId` is used as the Android notification's tag.
            notificationIds[i] = tag;
            origins[i] = NotificationPlatformBridge.getOriginFromNotificationTag(tag);
            resources[i * 3 + 0] = getNotificationIcon(notification);
            resources[i * 3 + 1] = getNotificationBadge(notification);
            resources[i * 3 + 2] = getNotificationImage(notification);
        }

        NotificationSuspenderJni.get()
                .storeNotificationResources(mProfile, notificationIds, origins, resources);
        return new ArrayList<String>(Arrays.asList(notificationIds));
    }

    /**
     * Unsuspends notifications from the given domains.
     *
     * This means re-displaying the notification using the prevously stored resources.
     *
     * @param fqdns The list of domain strings to unsuspend notifications from.
     */
    public void unsuspendNotificationsFromDomains(List<String> fqdns) {
        unsuspendNotificationsFromOrigins(getOriginsForDomains(fqdns));
    }

    /**
     * Unsuspends notifications from the given origins.
     *
     * @param fqdns The list of domain strings to unsuspend notifications from.
     */
    public void unsuspendNotificationsFromOrigins(List<Uri> origins) {
        if (origins.isEmpty()) {
            return;
        }

        NotificationSuspenderJni.get()
                .reDisplayNotifications(
                        mProfile,
                        origins.stream()
                                .map((origin) -> origin.toString())
                                .collect(Collectors.toList()));
    }

    /**
     * Retrieves the fully-qualified domain name of the website that displayed a notification.
     *
     * @param notification The notification whose originating domain to return.
     */
    public static String getValidFqdnOrEmptyString(NotificationWrapper notification) {
        String tag = notification.getMetadata().tag;
        String origin = NotificationPlatformBridge.getOriginFromNotificationTag(tag);
        if (TextUtils.isEmpty(origin)) return "";
        String host = Uri.parse(origin).getHost();
        return host == null ? "" : host;
    }

    /**
     * Cancels the notifications with the given notification IDs.
     *
     * @param notificationIds The IDs of notifications to cancel.
     */
    public void cancelNotificationsWithIds(List<String> notificationIds) {
        for (String notificationId : notificationIds) {
            mNotificationManager.cancel(
                    /* tag= */ notificationId, NotificationPlatformBridge.PLATFORM_ID);
        }
    }

    private List<Uri> getOriginsForDomains(List<String> fqdns) {
        final String[] notificationSchemes = {UrlConstants.HTTPS_SCHEME, UrlConstants.HTTP_SCHEME};
        ArrayList<Uri> origins = new ArrayList<Uri>();
        for (String fqdn : fqdns) {
            for (String scheme : notificationSchemes) {
                origins.add(new Uri.Builder().scheme(scheme).authority(fqdn).build());
            }
        }
        return origins;
    }

    public void getActiveNotificationsForOrigins(
            List<Uri> origins, Callback<List<NotificationWrapper>> callback) {
        List<NotificationWrapper> notifications = new ArrayList<>();

        if (origins.isEmpty()) {
            callback.onResult(notifications);
            return;
        }

        mNotificationManager.getActiveNotifications(
                (activeNotifications) -> {
                    for (NotificationManagerProxy.StatusBarNotificationProxy notification :
                            activeNotifications) {
                        if (notification.getId() != NotificationPlatformBridge.PLATFORM_ID) {
                            continue;
                        }
                        String tag = notification.getTag();
                        String origin =
                                NotificationPlatformBridge.getOriginFromNotificationTag(tag);
                        if (origin == null || !origins.contains(Uri.parse(origin))) continue;
                        NotificationMetadata metadata =
                                new NotificationMetadata(
                                        NotificationUmaTracker.SystemNotificationType.SITES,
                                        tag,
                                        NotificationPlatformBridge.PLATFORM_ID);
                        notifications.add(
                                new NotificationWrapper(notification.getNotification(), metadata));
                    }
                    callback.onResult(notifications);
                });
    }

    @RequiresApi(Build.VERSION_CODES.P)
    private Bitmap getBitmapFromIcon(Icon icon) {
        if (icon == null || icon.getType() != Icon.TYPE_BITMAP) return null;
        return ((BitmapDrawable) icon.loadDrawable(mContext)).getBitmap();
    }

    private Bitmap getNotificationIcon(Notification notification) {
        return getBitmapFromIcon(notification.getLargeIcon());
    }

    private Bitmap getNotificationBadge(Notification notification) {
        return getBitmapFromIcon(notification.getSmallIcon());
    }

    private Bitmap getNotificationImage(Notification notification) {
        return (Bitmap) notification.extras.get(Notification.EXTRA_PICTURE);
    }

    @NativeMethods
    interface Natives {
        // Stores the given |resources| to be displayed later again. Note that |resources| is
        // expected to have 3 entries (icon, badge, image in that order) for each notification id in
        // |notificationIds|. If a notification does not have a particular resource, pass null
        // instead. |origins| must be the same size as |notificationIds|.
        void storeNotificationResources(
                @JniType("Profile*") Profile profile,
                String[] notificationIds,
                String[] origins,
                Bitmap[] resources);

        // Displays all suspended notifications for the given |origins|.
        void reDisplayNotifications(
                @JniType("Profile*") Profile profile,
                @JniType("std::vector<std::string>") List<String> origins);
    }
}