// 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.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.StyleSpan;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.init.ChromeBrowserInitializer;
import org.chromium.chrome.browser.preferences.PrefServiceBridge;
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.webapps.ChromeWebApkHost;
import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.webapk.lib.client.WebApkValidator;
import java.net.URI;
import java.net.URISyntaxException;
import javax.annotation.Nullable;
/**
* Provides the ability for the NotificationPlatformBridgeAndroid to talk to the Android platform
* notification system.
*
* This class should only be used on the UI thread.
*/
public class NotificationPlatformBridge {
private static final String TAG = NotificationPlatformBridge.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 =
NotificationPlatformBridge.class.getSimpleName();
// 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 final int[] EMPTY_VIBRATION_PATTERN = new int[0];
private static NotificationPlatformBridge sInstance;
private static NotificationManagerProxy sNotificationManagerOverride;
private final long mNativeNotificationPlatformBridge;
private final Context mAppContext;
private final NotificationManagerProxy mNotificationManager;
private long mLastNotificationClickMs = 0L;
/**
* Creates a new instance of the NotificationPlatformBridge.
*
* @param nativeNotificationPlatformBridge Instance of the NotificationPlatformBridgeAndroid
* class.
*/
@CalledByNative
private static NotificationPlatformBridge create(long nativeNotificationPlatformBridge) {
if (sInstance != null) {
throw new IllegalStateException(
"There must only be a single NotificationPlatformBridge.");
}
sInstance = new NotificationPlatformBridge(nativeNotificationPlatformBridge);
return sInstance;
}
/**
* Returns the current instance of the NotificationPlatformBridge.
*
* @return The instance of the NotificationPlatformBridge, if any.
*/
@Nullable
@VisibleForTesting
static NotificationPlatformBridge getInstanceForTests() {
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 NotificationPlatformBridge(long nativeNotificationPlatformBridge) {
mNativeNotificationPlatformBridge = nativeNotificationPlatformBridge;
mAppContext = ContextUtils.getApplicationContext();
if (sNotificationManagerOverride != null) {
mNotificationManager = sNotificationManagerOverride;
} else {
mNotificationManager = new NotificationManagerProxyImpl(
(NotificationManager) mAppContext.getSystemService(
Context.NOTIFICATION_SERVICE));
}
}
/**
* Marks the current instance as being freed, allowing for a new NotificationPlatformBridge
* object to be initialized.
*/
@CalledByNative
private void destroy() {
assert sInstance == this;
sInstance = null;
}
/**
* Returns the package for the WebAPK which should handle the URL.
*
* @param url The url to check.
* @return Package name of the WebAPK which should handle the URL. Returns empty string if the
* URL should not be handled by a WebAPK.
*/
@CalledByNative
private String queryWebApkPackage(String url) {
if (!ChromeWebApkHost.isEnabled()) return "";
String webApkPackage =
WebApkValidator.queryWebApkPackage(mAppContext, url);
return webApkPackage == null ? "" : webApkPackage;
}
/**
* Invoked by the NotificationService when a Notification intent has been received. There may
* not be an active instance of the NotificationPlatformBridge at this time, so inform the
* native side through a static method, initializing both ends if needed.
*
* @param intent The intent as received by the Notification service.
* @return Whether the event could be handled by the native Notification bridge.
*/
public static boolean dispatchNotificationEvent(Intent intent) {
if (sInstance == null) {
nativeInitializeNotificationPlatformBridge();
if (sInstance == null) {
Log.e(TAG, "Unable to initialize the native NotificationPlatformBridge.");
return false;
}
}
String notificationId = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_ID);
String origin = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN);
String profileId =
intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID);
boolean incognito = intent.getBooleanExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO, false);
String tag = intent.getStringExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_TAG);
Log.i(TAG, "Dispatching notification event to native: " + notificationId);
if (NotificationConstants.ACTION_CLICK_NOTIFICATION.equals(intent.getAction())) {
String webApkPackage = "";
if (ChromeWebApkHost.isEnabled()) {
webApkPackage = intent.getStringExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE);
if (webApkPackage == null) {
webApkPackage = "";
}
}
int actionIndex = intent.getIntExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX, -1);
sInstance.onNotificationClicked(
notificationId, origin, profileId, incognito, tag, webApkPackage, actionIndex);
return true;
} else if (NotificationConstants.ACTION_CLOSE_NOTIFICATION.equals(intent.getAction())) {
// Notification deleteIntent is executed only "when the notification is explicitly
// dismissed by the user, either with the 'Clear All' button or by swiping it away
// individually" (though a third-party NotificationListenerService may also trigger it).
sInstance.onNotificationClosed(
notificationId, origin, profileId, incognito, tag, true /* byUser */);
return true;
}
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) {
// This method handles an intent fired by the Android system. There is no guarantee that the
// native library is loaded at this point. The native library is needed for the preferences
// activity, and it loads the library, but there are some native calls even before that
// activity is started: from RecordUserAction.record and (indirectly) from
// UrlFormatter.formatUrlForSecurityDisplay.
try {
ChromeBrowserInitializer.getInstance(context).handleSynchronousStartup();
} catch (ProcessInitException e) {
Log.e(TAG, "Failed to start browser process.", e);
// The library failed to initialize and nothing in the application can work, so kill
// the whole application.
System.exit(-1);
return;
}
// Use the application context because it lives longer. When using the 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 a bogus Uri used to make each intent unique according to Intent#filterEquals.
* Without this, the pending intents derived from the intent may be reused, because extras are
* not taken into account for the filterEquals comparison.
*
* @param notificationId The id of the notification.
* @param origin The origin to whom the notification belongs.
* @param actionIndex The zero-based index of the action button, or -1 if not applicable.
*/
private Uri makeIntentData(String notificationId, String origin, int actionIndex) {
return Uri.parse(origin).buildUpon().fragment(notificationId + "," + actionIndex).build();
}
/**
* Returns the PendingIntent for completing |action| on the notification identified by the data
* in the other parameters.
*
* @param action The action this pending intent will represent.
* @paramn notificationId The id of the notification.
* @param origin The origin to whom the notification belongs.
* @param profileId Id of the profile to which the notification belongs.
* @param incognito Whether the profile was in incognito mode.
* @param tag The tag of the notification. May be NULL.
* @param webApkPackage The package of the WebAPK associated with the notification. Empty if
* the notification is not associated with a WebAPK.
* @param actionIndex The zero-based index of the action button, or -1 if not applicable.
*/
private PendingIntent makePendingIntent(String action, String notificationId, String origin,
String profileId, boolean incognito, @Nullable String tag, String webApkPackage,
int actionIndex) {
Uri intentData = makeIntentData(notificationId, origin, actionIndex);
Intent intent = new Intent(action, intentData);
intent.setClass(mAppContext, NotificationService.Receiver.class);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_ID, notificationId);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ORIGIN, origin);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_ID, profileId);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_PROFILE_INCOGNITO, incognito);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_TAG, tag);
intent.putExtra(
NotificationConstants.EXTRA_NOTIFICATION_INFO_WEBAPK_PACKAGE, webApkPackage);
intent.putExtra(NotificationConstants.EXTRA_NOTIFICATION_INFO_ACTION_INDEX, actionIndex);
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 notificationId The 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.
*/
@VisibleForTesting
static String makePlatformTag(String notificationId, 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(notificationId);
} 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 notification 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 defaulting to the system settings.
*
* @param vibrationPatternLength Vibration pattern's size for the Notification.
* @param silent Whether the default sound, vibration and lights should be suppressed.
* @param vibrateEnabled Whether vibration is enabled in preferences.
* @return The generated notification's default value.
*/
@VisibleForTesting
static int makeDefaults(int vibrationPatternLength, boolean silent, boolean vibrateEnabled) {
assert !silent || vibrationPatternLength == 0;
if (silent) return 0;
int defaults = Notification.DEFAULT_ALL;
if (vibrationPatternLength > 0 || !vibrateEnabled) {
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.
*
* TODO(crbug.com/650302): Combine the 'action*' parameters into a single array of objects.
* @param notificationId The id of the notification.
* @param origin Full text of the origin, including the protocol, owning this notification.
* @param profileId Id of the profile that showed the notification.
* @param incognito if the session of the profile is an off the record one.
* @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 webApkPackage The package of the WebAPK associated with the notification. Empty if
* the notification is not associated with a WebAPK.
* @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 image Content image to be prominently displayed when the notification is expanded.
* @param icon Icon to be displayed in the notification. Valid Bitmap icons will be scaled to
* the platforms, whereas a default icon will be generated for invalid Bitmaps.
* @param badge An image to represent the notification in the status bar. It is also displayed
* inside the notification.
* @param vibrationPattern Vibration pattern following the Web Vibration syntax.
* @param timestamp The timestamp of the event for which the notification is being shown.
* @param renotify Whether the sound, vibration, and lights should be replayed if the
* notification is replacing another notification.
* @param silent Whether the default sound, vibration and lights should be suppressed.
* @param actionTitles Titles of actions to display alongside the notification.
* @param actionIcons Icons of actions to display alongside the notification.
* @param actionTypes Types of actions to display alongside the notification.
* @param actionPlaceholders Placeholders of actions to display alongside the notification.
* @see https://developer.android.com/reference/android/app/Notification.html
*/
@CalledByNative
private void displayNotification(String notificationId, String origin, String profileId,
boolean incognito, String tag, String webApkPackage, String title, String body,
Bitmap image, Bitmap icon, Bitmap badge, int[] vibrationPattern, long timestamp,
boolean renotify, boolean silent, String[] actionTitles, Bitmap[] actionIcons,
String[] actionTypes, String[] actionPlaceholders) {
if (actionTitles.length != actionIcons.length) {
throw new IllegalArgumentException("The number of action titles and icons must match.");
}
Resources res = mAppContext.getResources();
// Record whether it's known whether notifications can be shown to the user at all.
RecordHistogram.recordEnumeratedHistogram(
"Notifications.AppNotificationStatus",
NotificationSystemStatusUtil.determineAppNotificationStatus(mAppContext),
NotificationSystemStatusUtil.APP_NOTIFICATIONS_STATUS_BOUNDARY);
// Set up a pending intent for going to the settings screen for |origin|.
Intent settingsIntent = PreferencesLauncher.createIntentForSettingsPage(
mAppContext, SingleWebsitePreferences.class.getName());
settingsIntent.setData(makeIntentData(notificationId, origin, -1 /* actionIndex */));
settingsIntent.putExtra(Preferences.EXTRA_SHOW_FRAGMENT_ARGUMENTS,
SingleWebsitePreferences.createFragmentArgsForSite(origin));
PendingIntent pendingSettingsIntent = PendingIntent.getActivity(mAppContext,
PENDING_INTENT_REQUEST_CODE, settingsIntent, PendingIntent.FLAG_UPDATE_CURRENT);
PendingIntent clickIntent =
makePendingIntent(NotificationConstants.ACTION_CLICK_NOTIFICATION, notificationId,
origin, profileId, incognito, tag, webApkPackage, -1 /* actionIndex */);
PendingIntent closeIntent =
makePendingIntent(NotificationConstants.ACTION_CLOSE_NOTIFICATION, notificationId,
origin, profileId, incognito, tag, webApkPackage, -1 /* actionIndex */);
boolean hasImage = image != null;
NotificationBuilderBase notificationBuilder =
createNotificationBuilder(hasImage)
.setTitle(title)
.setBody(body)
.setImage(image)
.setLargeIcon(icon)
.setSmallIcon(R.drawable.ic_chrome)
.setSmallIcon(badge)
.setContentIntent(clickIntent)
.setDeleteIntent(closeIntent)
.setTicker(createTickerText(title, body))
.setTimestamp(timestamp)
.setRenotify(renotify)
.setOrigin(UrlFormatter.formatUrlForSecurityDisplay(
origin, false /* showScheme */));
for (int actionIndex = 0; actionIndex < actionTitles.length; actionIndex++) {
PendingIntent intent = makePendingIntent(
NotificationConstants.ACTION_CLICK_NOTIFICATION, notificationId, origin,
profileId, incognito, tag, webApkPackage, actionIndex);
// Don't show action button icons when there's an image, as then action buttons go on
// the same row as the Site Settings button, so icons wouldn't leave room for text.
Bitmap actionIcon = hasImage ? null : actionIcons[actionIndex];
// TODO(crbug.com/650302): Encode actionTypes with an enum, not a magic string!
if (actionTypes[actionIndex].equals("text")) {
notificationBuilder.addTextAction(actionIcon, actionTitles[actionIndex], intent,
actionPlaceholders[actionIndex]);
} else {
notificationBuilder.addButtonAction(actionIcon, actionTitles[actionIndex], intent);
}
}
// If action buttons are displayed, there isn't room for the full Site Settings button
// label and icon, so abbreviate it. This has the unfortunate side-effect of unnecessarily
// abbreviating it on Android Wear also (crbug.com/576656). If custom layouts are enabled,
// the label and icon provided here only affect Android Wear, so don't abbreviate them.
boolean abbreviateSiteSettings = actionTitles.length > 0 && !useCustomLayouts(hasImage);
int settingsIconId = abbreviateSiteSettings ? 0 : R.drawable.settings_cog;
CharSequence settingsTitle = abbreviateSiteSettings
? res.getString(R.string.notification_site_settings_button)
: res.getString(R.string.page_info_site_settings_button);
// If the settings button is displayed together with the other buttons it has to be the last
// one, so add it after the other actions.
notificationBuilder.addSettingsAction(settingsIconId, settingsTitle, pendingSettingsIntent);
// The Android framework applies a fallback vibration pattern for the sound when the device
// is in vibrate mode, there is no custom pattern, and the vibration default has been
// disabled. To truly prevent vibration, provide a custom empty pattern.
boolean vibrateEnabled = PrefServiceBridge.getInstance().isNotificationsVibrateEnabled();
if (!vibrateEnabled) {
vibrationPattern = EMPTY_VIBRATION_PATTERN;
}
notificationBuilder.setDefaults(
makeDefaults(vibrationPattern.length, silent, vibrateEnabled));
notificationBuilder.setVibrate(makeVibrationPattern(vibrationPattern));
String platformTag = makePlatformTag(notificationId, origin, tag);
if (webApkPackage.isEmpty()) {
mNotificationManager.notify(platformTag, PLATFORM_ID, notificationBuilder.build());
} else {
WebApkNotificationClient.notifyNotification(
webApkPackage, notificationBuilder, platformTag, PLATFORM_ID);
}
}
private NotificationBuilderBase createNotificationBuilder(boolean hasImage) {
if (useCustomLayouts(hasImage)) {
return new CustomNotificationBuilder(mAppContext);
}
return new StandardNotificationBuilder(mAppContext);
}
/**
* 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;
}
/**
* Determines whether to use standard notification layouts, using NotificationCompat.Builder,
* or custom layouts using Chrome's own templates.
*
* The --{enable,disable}-web-notification-custom-layouts command line flags take precedence.
*
* Normally a standard layout is used on Android N+, and a custom layout is used on older
* versions of Android. But if the notification has a content image, there isn't enough room for
* the Site Settings button to go on its own line when showing an image, nor is there enough
* room for action button icons, so a standard layout will be used here even on old versions.
*
* @param hasImage Whether the notification has a content image.
* @return Whether custom layouts should be used.
*/
@VisibleForTesting
static boolean useCustomLayouts(boolean hasImage) {
CommandLine commandLine = CommandLine.getInstance();
if (commandLine.hasSwitch(ChromeSwitches.ENABLE_WEB_NOTIFICATION_CUSTOM_LAYOUTS)) {
return true;
}
if (commandLine.hasSwitch(ChromeSwitches.DISABLE_WEB_NOTIFICATION_CUSTOM_LAYOUTS)) {
return false;
}
if (Build.VERSION.CODENAME.equals("N") || Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
return false;
}
if (hasImage) {
return false;
}
return true;
}
/**
* 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 profileId of the profile whose notification this is for.
* @param notificationId The id of the notification.
* @param origin The origin to which the notification belongs.
* @param tag The tag of the notification. May be NULL.
* @param webApkPackage The package of the WebAPK associated with the notification.
* Empty if the notification is not associated with a WebAPK.
*/
@CalledByNative
private void closeNotification(String profileId, String notificationId, String origin,
String tag, String webApkPackage) {
// TODO(miguelg) make profile_id part of the tag.
String platformTag = makePlatformTag(notificationId, origin, tag);
if (webApkPackage.isEmpty()) {
mNotificationManager.cancel(platformTag, PLATFORM_ID);
} else {
WebApkNotificationClient.cancelNotification(webApkPackage, platformTag, PLATFORM_ID);
}
}
/**
* Calls NotificationPlatformBridgeAndroid::OnNotificationClicked in native code to indicate
* that the notification with the given parameters has been clicked on.
*
* @param notificationId The id of the notification.
* @param origin The origin of the notification.
* @param profileId Id of the profile that showed the notification.
* @param incognito if the profile session was an off the record one.
* @param tag The tag of the notification. May be NULL.
* @param webApkPackage The package of the WebAPK associated with the notification.
* Empty if the notification is not associated with a WebAPK.
* @param actionIndex
*/
private void onNotificationClicked(String notificationId, String origin, String profileId,
boolean incognito, String tag, String webApkPackage, int actionIndex) {
mLastNotificationClickMs = System.currentTimeMillis();
nativeOnNotificationClicked(mNativeNotificationPlatformBridge, notificationId, origin,
profileId, incognito, tag, webApkPackage, actionIndex);
}
/**
* Calls NotificationPlatformBridgeAndroid::OnNotificationClosed in native code to indicate that
* the notification with the given parameters has been closed.
*
* @param notificationId The id of the notification.
* @param origin The origin of the notification.
* @param profileId Id of the profile that showed the notification.
* @param incognito if the profile session was an off the record one.
* @param tag The tag of the notification. May be NULL.
* @param byUser Whether the notification was closed by a user gesture.
*/
private void onNotificationClosed(String notificationId, String origin, String profileId,
boolean incognito, String tag, boolean byUser) {
nativeOnNotificationClosed(mNativeNotificationPlatformBridge, notificationId, origin,
profileId, incognito, tag, byUser);
}
private static native void nativeInitializeNotificationPlatformBridge();
private native void nativeOnNotificationClicked(long nativeNotificationPlatformBridgeAndroid,
String notificationId, String origin, String profileId, boolean incognito, String tag,
String webApkPackage, int actionIndex);
private native void nativeOnNotificationClosed(long nativeNotificationPlatformBridgeAndroid,
String notificationId, String origin, String profileId, boolean incognito, String tag,
boolean byUser);
}