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

import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;

import androidx.annotation.VisibleForTesting;

import org.jni_zero.CalledByNative;

import org.chromium.base.ContextUtils;
import org.chromium.base.IntentUtils;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.device.DeviceConditions;
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;

/** Manages ClickToCall related notifications for Android. */
public class ClickToCallMessageHandler {
    private static final String EXTRA_PHONE_NUMBER = "ClickToCallMessageHandler.EXTRA_PHONE_NUMBER";

    /**
     * Opens the dialer with the |phoneNumber| already prefilled.
     *
     * @param phoneNumber The phone number to show in the dialer.
     */
    private static void openDialer(String phoneNumber) {
        try {
            ContextUtils.getApplicationContext().startActivity(getDialIntent(phoneNumber));
            ClickToCallUma.recordDialerPresent(true);
        } catch (ActivityNotFoundException activityNotFound) {
            // Notify the user that no dialer app was available.
            ClickToCallUma.recordDialerPresent(false);
            displayDialerNotFoundNotification();
        }
    }

    /**
     * Shows an error notification suggesting the user to enable a dialer app to
     * use click to call.
     */
    public static void displayDialerNotFoundNotification() {
        Context context = ContextUtils.getApplicationContext();

        SharingNotificationUtil.showNotification(
                NotificationUmaTracker.SystemNotificationType.CLICK_TO_CALL,
                NotificationConstants.GROUP_CLICK_TO_CALL,
                NotificationConstants.NOTIFICATION_ID_CLICK_TO_CALL_ERROR,
                /* contentIntent= */ null,
                /* deleteIntent= */ null,
                /* confirmIntent= */ null,
                /* cancelIntent= */ null,
                context.getResources()
                        .getString(R.string.click_to_call_dialer_absent_notification_title),
                context.getResources()
                        .getString(R.string.click_to_call_dialer_absent_notification_text),
                R.drawable.ic_error_outline_red_24dp,
                R.drawable.ic_dialer_not_found_red_40dp,
                R.color.google_red_600,
                /* startsActivity= */ false);
    }

    /**
     * Handles the tapping of a notification by opening the dialer with the
     * phone number specified in the notification.
     */
    public static final class TapReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            openDialer(IntentUtils.safeGetStringExtra(intent, EXTRA_PHONE_NUMBER));
        }
    }

    /** Handles the device unlock event to remove notification and just show dialer. */
    public static final class PhoneUnlockedReceiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            // We only try to remove the notifications if we have opened the dialer.
            if (intent != null
                    && Intent.ACTION_USER_PRESENT.equals(intent.getAction())
                    && shouldOpenDialer()) {
                SharingNotificationUtil.dismissNotification(
                        NotificationConstants.GROUP_CLICK_TO_CALL,
                        NotificationConstants.NOTIFICATION_ID_CLICK_TO_CALL);
            }
        }
    }

    /**
     * Displays a notification that opens the dialer when clicked.
     *
     * @param phoneNumber The phone number to show in the dialer when the user taps the
     *                    notification.
     */
    private static void displayNotification(String phoneNumber) {
        Context context = ContextUtils.getApplicationContext();
        String contentTitle = Uri.decode(phoneNumber);
        SharingNotificationUtil.showNotification(
                NotificationUmaTracker.SystemNotificationType.CLICK_TO_CALL,
                NotificationConstants.GROUP_CLICK_TO_CALL,
                NotificationConstants.NOTIFICATION_ID_CLICK_TO_CALL,
                getContentIntentProvider(phoneNumber),
                /* deleteIntent= */ null,
                /* confirmIntent= */ null,
                /* cancelIntent= */ null,
                contentTitle,
                context.getResources().getString(R.string.click_to_call_notification_text),
                R.drawable.ic_devices_16dp,
                R.drawable.ic_dialer_icon_blue_40dp,
                R.color.default_icon_color_accent1_baseline,
                /* startsActivity= */ true);
    }

    private static Intent getDialIntent(String phoneNumber) {
        Intent dialIntent =
                TextUtils.isEmpty(phoneNumber)
                        ? new Intent(Intent.ACTION_DIAL)
                        : new Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + phoneNumber));
        dialIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return dialIntent;
    }

    private static PendingIntentProvider getContentIntentProvider(String phoneNumber) {
        Context context = ContextUtils.getApplicationContext();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
            // We can't use the TapReceiver broadcast to start the dialer Activity starting in
            // Android S. Use the dial intent directly instead.
            return PendingIntentProvider.getActivity(
                    context,
                    /* requestCode= */ 0,
                    getDialIntent(phoneNumber),
                    PendingIntent.FLAG_UPDATE_CURRENT);
        }

        return PendingIntentProvider.getBroadcast(
                context,
                /* requestCode= */ 0,
                new Intent(context, TapReceiver.class).putExtra(EXTRA_PHONE_NUMBER, phoneNumber),
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    /** Returns true if we should open the dialer straight away. */
    private static boolean shouldOpenDialer() {
        // On Android Q and above, we never open the dialer directly.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            return false;
        }

        // On Android N and below, we always open the dialer. If device is locked, we also show a
        // notification. We can listen to ACTION_USER_PRESENT and remove this notification when
        // device is unlocked.
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
            return true;
        }

        // On Android O and P, we open dialer only when device is unlocked since we cannot listen to
        // ACTION_USER_PRESENT.
        return DeviceConditions.isCurrentlyScreenOnAndUnlocked(
                ContextUtils.getApplicationContext());
    }

    /** Returns true if we should show notification to the user. */
    private static boolean shouldShowNotification() {
        // Always show the notification for Android Q and above. For pre-Q, only show notification
        // if device is locked.
        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
                || !DeviceConditions.isCurrentlyScreenOnAndUnlocked(
                        ContextUtils.getApplicationContext());
    }

    /**
     * Handles a phone number sent from another device.
     *
     * @param phoneNumber The phone number to call.
     */
    @CalledByNative
    @VisibleForTesting
    static void handleMessage(String phoneNumber) {
        if (shouldOpenDialer()) {
            openDialer(phoneNumber);
        }

        if (shouldShowNotification()) {
            displayNotification(phoneNumber);
        }
    }
}