Java tutorial
// 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); }