blob: 603c4169c169f822acc50df5cb910d533b53d7a1 [file] [log] [blame]
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08001/*
Mady Mellor3f2efdb2018-11-21 11:30:45 -08002 * Copyright (C) 2018 The Android Open Source Project
Mady Mellorc3d6f7d2018-11-07 09:36:56 -08003 *
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 com.android.systemui.bubbles;
18
Mady Mellor3f2efdb2018-11-21 11:30:45 -080019import android.annotation.Nullable;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080020import android.app.Notification;
21import android.content.Context;
Lyn Han4c1731f2019-06-19 19:14:24 -070022import android.graphics.Bitmap;
Mady Mellord20ff412019-07-31 17:42:52 -070023import android.graphics.Canvas;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080024import android.graphics.Color;
Lyn Hanecdd06e2019-07-10 18:19:37 -070025import android.graphics.Matrix;
Lyn Han0e82a3e2019-06-19 18:47:06 -070026import android.graphics.Path;
Lyn Han56a3ec52019-03-25 15:04:21 -070027import android.graphics.drawable.AdaptiveIconDrawable;
Mady Mellord20ff412019-07-31 17:42:52 -070028import android.graphics.drawable.BitmapDrawable;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080029import android.graphics.drawable.ColorDrawable;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080030import android.graphics.drawable.Drawable;
31import android.graphics.drawable.Icon;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080032import android.graphics.drawable.InsetDrawable;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080033import android.util.AttributeSet;
Lyn Hanecdd06e2019-07-10 18:19:37 -070034import android.util.PathParser;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080035import android.widget.FrameLayout;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080036
Mady Mellor3f2efdb2018-11-21 11:30:45 -080037import com.android.internal.graphics.ColorUtils;
Mady Mellord20ff412019-07-31 17:42:52 -070038import com.android.launcher3.icons.ShadowGenerator;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080039import com.android.systemui.Interpolators;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080040import com.android.systemui.R;
Ned Burnsf81c4c42019-01-07 14:10:43 -050041import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080042import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
43
44/**
Mady Mellor3f2efdb2018-11-21 11:30:45 -080045 * A floating object on the screen that can post message updates.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080046 */
Joshua Tsuji442b6272019-02-08 13:23:43 -050047public class BubbleView extends FrameLayout {
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080048
Lyn Han56a3ec52019-03-25 15:04:21 -070049 private static final int DARK_ICON_ALPHA = 180;
50 private static final double ICON_MIN_CONTRAST = 4.1;
Lyn Han1b4f25e2019-06-11 13:56:34 -070051 private static final int DEFAULT_BACKGROUND_COLOR = Color.LTGRAY;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080052 // Same value as Launcher3 badge code
53 private static final float WHITE_SCRIM_ALPHA = 0.54f;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080054 private Context mContext;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080055
56 private BadgedImageView mBadgedImageView;
Joshua Tsuji6549e702019-05-02 13:13:16 -040057 private int mBadgeColor;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080058 private int mIconInset;
Lyn Han4c1731f2019-06-19 19:14:24 -070059 private Drawable mUserBadgedAppIcon;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080060
Lyn Han1b4f25e2019-06-11 13:56:34 -070061 // mBubbleIconFactory cannot be static because it depends on Context.
62 private BubbleIconFactory mBubbleIconFactory;
63
Joshua Tsuji6549e702019-05-02 13:13:16 -040064 private boolean mSuppressDot = false;
65
Mady Mellor99a302602019-06-14 11:39:56 -070066 private Bubble mBubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080067
68 public BubbleView(Context context) {
69 this(context, null);
70 }
71
72 public BubbleView(Context context, AttributeSet attrs) {
73 this(context, attrs, 0);
74 }
75
76 public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) {
77 this(context, attrs, defStyleAttr, 0);
78 }
79
80 public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
81 super(context, attrs, defStyleAttr, defStyleRes);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080082 mContext = context;
Mady Mellor3f2efdb2018-11-21 11:30:45 -080083 mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset);
84 }
85
86 @Override
87 protected void onFinishInflate() {
88 super.onFinishInflate();
Joshua Tsuji614b1df2019-03-26 13:57:05 -040089 mBadgedImageView = findViewById(R.id.bubble_image);
Mady Mellor3f2efdb2018-11-21 11:30:45 -080090 }
91
92 @Override
93 protected void onAttachedToWindow() {
94 super.onAttachedToWindow();
Mady Mellor3f2efdb2018-11-21 11:30:45 -080095 }
96
Mady Mellorc3d6f7d2018-11-07 09:36:56 -080097 /**
Mady Mellor99a302602019-06-14 11:39:56 -070098 * Populates this view with a bubble.
Mady Mellor3f2efdb2018-11-21 11:30:45 -080099 * <p>
Mady Mellor99a302602019-06-14 11:39:56 -0700100 * 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)}.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800102 *
Mady Mellor99a302602019-06-14 11:39:56 -0700103 * @param bubble the bubble to display in this view.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800104 */
Mady Mellor99a302602019-06-14 11:39:56 -0700105 public void setBubble(Bubble bubble) {
106 mBubble = bubble;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800107 }
108
109 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800110 * The {@link NotificationEntry} associated with this view, if one exists.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800111 */
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800112 @Nullable
Ned Burnsf81c4c42019-01-07 14:10:43 -0500113 public NotificationEntry getEntry() {
Mady Mellor99a302602019-06-14 11:39:56 -0700114 return mBubble != null ? mBubble.getEntry() : null;
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800115 }
116
117 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800118 * The key for the {@link NotificationEntry} associated with this view, if one exists.
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800119 */
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800120 @Nullable
121 public String getKey() {
Mady Mellor99a302602019-06-14 11:39:56 -0700122 return (mBubble != null) ? mBubble.getKey() : null;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800123 }
124
125 /**
Mady Mellor99a302602019-06-14 11:39:56 -0700126 * Updates the UI based on the bubble, updates badge and animates messages as needed.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800127 */
Mady Mellor99a302602019-06-14 11:39:56 -0700128 public void update(Bubble bubble) {
129 mBubble = bubble;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800130 updateViews();
131 }
132
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800133 /**
Lyn Han1b4f25e2019-06-11 13:56:34 -0700134 * @param factory Factory for creating normalized bubble icons.
135 */
136 public void setBubbleIconFactory(BubbleIconFactory factory) {
137 mBubbleIconFactory = factory;
138 }
139
Lyn Han4c1731f2019-06-19 19:14:24 -0700140 public void setAppIcon(Drawable appIcon) {
141 mUserBadgedAppIcon = appIcon;
142 }
Lyn Han1b4f25e2019-06-11 13:56:34 -0700143 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800144 * @return the {@link ExpandableNotificationRow} view to display notification content when the
145 * bubble is expanded.
146 */
147 @Nullable
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800148 public ExpandableNotificationRow getRowView() {
Mady Mellor99a302602019-06-14 11:39:56 -0700149 return (mBubble != null) ? mBubble.getEntry().getRow() : null;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800150 }
151
Joshua Tsuji6549e702019-05-02 13:13:16 -0400152 /** Changes the dot's visibility to match the bubble view's state. */
153 void updateDotVisibility(boolean animate) {
154 updateDotVisibility(animate, null /* after */);
155 }
156
Joshua Tsuji6549e702019-05-02 13:13:16 -0400157
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 */
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. */
168 void setDotPosition(boolean onLeft, boolean animate) {
Lyn Han61d5d562019-07-01 17:39:38 -0700169 if (animate && onLeft != mBadgedImageView.getDotOnLeft() && !mSuppressDot) {
Joshua Tsuji6549e702019-05-02 13:13:16 -0400170 animateDot(false /* showDot */, () -> {
Lyn Han61d5d562019-07-01 17:39:38 -0700171 mBadgedImageView.setDotOnLeft(onLeft);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400172 animateDot(true /* showDot */, null);
173 });
174 } else {
Lyn Han61d5d562019-07-01 17:39:38 -0700175 mBadgedImageView.setDotOnLeft(onLeft);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400176 }
177 }
178
Lyn Han61d5d562019-07-01 17:39:38 -0700179 float[] getDotCenter() {
180 float[] unscaled = mBadgedImageView.getDotCenter();
181 return new float[]{unscaled[0], unscaled[1]};
182 }
183
Joshua Tsuji6549e702019-05-02 13:13:16 -0400184 boolean getDotPositionOnLeft() {
Lyn Han61d5d562019-07-01 17:39:38 -0700185 return mBadgedImageView.getDotOnLeft();
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800186 }
187
188 /**
Joshua Tsujidd4d9f92019-05-13 13:57:38 -0400189 * Changes the dot's visibility to match the bubble view's state, running the provided callback
190 * after animation if requested.
191 */
192 private void updateDotVisibility(boolean animate, Runnable after) {
Mady Mellordf48d0a2019-06-25 18:26:46 -0700193 boolean showDot = mBubble.showBubbleDot() && !mSuppressDot;
Joshua Tsujidd4d9f92019-05-13 13:57:38 -0400194
195 if (animate) {
196 animateDot(showDot, after);
197 } else {
198 mBadgedImageView.setShowDot(showDot);
199 }
200 }
201
202 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800203 * Animates the badge to show or hide.
204 */
Joshua Tsuji6549e702019-05-02 13:13:16 -0400205 private void animateDot(boolean showDot, Runnable after) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800206 if (mBadgedImageView.isShowingDot() != showDot) {
Joshua Tsujidd4d9f92019-05-13 13:57:38 -0400207 if (showDot) {
208 mBadgedImageView.setShowDot(true);
209 }
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800210 mBadgedImageView.clearAnimation();
211 mBadgedImageView.animate().setDuration(200)
212 .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
213 .setUpdateListener((valueAnimator) -> {
214 float fraction = valueAnimator.getAnimatedFraction();
Joshua Tsujidd4d9f92019-05-13 13:57:38 -0400215 fraction = showDot ? fraction : 1f - fraction;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800216 mBadgedImageView.setDotScale(fraction);
217 }).withEndAction(() -> {
Joshua Tsuji6549e702019-05-02 13:13:16 -0400218 if (!showDot) {
219 mBadgedImageView.setShowDot(false);
220 }
221
222 if (after != null) {
223 after.run();
224 }
Mark Renouf8eafa222019-01-23 17:01:55 -0500225 }).start();
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800226 }
227 }
228
Lyn Han80b80112019-04-04 14:03:40 -0700229 void updateViews() {
Mady Mellor99a302602019-06-14 11:39:56 -0700230 if (mBubble == null || mBubbleIconFactory == null) {
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800231 return;
232 }
Lyn Hanecdd06e2019-07-10 18:19:37 -0700233 // Update icon.
Mady Mellor99a302602019-06-14 11:39:56 -0700234 Notification.BubbleMetadata metadata = mBubble.getEntry().getBubbleMetadata();
235 Notification n = mBubble.getEntry().notification.getNotification();
236 Icon ic = metadata.getIcon();
237 boolean needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP;
238
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800239 Drawable iconDrawable = ic.loadDrawable(mContext);
Mady Mellor9848a6c2019-03-19 15:29:05 -0700240 if (needsTint) {
Lyn Hanecdd06e2019-07-10 18:19:37 -0700241 iconDrawable = buildIconWithTint(iconDrawable, n.color);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800242 }
Lyn Han4c1731f2019-06-19 19:14:24 -0700243 Bitmap bubbleIcon = mBubbleIconFactory.createBadgedIconBitmap(iconDrawable,
Lyn Han1b4f25e2019-06-11 13:56:34 -0700244 null /* user */,
Lyn Han4c1731f2019-06-19 19:14:24 -0700245 true /* shrinkNonAdaptiveIcons */).icon;
Mady Mellord20ff412019-07-31 17:42:52 -0700246
247 // Give it a shadow
248 Bitmap userBadgedBitmap = mBubbleIconFactory.createIconBitmap(mUserBadgedAppIcon,
249 1f, mBubbleIconFactory.getBadgeSize());
250 Canvas c = new Canvas();
251 ShadowGenerator shadowGenerator = new ShadowGenerator(mBubbleIconFactory.getBadgeSize());
252 c.setBitmap(userBadgedBitmap);
253 shadowGenerator.recreateIcon(Bitmap.createBitmap(userBadgedBitmap), c);
254
255 mBubbleIconFactory.badgeWithDrawable(bubbleIcon,
256 new BitmapDrawable(mContext.getResources(), userBadgedBitmap));
Lyn Han4c1731f2019-06-19 19:14:24 -0700257 mBadgedImageView.setImageBitmap(bubbleIcon);
Lyn Han1b4f25e2019-06-11 13:56:34 -0700258
Lyn Hanecdd06e2019-07-10 18:19:37 -0700259 // Update badge.
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800260 int badgeColor = determineDominateColor(iconDrawable, n.color);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400261 mBadgeColor = badgeColor;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800262 mBadgedImageView.setDotColor(badgeColor);
Lyn Hanecdd06e2019-07-10 18:19:37 -0700263
264 // Update dot.
265 Path iconPath = PathParser.createPathFromPathData(
266 getResources().getString(com.android.internal.R.string.config_icon_mask));
267 Matrix matrix = new Matrix();
268 float scale = mBubbleIconFactory.getNormalizer().getScale(iconDrawable,
269 null /* outBounds */, null /* path */, null /* outMaskShape */);
270 float radius = BadgedImageView.DEFAULT_PATH_SIZE / 2f;
271 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */,
272 radius /* pivot y */);
273 iconPath.transform(matrix);
274 mBadgedImageView.drawDot(iconPath);
275
Mady Mellore4348272019-07-29 17:43:36 -0700276 animateDot(mBubble.showBubbleDot() /* showDot */, null /* after */);
Joshua Tsuji6549e702019-05-02 13:13:16 -0400277 }
278
279 int getBadgeColor() {
280 return mBadgeColor;
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800281 }
282
Lyn Han0e82a3e2019-06-19 18:47:06 -0700283 private AdaptiveIconDrawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) {
Lyn Han80b80112019-04-04 14:03:40 -0700284 iconDrawable = checkTint(iconDrawable, backgroundColor);
285 InsetDrawable foreground = new InsetDrawable(iconDrawable, mIconInset);
286 ColorDrawable background = new ColorDrawable(backgroundColor);
287 return new AdaptiveIconDrawable(background, foreground);
288 }
289
290 private Drawable checkTint(Drawable iconDrawable, int backgroundColor) {
Lyn Han56a3ec52019-03-25 15:04:21 -0700291 backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255 /* alpha */);
292 if (backgroundColor == Color.TRANSPARENT) {
293 // ColorUtils throws exception when background is translucent.
294 backgroundColor = DEFAULT_BACKGROUND_COLOR;
295 }
296 iconDrawable.setTint(Color.WHITE);
297 double contrastRatio = ColorUtils.calculateContrast(Color.WHITE, backgroundColor);
298 if (contrastRatio < ICON_MIN_CONTRAST) {
299 int dark = ColorUtils.setAlphaComponent(Color.BLACK, DARK_ICON_ALPHA);
300 iconDrawable.setTint(dark);
301 }
Lyn Han80b80112019-04-04 14:03:40 -0700302 return iconDrawable;
Lyn Han56a3ec52019-03-25 15:04:21 -0700303 }
304
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800305 private int determineDominateColor(Drawable d, int defaultTint) {
306 // XXX: should we pull from the drawable, app icon, notif tint?
307 return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA);
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800308 }
Mady Mellorc3d6f7d2018-11-07 09:36:56 -0800309}