1 /*
2  * Copyright (C) 2018 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 com.android.systemui.bubbles;
18 
19 import android.annotation.Nullable;
20 import android.app.Notification;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Matrix;
26 import android.graphics.Path;
27 import android.graphics.drawable.AdaptiveIconDrawable;
28 import android.graphics.drawable.BitmapDrawable;
29 import android.graphics.drawable.ColorDrawable;
30 import android.graphics.drawable.Drawable;
31 import android.graphics.drawable.Icon;
32 import android.graphics.drawable.InsetDrawable;
33 import android.util.AttributeSet;
34 import android.util.PathParser;
35 import android.widget.FrameLayout;
36 
37 import com.android.internal.graphics.ColorUtils;
38 import com.android.launcher3.icons.ShadowGenerator;
39 import com.android.systemui.Interpolators;
40 import com.android.systemui.R;
41 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
42 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
43 
44 /**
45  * A floating object on the screen that can post message updates.
46  */
47 public class BubbleView extends FrameLayout {
48 
49     private static final int DARK_ICON_ALPHA = 180;
50     private static final double ICON_MIN_CONTRAST = 4.1;
51     private static final int DEFAULT_BACKGROUND_COLOR = Color.LTGRAY;
52     // Same value as Launcher3 badge code
53     private static final float WHITE_SCRIM_ALPHA = 0.54f;
54     private Context mContext;
55 
56     private BadgedImageView mBadgedImageView;
57     private int mBadgeColor;
58     private int mIconInset;
59     private Drawable mUserBadgedAppIcon;
60 
61     // mBubbleIconFactory cannot be static because it depends on Context.
62     private BubbleIconFactory mBubbleIconFactory;
63 
64     private boolean mSuppressDot;
65 
66     private Bubble mBubble;
67 
BubbleView(Context context)68     public BubbleView(Context context) {
69         this(context, null);
70     }
71 
BubbleView(Context context, AttributeSet attrs)72     public BubbleView(Context context, AttributeSet attrs) {
73         this(context, attrs, 0);
74     }
75 
BubbleView(Context context, AttributeSet attrs, int defStyleAttr)76     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
77         this(context, attrs, defStyleAttr, 0);
78     }
79 
BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)80     public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
81         super(context, attrs, defStyleAttr, defStyleRes);
82         mContext = context;
83         mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset);
84     }
85 
86     @Override
onFinishInflate()87     protected void onFinishInflate() {
88         super.onFinishInflate();
89         mBadgedImageView = findViewById(R.id.bubble_image);
90     }
91 
92     @Override
onAttachedToWindow()93     protected void onAttachedToWindow() {
94         super.onAttachedToWindow();
95     }
96 
97     /**
98      * Populates this view with a bubble.
99      * <p>
100      * This should only be called when a new bubble is being set on the view, updates to the
101      * current bubble should use {@link #update(Bubble)}.
102      *
103      * @param bubble the bubble to display in this view.
104      */
setBubble(Bubble bubble)105     public void setBubble(Bubble bubble) {
106         mBubble = bubble;
107     }
108 
109     /**
110      * The {@link NotificationEntry} associated with this view, if one exists.
111      */
112     @Nullable
getEntry()113     public NotificationEntry getEntry() {
114         return mBubble != null ? mBubble.getEntry() : null;
115     }
116 
117     /**
118      * The key for the {@link NotificationEntry} associated with this view, if one exists.
119      */
120     @Nullable
getKey()121     public String getKey() {
122         return (mBubble != null) ? mBubble.getKey() : null;
123     }
124 
125     /**
126      * Updates the UI based on the bubble, updates badge and animates messages as needed.
127      */
update(Bubble bubble)128     public void update(Bubble bubble) {
129         mBubble = bubble;
130         updateViews();
131     }
132 
133     /**
134      * @param factory Factory for creating normalized bubble icons.
135      */
setBubbleIconFactory(BubbleIconFactory factory)136     public void setBubbleIconFactory(BubbleIconFactory factory) {
137         mBubbleIconFactory = factory;
138     }
139 
setAppIcon(Drawable appIcon)140     public void setAppIcon(Drawable appIcon) {
141         mUserBadgedAppIcon = appIcon;
142     }
143 
144     /**
145      * @return the {@link ExpandableNotificationRow} view to display notification content when the
146      * bubble is expanded.
147      */
148     @Nullable
getRowView()149     public ExpandableNotificationRow getRowView() {
150         return (mBubble != null) ? mBubble.getEntry().getRow() : null;
151     }
152 
153     /** Changes the dot's visibility to match the bubble view's state. */
updateDotVisibility(boolean animate)154     void updateDotVisibility(boolean animate) {
155         updateDotVisibility(animate, null /* after */);
156     }
157 
158     /**
159      * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the
160      * flyout is visible or animating, to hide the dot until the flyout visually transforms into it.
161      */
setSuppressDot(boolean suppressDot, boolean animate)162     void setSuppressDot(boolean suppressDot, boolean animate) {
163         mSuppressDot = suppressDot;
164         updateDotVisibility(animate);
165     }
166 
167     /** Sets the position of the 'new' dot, animating it out and back in if requested. */
setDotPosition(boolean onLeft, boolean animate)168     void setDotPosition(boolean onLeft, boolean animate) {
169         if (animate && onLeft != mBadgedImageView.getDotOnLeft() && shouldShowDot()) {
170             animateDot(false /* showDot */, () -> {
171                 mBadgedImageView.setDotOnLeft(onLeft);
172                 animateDot(true /* showDot */, null);
173             });
174         } else {
175             mBadgedImageView.setDotOnLeft(onLeft);
176         }
177     }
178 
getDotCenter()179     float[] getDotCenter() {
180         float[] unscaled = mBadgedImageView.getDotCenter();
181         return new float[]{unscaled[0], unscaled[1]};
182     }
183 
getDotPositionOnLeft()184     boolean getDotPositionOnLeft() {
185         return mBadgedImageView.getDotOnLeft();
186     }
187 
188     /**
189      * Changes the dot's visibility to match the bubble view's state, running the provided callback
190      * after animation if requested.
191      */
updateDotVisibility(boolean animate, Runnable after)192     private void updateDotVisibility(boolean animate, Runnable after) {
193         final boolean showDot = shouldShowDot();
194         if (animate) {
195             animateDot(showDot, after);
196         } else {
197             mBadgedImageView.setShowDot(showDot);
198             mBadgedImageView.setDotScale(showDot ? 1f : 0f);
199         }
200     }
201 
202     /**
203      * Animates the badge to show or hide.
204      */
animateDot(boolean showDot, Runnable after)205     private void animateDot(boolean showDot, Runnable after) {
206         if (mBadgedImageView.isShowingDot() == showDot) {
207             return;
208         }
209         // Do NOT wait until after animation ends to setShowDot
210         // to avoid overriding more recent showDot states.
211         mBadgedImageView.setShowDot(showDot);
212         mBadgedImageView.clearAnimation();
213         mBadgedImageView.animate().setDuration(200)
214                 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
215                 .setUpdateListener((valueAnimator) -> {
216                     float fraction = valueAnimator.getAnimatedFraction();
217                     fraction = showDot ? fraction : 1f - fraction;
218                     mBadgedImageView.setDotScale(fraction);
219                 }).withEndAction(() -> {
220             mBadgedImageView.setDotScale(showDot ? 1f : 0f);
221             if (after != null) {
222                 after.run();
223             }
224         }).start();
225     }
226 
updateViews()227     void updateViews() {
228         if (mBubble == null || mBubbleIconFactory == null) {
229             return;
230         }
231         // Update icon.
232         Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata();
233         Notification n = mBubble.getEntry().notification.getNotification();
234         Icon ic = metadata.getIcon();
235         boolean needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP;
236 
237         Drawable iconDrawable = ic.loadDrawable(mContext);
238         if (needsTint) {
239             iconDrawable = buildIconWithTint(iconDrawable, n.color);
240         }
241         Bitmap bubbleIcon = mBubbleIconFactory.createBadgedIconBitmap(iconDrawable,
242                 null /* user */,
243                 true /* shrinkNonAdaptiveIcons */).icon;
244 
245         // Give it a shadow
246         Bitmap userBadgedBitmap = mBubbleIconFactory.createIconBitmap(mUserBadgedAppIcon,
247                 1f, mBubbleIconFactory.getBadgeSize());
248         Canvas c = new Canvas();
249         ShadowGenerator shadowGenerator = new ShadowGenerator(mBubbleIconFactory.getBadgeSize());
250         c.setBitmap(userBadgedBitmap);
251         shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
252 
253         mBubbleIconFactory.badgeWithDrawable(bubbleIcon,
254                 new BitmapDrawable(mContext.getResources(), userBadgedBitmap));
255         mBadgedImageView.setImageBitmap(bubbleIcon);
256 
257         // Update badge.
258         int badgeColor = determineDominateColor(iconDrawable, n.color);
259         mBadgeColor = badgeColor;
260         mBadgedImageView.setDotColor(badgeColor);
261 
262         // Update dot.
263         Path iconPath = PathParser.createPathFromPathData(
264                 getResources().getString(com.android.internal.R.string.config_icon_mask));
265         Matrix matrix = new Matrix();
266         float scale = mBubbleIconFactory.getNormalizer().getScale(iconDrawable,
267                 null /* outBounds */, null /* path */, null /* outMaskShape */);
268         float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
269         matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
270                 radius /* pivot y */);
271         iconPath.transform(matrix);
272         mBadgedImageView.drawDot(iconPath);
273 
274         animateDot(shouldShowDot(), null /* after */);
275     }
276 
shouldShowDot()277     boolean shouldShowDot() {
278         return mBubble.showBubbleDot() && !mSuppressDot;
279     }
280 
getBadgeColor()281     int getBadgeColor() {
282         return mBadgeColor;
283     }
284 
buildIconWithTint(Drawable iconDrawable, int backgroundColor)285     private AdaptiveIconDrawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) {
286         iconDrawable = checkTint(iconDrawable, backgroundColor);
287         InsetDrawable foreground = new InsetDrawable(iconDrawable, mIconInset);
288         ColorDrawable background = new ColorDrawable(backgroundColor);
289         return new AdaptiveIconDrawable(background, foreground);
290     }
291 
checkTint(Drawable iconDrawable, int backgroundColor)292     private Drawable checkTint(Drawable iconDrawable, int backgroundColor) {
293         backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255 /* alpha */);
294         if (backgroundColor == Color.TRANSPARENT) {
295             // ColorUtils throws exception when background is translucent.
296             backgroundColor = DEFAULT_BACKGROUND_COLOR;
297         }
298         iconDrawable.setTint(Color.WHITE);
299         double contrastRatio = ColorUtils.calculateContrast(Color.WHITE, backgroundColor);
300         if (contrastRatio < ICON_MIN_CONTRAST) {
301             int dark = ColorUtils.setAlphaComponent(Color.BLACK, DARK_ICON_ALPHA);
302             iconDrawable.setTint(dark);
303         }
304         return iconDrawable;
305     }
306 
determineDominateColor(Drawable d, int defaultTint)307     private int determineDominateColor(Drawable d, int defaultTint) {
308         // XXX: should we pull from the drawable, app icon, notif tint?
309         return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA);
310     }
311 }
312