chromium/chrome/android/java/src/org/chromium/chrome/browser/notifications/scheduler/DisplayAgent.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.scheduler;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.drawable.Icon;

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

import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.init.BrowserParts;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.init.EmptyBrowserParts;
import org.chromium.chrome.browser.notifications.NotificationIntentInterceptor;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker.SystemNotificationType;
import org.chromium.chrome.browser.notifications.NotificationWrapperBuilderFactory;
import org.chromium.chrome.browser.notifications.channels.ChromeChannelDefinitions.ChannelId;
import org.chromium.components.browser_ui.notifications.BaseNotificationManagerProxyFactory;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
import org.chromium.components.browser_ui.notifications.NotificationWrapper;
import org.chromium.components.browser_ui.notifications.NotificationWrapperBuilder;
import org.chromium.components.browser_ui.notifications.PendingIntentProvider;

import java.util.ArrayList;
import java.util.HashMap;

/** Used by notification scheduler to display the notification in Android UI. */
public class DisplayAgent {
    private static final String TAG = "DisplayAgent";
    private static final String DISPLAY_AGENT_TAG = "NotificationSchedulerDisplayAgent";

    private static final String EXTRA_INTENT_TYPE =
            "org.chromium.chrome.browser.notifications.scheduler.EXTRA_INTENT_TYPE";
    private static final String EXTRA_GUID =
            "org.chromium.chrome.browser.notifications.scheduler.EXTRA_GUID";
    private static final String EXTRA_ACTION_BUTTON_TYPE =
            "org.chromium.chrome.browser.notifications.scheduler.EXTRA_ACTION_BUTTON_TYPE";
    private static final String EXTRA_ACTION_BUTTON_ID =
            "org.chromium.chrome.browser.notifications.scheduler.EXTRA_ACTION_BUTTON_ID";
    private static final String EXTRA_SCHEDULER_CLIENT_TYPE =
            "org.chromium.chrome.browser.notifications.scheduler.EXTRA_SCHEDULER_CLIENT_TYPE ";

    /** Contains icon info on the notification. */
    private static class IconBundle {
        public final Bitmap bitmap;
        public final int resourceId;

        public IconBundle() {
            bitmap = null;
            resourceId = 0;
        }

        public IconBundle(Bitmap bitmap) {
            this.bitmap = bitmap;
            this.resourceId = 0;
        }

        public IconBundle(int resourceId) {
            this.bitmap = null;
            this.resourceId = resourceId;
        }
    }

    /** Contains button info on the notification. */
    private static class Button {
        public final String text;
        public final @ActionButtonType int type;
        public final String id;

        public Button(String text, @ActionButtonType int type, String id) {
            this.text = text;
            this.type = type;
            this.id = id;
        }
    }

    /**
     * Contains all data needed to build Android notification in the UI, specified by the client.
     */
    private static class NotificationData {
        public final String title;
        public final String message;
        public HashMap<Integer /*@IconType*/, IconBundle> icons = new HashMap<>();
        public ArrayList<Button> buttons = new ArrayList<>();

        private NotificationData(String title, String message) {
            this.title = title;
            this.message = message;
        }
    }

    @CalledByNative
    private static void addButton(
            NotificationData notificationData,
            @JniType("std::u16string") String text,
            @ActionButtonType int type,
            @JniType("std::string") String id) {
        notificationData.buttons.add(new Button(text, type, id));
    }

    @CalledByNative
    private static void addIcon(
            NotificationData notificationData,
            @IconType int type,
            @JniType("SkBitmap") Bitmap bitmap,
            int resourceId) {
        assert ((bitmap == null && resourceId != 0) || (bitmap != null && resourceId == 0));
        if (resourceId != 0) {
            notificationData.icons.put(type, new IconBundle(resourceId));
        } else {
            notificationData.icons.put(type, new IconBundle(bitmap));
        }
    }

    @CalledByNative
    private static NotificationData buildNotificationData(
            @JniType("std::u16string") String title, @JniType("std::u16string") String message) {
        return new NotificationData(title, message);
    }

    /**
     * Contains data used used by the notification scheduling system internally to build the
     * notification.
     */
    private static class SystemData {
        public @SchedulerClientType int type;
        public final String guid;

        public SystemData(@SchedulerClientType int type, String guid) {
            this.type = type;
            this.guid = guid;
        }
    }

