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
17package android.support.v7.app;
18
19import android.app.Notification;
20import android.app.PendingIntent;
21import android.content.Context;
22import android.content.res.Resources;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25import android.graphics.Color;
26import android.graphics.PorterDuff;
27import android.graphics.PorterDuffColorFilter;
28import android.graphics.drawable.Drawable;
29import android.os.Build;
30import android.os.SystemClock;
31import android.support.annotation.RequiresApi;
32import android.support.v4.app.NotificationBuilderWithBuilderAccessor;
33import android.support.v4.app.NotificationCompat;
34import android.support.v4.app.NotificationCompatBase;
35import android.support.v7.appcompat.R;
36import android.util.TypedValue;
37import android.view.View;
38import android.widget.RemoteViews;
39
40import java.text.NumberFormat;
41import java.util.ArrayList;
42import 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)
49class 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