Home | History | Annotate | Download | only in app
      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