org.chromium.chrome.browser.notifications.NotificationUIManager.java Source code

Java tutorial

Introduction

Here is the source code for org.chromium.chrome.browser.notifications.NotificationUIManager.java

Source

// Copyright 2014 The Chromium Authors. All rights reserved.
// 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.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.NotificationCompat;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import android.util.Log;

import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.preferences.Preferences;
import org.chromium.chrome.browser.preferences.PreferencesLauncher;
import org.chromium.chrome.browser.preferences.website.SingleCategoryPreferences;
import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences;
import org.chromium.chrome.browser.preferences.website.SiteSettingsCategory;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;

import java.net.URI;
import java.net.URISyntaxException;

import javax.annotation.Nullable;

/**
 * Provides the ability for the NotificationUIManagerAndroid to talk to the Android platform
 * notification manager.
 *
 * This class should only be used on the UI thread.
 */
public class NotificationUIManager {
    private static final String TAG = NotificationUIManager.class.getSimpleName();

    // We always use the same integer id when showing and closing notifications. The notification
    // tag is always set, which is a safe and sufficient way of identifying a notification, so the
    // integer id is not needed anymore except it must not vary in an uncontrolled way.
    @VisibleForTesting
    static final int PLATFORM_ID = -1;

    // Prefix for platform tags generated by this class. This allows us to verify when reading a tag
    // that it was set by us.
    private static final String PLATFORM_TAG_PREFIX = NotificationUIManager.class.getSimpleName();

    private static final int NOTIFICATION_ICON_BG_COLOR = Color.rgb(150, 150, 150);
    private static final int NOTIFICATION_TEXT_SIZE_DP = 28;

    // We always use the same request code for pending intents. We use other ways to force
    // uniqueness of pending intents when necessary.
    private static final int PENDING_INTENT_REQUEST_CODE = 0;

    private static NotificationUIManager sInstance;
    private static NotificationManagerProxy sNotificationManagerOverride;

    private final long mNativeNotificationManager;

    private final Context mAppContext;
    private final NotificationManagerProxy mNotificationManager;

    private RoundedIconGenerator mIconGenerator;

    private long mLastNotificationClickMs = 0L;

    /**
     * Creates a new instance of the NotificationUIManager.
     *
     * @param nativeNotificationManager Instance of the NotificationUIManagerAndroid class.
     * @param context Application context for this instance of Chrome.
     */
    @CalledByNative
    private static NotificationUIManager create(long nativeNotificationManager, Context context) {
        if (sInstance != null) {
            throw new IllegalStateException("There must only be a single NotificationUIManager.");
        }

        sInstance = new NotificationUIManager(nativeNotificationManager, context);
        return sInstance;
    }

    /**
     * Overrides the notification manager which is to be used for displaying Notifications on the
     * Android framework. Should only be used for testing. Tests are expected to clean up after
     * themselves by setting this to NULL again.
     *
     * @param proxy The notification manager instance to use instead of the system's.
     */
    @VisibleForTesting
    public static void overrideNotificationManagerForTesting(NotificationManagerProxy notificationManager) {
        sNotificationManagerOverride = notificationManager;
    }

