chromium/chrome/android/java/src/org/chromium/chrome/browser/sharing/sms_fetcher/SmsFetcherMessageHandler.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.chrome.browser.sharing.sms_fetcher;

import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;

import androidx.annotation.Nullable;

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

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.device.DeviceConditions;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.notifications.NotificationConstants;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.sharing.SharingNotificationUtil;
import org.chromium.components.browser_ui.notifications.PendingIntentProvider;

/** Handles Sms Fetcher messages and notifications for Android. */
public class SmsFetcherMessageHandler {
    private static final String NOTIFICATION_ACTION_CONFIRM = "sms_fetcher_notification.confirm";
    private static final String NOTIFICATION_ACTION_CANCEL = "sms_fetcher_notification.cancel";
    private static final String TAG = "SmsMessageHandler";
    private static final boolean DEBUG = false;
    private static long sSmsFetcherMessageHandlerAndroid;
    private static String sTopOrigin;
    private static String sEmbeddedOrigin;

    /** Handles the interaction of an incoming notification when an expected SMS arrives. */
    public static final class NotificationReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            boolean nativeIsDestroyed = sSmsFetcherMessageHandlerAndroid == 0;
            RecordHistogram.recordBooleanHistogram(
                    "Sharing.SmsFetcherTapWithChromeDestroyed", nativeIsDestroyed);
            SharingNotificationUtil.dismissNotification(
                    NotificationConstants.GROUP_SMS_FETCHER,
                    NotificationConstants.NOTIFICATION_ID_SMS_FETCHER_INCOMING);
            // This could happen if the user manually swipes away Chrome from the task switcher or
            // the OS decides to destroy Chrome due to lack of memory etc. In these cases we just
            // close the notification.
            if (nativeIsDestroyed) return;
            switch (action) {
                case NOTIFICATION_ACTION_CONFIRM:
                    if (DEBUG) Log.d(TAG, "Notification confirmed");
                    SmsFetcherMessageHandlerJni.get()
                            .onConfirm(
                                    sSmsFetcherMessageHandlerAndroid, sTopOrigin, sEmbeddedOrigin);
                    break;
                case NOTIFICATION_ACTION_CANCEL:
                    if (DEBUG) Log.d(TAG, "Notification canceled");
                    SmsFetcherMessageHandlerJni.get()
                            .onDismiss(
                                    sSmsFetcherMessageHandlerAndroid, sTopOrigin, sEmbeddedOrigin);
                    break;
            }
        }
    }

    /**
     * Returns the notification title string.
     *
     * @param oneTimeCode The one time code from SMS
     * @param topOrigin The top frame origin from the SMS
     * @param embeddedOrigin The embedded frame origin from the SMS. Null if the SMS does not
     *         contain an iframe origin.
     * @param clientName The client name where the remote request comes from
     */
    private static String getNotificationTitle(
            String oneTimeCode, String topOrigin, String embeddedOrigin, String clientName) {
        Resources resources = ContextUtils.getApplicationContext().getResources();
        if (ChromeFeatureList.isEnabled(ChromeFeatureList.WEB_OTP_CROSS_DEVICE_SIMPLE_STRING)) {
            if (embeddedOrigin == null) {
                return resources.getString(
                        R.string.sms_fetcher_notification_title_simple_string,
                        oneTimeCode,
                        topOrigin);
            }
            return resources.getString(
                    R.string.sms_fetcher_notification_title_simple_string,
                    oneTimeCode,
                    embeddedOrigin);
        }
        return resources.getString(
                R.string.sms_fetcher_notification_title, oneTimeCode, clientName);
    }

    /**
     * Returns the notification text string.
     *
     * @param oneTimeCode The one time code from SMS
     * @param topOrigin The top frame origin from the SMS
     * @param embeddedOrigin The embedded frame origin from the SMS. Null if the SMS does not
     *         contain an iframe origin.
     * @param clientName The client name where the remote request comes from
     */
    private static String getNotificationText(
            String oneTimeCode, String topOrigin, String embeddedOrigin, String clientName) {
        Resources resources = ContextUtils.getApplicationContext().getResources();
        if (ChromeFeatureList.isEnabled(ChromeFeatureList.WEB_OTP_CROSS_DEVICE_SIMPLE_STRING)) {
            if (embeddedOrigin == null) return clientName;
            return topOrigin + " · " + clientName;
        }
        return embeddedOrigin == null
                ? resources.getString(R.string.sms_fetcher_notification_text, topOrigin)
                : resources.getString(
                        R.string.sms_fetcher_notification_text_for_embedded_frame,
                        topOrigin,
                        embeddedOrigin);
    }

    /**
     * Ask users to interact with the notification to allow Chrome to submit the code to the remote
     * device.
     *
     * @param oneTimeCode The one time code from SMS
     * @param topOrigin The top frame origin from the SMS
     * @param embeddedOrigin The embedded frame origin from the SMS. Null if the SMS does not
     *     contain an iframe origin.
     * @param clientName The client name where the remote request comes from
     * @param smsFetcherMessageHandlerAndroid The native handler
     */
    @CalledByNative
    private static void showNotification(
            @JniType("std::string") String oneTimeCode,
            String topOrigin,
            @Nullable String embeddedOrigin,
            @JniType("std::string") String clientName,
            long smsFetcherMessageHandlerAndroid) {
        sTopOrigin = topOrigin;
        sEmbeddedOrigin = embeddedOrigin;
        sSmsFetcherMessageHandlerAndroid = smsFetcherMessageHandlerAndroid;
        Context context = ContextUtils.getApplicationContext();
        RecordHistogram.recordBooleanHistogram(
                "Sharing.SmsFetcherScreenOnAndUnlocked",
                DeviceConditions.isCurrentlyScreenOnAndUnlocked(context));
        PendingIntentProvider confirmIntent =
                PendingIntentProvider.getBroadcast(
                        context,
                        /* requestCode= */ 0,
                        new Intent(context, NotificationReceiver.class)
                                .setAction(NOTIFICATION_ACTION_CONFIRM),
                        PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntentProvider cancelIntent =
                PendingIntentProvider.getBroadcast(
                        context,
                        /* requestCode= */ 0,
                        new Intent(context, NotificationReceiver.class)
                                .setAction(NOTIFICATION_ACTION_CANCEL),
                        PendingIntent.FLAG_UPDATE_CURRENT);
        SharingNotificationUtil.showNotification(
                NotificationUmaTracker.SystemNotificationType.SMS_FETCHER,
                NotificationConstants.GROUP_SMS_FETCHER,
                NotificationConstants.NOTIFICATION_ID_SMS_FETCHER_INCOMING,
                /* contentIntent= */ null,
                /* deleteIntent= */ cancelIntent,
                confirmIntent,
                cancelIntent,
                getNotificationTitle(oneTimeCode, topOrigin, embeddedOrigin, clientName),
                getNotificationText(oneTimeCode, topOrigin, embeddedOrigin, clientName),
                R.drawable.ic_chrome,
                /* largeIconId= */ 0,
                R.color.default_icon_color_accent1_baseline,
                /* startsActivity= */ false);
    }

    @CalledByNative
    private static void dismissNotification() {
        SharingNotificationUtil.dismissNotification(
                NotificationConstants.GROUP_SMS_FETCHER,
                NotificationConstants.NOTIFICATION_ID_SMS_FETCHER_INCOMING);
    }

    @CalledByNative
    private static void reset() {
        sSmsFetcherMessageHandlerAndroid = 0;
        sTopOrigin = null;
        sEmbeddedOrigin = null;
    }

    @NativeMethods
    interface Natives {
        void onConfirm(long nativeSmsFetchRequestHandler, String topOrigin, String embeddedOrigin);

        void onDismiss(long nativeSmsFetchRequestHandler, String topOrigin, String embeddedOrigin);
    }
}