Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 1 | /* |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 2 | * Copyright (C) 2018 The Android Open Source Project |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 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 | |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 19 | import android.annotation.Nullable; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 20 | import android.app.Notification; |
| 21 | import android.content.Context; |
| 22 | import android.graphics.Color; |
Lyn Han | 56a3ec5 | 2019-03-25 15:04:21 -0700 | [diff] [blame] | 23 | import android.graphics.drawable.AdaptiveIconDrawable; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 24 | import android.graphics.drawable.ColorDrawable; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 25 | import android.graphics.drawable.Drawable; |
| 26 | import android.graphics.drawable.Icon; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 27 | import android.graphics.drawable.InsetDrawable; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 28 | import android.util.AttributeSet; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 29 | import android.widget.FrameLayout; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 30 | |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 31 | import com.android.internal.graphics.ColorUtils; |
| 32 | import com.android.systemui.Interpolators; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 33 | import com.android.systemui.R; |
Ned Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 34 | import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 35 | import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; |
| 36 | |
| 37 | /** |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 38 | * A floating object on the screen that can post message updates. |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 39 | */ |
Joshua Tsuji | 442b627 | 2019-02-08 13:23:43 -0500 | [diff] [blame] | 40 | public class BubbleView extends FrameLayout { |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 41 | private static final String TAG = "BubbleView"; |
| 42 | |
Lyn Han | 56a3ec5 | 2019-03-25 15:04:21 -0700 | [diff] [blame] | 43 | private static final int DARK_ICON_ALPHA = 180; |
| 44 | private static final double ICON_MIN_CONTRAST = 4.1; |
| 45 | private static final int DEFAULT_BACKGROUND_COLOR = Color.LTGRAY; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 46 | // Same value as Launcher3 badge code |
| 47 | private static final float WHITE_SCRIM_ALPHA = 0.54f; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 48 | private Context mContext; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 49 | |
| 50 | private BadgedImageView mBadgedImageView; |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 51 | private int mBadgeColor; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 52 | private int mPadding; |
| 53 | private int mIconInset; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 54 | |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 55 | private boolean mSuppressDot = false; |
| 56 | |
Ned Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 57 | private NotificationEntry mEntry; |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 58 | |
| 59 | public BubbleView(Context context) { |
| 60 | this(context, null); |
| 61 | } |
| 62 | |
| 63 | public BubbleView(Context context, AttributeSet attrs) { |
| 64 | this(context, attrs, 0); |
| 65 | } |
| 66 | |
| 67 | public BubbleView(Context context, AttributeSet attrs, int defStyleAttr) { |
| 68 | this(context, attrs, defStyleAttr, 0); |
| 69 | } |
| 70 | |
| 71 | public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| 72 | super(context, attrs, defStyleAttr, defStyleRes); |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 73 | mContext = context; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 74 | // XXX: can this padding just be on the view and we look it up? |
| 75 | mPadding = getResources().getDimensionPixelSize(R.dimen.bubble_view_padding); |
| 76 | mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset); |
| 77 | } |
| 78 | |
| 79 | @Override |
| 80 | protected void onFinishInflate() { |
| 81 | super.onFinishInflate(); |
Joshua Tsuji | 614b1df | 2019-03-26 13:57:05 -0400 | [diff] [blame] | 82 | mBadgedImageView = findViewById(R.id.bubble_image); |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 83 | } |
| 84 | |
| 85 | @Override |
| 86 | protected void onAttachedToWindow() { |
| 87 | super.onAttachedToWindow(); |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 88 | } |
| 89 | |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 90 | /** |
| 91 | * Populates this view with a notification. |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 92 | * <p> |
| 93 | * This should only be called when a new notification is being set on the view, updates to the |
| 94 | * current notification should use {@link #update(NotificationEntry)}. |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 95 | * |
| 96 | * @param entry the notification to display as a bubble. |
| 97 | */ |
Ned Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 98 | public void setNotif(NotificationEntry entry) { |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 99 | mEntry = entry; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 100 | updateViews(); |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 101 | } |
| 102 | |
| 103 | /** |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 104 | * The {@link NotificationEntry} associated with this view, if one exists. |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 105 | */ |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 106 | @Nullable |
Ned Burns | f81c4c4 | 2019-01-07 14:10:43 -0500 | [diff] [blame] | 107 | public NotificationEntry getEntry() { |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 108 | return mEntry; |
| 109 | } |
| 110 | |
| 111 | /** |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 112 | * The key for the {@link NotificationEntry} associated with this view, if one exists. |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 113 | */ |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 114 | @Nullable |
| 115 | public String getKey() { |
| 116 | return (mEntry != null) ? mEntry.key : null; |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * Updates the UI based on the entry, updates badge and animates messages as needed. |
| 121 | */ |
| 122 | public void update(NotificationEntry entry) { |
| 123 | mEntry = entry; |
| 124 | updateViews(); |
| 125 | } |
| 126 | |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 127 | /** |
| 128 | * @return the {@link ExpandableNotificationRow} view to display notification content when the |
| 129 | * bubble is expanded. |
| 130 | */ |
| 131 | @Nullable |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 132 | public ExpandableNotificationRow getRowView() { |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 133 | return (mEntry != null) ? mEntry.getRow() : null; |
| 134 | } |
| 135 | |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 136 | /** Changes the dot's visibility to match the bubble view's state. */ |
| 137 | void updateDotVisibility(boolean animate) { |
| 138 | updateDotVisibility(animate, null /* after */); |
| 139 | } |
| 140 | |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 141 | |
| 142 | /** |
| 143 | * Sets whether or not to hide the dot even if we'd otherwise show it. This is used while the |
| 144 | * flyout is visible or animating, to hide the dot until the flyout visually transforms into it. |
| 145 | */ |
| 146 | void setSuppressDot(boolean suppressDot, boolean animate) { |
| 147 | mSuppressDot = suppressDot; |
| 148 | updateDotVisibility(animate); |
| 149 | } |
| 150 | |
| 151 | /** Sets the position of the 'new' dot, animating it out and back in if requested. */ |
| 152 | void setDotPosition(boolean onLeft, boolean animate) { |
| 153 | if (animate && onLeft != mBadgedImageView.getDotPosition() && !mSuppressDot) { |
| 154 | animateDot(false /* showDot */, () -> { |
| 155 | mBadgedImageView.setDotPosition(onLeft); |
| 156 | animateDot(true /* showDot */, null); |
| 157 | }); |
| 158 | } else { |
| 159 | mBadgedImageView.setDotPosition(onLeft); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | boolean getDotPositionOnLeft() { |
| 164 | return mBadgedImageView.getDotPosition(); |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 165 | } |
| 166 | |
| 167 | /** |
Joshua Tsuji | dd4d9f9 | 2019-05-13 13:57:38 -0400 | [diff] [blame] | 168 | * Changes the dot's visibility to match the bubble view's state, running the provided callback |
| 169 | * after animation if requested. |
| 170 | */ |
| 171 | private void updateDotVisibility(boolean animate, Runnable after) { |
| 172 | boolean showDot = getEntry().showInShadeWhenBubble() && !mSuppressDot; |
| 173 | |
| 174 | if (animate) { |
| 175 | animateDot(showDot, after); |
| 176 | } else { |
| 177 | mBadgedImageView.setShowDot(showDot); |
| 178 | } |
| 179 | } |
| 180 | |
| 181 | /** |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 182 | * Animates the badge to show or hide. |
| 183 | */ |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 184 | private void animateDot(boolean showDot, Runnable after) { |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 185 | if (mBadgedImageView.isShowingDot() != showDot) { |
Joshua Tsuji | dd4d9f9 | 2019-05-13 13:57:38 -0400 | [diff] [blame] | 186 | if (showDot) { |
| 187 | mBadgedImageView.setShowDot(true); |
| 188 | } |
| 189 | |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 190 | mBadgedImageView.clearAnimation(); |
| 191 | mBadgedImageView.animate().setDuration(200) |
| 192 | .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) |
| 193 | .setUpdateListener((valueAnimator) -> { |
| 194 | float fraction = valueAnimator.getAnimatedFraction(); |
Joshua Tsuji | dd4d9f9 | 2019-05-13 13:57:38 -0400 | [diff] [blame] | 195 | fraction = showDot ? fraction : 1f - fraction; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 196 | mBadgedImageView.setDotScale(fraction); |
| 197 | }).withEndAction(() -> { |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 198 | if (!showDot) { |
| 199 | mBadgedImageView.setShowDot(false); |
| 200 | } |
| 201 | |
| 202 | if (after != null) { |
| 203 | after.run(); |
| 204 | } |
Mark Renouf | 8eafa22 | 2019-01-23 17:01:55 -0500 | [diff] [blame] | 205 | }).start(); |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 206 | } |
| 207 | } |
| 208 | |
Lyn Han | 80b8011 | 2019-04-04 14:03:40 -0700 | [diff] [blame] | 209 | void updateViews() { |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 210 | if (mEntry == null) { |
| 211 | return; |
| 212 | } |
Mady Mellor | 9848a6c | 2019-03-19 15:29:05 -0700 | [diff] [blame] | 213 | Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata(); |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 214 | Notification n = mEntry.notification.getNotification(); |
Mady Mellor | 9848a6c | 2019-03-19 15:29:05 -0700 | [diff] [blame] | 215 | Icon ic; |
| 216 | boolean needsTint; |
| 217 | if (metadata != null) { |
| 218 | ic = metadata.getIcon(); |
| 219 | needsTint = ic.getType() != Icon.TYPE_ADAPTIVE_BITMAP; |
| 220 | } else { |
| 221 | needsTint = n.getLargeIcon() == null; |
| 222 | ic = needsTint ? n.getSmallIcon() : n.getLargeIcon(); |
| 223 | } |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 224 | Drawable iconDrawable = ic.loadDrawable(mContext); |
Mady Mellor | 9848a6c | 2019-03-19 15:29:05 -0700 | [diff] [blame] | 225 | if (needsTint) { |
Lyn Han | 56a3ec5 | 2019-03-25 15:04:21 -0700 | [diff] [blame] | 226 | mBadgedImageView.setImageDrawable(buildIconWithTint(iconDrawable, n.color)); |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 227 | } else { |
| 228 | mBadgedImageView.setImageDrawable(iconDrawable); |
| 229 | } |
| 230 | int badgeColor = determineDominateColor(iconDrawable, n.color); |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 231 | mBadgeColor = badgeColor; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 232 | mBadgedImageView.setDotColor(badgeColor); |
Joshua Tsuji | 6549e70 | 2019-05-02 13:13:16 -0400 | [diff] [blame] | 233 | animateDot(mEntry.showInShadeWhenBubble() /* showDot */, null /* after */); |
| 234 | } |
| 235 | |
| 236 | int getBadgeColor() { |
| 237 | return mBadgeColor; |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 238 | } |
| 239 | |
Lyn Han | 56a3ec5 | 2019-03-25 15:04:21 -0700 | [diff] [blame] | 240 | private Drawable buildIconWithTint(Drawable iconDrawable, int backgroundColor) { |
Lyn Han | 80b8011 | 2019-04-04 14:03:40 -0700 | [diff] [blame] | 241 | iconDrawable = checkTint(iconDrawable, backgroundColor); |
| 242 | InsetDrawable foreground = new InsetDrawable(iconDrawable, mIconInset); |
| 243 | ColorDrawable background = new ColorDrawable(backgroundColor); |
| 244 | return new AdaptiveIconDrawable(background, foreground); |
| 245 | } |
| 246 | |
| 247 | private Drawable checkTint(Drawable iconDrawable, int backgroundColor) { |
Lyn Han | 56a3ec5 | 2019-03-25 15:04:21 -0700 | [diff] [blame] | 248 | backgroundColor = ColorUtils.setAlphaComponent(backgroundColor, 255 /* alpha */); |
| 249 | if (backgroundColor == Color.TRANSPARENT) { |
| 250 | // ColorUtils throws exception when background is translucent. |
| 251 | backgroundColor = DEFAULT_BACKGROUND_COLOR; |
| 252 | } |
| 253 | iconDrawable.setTint(Color.WHITE); |
| 254 | double contrastRatio = ColorUtils.calculateContrast(Color.WHITE, backgroundColor); |
| 255 | if (contrastRatio < ICON_MIN_CONTRAST) { |
| 256 | int dark = ColorUtils.setAlphaComponent(Color.BLACK, DARK_ICON_ALPHA); |
| 257 | iconDrawable.setTint(dark); |
| 258 | } |
Lyn Han | 80b8011 | 2019-04-04 14:03:40 -0700 | [diff] [blame] | 259 | return iconDrawable; |
Lyn Han | 56a3ec5 | 2019-03-25 15:04:21 -0700 | [diff] [blame] | 260 | } |
| 261 | |
Mady Mellor | 3f2efdb | 2018-11-21 11:30:45 -0800 | [diff] [blame] | 262 | private int determineDominateColor(Drawable d, int defaultTint) { |
| 263 | // XXX: should we pull from the drawable, app icon, notif tint? |
| 264 | return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA); |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 265 | } |
Mady Mellor | c3d6f7d | 2018-11-07 09:36:56 -0800 | [diff] [blame] | 266 | } |