OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 package org.chromium.chrome.browser.notifications; | 5 package org.chromium.chrome.browser.notifications; |
6 | 6 |
| 7 import android.annotation.TargetApi; |
7 import android.app.Notification; | 8 import android.app.Notification; |
8 import android.app.PendingIntent; | 9 import android.app.PendingIntent; |
9 import android.content.Context; | 10 import android.content.Context; |
10 import android.content.res.Resources; | 11 import android.content.res.Resources; |
11 import android.graphics.Bitmap; | 12 import android.graphics.Bitmap; |
12 import android.graphics.BitmapFactory; | 13 import android.graphics.BitmapFactory; |
13 import android.graphics.drawable.BitmapDrawable; | 14 import android.graphics.drawable.BitmapDrawable; |
14 import android.graphics.drawable.Drawable; | 15 import android.graphics.drawable.Drawable; |
| 16 import android.graphics.drawable.Icon; |
15 import android.os.Build; | 17 import android.os.Build; |
16 import android.support.v4.app.NotificationCompat; | |
17 import android.support.v4.app.NotificationCompat.Action; | |
18 import android.text.format.DateFormat; | 18 import android.text.format.DateFormat; |
19 import android.util.DisplayMetrics; | 19 import android.util.DisplayMetrics; |
20 import android.util.TypedValue; | 20 import android.util.TypedValue; |
21 import android.view.View; | 21 import android.view.View; |
22 import android.widget.RemoteViews; | 22 import android.widget.RemoteViews; |
23 | 23 |
24 import org.chromium.base.ApiCompatibilityUtils; | 24 import org.chromium.base.ApiCompatibilityUtils; |
25 import org.chromium.base.VisibleForTesting; | 25 import org.chromium.base.VisibleForTesting; |
26 import org.chromium.chrome.R; | 26 import org.chromium.chrome.R; |
27 import org.chromium.ui.base.LocalizationUtils; | 27 import org.chromium.ui.base.LocalizationUtils; |
28 | 28 |
29 import java.util.ArrayList; | 29 import java.util.ArrayList; |
30 import java.util.Arrays; | 30 import java.util.Arrays; |
31 import java.util.Date; | 31 import java.util.Date; |
32 import java.util.List; | 32 import java.util.List; |
33 | 33 |
34 import javax.annotation.Nullable; | 34 import javax.annotation.Nullable; |
35 | 35 |
36 /** | 36 /** |
37 * Builds a notification using the given inputs. Uses RemoteViews to provide a c
ustom layout. | 37 * Builds a notification using the given inputs. Uses RemoteViews to provide a c
ustom layout. |
38 */ | 38 */ |
39 public class CustomNotificationBuilder implements NotificationBuilder { | 39 public class CustomNotificationBuilder implements NotificationBuilder { |
| 40 // Notification.Action was only introduced in API level 19. |
| 41 private static class Action { |
| 42 public int iconId; |
| 43 @Nullable |
| 44 public Bitmap icon; |
| 45 public CharSequence title; |
| 46 public PendingIntent intent; |
| 47 |
| 48 Action(int iconId, CharSequence title, PendingIntent intent) { |
| 49 this.iconId = iconId; |
| 50 this.title = title; |
| 51 this.intent = intent; |
| 52 } |
| 53 |
| 54 Action(Bitmap icon, CharSequence title, PendingIntent intent) { |
| 55 this.icon = icon; |
| 56 this.title = title; |
| 57 this.intent = intent; |
| 58 } |
| 59 } |
| 60 |
40 /** | 61 /** |
41 * Maximum length of CharSequence inputs to prevent excessive memory consump
tion. At current | 62 * Maximum length of CharSequence inputs to prevent excessive memory consump
tion. At current |
42 * screen sizes we display about 500 characters at most, so this is a pretty
generous limit, and | 63 * screen sizes we display about 500 characters at most, so this is a pretty
generous limit, and |
43 * it matches what NotificationCompat does. | 64 * it matches what the Notification class does. |
44 */ | 65 */ |
45 @VisibleForTesting static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024; | 66 @VisibleForTesting static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024; |
46 | 67 |
47 /** | 68 /** |
48 * The maximum number of action buttons. One is for the settings button, and
two more slots are | 69 * The maximum number of developer provided action buttons. |
49 * for developer provided buttons. | |
50 */ | 70 */ |
51 private static final int MAX_ACTION_BUTTONS = 3; | 71 private static final int MAX_ACTION_BUTTONS = 2; |
| 72 |
| 73 /** |
| 74 * The maximum width of action icons in dp units. |
| 75 */ |
| 76 private static final int MAX_ACTION_ICON_WIDTH_DP = 32; |
52 | 77 |
53 /** | 78 /** |
54 * The maximum number of lines of body text for the expanded state. Fewer li
nes are used when | 79 * The maximum number of lines of body text for the expanded state. Fewer li
nes are used when |
55 * the text is scaled up, with a minimum of one line. | 80 * the text is scaled up, with a minimum of one line. |
56 */ | 81 */ |
57 private static final int MAX_BODY_LINES = 7; | 82 private static final int MAX_BODY_LINES = 7; |
58 | 83 |
59 /** | 84 /** |
60 * The fontScale considered large for the purposes of layout. | 85 * The fontScale considered large for the purposes of layout. |
61 */ | 86 */ |
(...skipping 82 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
144 configureSettingsButton(bigView); | 169 configureSettingsButton(bigView); |
145 | 170 |
146 if (useMaterial()) { | 171 if (useMaterial()) { |
147 compactView.setViewVisibility(R.id.small_icon_overlay, View.VISIBLE)
; | 172 compactView.setViewVisibility(R.id.small_icon_overlay, View.VISIBLE)
; |
148 bigView.setViewVisibility(R.id.small_icon_overlay, View.VISIBLE); | 173 bigView.setViewVisibility(R.id.small_icon_overlay, View.VISIBLE); |
149 } else { | 174 } else { |
150 compactView.setViewVisibility(R.id.small_icon_footer, View.VISIBLE); | 175 compactView.setViewVisibility(R.id.small_icon_footer, View.VISIBLE); |
151 bigView.setViewVisibility(R.id.small_icon_footer, View.VISIBLE); | 176 bigView.setViewVisibility(R.id.small_icon_footer, View.VISIBLE); |
152 } | 177 } |
153 | 178 |
154 NotificationCompat.Builder builder = new NotificationCompat.Builder(mCon
text); | 179 Notification.Builder builder = new Notification.Builder(mContext); |
155 builder.setTicker(mTickerText); | 180 builder.setTicker(mTickerText); |
156 builder.setSmallIcon(mSmallIconId); | 181 builder.setSmallIcon(mSmallIconId); |
157 builder.setContentIntent(mContentIntent); | 182 builder.setContentIntent(mContentIntent); |
158 builder.setDeleteIntent(mDeleteIntent); | 183 builder.setDeleteIntent(mDeleteIntent); |
159 builder.setDefaults(mDefaults); | 184 builder.setDefaults(mDefaults); |
160 builder.setVibrate(mVibratePattern); | 185 builder.setVibrate(mVibratePattern); |
161 builder.setContent(compactView); | 186 builder.setContent(compactView); |
162 | 187 |
163 // Some things are duplicated in the builder to ensure the notification
shows correctly on | 188 // Some things are duplicated in the builder to ensure the notification
shows correctly on |
164 // Wear devices and custom lock screens. | 189 // Wear devices and custom lock screens. |
165 builder.setContentTitle(mTitle); | 190 builder.setContentTitle(mTitle); |
166 builder.setContentText(mBody); | 191 builder.setContentText(mBody); |
167 builder.setSubText(mOrigin); | 192 builder.setSubText(mOrigin); |
168 builder.setLargeIcon(mLargeIcon); | 193 builder.setLargeIcon(mLargeIcon); |
169 for (Action action : mActions) { | 194 for (Action action : mActions) { |
170 builder.addAction(action); | 195 addAction(builder, action); |
171 } | 196 } |
172 if (mSettingsAction != null) { | 197 if (mSettingsAction != null) { |
173 builder.addAction(mSettingsAction); | 198 addAction(builder, mSettingsAction); |
174 } | 199 } |
175 | 200 |
176 Notification notification = builder.build(); | 201 Notification notification = builder.build(); |
177 notification.bigContentView = bigView; | 202 notification.bigContentView = bigView; |
178 return notification; | 203 return notification; |
179 } | 204 } |
180 | 205 |
181 @Override | 206 @Override |
182 public NotificationBuilder setTitle(CharSequence title) { | 207 public NotificationBuilder setTitle(CharSequence title) { |
183 mTitle = limitLength(title); | 208 mTitle = limitLength(title); |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
229 @Override | 254 @Override |
230 public NotificationBuilder addAction(int iconId, CharSequence title, Pending
Intent intent) { | 255 public NotificationBuilder addAction(int iconId, CharSequence title, Pending
Intent intent) { |
231 if (mActions.size() == MAX_ACTION_BUTTONS) { | 256 if (mActions.size() == MAX_ACTION_BUTTONS) { |
232 throw new IllegalStateException( | 257 throw new IllegalStateException( |
233 "Cannot add more than " + MAX_ACTION_BUTTONS + " actions."); | 258 "Cannot add more than " + MAX_ACTION_BUTTONS + " actions."); |
234 } | 259 } |
235 mActions.add(new Action(iconId, limitLength(title), intent)); | 260 mActions.add(new Action(iconId, limitLength(title), intent)); |
236 return this; | 261 return this; |
237 } | 262 } |
238 | 263 |
| 264 public NotificationBuilder addAction(Bitmap icon, CharSequence title, Pendin
gIntent intent) { |
| 265 if (mActions.size() == MAX_ACTION_BUTTONS) { |
| 266 throw new IllegalStateException( |
| 267 "Cannot add more than " + MAX_ACTION_BUTTONS + " actions."); |
| 268 } |
| 269 mActions.add(new Action(icon, limitLength(title), intent)); |
| 270 return this; |
| 271 } |
| 272 |
239 @Override | 273 @Override |
240 public NotificationBuilder addSettingsAction( | 274 public NotificationBuilder addSettingsAction( |
241 int iconId, CharSequence title, PendingIntent intent) { | 275 int iconId, CharSequence title, PendingIntent intent) { |
242 mSettingsAction = new Action(iconId, limitLength(title), intent); | 276 mSettingsAction = new Action(iconId, limitLength(title), intent); |
243 return this; | 277 return this; |
244 } | 278 } |
245 | 279 |
246 @Override | 280 @Override |
247 public NotificationBuilder setDefaults(int defaults) { | 281 public NotificationBuilder setDefaults(int defaults) { |
248 mDefaults = defaults; | 282 mDefaults = defaults; |
(...skipping 18 matching lines...) Expand all Loading... |
267 int visibility = mActions.isEmpty() ? View.GONE : View.VISIBLE; | 301 int visibility = mActions.isEmpty() ? View.GONE : View.VISIBLE; |
268 bigView.setViewVisibility(R.id.button_divider, visibility); | 302 bigView.setViewVisibility(R.id.button_divider, visibility); |
269 bigView.setViewVisibility(R.id.buttons, visibility); | 303 bigView.setViewVisibility(R.id.buttons, visibility); |
270 | 304 |
271 Resources resources = mContext.getResources(); | 305 Resources resources = mContext.getResources(); |
272 DisplayMetrics metrics = resources.getDisplayMetrics(); | 306 DisplayMetrics metrics = resources.getDisplayMetrics(); |
273 for (Action action : mActions) { | 307 for (Action action : mActions) { |
274 RemoteViews view = | 308 RemoteViews view = |
275 new RemoteViews(mContext.getPackageName(), R.layout.web_noti
fication_button); | 309 new RemoteViews(mContext.getPackageName(), R.layout.web_noti
fication_button); |
276 | 310 |
277 if (action.getIcon() != 0) { | 311 if ((action.icon != null && useActionIconBitmaps()) || action.iconId
!= 0) { |
278 // TODO(mvanouwerkerk): If the icon can be provided by web devel
opers, limit its | 312 // TODO(mvanouwerkerk): If the icon can be provided by web devel
opers, limit its |
279 // dimensions and decide whether or not to paint it. | 313 // dimensions and decide whether or not to paint it. |
280 if (useMaterial()) { | 314 if (useMaterial()) { |
281 view.setInt(R.id.button_icon, "setColorFilter", BUTTON_ICON_
COLOR_MATERIAL); | 315 view.setInt(R.id.button_icon, "setColorFilter", BUTTON_ICON_
COLOR_MATERIAL); |
282 } | 316 } |
283 view.setImageViewResource(R.id.button_icon, action.getIcon()); | 317 |
| 318 int iconWidth = 0; |
| 319 if (action.icon != null && useActionIconBitmaps()) { |
| 320 view.setImageViewBitmap(R.id.button_icon, action.icon); |
| 321 iconWidth = action.icon.getWidth(); |
| 322 } else if (action.iconId != 0) { |
| 323 view.setImageViewResource(R.id.button_icon, action.iconId); |
| 324 BitmapFactory.Options options = new BitmapFactory.Options(); |
| 325 options.inJustDecodeBounds = true; |
| 326 BitmapFactory.decodeResource(resources, action.iconId, optio
ns); |
| 327 iconWidth = options.outWidth; |
| 328 } |
| 329 iconWidth = dpToPx( |
| 330 Math.min(pxToDp(iconWidth, metrics), MAX_ACTION_ICON_WID
TH_DP), metrics); |
284 | 331 |
285 // Set the padding of the button so the text does not overlap wi
th the icon. Flip | 332 // Set the padding of the button so the text does not overlap wi
th the icon. Flip |
286 // between left and right manually as RemoteViews does not expos
e a method that sets | 333 // between left and right manually as RemoteViews does not expos
e a method that sets |
287 // padding in a writing-direction independent way. | 334 // padding in a writing-direction independent way. |
288 BitmapFactory.Options options = new BitmapFactory.Options(); | |
289 options.inJustDecodeBounds = true; | |
290 BitmapFactory.decodeResource(resources, action.getIcon(), option
s); | |
291 int buttonPadding = | 335 int buttonPadding = |
292 dpToPx(BUTTON_PADDING_START_DP + BUTTON_ICON_PADDING_DP,
metrics) | 336 dpToPx(BUTTON_PADDING_START_DP + BUTTON_ICON_PADDING_DP,
metrics) |
293 + options.outWidth; | 337 + iconWidth; |
294 int buttonPaddingLeft = LocalizationUtils.isLayoutRtl() ? 0 : bu
ttonPadding; | 338 int buttonPaddingLeft = LocalizationUtils.isLayoutRtl() ? 0 : bu
ttonPadding; |
295 int buttonPaddingRight = LocalizationUtils.isLayoutRtl() ? butto
nPadding : 0; | 339 int buttonPaddingRight = LocalizationUtils.isLayoutRtl() ? butto
nPadding : 0; |
296 view.setViewPadding(R.id.button, buttonPaddingLeft, 0, buttonPad
dingRight, 0); | 340 view.setViewPadding(R.id.button, buttonPaddingLeft, 0, buttonPad
dingRight, 0); |
297 } | 341 } |
298 | 342 |
299 view.setTextViewText(R.id.button, action.getTitle()); | 343 view.setTextViewText(R.id.button, action.title); |
300 view.setOnClickPendingIntent(R.id.button, action.getActionIntent()); | 344 view.setOnClickPendingIntent(R.id.button, action.intent); |
301 bigView.addView(R.id.buttons, view); | 345 bigView.addView(R.id.buttons, view); |
302 } | 346 } |
303 } | 347 } |
304 | 348 |
305 private void configureSettingsButton(RemoteViews bigView) { | 349 private void configureSettingsButton(RemoteViews bigView) { |
306 if (mSettingsAction == null) { | 350 if (mSettingsAction == null) { |
307 return; | 351 return; |
308 } | 352 } |
309 bigView.setOnClickPendingIntent(R.id.origin, mSettingsAction.getActionIn
tent()); | 353 bigView.setOnClickPendingIntent(R.id.origin, mSettingsAction.intent); |
310 if (useMaterial()) { | 354 if (useMaterial()) { |
311 bigView.setInt(R.id.origin_settings_icon, "setColorFilter", BUTTON_I
CON_COLOR_MATERIAL); | 355 bigView.setInt(R.id.origin_settings_icon, "setColorFilter", BUTTON_I
CON_COLOR_MATERIAL); |
312 } | 356 } |
313 } | 357 } |
314 | 358 |
315 /** | 359 /** |
316 * Shows the work profile badge if it is needed. | 360 * Shows the work profile badge if it is needed. |
317 */ | 361 */ |
318 private void addWorkProfileBadge(RemoteViews view) { | 362 private void addWorkProfileBadge(RemoteViews view) { |
319 Resources resources = mContext.getResources(); | 363 Resources resources = mContext.getResources(); |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
369 * Scales down the maximum amount of flexible padding to use if font scaling
is over 1.0. Never | 413 * Scales down the maximum amount of flexible padding to use if font scaling
is over 1.0. Never |
370 * scales up the amount of padding, as on some devices the notification text
is rendered in dp | 414 * scales up the amount of padding, as on some devices the notification text
is rendered in dp |
371 * units (which do not scale) and additional padding could lead to cropping
at the bottom of the | 415 * units (which do not scale) and additional padding could lead to cropping
at the bottom of the |
372 * notification. Never scales the padding below zero. | 416 * notification. Never scales the padding below zero. |
373 * | 417 * |
374 * @param fontScale The current system font scaling factor. | 418 * @param fontScale The current system font scaling factor. |
375 * @param displayMetrics The display metrics for the current context. | 419 * @param displayMetrics The display metrics for the current context. |
376 * @return The amount of padding to be used, in pixels. | 420 * @return The amount of padding to be used, in pixels. |
377 */ | 421 */ |
378 @VisibleForTesting | 422 @VisibleForTesting |
379 static int calculateScaledPadding(float fontScale, DisplayMetrics displayMet
rics) { | 423 static int calculateScaledPadding(float fontScale, DisplayMetrics metrics) { |
380 float paddingScale = 1.0f; | 424 float paddingScale = 1.0f; |
381 if (fontScale > 1.0f) { | 425 if (fontScale > 1.0f) { |
382 fontScale = Math.min(fontScale, FONT_SCALE_LARGE); | 426 fontScale = Math.min(fontScale, FONT_SCALE_LARGE); |
383 paddingScale = (FONT_SCALE_LARGE - fontScale) / (FONT_SCALE_LARGE -
1.0f); | 427 paddingScale = (FONT_SCALE_LARGE - fontScale) / (FONT_SCALE_LARGE -
1.0f); |
384 } | 428 } |
385 return dpToPx(paddingScale * MAX_SCALABLE_PADDING_DP, displayMetrics); | 429 return dpToPx(paddingScale * MAX_SCALABLE_PADDING_DP, metrics); |
386 } | 430 } |
387 | 431 |
388 /** | 432 /** |
389 * Converts a dp value to a px value. | 433 * Converts a dp value to a px value. |
390 */ | 434 */ |
391 private static int dpToPx(float value, DisplayMetrics displayMetrics) { | 435 private static int dpToPx(float value, DisplayMetrics metrics) { |
392 return Math.round( | 436 return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
value, metrics)); |
393 TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, di
splayMetrics)); | |
394 } | 437 } |
395 | 438 |
396 /** | 439 /** |
| 440 * Converts a px value to a dp value. |
| 441 */ |
| 442 private static int pxToDp(int value, DisplayMetrics metrics) { |
| 443 return Math.round(value / (metrics.densityDpi / DisplayMetrics.DENSITY_D
EFAULT)); |
| 444 } |
| 445 |
| 446 /** |
397 * Whether to use the Material look and feel or fall back to Holo. | 447 * Whether to use the Material look and feel or fall back to Holo. |
398 */ | 448 */ |
399 private static boolean useMaterial() { | 449 private static boolean useMaterial() { |
400 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; | 450 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; |
401 } | 451 } |
| 452 |
| 453 /** |
| 454 * Whether to use bitmaps for action icons, if they are specified. |
| 455 */ |
| 456 private static boolean useActionIconBitmaps() { |
| 457 // The Notification.Action.Builder(Icon, CharSequence, PendingIntent) co
nstructor is API |
| 458 // level 23. |
| 459 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; |
| 460 } |
| 461 |
| 462 @SuppressWarnings("deprecation") // For addAction(int, CharSequence, Pending
Intent) |
| 463 @TargetApi(Build.VERSION_CODES.M) // For Icon. |
| 464 private static void addAction(Notification.Builder builder, Action action) { |
| 465 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && action.icon != nul
l) { |
| 466 Icon icon = Icon.createWithBitmap(action.icon); |
| 467 builder.addAction( |
| 468 new Notification.Action.Builder(icon, action.title, action.i
ntent).build()); |
| 469 } else { |
| 470 builder.addAction(action.iconId, action.title, action.intent); |
| 471 } |
| 472 } |
402 } | 473 } |
OLD | NEW |