1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package android.support.v7.app; 18 19 import android.app.Notification; 20 import android.app.PendingIntent; 21 import android.content.Context; 22 import android.content.res.Resources; 23 import android.graphics.Bitmap; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.PorterDuff; 27 import android.graphics.PorterDuffColorFilter; 28 import android.graphics.drawable.Drawable; 29 import android.os.Build; 30 import android.os.SystemClock; 31 import android.support.annotation.RequiresApi; 32 import android.support.v4.app.NotificationBuilderWithBuilderAccessor; 33 import android.support.v4.app.NotificationCompat; 34 import android.support.v4.app.NotificationCompatBase; 35 import android.support.v7.appcompat.R; 36 import android.util.TypedValue; 37 import android.view.View; 38 import android.widget.RemoteViews; 39 40 import java.text.NumberFormat; 41 import java.util.ArrayList; 42 import java.util.List; 43 44 /** 45 * Helper class to generate MediaStyle notifications for pre-Lollipop platforms. Overrides 46 * contentView and bigContentView of the notification. 47 */ 48 @RequiresApi(9) 49 class NotificationCompatImplBase { 50 51 static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 3; 52 static final int MAX_MEDIA_BUTTONS = 5; 53 private static final int MAX_ACTION_BUTTONS = 3; 54 55 @RequiresApi(11) 56 public static <T extends NotificationCompatBase.Action> RemoteViews overrideContentViewMedia( 57 NotificationBuilderWithBuilderAccessor builder, 58 Context context, CharSequence contentTitle, CharSequence contentText, 59 CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText, 60 boolean useChronometer, long when, int priority, List<T> actions, 61 int[] actionsToShowInCompact, boolean showCancelButton, 62 PendingIntent cancelButtonIntent, boolean isDecoratedCustomView) { 63 RemoteViews views = generateContentViewMedia(context, contentTitle, contentText, contentInfo, 64 number, largeIcon, subText, useChronometer, when, priority, actions, 65 actionsToShowInCompact, showCancelButton, cancelButtonIntent, 66 isDecoratedCustomView); 67 builder.getBuilder().setContent(views); 68 if (showCancelButton) { 69 builder.getBuilder().setOngoing(true); 70 } 71 return views; 72 } 73 74 @RequiresApi(11) 75 private static <T extends NotificationCompatBase.Action> RemoteViews generateContentViewMedia( 76 Context context, CharSequence contentTitle, CharSequence contentText, 77 CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText, 78 boolean useChronometer, long when, int priority, List<T> actions, 79 int[] actionsToShowInCompact, boolean showCancelButton, 80 PendingIntent cancelButtonIntent, boolean isDecoratedCustomView) { 81 RemoteViews view = applyStandardTemplate(context, contentTitle, contentText, contentInfo, 82 number, 0 /* smallIcon */, largeIcon, subText, useChronometer, when, priority, 83 0 /* color is unused on media */, 84 isDecoratedCustomView ? R.layout.notification_template_media_custom 85 : R.layout.notification_template_media, 86 true /* fitIn1U */); 87 88 final int numActions = actions.size(); 89 final int N = actionsToShowInCompact == null 90 ? 0 91 : Math.min(actionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT); 92 view.removeAllViews(R.id.media_actions); 93 if (N > 0) { 94 for (int i = 0; i < N; i++) { 95 if (i >= numActions) { 96 throw new IllegalArgumentException(String.format( 97 "setShowActionsInCompactView: action %d out of bounds (max %d)", 98 i, numActions - 1)); 99 } 100 101 final NotificationCompatBase.Action action = actions.get(actionsToShowInCompact[i]); 102 final RemoteViews button = generateMediaActionButton(context, action); 103 view.addView(R.id.media_actions, button); 104 } 105 } 106 if (showCancelButton) { 107 view.setViewVisibility(R.id.end_padder, View.GONE); 108 view.setViewVisibility(R.id.cancel_action, View.VISIBLE); 109 view.setOnClickPendingIntent(R.id.cancel_action, cancelButtonIntent); 110 view.setInt(R.id.cancel_action, "setAlpha", 111 context.getResources().getInteger(R.integer.cancel_button_image_alpha)); 112 } else { 113 view.setViewVisibility(R.id.end_padder, View.VISIBLE); 114 view.setViewVisibility(R.id.cancel_action, View.GONE); 115 } 116 return view; 117 } 118 119 @RequiresApi(16) 120 public static <T extends NotificationCompatBase.Action> void overrideMediaBigContentView( 121 Notification n, Context context, CharSequence contentTitle, CharSequence contentText, 122 CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText, 123 boolean useChronometer, long when, int priority, int color, List<T> actions, 124 boolean showCancelButton, PendingIntent cancelButtonIntent, 125 boolean decoratedCustomView) { 126 n.bigContentView = generateMediaBigView(context, contentTitle, contentText, contentInfo, 127 number, largeIcon, subText, useChronometer, when, priority, color, actions, 128 showCancelButton, cancelButtonIntent, decoratedCustomView); 129 if (showCancelButton) { 130 n.flags |= Notification.FLAG_ONGOING_EVENT; 131 } 132 } 133 134 @RequiresApi(11) 135 public static <T extends NotificationCompatBase.Action> RemoteViews generateMediaBigView( 136 Context context, CharSequence contentTitle, CharSequence contentText, 137 CharSequence contentInfo, int number, Bitmap largeIcon, CharSequence subText, 138 boolean useChronometer, long when, int priority, int color, List<T> actions, 139 boolean showCancelButton, PendingIntent cancelButtonIntent, 140 boolean decoratedCustomView) { 141 final int actionCount = Math.min(actions.size(), MAX_MEDIA_BUTTONS); 142 RemoteViews big = applyStandardTemplate(context, contentTitle, contentText, contentInfo, 143 number, 0 /* smallIcon */, largeIcon, subText, useChronometer, when, priority, 144 color, /* fitIn1U */getBigMediaLayoutResource(decoratedCustomView, actionCount), 145 false); 146 147 big.removeAllViews(R.id.media_actions); 148 if (actionCount > 0) { 149 for (int i = 0; i < actionCount; i++) { 150 final RemoteViews button = generateMediaActionButton(context, actions.get(i)); 151 big.addView(R.id.media_actions, button); 152 } 153 } 154 if (showCancelButton) { 155 big.setViewVisibility(R.id.cancel_action, View.VISIBLE); 156 big.setInt(R.id.cancel_action, "setAlpha", 157 context.getResources().getInteger(R.integer.cancel_button_image_alpha)); 158 big.setOnClickPendingIntent(R.id.cancel_action, cancelButtonIntent); 159 } else { 160 big.setViewVisibility(R.id.cancel_action, View.GONE); 161 } 162 return big; 163 } 164 165 @RequiresApi(11) 166 private static RemoteViews generateMediaActionButton(Context context, 167 NotificationCompatBase.Action action) { 168 final boolean tombstone = (action.getActionIntent() == null); 169 RemoteViews button = new RemoteViews(context.getPackageName(), 170 R.layout.notification_media_action); 171 button.setImageViewResource(R.id.action0, action.getIcon()); 172 if (!tombstone) { 173 button.setOnClickPendingIntent(R.id.action0, action.getActionIntent()); 174 } 175 if (Build.VERSION.SDK_INT >= 15) { 176 button.setContentDescription(R.id.action0, action.getTitle()); 177 } 178 return button; 179 } 180 181 @RequiresApi(11) 182 private static int getBigMediaLayoutResource(boolean decoratedCustomView, int actionCount) { 183 if (actionCount <= 3) { 184 return decoratedCustomView 185 ? R.layout.notification_template_big_media_narrow_custom 186 : R.layout.notification_template_big_media_narrow; 187 } else { 188 return decoratedCustomView 189 ? R.layout.notification_template_big_media_custom 190 : R.layout.notification_template_big_media; 191 } 192 } 193 194 public static RemoteViews applyStandardTemplateWithActions(Context context, 195 CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, 196 int number, int smallIcon, Bitmap largeIcon, CharSequence subText, 197 boolean useChronometer, long when, int priority, int color, int resId, boolean fitIn1U, 198 ArrayList<NotificationCompat.Action> actions) { 199 RemoteViews remoteViews = applyStandardTemplate(context, contentTitle, contentText, 200 contentInfo, number, smallIcon, largeIcon, subText, useChronometer, when, priority, 201 color, resId, fitIn1U); 202 remoteViews.removeAllViews(R.id.actions); 203 boolean actionsVisible = false; 204 if (actions != null) { 205 int N = actions.size(); 206 if (N > 0) { 207 actionsVisible = true; 208 if (N > MAX_ACTION_BUTTONS) N = MAX_ACTION_BUTTONS; 209 for (int i = 0; i < N; i++) { 210 final RemoteViews button = generateActionButton(context, actions.get(i)); 211 remoteViews.addView(R.id.actions, button); 212 } 213 } 214 } 215 int actionVisibility = actionsVisible ? View.VISIBLE : View.GONE; 216 remoteViews.setViewVisibility(R.id.actions, actionVisibility); 217 remoteViews.setViewVisibility(R.id.action_divider, actionVisibility); 218 return remoteViews; 219 } 220 221 private static RemoteViews generateActionButton(Context context, 222 NotificationCompat.Action action) { 223 final boolean tombstone = (action.actionIntent == null); 224 RemoteViews button = new RemoteViews(context.getPackageName(), 225 tombstone ? getActionTombstoneLayoutResource() 226 : getActionLayoutResource()); 227 button.setImageViewBitmap(R.id.action_image, 228 createColoredBitmap(context, action.getIcon(), 229 context.getResources().getColor(R.color.notification_action_color_filter))); 230 button.setTextViewText(R.id.action_text, action.title); 231 if (!tombstone) { 232 button.setOnClickPendingIntent(R.id.action_container, action.actionIntent); 233 } 234 if (Build.VERSION.SDK_INT >= 15) { 235 button.setContentDescription(R.id.action_container, action.title); 236 } 237 return button; 238 } 239 240 private static Bitmap createColoredBitmap(Context context, int iconId, int color) { 241 return createColoredBitmap(context, iconId, color, 0); 242 } 243 244 private static Bitmap createColoredBitmap(Context context, int iconId, int color, int size) { 245 Drawable drawable = context.getResources().getDrawable(iconId); 246 int width = size == 0 ? drawable.getIntrinsicWidth() : size; 247 int height = size == 0 ? drawable.getIntrinsicHeight() : size; 248 Bitmap resultBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 249 drawable.setBounds(0, 0, width, height); 250 if (color != 0) { 251 drawable.mutate().setColorFilter( 252 new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); 253 } 254 Canvas canvas = new Canvas(resultBitmap); 255 drawable.draw(canvas); 256 return resultBitmap; 257 } 258 259 private static int getActionLayoutResource() { 260 return R.layout.notification_action; 261 } 262 263 private static int getActionTombstoneLayoutResource() { 264 return R.layout.notification_action_tombstone; 265 } 266 267 public static RemoteViews applyStandardTemplate(Context context, 268 CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, 269 int number, int smallIcon, Bitmap largeIcon, CharSequence subText, 270 boolean useChronometer, long when, int priority, int color, int resId, 271 boolean fitIn1U) { 272 Resources res = context.getResources(); 273 RemoteViews contentView = new RemoteViews(context.getPackageName(), resId); 274 boolean showLine3 = false; 275 boolean showLine2 = false; 276 277 boolean minPriority = priority < NotificationCompat.PRIORITY_LOW; 278 if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 21) { 279 // lets color the backgrounds 280 if (minPriority) { 281 contentView.setInt(R.id.notification_background, 282 "setBackgroundResource", R.drawable.notification_bg_low); 283 contentView.setInt(R.id.icon, 284 "setBackgroundResource", R.drawable.notification_template_icon_low_bg); 285 } else { 286 contentView.setInt(R.id.notification_background, 287 "setBackgroundResource", R.drawable.notification_bg); 288 contentView.setInt(R.id.icon, 289 "setBackgroundResource", R.drawable.notification_template_icon_bg); 290 } 291 } 292 293 if (largeIcon != null) { 294 // On versions before Jellybean, the large icon was shown by SystemUI, so we need to hide 295 // it here. 296 if (Build.VERSION.SDK_INT >= 16) { 297 contentView.setViewVisibility(R.id.icon, View.VISIBLE); 298 contentView.setImageViewBitmap(R.id.icon, largeIcon); 299 } else { 300 contentView.setViewVisibility(R.id.icon, View.GONE); 301 } 302 if (smallIcon != 0) { 303 int backgroundSize = res.getDimensionPixelSize( 304 R.dimen.notification_right_icon_size); 305 int iconSize = backgroundSize - res.getDimensionPixelSize( 306 R.dimen.notification_small_icon_background_padding) * 2; 307 if (Build.VERSION.SDK_INT >= 21) { 308 Bitmap smallBit = createIconWithBackground(context, 309 smallIcon, 310 backgroundSize, 311 iconSize, 312 color); 313 contentView.setImageViewBitmap(R.id.right_icon, smallBit); 314 } else { 315 contentView.setImageViewBitmap(R.id.right_icon, 316 createColoredBitmap(context, smallIcon, Color.WHITE)); 317 } 318 contentView.setViewVisibility(R.id.right_icon, View.VISIBLE); 319 } 320 } else if (smallIcon != 0) { // small icon at left 321 contentView.setViewVisibility(R.id.icon, View.VISIBLE); 322 if (Build.VERSION.SDK_INT >= 21) { 323 int backgroundSize = res.getDimensionPixelSize( 324 R.dimen.notification_large_icon_width) 325 - res.getDimensionPixelSize(R.dimen.notification_big_circle_margin); 326 int iconSize = res.getDimensionPixelSize( 327 R.dimen.notification_small_icon_size_as_large); 328 Bitmap smallBit = createIconWithBackground(context, 329 smallIcon, 330 backgroundSize, 331 iconSize, 332 color); 333 contentView.setImageViewBitmap(R.id.icon, smallBit); 334 } else { 335 contentView.setImageViewBitmap(R.id.icon, 336 createColoredBitmap(context, smallIcon, Color.WHITE)); 337 } 338 } 339 if (contentTitle != null) { 340 contentView.setTextViewText(R.id.title, contentTitle); 341 } 342 if (contentText != null) { 343 contentView.setTextViewText(R.id.text, contentText); 344 showLine3 = true; 345 } 346 // If there is a large icon we have a right side 347 boolean hasRightSide = !(Build.VERSION.SDK_INT >= 21) && largeIcon != null; 348 if (contentInfo != null) { 349 contentView.setTextViewText(R.id.info, contentInfo); 350 contentView.setViewVisibility(R.id.info, View.VISIBLE); 351 showLine3 = true; 352 hasRightSide = true; 353 } else if (number > 0) { 354 final int tooBig = res.getInteger( 355 R.integer.status_bar_notification_info_maxnum); 356 if (number > tooBig) { 357 contentView.setTextViewText(R.id.info, ((Resources) res).getString( 358 R.string.status_bar_notification_info_overflow)); 359 } else { 360 NumberFormat f = NumberFormat.getIntegerInstance(); 361 contentView.setTextViewText(R.id.info, f.format(number)); 362 } 363 contentView.setViewVisibility(R.id.info, View.VISIBLE); 364 showLine3 = true; 365 hasRightSide = true; 366 } else { 367 contentView.setViewVisibility(R.id.info, View.GONE); 368 } 369 370 // Need to show three lines? Only allow on Jellybean+ 371 if (subText != null && Build.VERSION.SDK_INT >= 16) { 372 contentView.setTextViewText(R.id.text, subText); 373 if (contentText != null) { 374 contentView.setTextViewText(R.id.text2, contentText); 375 contentView.setViewVisibility(R.id.text2, View.VISIBLE); 376 showLine2 = true; 377 } else { 378 contentView.setViewVisibility(R.id.text2, View.GONE); 379 } 380 } 381 382 // RemoteViews.setViewPadding and RemoteViews.setTextViewTextSize is not available on ICS- 383 if (showLine2 && Build.VERSION.SDK_INT >= 16) { 384 if (fitIn1U) { 385 // need to shrink all the type to make sure everything fits 386 final float subTextSize = res.getDimensionPixelSize( 387 R.dimen.notification_subtext_size); 388 contentView.setTextViewTextSize(R.id.text, TypedValue.COMPLEX_UNIT_PX, subTextSize); 389 } 390 // vertical centering 391 contentView.setViewPadding(R.id.line1, 0, 0, 0, 0); 392 } 393 394 if (when != 0) { 395 if (useChronometer && Build.VERSION.SDK_INT >= 16) { 396 contentView.setViewVisibility(R.id.chronometer, View.VISIBLE); 397 contentView.setLong(R.id.chronometer, "setBase", 398 when + (SystemClock.elapsedRealtime() - System.currentTimeMillis())); 399 contentView.setBoolean(R.id.chronometer, "setStarted", true); 400 } else { 401 contentView.setViewVisibility(R.id.time, View.VISIBLE); 402 contentView.setLong(R.id.time, "setTime", when); 403 } 404 hasRightSide = true; 405 } 406 contentView.setViewVisibility(R.id.right_side, hasRightSide ? View.VISIBLE : View.GONE); 407 contentView.setViewVisibility(R.id.line3, showLine3 ? View.VISIBLE : View.GONE); 408 return contentView; 409 } 410 411 public static Bitmap createIconWithBackground(Context ctx, int iconId, int size, int iconSize, 412 int color) { 413 Bitmap coloredBitmap = createColoredBitmap(ctx, R.drawable.notification_icon_background, 414 color == NotificationCompat.COLOR_DEFAULT ? 0 : color, size); 415 Canvas canvas = new Canvas(coloredBitmap); 416 Drawable icon = ctx.getResources().getDrawable(iconId).mutate(); 417 icon.setFilterBitmap(true); 418 int inset = (size - iconSize) / 2; 419 icon.setBounds(inset, inset, iconSize + inset, iconSize + inset); 420 icon.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP)); 421 icon.draw(canvas); 422 return coloredBitmap; 423 } 424 425 public static void buildIntoRemoteViews(Context ctx, RemoteViews outerView, 426 RemoteViews innerView) { 427 // this needs to be done fore the other calls, since otherwise we might hide the wrong 428 // things if our ids collide. 429 hideNormalContent(outerView); 430 outerView.removeAllViews(R.id.notification_main_column); 431 outerView.addView(R.id.notification_main_column, innerView.clone()); 432 outerView.setViewVisibility(R.id.notification_main_column, View.VISIBLE); 433 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 434 // Adjust padding depending on font size. 435 outerView.setViewPadding(R.id.notification_main_column_container, 436 0, calculateTopPadding(ctx), 0, 0); 437 } 438 } 439 440 private static void hideNormalContent(RemoteViews outerView) { 441 outerView.setViewVisibility(R.id.title, View.GONE); 442 outerView.setViewVisibility(R.id.text2, View.GONE); 443 outerView.setViewVisibility(R.id.text, View.GONE); 444 } 445 446 public static int calculateTopPadding(Context ctx) { 447 int padding = ctx.getResources().getDimensionPixelSize(R.dimen.notification_top_pad); 448 int largePadding = ctx.getResources().getDimensionPixelSize( 449 R.dimen.notification_top_pad_large_text); 450 float fontScale = ctx.getResources().getConfiguration().fontScale; 451 float largeFactor = (constrain(fontScale, 1.0f, 1.3f) - 1f) / (1.3f - 1f); 452 453 // Linearly interpolate the padding between large and normal with the font scale ranging 454 // from 1f to LARGE_TEXT_SCALE 455 return Math.round((1 - largeFactor) * padding + largeFactor * largePadding); 456 } 457 458 public static float constrain(float amount, float low, float high) { 459 return amount < low ? low : (amount > high ? high : amount); 460 } 461 } 462