Commit b75661d3 authored by awdf's avatar awdf Committed by Commit bot

Notifications in sensitive contexts now display origin + small icon

BUG=498716

Review-Url: https://codereview.chromium.org/2316263002
Cr-Commit-Position: refs/heads/master@{#418817}
parent 9f6cd0eb
...@@ -79,6 +79,7 @@ public class CustomNotificationBuilder extends NotificationBuilderBase { ...@@ -79,6 +79,7 @@ public class CustomNotificationBuilder extends NotificationBuilderBase {
private final Context mContext; private final Context mContext;
public CustomNotificationBuilder(Context context) { public CustomNotificationBuilder(Context context) {
super(context.getResources());
mContext = context; mContext = context;
} }
...@@ -153,7 +154,7 @@ public class CustomNotificationBuilder extends NotificationBuilderBase { ...@@ -153,7 +154,7 @@ public class CustomNotificationBuilder extends NotificationBuilderBase {
builder.setContentTitle(mTitle); builder.setContentTitle(mTitle);
builder.setContentText(mBody); builder.setContentText(mBody);
builder.setSubText(mOrigin); builder.setSubText(mOrigin);
builder.setLargeIcon(mLargeIcon); builder.setLargeIcon(getNormalizedLargeIcon());
setSmallIconOnBuilder(builder, mSmallIconId, mSmallIconBitmap); setSmallIconOnBuilder(builder, mSmallIconId, mSmallIconBitmap);
for (Action action : mActions) { for (Action action : mActions) {
addActionToBuilder(builder, action); addActionToBuilder(builder, action);
...@@ -161,6 +162,10 @@ public class CustomNotificationBuilder extends NotificationBuilderBase { ...@@ -161,6 +162,10 @@ public class CustomNotificationBuilder extends NotificationBuilderBase {
if (mSettingsAction != null) { if (mSettingsAction != null) {
addActionToBuilder(builder, mSettingsAction); addActionToBuilder(builder, mSettingsAction);
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Notification.Builder.setPublicVersion was added in Android L.
builder.setPublicVersion(createPublicNotification(mContext));
}
Notification notification = builder.build(); Notification notification = builder.build();
notification.bigContentView = bigView; notification.bigContentView = bigView;
......
...@@ -7,6 +7,8 @@ package org.chromium.chrome.browser.notifications; ...@@ -7,6 +7,8 @@ package org.chromium.chrome.browser.notifications;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Notification; import android.app.Notification;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
...@@ -17,6 +19,7 @@ import android.graphics.drawable.Icon; ...@@ -17,6 +19,7 @@ import android.graphics.drawable.Icon;
import android.os.Build; import android.os.Build;
import org.chromium.base.VisibleForTesting; import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
...@@ -55,12 +58,28 @@ public abstract class NotificationBuilderBase { ...@@ -55,12 +58,28 @@ public abstract class NotificationBuilderBase {
@VisibleForTesting @VisibleForTesting
static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024; 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 * The maximum number of author provided action buttons. The settings button is not part of this
* count. * count.
*/ */
private static final int MAX_AUTHOR_PROVIDED_ACTION_BUTTONS = 2; 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 mTitle;
protected CharSequence mBody; protected CharSequence mBody;
protected CharSequence mOrigin; protected CharSequence mOrigin;
...@@ -78,6 +97,14 @@ public abstract class NotificationBuilderBase { ...@@ -78,6 +97,14 @@ public abstract class NotificationBuilderBase {
protected long mTimestamp; protected long mTimestamp;
protected boolean mRenotify; protected boolean mRenotify;
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. * Combines all of the options that have been set and returns a new Notification object.
*/ */
...@@ -238,6 +265,79 @@ public abstract class NotificationBuilderBase { ...@@ -238,6 +265,79 @@ public abstract class NotificationBuilderBase {
return this; 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.
*/
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);
// TODO Change the following version check to '== Build.VERSION_CODES.N' when we bump the
// targetSdkVersion to 24.
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);
}
// 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 @Nullable
private static CharSequence limitLength(@Nullable CharSequence input) { private static CharSequence limitLength(@Nullable CharSequence input) {
if (input == null) { if (input == null) {
...@@ -290,4 +390,16 @@ public abstract class NotificationBuilderBase { ...@@ -290,4 +390,16 @@ public abstract class NotificationBuilderBase {
Canvas canvas = new Canvas(bitmap); Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(bitmap, 0, 0, paint); 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);
}
} }
...@@ -37,7 +37,6 @@ import org.chromium.chrome.browser.preferences.website.SingleCategoryPreferences ...@@ -37,7 +37,6 @@ import org.chromium.chrome.browser.preferences.website.SingleCategoryPreferences
import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences; import org.chromium.chrome.browser.preferences.website.SingleWebsitePreferences;
import org.chromium.chrome.browser.preferences.website.SiteSettingsCategory; import org.chromium.chrome.browser.preferences.website.SiteSettingsCategory;
import org.chromium.chrome.browser.webapps.ChromeWebApkHost; import org.chromium.chrome.browser.webapps.ChromeWebApkHost;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import org.chromium.components.url_formatter.UrlFormatter; import org.chromium.components.url_formatter.UrlFormatter;
import org.chromium.webapk.lib.client.WebApkValidator; import org.chromium.webapk.lib.client.WebApkValidator;
...@@ -65,9 +64,6 @@ public class NotificationPlatformBridge { ...@@ -65,9 +64,6 @@ public class NotificationPlatformBridge {
private static final String PLATFORM_TAG_PREFIX = private static final String PLATFORM_TAG_PREFIX =
NotificationPlatformBridge.class.getSimpleName(); NotificationPlatformBridge.class.getSimpleName();
private static final int NOTIFICATION_ICON_BG_COLOR = 0xFF969696;
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 // We always use the same request code for pending intents. We use other ways to force
// uniqueness of pending intents when necessary. // uniqueness of pending intents when necessary.
private static final int PENDING_INTENT_REQUEST_CODE = 0; private static final int PENDING_INTENT_REQUEST_CODE = 0;
...@@ -82,11 +78,6 @@ public class NotificationPlatformBridge { ...@@ -82,11 +78,6 @@ public class NotificationPlatformBridge {
private final Context mAppContext; private final Context mAppContext;
private final NotificationManagerProxy mNotificationManager; private final NotificationManagerProxy mNotificationManager;
@VisibleForTesting public RoundedIconGenerator mIconGenerator;
private final int mLargeIconWidthPx;
private final int mLargeIconHeightPx;
private final float mDensity;
private long mLastNotificationClickMs = 0L; private long mLastNotificationClickMs = 0L;
/** /**
...@@ -141,14 +132,6 @@ public class NotificationPlatformBridge { ...@@ -141,14 +132,6 @@ public class NotificationPlatformBridge {
(NotificationManager) mAppContext.getSystemService( (NotificationManager) mAppContext.getSystemService(
Context.NOTIFICATION_SERVICE)); Context.NOTIFICATION_SERVICE));
} }
Resources resources = mAppContext.getResources();
mLargeIconWidthPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
mLargeIconHeightPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
mDensity = resources.getDisplayMetrics().density;
} }
/** /**
...@@ -524,7 +507,7 @@ public class NotificationPlatformBridge { ...@@ -524,7 +507,7 @@ public class NotificationPlatformBridge {
.setTitle(title) .setTitle(title)
.setBody(body) .setBody(body)
.setImage(image) .setImage(image)
.setLargeIcon(ensureNormalizedIcon(icon, origin)) .setLargeIcon(icon)
.setSmallIcon(R.drawable.ic_chrome) .setSmallIcon(R.drawable.ic_chrome)
.setSmallIcon(badge) .setSmallIcon(badge)
.setContentIntent(clickIntent) .setContentIntent(clickIntent)
...@@ -604,40 +587,6 @@ public class NotificationPlatformBridge { ...@@ -604,40 +587,6 @@ public class NotificationPlatformBridge {
return spannableStringBuilder; return spannableStringBuilder;
} }
/**
* 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, String origin) {
if (icon == null || icon.getWidth() == 0) {
if (mIconGenerator == null) {
int cornerRadiusPx = Math.min(mLargeIconWidthPx, mLargeIconHeightPx) / 2;
mIconGenerator =
new RoundedIconGenerator(mLargeIconWidthPx, mLargeIconHeightPx,
cornerRadiusPx,
NOTIFICATION_ICON_BG_COLOR,
NOTIFICATION_TEXT_SIZE_DP * mDensity);
}
return mIconGenerator.generateIconForUrl(origin, true);
}
if (icon.getWidth() > mLargeIconWidthPx || icon.getHeight() > mLargeIconHeightPx) {
return icon.createScaledBitmap(icon, mLargeIconWidthPx, mLargeIconHeightPx,
false /* not filtered */);
}
return icon;
}
/** /**
* Determines whether to use standard notification layouts, using NotificationCompat.Builder, * Determines whether to use standard notification layouts, using NotificationCompat.Builder,
* or custom layouts using Chrome's own templates. * or custom layouts using Chrome's own templates.
......
...@@ -15,6 +15,7 @@ public class StandardNotificationBuilder extends NotificationBuilderBase { ...@@ -15,6 +15,7 @@ public class StandardNotificationBuilder extends NotificationBuilderBase {
private final Context mContext; private final Context mContext;
public StandardNotificationBuilder(Context context) { public StandardNotificationBuilder(Context context) {
super(context.getResources());
mContext = context; mContext = context;
} }
...@@ -41,7 +42,7 @@ public class StandardNotificationBuilder extends NotificationBuilderBase { ...@@ -41,7 +42,7 @@ public class StandardNotificationBuilder extends NotificationBuilderBase {
// If there is no image, let the body text wrap only multiple lines when expanded. // If there is no image, let the body text wrap only multiple lines when expanded.
builder.setStyle(new Notification.BigTextStyle().bigText(mBody)); builder.setStyle(new Notification.BigTextStyle().bigText(mBody));
} }
builder.setLargeIcon(mLargeIcon); builder.setLargeIcon(getNormalizedLargeIcon());
setSmallIconOnBuilder(builder, mSmallIconId, mSmallIconBitmap); setSmallIconOnBuilder(builder, mSmallIconId, mSmallIconBitmap);
builder.setContentIntent(mContentIntent); builder.setContentIntent(mContentIntent);
builder.setDeleteIntent(mDeleteIntent); builder.setDeleteIntent(mDeleteIntent);
...@@ -55,6 +56,10 @@ public class StandardNotificationBuilder extends NotificationBuilderBase { ...@@ -55,6 +56,10 @@ public class StandardNotificationBuilder extends NotificationBuilderBase {
builder.setVibrate(mVibratePattern); builder.setVibrate(mVibratePattern);
builder.setWhen(mTimestamp); builder.setWhen(mTimestamp);
builder.setOnlyAlertOnce(!mRenotify); builder.setOnlyAlertOnce(!mRenotify);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Notification.Builder.setPublicVersion was added in Android L.
builder.setPublicVersion(createPublicNotification(mContext));
}
return builder.build(); return builder.build();
} }
} }
...@@ -1636,6 +1636,9 @@ To obtain new licenses, connect to the internet and play your downloaded content ...@@ -1636,6 +1636,9 @@ To obtain new licenses, connect to the internet and play your downloaded content
</message> </message>
<!-- Web Notifications API --> <!-- Web Notifications API -->
<message name="IDS_NOTIFICATION_HIDDEN_TEXT" desc="Text shown in place of notification contents when the notification's contents have been hidden on a secure lockscreen [CHAR-LIMIT=32]">
Contents hidden
</message>
<message name="IDS_NOTIFICATION_SITE_SETTINGS_BUTTON" desc="Text of a button shown on notifications, that opens the Site Settings of the website that showed the notification. (If it isn't possible to fit within the char limit, the string will be ellipsized if 3 buttons are shown side by side, so try to ensure that the user can guess the meaning from the first 8 characters). [CHAR-LIMIT=9]"> <message name="IDS_NOTIFICATION_SITE_SETTINGS_BUTTON" desc="Text of a button shown on notifications, that opens the Site Settings of the website that showed the notification. (If it isn't possible to fit within the char limit, the string will be ellipsized if 3 buttons are shown side by side, so try to ensure that the user can guess the meaning from the first 8 characters). [CHAR-LIMIT=9]">
Settings Settings
</message> </message>
......
...@@ -1235,6 +1235,7 @@ chrome_test_java_sources = [ ...@@ -1235,6 +1235,7 @@ chrome_test_java_sources = [
"javatests/src/org/chromium/chrome/browser/notifications/NotificationTestBase.java", "javatests/src/org/chromium/chrome/browser/notifications/NotificationTestBase.java",
"javatests/src/org/chromium/chrome/browser/notifications/NotificationTestUtil.java", "javatests/src/org/chromium/chrome/browser/notifications/NotificationTestUtil.java",
"javatests/src/org/chromium/chrome/browser/notifications/StandardNotificationBuilderTest.java", "javatests/src/org/chromium/chrome/browser/notifications/StandardNotificationBuilderTest.java",
"javatests/src/org/chromium/chrome/browser/notifications/NotificationBuilderBaseTest.java",
"javatests/src/org/chromium/chrome/browser/ntp/FakeMostVisitedSites.java", "javatests/src/org/chromium/chrome/browser/ntp/FakeMostVisitedSites.java",
"javatests/src/org/chromium/chrome/browser/ntp/NewTabPageNavigationTest.java", "javatests/src/org/chromium/chrome/browser/ntp/NewTabPageNavigationTest.java",
"javatests/src/org/chromium/chrome/browser/ntp/NewTabPageTest.java", "javatests/src/org/chromium/chrome/browser/ntp/NewTabPageTest.java",
......
...@@ -101,6 +101,14 @@ public class CustomNotificationBuilderTest extends NativeLibraryTestBase { ...@@ -101,6 +101,14 @@ public class CustomNotificationBuilderTest extends NativeLibraryTestBase {
assertSame(contentIntent, notification.contentIntent); assertSame(contentIntent, notification.contentIntent);
assertSame(deleteIntent, notification.deleteIntent); assertSame(deleteIntent, notification.deleteIntent);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Notification.publicVersion was added in Android L.
assertNotNull(notification.publicVersion);
assertEquals("origin", Build.VERSION.SDK_INT <= Build.VERSION_CODES.M
? NotificationTestUtil.getExtraTitle(notification.publicVersion)
: NotificationTestUtil.getExtraSubText(notification.publicVersion));
}
// The regular actions and the settings action are added together in the notification // The regular actions and the settings action are added together in the notification
// actions array, so they can be exposed on e.g. Wear and custom lockscreens. // actions array, so they can be exposed on e.g. Wear and custom lockscreens.
assertEquals(3, NotificationTestUtil.getActions(notification).length); assertEquals(3, NotificationTestUtil.getActions(notification).length);
......
// Copyright 2015 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.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.test.suitebuilder.annotation.MediumTest;
import org.chromium.base.test.util.Feature;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import org.chromium.content.browser.test.NativeLibraryTestBase;
/**
* Instrumentation unit tests for NotificationBuilderBase.
*
* Extends NativeLibraryTestBase so that {@link UrlUtilities#getDomainAndRegistry} can access
* native GetDomainAndRegistry, when called by {@link RoundedIconGenerator#getIconTextForUrl} during
* testEnsureNormalizedIconBehavior().
*/
public class NotificationBuilderBaseTest extends NativeLibraryTestBase {
@Override
public void setUp() throws Exception {
super.setUp();
// Not initializing the browser process is safe because GetDomainAndRegistry() is
// stand-alone.
loadNativeLibraryNoBrowserProcess();
}
/**
* Tests the three paths for ensuring that a notification will be shown with a normalized icon:
* (1) NULL bitmaps should have an auto-generated image.
* (2) Large bitmaps should be resized to the device's intended size.
* (3) Smaller bitmaps should be left alone.
*/
@MediumTest
@Feature({"Browser", "Notifications"})
public void testEnsureNormalizedIconBehavior() throws Exception {
// Get the dimensions of the notification icon that will be presented to the user.
Context appContext = getInstrumentation().getTargetContext().getApplicationContext();
Resources resources = appContext.getResources();
int largeIconWidthPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
int largeIconHeightPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
String origin = "https://example.com";
NotificationBuilderBase notificationBuilder = new NotificationBuilderBase(resources) {
@Override
public Notification build() {
return null;
}
};
Bitmap fromNullIcon = notificationBuilder.ensureNormalizedIcon(null, origin);
assertNotNull(fromNullIcon);
assertEquals(largeIconWidthPx, fromNullIcon.getWidth());
assertEquals(largeIconHeightPx, fromNullIcon.getHeight());
Bitmap largeIcon = Bitmap.createBitmap(
largeIconWidthPx * 2, largeIconHeightPx * 2, Bitmap.Config.ALPHA_8);
Bitmap fromLargeIcon = notificationBuilder.ensureNormalizedIcon(largeIcon, origin);
assertNotNull(fromLargeIcon);
assertEquals(largeIconWidthPx, fromLargeIcon.getWidth());
assertEquals(largeIconHeightPx, fromLargeIcon.getHeight());
Bitmap smallIcon = Bitmap.createBitmap(
largeIconWidthPx / 2, largeIconHeightPx / 2, Bitmap.Config.ALPHA_8);
Bitmap fromSmallIcon = notificationBuilder.ensureNormalizedIcon(smallIcon, origin);
assertNotNull(fromSmallIcon);
assertEquals(smallIcon, fromSmallIcon);
}
}
...@@ -8,7 +8,6 @@ import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout; ...@@ -8,7 +8,6 @@ import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
...@@ -182,14 +181,16 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase { ...@@ -182,14 +181,16 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase {
@RetryOnFailure @RetryOnFailure
public void testDefaultNotificationProperties() throws Exception { public void testDefaultNotificationProperties() throws Exception {
setNotificationContentSettingForCurrentOrigin(ContentSetting.ALLOW); setNotificationContentSettingForCurrentOrigin(ContentSetting.ALLOW);
Context context = getInstrumentation().getTargetContext();
Notification notification = showAndGetNotification("MyNotification", "{ body: 'Hello' }"); Notification notification = showAndGetNotification("MyNotification", "{ body: 'Hello' }");
String expectedOrigin =
UrlFormatter.formatUrlForSecurityDisplay(getOrigin(), false /* showScheme */);
// Validate the contents of the notification. // Validate the contents of the notification.
assertEquals("MyNotification", NotificationTestUtil.getExtraTitle(notification)); assertEquals("MyNotification", NotificationTestUtil.getExtraTitle(notification));
assertEquals("Hello", NotificationTestUtil.getExtraText(notification)); assertEquals("Hello", NotificationTestUtil.getExtraText(notification));
assertEquals(UrlFormatter.formatUrlForSecurityDisplay(getOrigin(), false /* showScheme */), assertEquals(expectedOrigin, NotificationTestUtil.getExtraSubText(notification));
NotificationTestUtil.getExtraSubText(notification));
// Verify that the ticker text contains the notification's title and body. // Verify that the ticker text contains the notification's title and body.
String tickerText = notification.tickerText.toString(); String tickerText = notification.tickerText.toString();
...@@ -197,12 +198,28 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase { ...@@ -197,12 +198,28 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase {
assertTrue(tickerText.contains("MyNotification")); assertTrue(tickerText.contains("MyNotification"));
assertTrue(tickerText.contains("Hello")); assertTrue(tickerText.contains("Hello"));
// On L+, verify the public version of the notification contains the notification's origin,
// and that the body text has been replaced.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
assertNotNull(notification.publicVersion);
assertEquals(context.getString(R.string.notification_hidden_text),
NotificationTestUtil.getExtraText(notification.publicVersion));
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
// On N+, origin should be set as the subtext of the public notification.
assertEquals(expectedOrigin,
NotificationTestUtil.getExtraSubText(notification.publicVersion));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// On L/M, origin should be the title of the public notification.
assertEquals(
expectedOrigin, NotificationTestUtil.getExtraTitle(notification.publicVersion));
}
// Verify that the notification's timestamp is set in the past 60 seconds. This number has // Verify that the notification's timestamp is set in the past 60 seconds. This number has
// no significance, but needs to be high enough to not cause flakiness as it's set by the // no significance, but needs to be high enough to not cause flakiness as it's set by the
// renderer process on notification creation. // renderer process on notification creation.
assertTrue(Math.abs(System.currentTimeMillis() - notification.when) < 60 * 1000); assertTrue(Math.abs(System.currentTimeMillis() - notification.when) < 60 * 1000);
Context context = getInstrumentation().getTargetContext();
assertNotNull(NotificationTestUtil.getLargeIconFromNotification(context, notification)); assertNotNull(NotificationTestUtil.getLargeIconFromNotification(context, notification));
// Validate the notification's behavior. // Validate the notification's behavior.
...@@ -315,8 +332,9 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase { ...@@ -315,8 +332,9 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase {
/** /**
* Verifies that on Android M+, notifications which specify a badge will have that icon * Verifies that on Android M+, notifications which specify a badge will have that icon
* fetched and included as the small icon in the notification. * fetched and included as the small icon in the notification and public version.
* If the test target is L or below, verifies the small icon is the expected chrome logo. * If the test target is L or below, verifies the small icon (and public small icon on L) is
* the expected chrome logo.
*/ */
@MediumTest @MediumTest
@Feature({"Browser", "Notifications"}) @Feature({"Browser", "Notifications"})
...@@ -333,16 +351,35 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase { ...@@ -333,16 +351,35 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase {
assertNotNull(smallIcon); assertNotNull(smallIcon);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Custom badges are only supported on M+.
// 1. Check the notification badge.
URL badgeUrl = URL badgeUrl =
new URL(getTestServer().getURL("/chrome/test/data/notifications/badge.png")); new URL(getTestServer().getURL("/chrome/test/data/notifications/badge.png"));
Bitmap bitmap = BitmapFactory.decodeStream(badgeUrl.openStream()); Bitmap bitmap = BitmapFactory.decodeStream(badgeUrl.openStream());
Bitmap expected = bitmap.copy(bitmap.getConfig(), true); Bitmap expected = bitmap.copy(bitmap.getConfig(), true);
NotificationBuilderBase.applyWhiteOverlayToBitmap(expected); NotificationBuilderBase.applyWhiteOverlayToBitmap(expected);
assertTrue(expected.sameAs(smallIcon)); assertTrue(expected.sameAs(smallIcon));
// 2. Check the public notification badge.
assertNotNull(notification.publicVersion);
Bitmap publicSmallIcon = NotificationTestUtil.getSmallIconFromNotification(
context, notification.publicVersion);
assertNotNull(publicSmallIcon);
assertEquals(expected.getWidth(), publicSmallIcon.getWidth());
assertEquals(expected.getHeight(), publicSmallIcon.getHeight());
assertTrue(expected.sameAs(publicSmallIcon));
} else { } else {
Bitmap expected = Bitmap expected =
BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_chrome); BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_chrome);
assertTrue(expected.sameAs(smallIcon)); assertTrue(expected.sameAs(smallIcon));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Public versions of notifications are only supported on L+.
assertNotNull(notification.publicVersion);
Bitmap publicSmallIcon = NotificationTestUtil.getSmallIconFromNotification(
context, notification.publicVersion);
assertTrue(expected.sameAs(publicSmallIcon));
}
} }
} }
...@@ -390,8 +427,8 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase { ...@@ -390,8 +427,8 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase {
// Create a second rounded icon for the test's origin, and compare its dimensions against // Create a second rounded icon for the test's origin, and compare its dimensions against
// those of the icon associated to the notification itself. // those of the icon associated to the notification itself.
RoundedIconGenerator generator = notificationBridge.mIconGenerator; RoundedIconGenerator generator =
assertNotNull(generator); NotificationBuilderBase.createIconGenerator(context.getResources());
Bitmap generatedIcon = generator.generateIconForUrl(getOrigin()); Bitmap generatedIcon = generator.generateIconForUrl(getOrigin());
assertNotNull(generatedIcon); assertNotNull(generatedIcon);
...@@ -399,56 +436,6 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase { ...@@ -399,56 +436,6 @@ public class NotificationPlatformBridgeTest extends NotificationTestBase {
NotificationTestUtil.getLargeIconFromNotification(context, notification))); NotificationTestUtil.getLargeIconFromNotification(context, notification)));
} }
/**
* Tests the three paths for ensuring that a notification will be shown with a normalized icon:
* (1) NULL bitmaps should have an auto-generated image.
* (2) Large bitmaps should be resized to the device's intended size.
* (3) Smaller bitmaps should be left alone.
*/
@MediumTest
@Feature({"Browser", "Notifications"})
public void testEnsureNormalizedIconBehavior() throws Exception {
setNotificationContentSettingForCurrentOrigin(ContentSetting.ALLOW);
// Create a notification to ensure that the NotificationPlatformBridge is initialized.
showAndGetNotification("MyNotification", "{}");
// Get the dimensions of the notification icon that will be presented to the user.
Context appContext = getInstrumentation().getTargetContext().getApplicationContext();
Resources resources = appContext.getResources();
int largeIconWidthPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
int largeIconHeightPx =
resources.getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
String origin = "https://example.com";
NotificationPlatformBridge notificationBridge =
NotificationPlatformBridge.getInstanceForTests();
assertNotNull(notificationBridge);
Bitmap fromNullIcon = notificationBridge.ensureNormalizedIcon(null, origin);
assertNotNull(fromNullIcon);
assertEquals(largeIconWidthPx, fromNullIcon.getWidth());
assertEquals(largeIconHeightPx, fromNullIcon.getHeight());
Bitmap largeIcon = Bitmap.createBitmap(largeIconWidthPx * 2, largeIconHeightPx * 2,
Bitmap.Config.ALPHA_8);
Bitmap fromLargeIcon = notificationBridge.ensureNormalizedIcon(largeIcon, origin);
assertNotNull(fromLargeIcon);
assertEquals(largeIconWidthPx, fromLargeIcon.getWidth());
assertEquals(largeIconHeightPx, fromLargeIcon.getHeight());
Bitmap smallIcon = Bitmap.createBitmap(largeIconWidthPx / 2, largeIconHeightPx / 2,
Bitmap.Config.ALPHA_8);
Bitmap fromSmallIcon = notificationBridge.ensureNormalizedIcon(smallIcon, origin);
assertNotNull(fromSmallIcon);
assertEquals(smallIcon, fromSmallIcon);
}
/* /*
* Verifies that starting the PendingIntent stored as the notification's content intent will * Verifies that starting the PendingIntent stored as the notification's content intent will
* start up the associated Service Worker, where the JavaScript code will close the notification * start up the associated Service Worker, where the JavaScript code will close the notification
......
...@@ -12,17 +12,31 @@ import android.graphics.Bitmap; ...@@ -12,17 +12,31 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build; import android.os.Build;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.Feature;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.browser.widget.RoundedIconGenerator;
import org.chromium.content.browser.test.NativeLibraryTestBase;
/** /**
* Instrumentation unit tests for StandardNotificationBuilder. * Instrumentation unit tests for StandardNotificationBuilder.
*
* Extends NativeLibraryTestBase so that {@link UrlUtilities#getDomainAndRegistry} can access
* native GetDomainAndRegistry, when called by {@link RoundedIconGenerator#getIconTextForUrl} during
* notification construction.
*/ */
public class StandardNotificationBuilderTest extends InstrumentationTestCase { public class StandardNotificationBuilderTest extends NativeLibraryTestBase {
@Override
public void setUp() throws Exception {
super.setUp();
// Not initializing the browser process is safe because GetDomainAndRegistry() is
// stand-alone.
loadNativeLibraryNoBrowserProcess();
}
private NotificationBuilderBase createAllOptionsBuilder( private NotificationBuilderBase createAllOptionsBuilder(
PendingIntent[] outContentAndDeleteIntents) { PendingIntent[] outContentAndDeleteIntents) {
if (outContentAndDeleteIntents == null || outContentAndDeleteIntents.length != 2) if (outContentAndDeleteIntents == null || outContentAndDeleteIntents.length != 2)
...@@ -104,6 +118,19 @@ public class StandardNotificationBuilderTest extends InstrumentationTestCase { ...@@ -104,6 +118,19 @@ public class StandardNotificationBuilderTest extends InstrumentationTestCase {
assertEquals("button 1", NotificationTestUtil.getActionTitle(actions[0])); assertEquals("button 1", NotificationTestUtil.getActionTitle(actions[0]));
assertEquals("button 2", NotificationTestUtil.getActionTitle(actions[1])); assertEquals("button 2", NotificationTestUtil.getActionTitle(actions[1]));
assertEquals("settings", NotificationTestUtil.getActionTitle(actions[2])); assertEquals("settings", NotificationTestUtil.getActionTitle(actions[2]));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Notification.publicVersion was added in Android L.
assertNotNull(notification.publicVersion);
assertEquals(context.getString(R.string.notification_hidden_text),
NotificationTestUtil.getExtraText(notification.publicVersion));
}
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
assertEquals(
"origin", NotificationTestUtil.getExtraSubText(notification.publicVersion));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
assertEquals("origin", NotificationTestUtil.getExtraTitle(notification.publicVersion));
}
} }
@SmallTest @SmallTest
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment