// Copyright 2016 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.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Icon;
import android.os.Build;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
/**
* Abstract base class for building a notification. Stores all given arguments for later use.
*/
public abstract class NotificationBuilderBase {
protected static class Action {
enum Type {
/**
* Regular action that triggers the provided intent when tapped.
*/
BUTTON,
/**
* Action that triggers a remote input when tapped, for Android Wear input and inline
* replies from Android N.
*/
TEXT
}
public int iconId;
public Bitmap iconBitmap;
public CharSequence title;
public PendingIntent intent;
public Type type;
/**
* If the action.type is TEXT, this corresponds to the placeholder text for the input.
*/
public String placeholder;
Action(int iconId, CharSequence title, PendingIntent intent, Type type,
String placeholder) {
this.iconId = iconId;
this.title = title;
this.intent = intent;
this.type = type;
this.placeholder = placeholder;
}
Action(Bitmap iconBitmap, CharSequence title, PendingIntent intent, Type type,
String placeholder) {
this.iconBitmap = iconBitmap;
this.title = title;
this.intent = intent;
this.type = type;
this.placeholder = placeholder;
}
}
/**
* Maximum length of CharSequence inputs to prevent excessive memory consumption. At current
* screen sizes we display about 500 characters at most, so this is a pretty generous limit, and
* it matches what the Notification class does.
*/
@VisibleForTesting
static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024;
/**
* Background color for generated notification icons.
*/
@VisibleForTesting
static final int NOTIFICATION_ICON_BG_COLOR = 0xFF969696;
/**
* Density-independent text size for generated notification icons.
*/
@VisibleForTesting
static final int NOTIFICATION_ICON_TEXT_SIZE_DP = 28;
/**
* The maximum number of author provided action buttons. The settings button is not part of this
* count.
*/
private static final int MAX_AUTHOR_PROVIDED_ACTION_BUTTONS = 2;
private final int mLargeIconWidthPx;
private final int mLargeIconHeightPx;
private final RoundedIconGenerator mIconGenerator;
protected CharSequence mTitle;
protected CharSequence mBody;
protected CharSequence mOrigin;
protected CharSequence mTickerText;
protected Bitmap mImage;
protected int mSmallIconId;
protected Bitmap mSmallIconBitmap;
protected PendingIntent mContentIntent;
protected PendingIntent mDeleteIntent;
protected List<Action> mActions = new ArrayList<>(MAX_AUTHOR_PROVIDED_ACTION_BUTTONS);
protected Action mSettingsAction;
protected int mDefaults = Notification.DEFAULT_ALL;
protected long[] mVibratePattern;
protected long mTimestamp;
protected boolean mRenotify;
private Bitmap mLargeIcon;
public NotificationBuilderBase(Resources resources) {
mLargeIconWidthPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
mLargeIconHeightPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
mIconGenerator = createIconGenerator(resources);
}
/**
* Combines all of the options that have been set and returns a new Notification object.
*/
public abstract Notification build();
/**
* Sets the title text of the notification.
*/
public NotificationBuilderBase setTitle(@Nullable CharSequence title) {
mTitle = limitLength(title);
return this;
}
/**
* Sets the body text of the notification.
*/
public NotificationBuilderBase setBody(@Nullable CharSequence body) {
mBody = limitLength(body);
return this;
}
/**
* Sets the origin text of the notification.
*/
public NotificationBuilderBase setOrigin(@Nullable CharSequence origin) {
mOrigin = limitLength(origin);
return this;
}
/**
* Sets the text that is displayed in the status bar when the notification first arrives.
*/
public NotificationBuilderBase setTicker(@Nullable CharSequence tickerText) {
mTickerText = limitLength(tickerText);
return this;
}
/**
* Sets the content image to be prominently displayed when the notification is expanded.
*/
public NotificationBuilderBase setImage(@Nullable Bitmap image) {
mImage = image;
return this;
}
/**
* Sets the large icon that is shown in the notification.
*/
public NotificationBuilderBase setLargeIcon(@Nullable Bitmap icon) {
mLargeIcon = icon;
return this;
}
/**
* Sets the small icon that is shown in the notification and in the status bar. Wherever the
* platform supports using a small icon bitmap, and a non-null {@code Bitmap} is provided, it
* will take precedence over one specified as a resource id.
*/
public NotificationBuilderBase setSmallIcon(int iconId) {
mSmallIconId = iconId;
return this;
}
/**
* Sets the small icon that is shown in the notification and in the status bar. Wherever the
* platform supports using a small icon bitmap, and a non-null {@code Bitmap} is provided, it
* will take precedence over one specified as a resource id.
*/
public NotificationBuilderBase setSmallIcon(@Nullable Bitmap iconBitmap) {
Bitmap copyOfBitmap = null;
if (iconBitmap != null) {
copyOfBitmap = iconBitmap.copy(iconBitmap.getConfig(), true /* isMutable */);
applyWhiteOverlayToBitmap(copyOfBitmap);
}
mSmallIconBitmap = copyOfBitmap;
return this;
}
/**
* Sets the PendingIntent to send when the notification is clicked.
*/
public NotificationBuilderBase setContentIntent(@Nullable PendingIntent intent) {
mContentIntent = intent;
return this;
}
/**
* Sets the PendingIntent to send when the notification is cleared by the user directly from the
* notification panel.
*/
public NotificationBuilderBase setDeleteIntent(@Nullable PendingIntent intent) {
mDeleteIntent = intent;
return this;
}
/**
* Adds an action to the notification, displayed as a button adjacent to the notification
* content.
*/
public NotificationBuilderBase addButtonAction(@Nullable Bitmap iconBitmap,
@Nullable CharSequence title, @Nullable PendingIntent intent) {
addAuthorProvidedAction(iconBitmap, title, intent, Action.Type.BUTTON, null);
return this;
}
/**
* Adds an action to the notification, displayed as a button adjacent to the notification
* content, which when tapped will trigger a remote input. This enables Android Wear input and,
* from Android N, displays a text box within the notification for inline replies.
*/
public NotificationBuilderBase addTextAction(@Nullable Bitmap iconBitmap,
@Nullable CharSequence title, @Nullable PendingIntent intent, String placeholder) {
addAuthorProvidedAction(iconBitmap, title, intent, Action.Type.TEXT, placeholder);
return this;
}
private void addAuthorProvidedAction(@Nullable Bitmap iconBitmap, @Nullable CharSequence title,
@Nullable PendingIntent intent, Action.Type actionType, @Nullable String placeholder) {
if (mActions.size() == MAX_AUTHOR_PROVIDED_ACTION_BUTTONS) {
throw new IllegalStateException(
"Cannot add more than " + MAX_AUTHOR_PROVIDED_ACTION_BUTTONS + " actions.");
}
if (iconBitmap != null) {
applyWhiteOverlayToBitmap(iconBitmap);
}
mActions.add(new Action(iconBitmap, limitLength(title), intent, actionType, placeholder));
}
/**
* Adds an action to the notification for opening the settings screen.
*/
public NotificationBuilderBase addSettingsAction(
int iconId, @Nullable CharSequence title, @Nullable PendingIntent intent) {
mSettingsAction = new Action(iconId, limitLength(title), intent, Action.Type.BUTTON, null);
return this;
}
/**
* Sets the default notification options that will be used.
* <p>
* The value should be one or more of the following fields combined with
* bitwise-or:
* {@link Notification#DEFAULT_SOUND}, {@link Notification#DEFAULT_VIBRATE},
* {@link Notification#DEFAULT_LIGHTS}.
* <p>
* For all default values, use {@link Notification#DEFAULT_ALL}.
*/
public NotificationBuilderBase setDefaults(int defaults) {
mDefaults = defaults;
return this;
}
/**
* Sets the vibration pattern to use.
*/
public NotificationBuilderBase setVibrate(long[] pattern) {
mVibratePattern = Arrays.copyOf(pattern, pattern.length);
return this;
}
/**
* Sets the timestamp at which the event of the notification took place.
*/
public NotificationBuilderBase setTimestamp(long timestamp) {
mTimestamp = timestamp;
return this;
}
/**
* Sets the behavior for when the notification is replaced.
*/
public NotificationBuilderBase setRenotify(boolean renotify) {
mRenotify = renotify;
return this;
}
/**
* Gets the large icon for the notification.
*
* If a large icon was supplied to the builder, returns this icon, scaled to an appropriate size
* if necessary.
*
* If no large icon was supplied then returns a default icon based on the notification origin.
*
* See {@link NotificationBuilderBase#ensureNormalizedIcon} for more details.
*/
protected Bitmap getNormalizedLargeIcon() {
return ensureNormalizedIcon(mLargeIcon, mOrigin);
}
/**
* Ensures the availability of an icon for the notification.
*
* If |icon| is a valid, non-empty Bitmap, the bitmap will be scaled to be of an appropriate
* size for the current Android device. Otherwise, a default icon will be created based on the
* origin the notification is being displayed for.
*
* @param icon The developer-provided icon they intend to use for the notification.
* @param origin The origin the notification is being displayed for.
* @return An appropriately sized icon to use for the notification.
*/
@VisibleForTesting
public Bitmap ensureNormalizedIcon(Bitmap icon, CharSequence origin) {
if (icon == null || icon.getWidth() == 0) {
return origin != null ? mIconGenerator.generateIconForUrl(origin.toString(), true)
: null;
}
if (icon.getWidth() > mLargeIconWidthPx || icon.getHeight() > mLargeIconHeightPx) {
return Bitmap.createScaledBitmap(
icon, mLargeIconWidthPx, mLargeIconHeightPx, false /* not filtered */);
}
return icon;
}
/**
* Creates a public version of the notification to be displayed in sensitive contexts, such as
* on the lockscreen, displaying just the site origin and badge or generated icon.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
protected Notification createPublicNotification(Context context) {
// Use Android's Notification.Builder because we want the default small icon behaviour.
Notification.Builder builder =
new Notification.Builder(context)
.setContentText(context.getString(
org.chromium.chrome.R.string.notification_hidden_text))
.setSmallIcon(org.chromium.chrome.R.drawable.ic_chrome);
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
// On N, 'subtext' displays at the top of the notification and this looks better.
builder.setSubText(mOrigin);
} else {
// Set origin as title on L & M, because they look odd without one.
builder.setContentTitle(mOrigin);
// Hide the timestamp to match Android's default public notifications on L and M.
builder.setShowWhen(false);
}
// Use the badge if provided and SDK supports it, else use a generated icon.
if (mSmallIconBitmap != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// The Icon class was added in Android M.
Bitmap publicIcon = mSmallIconBitmap.copy(mSmallIconBitmap.getConfig(), true);
builder.setSmallIcon(Icon.createWithBitmap(publicIcon));
} else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M && mOrigin != null) {
// Only set the large icon for L & M because on N(+?) it would add an extra icon on
// the right hand side, which looks odd without a notification title.
builder.setLargeIcon(mIconGenerator.generateIconForUrl(mOrigin.toString(), true));
}
return builder.build();
}
@Nullable
private static CharSequence limitLength(@Nullable CharSequence input) {
if (input == null) {
return input;
}
if (input.length() > MAX_CHARSEQUENCE_LENGTH) {
return input.subSequence(0, MAX_CHARSEQUENCE_LENGTH);
}
return input;
}
/**
* Sets the small icon on {@code builder} using a {@code Bitmap} if a non-null bitmap is
* provided and the API level is high enough, otherwise the resource id is used.
*/
@TargetApi(Build.VERSION_CODES.M) // For the Icon class.
protected static void setSmallIconOnBuilder(
Notification.Builder builder, int iconId, @Nullable Bitmap iconBitmap) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && iconBitmap != null) {
builder.setSmallIcon(Icon.createWithBitmap(iconBitmap));
} else {
builder.setSmallIcon(iconId);
}
}
/**
* Adds an action to {@code builder} using a {@code Bitmap} if a bitmap is provided and the API
* level is high enough, otherwise a resource id is used.
*/
@SuppressWarnings("deprecation") // For addAction(int, CharSequence, PendingIntent)
protected static void addActionToBuilder(Notification.Builder builder, Action action) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
// Notification.Action.Builder and RemoteInput were added in KITKAT_WATCH.
Notification.Action.Builder actionBuilder = getActionBuilder(action);
if (action.type == Action.Type.TEXT) {
assert action.placeholder != null;
actionBuilder.addRemoteInput(
new RemoteInput.Builder(NotificationConstants.KEY_TEXT_REPLY)
.setLabel(action.placeholder)
.build());
}
builder.addAction(actionBuilder.build());
} else {
builder.addAction(action.iconId, action.title, action.intent);
}
}
@TargetApi(Build.VERSION_CODES.KITKAT_WATCH) // For Notification.Action.Builder
@SuppressWarnings("deprecation") // For Builder(int, CharSequence, PendingIntent)
private static Notification.Action.Builder getActionBuilder(Action action) {
Notification.Action.Builder actionBuilder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && action.iconBitmap != null) {
// Icon was added in Android M.
Icon icon = Icon.createWithBitmap(action.iconBitmap);
actionBuilder = new Notification.Action.Builder(icon, action.title, action.intent);
} else {
actionBuilder =
new Notification.Action.Builder(action.iconId, action.title, action.intent);
}
return actionBuilder;
}
/**
* Paints {@code bitmap} white. This processing should be performed if the Android system
* expects a bitmap to be white, and the bitmap is not already known to be white. The bitmap
* must be mutable.
*/
static void applyWhiteOverlayToBitmap(Bitmap bitmap) {
Paint paint = new Paint();
paint.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP));
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(bitmap, 0, 0, paint);
}
@VisibleForTesting
static RoundedIconGenerator createIconGenerator(Resources resources) {
int largeIconWidthPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
int largeIconHeightPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
float density = resources.getDisplayMetrics().density;
int cornerRadiusPx = Math.min(largeIconWidthPx, largeIconHeightPx) / 2;
return new RoundedIconGenerator(largeIconWidthPx, largeIconHeightPx, cornerRadiusPx,
NOTIFICATION_ICON_BG_COLOR, NOTIFICATION_ICON_TEXT_SIZE_DP * density);
}
}