    private NotificationUIManager(long nativeNotificationManager, Context context) {
        mNativeNotificationManager = nativeNotificationManager;
        mAppContext = context.getApplicationContext();

        if (sNotificationManagerOverride != null) {
            mNotificationManager = sNotificationManagerOverride;
        } else {
            mNotificationManager = new NotificationManagerProxyImpl(
                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE));
        }
    }

    /**
     * Marks the current instance as being freed, allowing for a new NotificationUIManager
     * object to be initialized.
     */
    @CalledByNative
    private void destroy() {
        assert sInstance == this;
        sInstance = null;
    }

    /**
     * Invoked by the NotificationService when a Notification intent has been received. There may
     * not be an active instance of the NotificationUIManager at this time, so inform the native
     * side through a static method, initializing the manager if needed.
     *
     * @param intent The intent as received by the Notification service.
     * @return Whether the event could be handled by the native Notification manager.
     */
    public static boolean dispatchNotificationEvent(Intent intent) {
        if (sInstance == null) {
            nativeInitializeNotificationUIManager();
            if (sInstance == null) {
                Log.e(TAG, "Unable to initialize the native NotificationUIManager.");
                return false;
            }
        }

        long persistentNotificationId = intent.getLongExtra(NotificationConstants.EXTRA_PERSISTENT_NOTIFICATION_ID,
                -1);
        String origin = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN);
        String tag = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_TAG);

        if (NotificationConstants.ACTION_CLICK_NOTIFICATION.equals(intent.getAction())) {
            return sInstance.onNotificationClicked(persistentNotificationId, origin, tag);
        } else if (NotificationConstants.ACTION_CLOSE_NOTIFICATION.equals(intent.getAction())) {
            return sInstance.onNotificationClosed(persistentNotificationId, origin, tag);
        }

        Log.e(TAG, "Unrecognized Notification action: " + intent.getAction());
        return false;
    }

    /**
     * Launches the notifications preferences screen. If the received intent indicates it came
     * from the gear button on a flipped notification, this launches the site specific preferences
     * screen.
     *
     * @param context The context that received the intent.
     * @param incomingIntent The received intent.
     */
    public static void launchNotificationPreferences(Context context, Intent incomingIntent) {
        // Use the application context because it lives longer. When using he given context, it
        // may be stopped before the preferences intent is handled.
        Context applicationContext = context.getApplicationContext();

        // If we can read an origin from the intent, use it to open the settings screen for that
        // origin.
        String origin = getOriginFromTag(
                incomingIntent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_TAG));
        boolean launchSingleWebsitePreferences = origin != null;

        String fragmentName = launchSingleWebsitePreferences ? SingleWebsitePreferences.class.getName()
                : SingleCategoryPreferences.class.getName();
        Intent preferencesIntent = PreferencesLauncher.createIntentForSettingsPage(applicationContext,
                fragmentName);

        Bundle fragmentArguments;
        if (launchSingleWebsitePreferences) {
            // Record that the user has clicked on the [Site Settings] button.
            RecordUserAction.record("Notifications.ShowSiteSettings");

            // All preferences for a specific origin.
            fragmentArguments = SingleWebsitePreferences.createFragmentArgsForSite(origin);
        } else {
            // Notification preferences for all origins.
            fragmentArguments = new Bundle();
            fragmentArguments.putString(SingleCategoryPreferences.EXTRA_CATEGORY,
                    SiteSettingsCategory.CATEGORY_NOTIFICATIONS);
            fragmentArguments.putString(SingleCategoryPreferences.EXTRA_TITLE,
                    applicationContext.getResources().getString(R.string.push_notifications_permission_title));
        }
        preferencesIntent.putExtra(Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS, fragmentArguments);

        // We need to ensure that no existing preference tasks are being re-used in order for the
        // new activity to appear on top.
        preferencesIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);

        applicationContext.startActivity(preferencesIntent);
    }

    /**
     * Returns the PendingIntent for completing |action| on the notification identified by the data
     * in the other parameters. |intentData| is used to ensure uniqueness of the PendingIntent.
     *
     * @param action The action this pending intent will represent.
     * @param persistentNotificationId The persistent id of the notification.
     * @param origin The origin to whom the notification belongs.
     * @param tag The tag of the notification. May be NULL.
     * @param intentData URI used to ensure uniqueness of the created PendingIntent.
     */
    private PendingIntent getPendingIntent(String action, long persistentNotificationId, String origin,
            @Nullable String tag, Uri intentData) {
        Intent intent = new Intent(action, intentData);
        intent.setClass(mAppContext, NotificationService.Receiver.class);

        intent.putExtra(NotificationConstants.EXTRA_PERSISTENT_NOTIFICATION_ID, persistentNotificationId);
        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN, origin);
        intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_TAG, tag);

        return PendingIntent.getBroadcast(mAppContext, PENDING_INTENT_REQUEST_CODE, intent,
                PendingIntent.FLAG_UPDATE_CURRENT);
    }

    /**
     * Generates the tag to be passed to the notification manager.
     *
     * If the generated tag is the same as that of a previous notification, a new notification shown
     * with this tag will replace it.
     *
     * If the input tag is not empty the output is: PREFIX + SEPARATOR + ORIGIN + SEPARATOR + TAG.
     * This output will be the same for notifications from the same origin that have the same input
     * tag.
     *
     * If the input tag is empty the output is PREFIX + SEPARATOR + ORIGIN + SEPARATOR +
     * NOTIFICATION_ID.
     *
     * @param persistentNotificationId The persistent id of the notification.
     * @param origin The origin for which the notification is shown.
     * @param tag A string identifier for this notification.
     * @return The generated platform tag.
     */
    private static String makePlatformTag(long persistentNotificationId, String origin, @Nullable String tag) {
        // The given tag may contain the separator character, so add it last to make reading the
        // preceding origin token reliable. If no tag was specified (it is the default empty
        // string), make the platform tag unique by appending the notification id.
        StringBuilder builder = new StringBuilder();
        builder.append(PLATFORM_TAG_PREFIX).append(NotificationConstants.NOTIFICATION_TAG_SEPARATOR).append(origin)
                .append(NotificationConstants.NOTIFICATION_TAG_SEPARATOR);

        if (TextUtils.isEmpty(tag)) {
            builder.append(persistentNotificationId);
        } else {
            builder.append(tag);
        }

        return builder.toString();
    }

    /**
     * Attempts to extract an origin from the tag extra in the given intent.
     *
     * See {@link #makePlatformTag} for details about the format of the tag.
     *
     * @param tag The tag from the intent extra. May be null.
     * @return The origin string. Returns null if there was no tag extra in the given intent, or if
     *         the tag value did not match the expected format.
     */
    @Nullable
    @VisibleForTesting
    static String getOriginFromTag(@Nullable String tag) {
        // If the user touched the settings cog on a flipped notification originating from this
        // class, there will be a notification tag extra in a specific format. From the tag we can
        // read the origin of the notification.
        if (tag == null || !tag.startsWith(PLATFORM_TAG_PREFIX))
            return null;

        String[] parts = tag.split(NotificationConstants.NOTIFICATION_TAG_SEPARATOR);
        assert parts.length >= 3;
        try {
            URI uri = new URI(parts[1]);
            if (uri.getHost() != null)
                return parts[1];
        } catch (URISyntaxException e) {
            Log.e(TAG, "Expected to find a valid url in the notification tag extra.", e);
            return null;
        }
        return null;
    }

    /**
     * Generates the notfiication defaults from vibrationPattern's size and silent.
     *
     * Use the system's default ringtone, vibration and indicator lights unless the notification
     * has been marked as being silent.
     * If a vibration pattern is set, the notification should use the provided pattern
     * rather than the defaulting to system settings.
     *
     * @param vibrationPatternLength Vibration pattern's size for the Notification.
     * @param silent Whether the default sound, vibration and lights should be suppressed.
     * @return The generated notification's default value.
    */
    @VisibleForTesting
    static int makeDefaults(int vibrationPatternLength, boolean silent) {
        assert !silent || vibrationPatternLength == 0;

        if (silent)
            return 0;

        int defaults = Notification.DEFAULT_ALL;
        if (vibrationPatternLength > 0) {
            defaults &= ~Notification.DEFAULT_VIBRATE;
        }
        return defaults;
    }

    /**
     * Generates the vibration pattern used in Android notification.
     *
     * Android takes a long array where the first entry indicates the number of milliseconds to wait
     * prior to starting the vibration, whereas Chrome follows the syntax of the Web Vibration API.
     *
     * @param vibrationPattern Vibration pattern following the Web Vibration API syntax.
     * @return Vibration pattern following the Android syntax.
    */
    @VisibleForTesting
    static long[] makeVibrationPattern(int[] vibrationPattern) {
        long[] pattern = new long[vibrationPattern.length + 1];
        for (int i = 0; i < vibrationPattern.length; ++i) {
            pattern[i + 1] = vibrationPattern[i];
        }
        return pattern;
    }

    /**
     * Displays a notification with the given details.
     *
     * @param persistentNotificationId The persistent id of the notification.
     * @param origin Full text of the origin, including the protocol, owning this notification.
     * @param tag A string identifier for this notification. If the tag is not empty, the new
     *            notification will replace the previous notification with the same tag and origin,
     *            if present. If no matching previous notification is present, the new one will just
     *            be added.
     * @param title Title to be displayed in the notification.
     * @param body Message to be displayed in the notification. Will be trimmed to one line of
     *             text by the Android notification system.
     * @param icon Icon to be displayed in the notification. When this isn't a valid Bitmap, a
     *             default icon will be generated instead.
     * @param vibrationPattern Vibration pattern following the Web Vibration syntax.
     * @param silent Whether the default sound, vibration and lights should be suppressed.
     * @see https://developer.android.com/reference/android/app/Notification.html
     */
    @CalledByNative
    private void displayNotification(long persistentNotificationId, String origin, String tag, String title,
            String body, Bitmap icon, int[] vibrationPattern, boolean silent) {
        if (icon == null || icon.getWidth() == 0) {
            icon = getIconGenerator().generateIconForUrl(origin, true);
        }

        Resources res = mAppContext.getResources();

        // The data used to make each intent unique according to the rules of Intent#filterEquals.
        // Without this, the pending intents derived from them may be reused, because extras are
        // not taken into account for the filterEquals comparison.
        Uri intentData = Uri.parse(origin).buildUpon().fragment(String.valueOf(persistentNotificationId)).build();

        // Set up a pending intent for going to the settings screen for |origin|.
        Intent settingsIntent = PreferencesLauncher.createIntentForSettingsPage(mAppContext,
                SingleWebsitePreferences.class.getName());
        settingsIntent.setData(intentData);
        settingsIntent.putExtra(Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS,
                SingleWebsitePreferences.createFragmentArgsForSite(origin));

        PendingIntent pendingSettingsIntent = PendingIntent.getActivity(mAppContext, PENDING_INTENT_REQUEST_CODE,
                settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(mAppContext)
                .setContentTitle(title).setContentText(body)
                .setStyle(new NotificationCompat.BigTextStyle().bigText(body)).setLargeIcon(icon)
                .setSmallIcon(R.drawable.ic_chrome)
                .setContentIntent(getPendingIntent(NotificationConstants.ACTION_CLICK_NOTIFICATION,
                        persistentNotificationId, origin, tag, intentData))
                .setDeleteIntent(getPendingIntent(NotificationConstants.ACTION_CLOSE_NOTIFICATION,
                        persistentNotificationId, origin, tag, intentData))
                .addAction(R.drawable.settings_cog, res.getString(R.string.page_info_site_settings_button),
                        pendingSettingsIntent)
                .setTicker(createTickerText(title, body)).setSubText(origin);

        notificationBuilder.setDefaults(makeDefaults(vibrationPattern.length, silent));
        if (vibrationPattern.length > 0) {
            notificationBuilder.setVibrate(makeVibrationPattern(vibrationPattern));
        }

        String platformTag = makePlatformTag(persistentNotificationId, origin, tag);
        mNotificationManager.notify(platformTag, PLATFORM_ID, notificationBuilder.build());
    }

    /**
     * Creates the ticker text for a notification having |title| and |body|. The notification's
     * title will be printed in bold, followed by the text of the body.
     *
     * @param title Title of the notification.
     * @param body Textual contents of the notification.
     * @return A character sequence containing the ticker's text.
     */
    private CharSequence createTickerText(String title, String body) {
        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();

        spannableStringBuilder.append(title);
        spannableStringBuilder.append("\n");
        spannableStringBuilder.append(body);

        // Mark the title of the notification as being bold.
        spannableStringBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), 0, title.length(),
                Spannable.SPAN_INCLUSIVE_INCLUSIVE);

        return spannableStringBuilder;
    }

    /**
     * Ensures the existance of an icon generator, which is created lazily.
     *
     * @return The icon generator which can be used.
     */
    private RoundedIconGenerator getIconGenerator() {
        if (mIconGenerator == null) {
            mIconGenerator = createRoundedIconGenerator(mAppContext);
        }

        return mIconGenerator;
    }

    /**
     * Creates the rounded icon generator to use for notifications based on the dimensions
     * and resolution of the device we're running on.
     *
     * @param appContext The application context to retrieve resources from.
     * @return The newly created rounded icon generator.
     */
    @VisibleForTesting
    public static RoundedIconGenerator createRoundedIconGenerator(Context appContext) {
        Resources res = appContext.getResources();
        float density = res.getDisplayMetrics().density;

        int widthPx = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
        int heightPx = res.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);

        return new RoundedIconGenerator(widthPx, heightPx, Math.min(widthPx, heightPx) / 2,
                NOTIFICATION_ICON_BG_COLOR, NOTIFICATION_TEXT_SIZE_DP * density);
    }

    /**
     * Returns whether a notification has been clicked in the last 5 seconds.
     * Used for Startup.BringToForegroundReason UMA histogram.
     */
    public static boolean wasNotificationRecentlyClicked() {
        if (sInstance == null)
            return false;
        long now = System.currentTimeMillis();
        return now - sInstance.mLastNotificationClickMs < 5 * 1000;
    }

    /**
     * Closes the notification associated with the given parameters.
     *
     * @param persistentNotificationId The persistent id of the notification.
     * @param origin The origin to which the notification belongs.
     * @param tag The tag of the notification. May be NULL.
     */
    @CalledByNative
    private void closeNotification(long persistentNotificationId, String origin, String tag) {
        String platformTag = makePlatformTag(persistentNotificationId, origin, tag);
        mNotificationManager.cancel(platformTag, PLATFORM_ID);
    }

    /**
     * Calls NotificationUIManagerAndroid::OnNotificationClicked in native code to indicate that
     * the notification with the given parameters has been clicked on.
     *
     * @param persistentNotificationId The persistent id of the notification.
     * @param origin The origin of the notification.
     * @param tag The tag of the notification. May be NULL.
     * @return Whether the manager could handle the click event.
     */
    private boolean onNotificationClicked(long persistentNotificationId, String origin, String tag) {
        mLastNotificationClickMs = System.currentTimeMillis();
        return nativeOnNotificationClicked(mNativeNotificationManager, persistentNotificationId, origin, tag);
    }

    /**
     * Calls NotificationUIManagerAndroid::OnNotificationClosed in native code to indicate that
     * the notification with the given parameters has been closed. This could be the result of
     * user interaction or an action initiated by the framework.
     *
     * @param persistentNotificationId The persistent id of the notification.
     * @param origin The origin of the notification.
     * @param tag The tag of the notification. May be NULL.
     * @return Whether the manager could handle the close event.
     */
    private boolean onNotificationClosed(long persistentNotificationId, String origin, String tag) {
        return nativeOnNotificationClosed(mNativeNotificationManager, persistentNotificationId, origin, tag);
    }

    private static native void nativeInitializeNotificationUIManager();

    private native boolean nativeOnNotificationClicked(long nativeNotificationUIManagerAndroid,
            long persistentNotificationId, String origin, String tag);

    private native boolean nativeOnNotificationClosed(long nativeNotificationUIManagerAndroid,
            long persistentNotificationId, String origin, String tag);
}