    @CalledByNative
    private static SystemData buildSystemData(
            @SchedulerClientType int type, @JniType("std::string") String guid) {
        return new SystemData(type, guid);
    }

    /** Receives notification events from Android, like clicks, dismiss, etc. */
    public static final class Receiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            final BrowserParts parts =
                    new EmptyBrowserParts() {
                        @Override
                        public void finishNativeInitialization() {
                            handleUserAction(intent);
                        }
                    };

            // Try to load native.
            ChromeBrowserInitializer.getInstance().handlePreNativeStartupAndLoadLibraries(parts);
            ChromeBrowserInitializer.getInstance().handlePostNativeStartup(true, parts);
        }
    }

    private static void handleUserAction(Intent intent) {
        @NotificationIntentInterceptor.IntentType
        int intentType =
                IntentUtils.safeGetIntExtra(
                        intent,
                        EXTRA_INTENT_TYPE,
                        NotificationIntentInterceptor.IntentType.UNKNOWN);
        String guid = IntentUtils.safeGetStringExtra(intent, EXTRA_GUID);
        @SchedulerClientType
        int clientType =
                IntentUtils.safeGetIntExtra(
                        intent, EXTRA_SCHEDULER_CLIENT_TYPE, SchedulerClientType.UNKNOWN);
        switch (intentType) {
            case NotificationIntentInterceptor.IntentType.UNKNOWN:
                break;
            case NotificationIntentInterceptor.IntentType.CONTENT_INTENT:
                DisplayAgentJni.get()
                        .onUserAction(
                                clientType,
                                UserActionType.CLICK,
                                guid,
                                ActionButtonType.UNKNOWN_ACTION,
                                null);
                closeNotification(guid);
                break;
            case NotificationIntentInterceptor.IntentType.DELETE_INTENT:
                DisplayAgentJni.get()
                        .onUserAction(
                                clientType,
                                UserActionType.DISMISS,
                                guid,
                                ActionButtonType.UNKNOWN_ACTION,
                                null);
                break;
            case NotificationIntentInterceptor.IntentType.ACTION_INTENT:
                int actionButtonType =
                        IntentUtils.safeGetIntExtra(
                                intent, EXTRA_ACTION_BUTTON_TYPE, ActionButtonType.UNKNOWN_ACTION);
                String buttonId = IntentUtils.safeGetStringExtra(intent, EXTRA_ACTION_BUTTON_ID);
                DisplayAgentJni.get()
                        .onUserAction(
                                clientType,
                                UserActionType.BUTTON_CLICK,
                                guid,
                                actionButtonType,
                                buttonId);
                closeNotification(guid);
                break;
        }
    }

    private static void closeNotification(String guid) {
        BaseNotificationManagerProxyFactory.create(ContextUtils.getApplicationContext())
                .cancel(DISPLAY_AGENT_TAG, guid.hashCode());
    }

    /** Contains Android platform specific data to construct a notification. */
    private static class AndroidNotificationData {
        public final @ChannelId String channel;
        public final @SystemNotificationType int systemNotificationType;

        public AndroidNotificationData(String channel, int systemNotificationType) {
            this.channel = channel;
            this.systemNotificationType = systemNotificationType;
        }
    }

    private static AndroidNotificationData toAndroidNotificationData(SystemData systemData) {
        @ChannelId String channel = ChannelId.BROWSER;
        @SystemNotificationType int systemNotificationType = SystemNotificationType.UNKNOWN;
        return new AndroidNotificationData(channel, systemNotificationType);
    }

    private static Intent buildIntent(
            Context context,
            @NotificationIntentInterceptor.IntentType int intentType,
            SystemData systemData) {
        Intent intent = new Intent(context, DisplayAgent.Receiver.class);
        intent.putExtra(EXTRA_INTENT_TYPE, intentType);
        intent.putExtra(EXTRA_SCHEDULER_CLIENT_TYPE, systemData.type);
        intent.putExtra(EXTRA_GUID, systemData.guid);
        return intent;
    }

    @CalledByNative
    private static void showNotification(NotificationData notificationData, SystemData systemData) {
        AndroidNotificationData platformData = toAndroidNotificationData(systemData);
        // TODO(xingliu): Plumb platform specific data from native.
        // mode and provide correct notification id. Support buttons.
        Context context = ContextUtils.getApplicationContext();

        NotificationWrapperBuilder builder =
                NotificationWrapperBuilderFactory.createNotificationWrapperBuilder(
                        platformData.channel,
                        new NotificationMetadata(
                                platformData.systemNotificationType,
                                DISPLAY_AGENT_TAG,
                                systemData.guid.hashCode()));
        builder.setContentTitle(notificationData.title);
        builder.setContentText(notificationData.message);

        boolean hasSmallIcon = notificationData.icons.containsKey(IconType.SMALL_ICON);

        if (hasSmallIcon && notificationData.icons.get(IconType.SMALL_ICON).bitmap != null) {
            // Use bitmap as small icon.
            Icon smallIcon =
                    Icon.createWithBitmap(notificationData.icons.get(IconType.SMALL_ICON).bitmap);
            builder.setSmallIcon(smallIcon);
        } else {
            // Use resource Id as small icon, if invalid, use default Chrome icon instead.
            int resourceId = R.drawable.ic_chrome;
            if (hasSmallIcon && notificationData.icons.get(IconType.SMALL_ICON).resourceId != 0) {
                resourceId = notificationData.icons.get(IconType.SMALL_ICON).resourceId;
            }
            builder.setSmallIcon(resourceId);
        }

        if (notificationData.icons.containsKey(IconType.LARGE_ICON)
                && notificationData.icons.get(IconType.LARGE_ICON).bitmap != null) {
            builder.setLargeIcon(notificationData.icons.get(IconType.LARGE_ICON).bitmap);
        }

        // Default content click behavior.
        Intent contentIntent =
                buildIntent(
                        context,
                        NotificationIntentInterceptor.IntentType.CONTENT_INTENT,
                        systemData);
        builder.setContentIntent(
                PendingIntentProvider.getBroadcast(
                        context,
                        getRequestCode(
                                NotificationIntentInterceptor.IntentType.CONTENT_INTENT,
                                systemData.guid),
                        contentIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT));

        // Default dismiss behavior.
        Intent dismissIntent =
                buildIntent(
                        context,
                        NotificationIntentInterceptor.IntentType.DELETE_INTENT,
                        systemData);
        builder.setDeleteIntent(
                PendingIntentProvider.getBroadcast(
                        context,
                        getRequestCode(
                                NotificationIntentInterceptor.IntentType.DELETE_INTENT,
                                systemData.guid),
                        dismissIntent,
                        PendingIntent.FLAG_UPDATE_CURRENT));

        // Add the buttons.
        for (int i = 0; i < notificationData.buttons.size(); i++) {
            Button button = notificationData.buttons.get(i);
            Intent actionIntent =
                    buildIntent(
                            context,
                            NotificationIntentInterceptor.IntentType.ACTION_INTENT,
                            systemData);
            actionIntent.putExtra(EXTRA_ACTION_BUTTON_TYPE, button.type);
            actionIntent.putExtra(EXTRA_ACTION_BUTTON_ID, button.id);

            // TODO(xingliu): Support button icon. See https://crbug.com/983354
            builder.addAction(
                    /* icon_id= */ 0,
                    button.text,
                    PendingIntentProvider.getBroadcast(
                            context,
                            getRequestCode(
                                    NotificationIntentInterceptor.IntentType.ACTION_INTENT,
                                    systemData.guid),
                            actionIntent,
                            PendingIntent.FLAG_UPDATE_CURRENT),
                    NotificationUmaTracker.ActionType.UNKNOWN);
        }

        NotificationWrapper notification = builder.buildNotificationWrapper();
        BaseNotificationManagerProxyFactory.create(ContextUtils.getApplicationContext())
                .notify(notification);
        NotificationUmaTracker.getInstance()
                .onNotificationShown(
                        platformData.systemNotificationType, notification.getNotification());
    }

    /**
     * Returns the request code for a specific intent. Android will not distinguish intents based on
     * extra data. Different intent must have different request code.
     */
    private static int getRequestCode(
            @NotificationIntentInterceptor.IntentType int intentType, String guid) {
        int hash = guid.hashCode();
        hash += 31 * hash + intentType;
        return hash;
    }

    private DisplayAgent() {}

    @NativeMethods
    interface Natives {
        void onUserAction(
                @SchedulerClientType int clientType,
                @UserActionType int actionType,
                @JniType("std::string") String guid,
                @ActionButtonType int type,
                @JniType("std::string") String buttonId);
    }
}