diff options
| author | 2018-11-21 11:30:45 -0800 | |
|---|---|---|
| committer | 2019-01-11 12:53:32 -0800 | |
| commit | 3f2efdbf5d2cd1bb47066c649ab2079df524e2c7 (patch) | |
| tree | ce0a8841da097924a3d75d692f38a65813a73680 | |
| parent | b4991e60dbf698bfc7460901814c3aafac7942a9 (diff) | |
Closer to notification model & updates on bubbles
* Introduces BadgedImageView / BadgeRenderer for icon & badging
-> These are both semi-temporary until I move things over to using
icon library
* Introduces "shouldShowInShade" bit on NotificationData, this is used
to indicate whether a bubble's notification should display in the
shade or not
* BubbleController uses NotificationEntryListener to annotate notifs
bubble state & add / update / remove bubbles
* Cleans up expansion / dismissing / visibility in BubbleController
General notif / dot / bubble behaviour:
* When a bubble is posted, the notification is also in the shade and
the bubble displays a 'dot' a la notification dots on the launcher
* When the bubble is opened the dot goes away and the notif goes away
* When the notif is dismissed the dot will also go away
* If the bubble is dismissed with unseen notif, we keep the notif in shade
go/bubbles-notifs-manual has more detailed behavior / my manual tests
Bug: 111236845
Test: manual (go/bubbles-notifs-manual) and atest BubbleControllerTests
Change-Id: Ie30f1666f2fc1d094772b0dc352b798279ea72de
20 files changed, 797 insertions, 279 deletions
diff --git a/core/java/com/android/internal/util/ContrastColorUtil.java b/core/java/com/android/internal/util/ContrastColorUtil.java index a403c068c7c4..e0ba317f5eaa 100644 --- a/core/java/com/android/internal/util/ContrastColorUtil.java +++ b/core/java/com/android/internal/util/ContrastColorUtil.java @@ -586,7 +586,7 @@ public class ContrastColorUtil { * * @param color the base color to use * @param amount the amount from 1 to 100 how much to modify the color - * @return the now color that was modified + * @return the new color that was modified */ public static int getShiftedColor(int color, int amount) { final double[] result = ColorUtilsFromCompat.getTempDouble3Array(); @@ -599,6 +599,19 @@ public class ContrastColorUtil { return ColorUtilsFromCompat.LABToColor(result[0], result[1], result[2]); } + /** + * Blends the provided color with white to create a muted version. + * + * @param color the color to mute + * @param alpha the amount from 0 to 1 to set the alpha component of the white scrim + * @return the new color that was modified + */ + public static int getMutedColor(int color, float alpha) { + int whiteScrim = ColorUtilsFromCompat.setAlphaComponent( + Color.WHITE, (int) (255 * alpha)); + return compositeColors(whiteScrim, color); + } + private static boolean shouldUseDark(int backgroundColor, boolean defaultBackgroundIsDark) { if (backgroundColor == Notification.COLOR_DEFAULT) { return !defaultBackgroundIsDark; @@ -675,6 +688,18 @@ public class ContrastColorUtil { } /** + * Set the alpha component of {@code color} to be {@code alpha}. + */ + @ColorInt + public static int setAlphaComponent(@ColorInt int color, + @IntRange(from = 0x0, to = 0xFF) int alpha) { + if (alpha < 0 || alpha > 255) { + throw new IllegalArgumentException("alpha must be between 0 and 255."); + } + return (color & 0x00ffffff) | (alpha << 24); + } + + /** * Returns the luminance of a color as a float between {@code 0.0} and {@code 1.0}. * <p>Defined as the Y component in the XYZ representation of {@code color}.</p> */ diff --git a/packages/SystemUI/res/layout/bubble_view.xml b/packages/SystemUI/res/layout/bubble_view.xml new file mode 100644 index 000000000000..204408cda81f --- /dev/null +++ b/packages/SystemUI/res/layout/bubble_view.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2018 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<com.android.systemui.bubbles.BubbleView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:id="@+id/bubble_view"> + + <com.android.systemui.bubbles.BadgedImageView + android:id="@+id/bubble_image" + android:layout_width="@dimen/bubble_size" + android:layout_height="@dimen/bubble_size" + android:padding="@dimen/bubble_view_padding" + android:clipToPadding="false"/> + + <TextView + android:id="@+id/message_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/bubble_message_min_width" + android:maxWidth="@dimen/bubble_message_max_width" + android:padding="@dimen/bubble_message_padding"/> + +</com.android.systemui.bubbles.BubbleView> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 10e5f74983a3..ef16bcaf0fd2 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -981,14 +981,16 @@ <!-- How much a bubble is elevated --> <dimen name="bubble_elevation">8dp</dimen> + <!-- Padding around a collapsed bubble --> + <dimen name="bubble_view_padding">0dp</dimen> <!-- Padding between bubbles when displayed in expanded state --> <dimen name="bubble_padding">8dp</dimen> - <!-- Padding around the view displayed when the bubble is expanded --> - <dimen name="bubble_expanded_view_padding">8dp</dimen> <!-- Size of the collapsed bubble --> <dimen name="bubble_size">56dp</dimen> - <!-- Size of an icon displayed within the bubble --> - <dimen name="bubble_icon_size">24dp</dimen> + <!-- How much to inset the icon in the circle --> + <dimen name="bubble_icon_inset">16dp</dimen> + <!-- Padding around the view displayed when the bubble is expanded --> + <dimen name="bubble_expanded_view_padding">8dp</dimen> <!-- Default height of the expanded view shown when the bubble is expanded --> <dimen name="bubble_expanded_default_height">400dp</dimen> <!-- Height of the triangle that points to the expanded bubble --> @@ -1001,4 +1003,10 @@ <dimen name="bubble_expanded_header_height">48dp</dimen> <!-- Left and right padding applied to the header. --> <dimen name="bubble_expanded_header_horizontal_padding">24dp</dimen> + <!-- Max width of the message bubble--> + <dimen name="bubble_message_max_width">144dp</dimen> + <!-- Min width of the message bubble --> + <dimen name="bubble_message_min_width">32dp</dimen> + <!-- Interior padding of the message bubble --> + <dimen name="bubble_message_padding">4dp</dimen> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java new file mode 100644 index 000000000000..845b08483064 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgeRenderer.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.bubbles; + +import static android.graphics.Paint.ANTI_ALIAS_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.Log; + +// XXX: Mostly opied from launcher code / can we share? +/** + * Contains parameters necessary to draw a badge for an icon (e.g. the size of the badge). + */ +public class BadgeRenderer { + + private static final String TAG = "BadgeRenderer"; + + // The badge sizes are defined as percentages of the app icon size. + private static final float SIZE_PERCENTAGE = 0.38f; + + // Extra scale down of the dot + private static final float DOT_SCALE = 0.6f; + + private final float mDotCenterOffset; + private final float mCircleRadius; + private final Paint mCirclePaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG); + + public BadgeRenderer(int iconSizePx) { + mDotCenterOffset = SIZE_PERCENTAGE * iconSizePx; + int size = (int) (DOT_SCALE * mDotCenterOffset); + mCircleRadius = size / 2f; + } + + /** + * Draw a circle in the top right corner of the given bounds. + * + * @param color The color (based on the icon) to use for the badge. + * @param iconBounds The bounds of the icon being badged. + * @param badgeScale The progress of the animation, from 0 to 1. + * @param spaceForOffset How much space to offset the badge up and to the left or right. + * @param onLeft Whether the badge should be draw on left or right side. + */ + public void draw(Canvas canvas, int color, Rect iconBounds, float badgeScale, + Point spaceForOffset, boolean onLeft) { + if (iconBounds == null) { + Log.e(TAG, "Invalid null argument(s) passed in call to draw."); + return; + } + canvas.save(); + // We draw the badge relative to its center. + int x = onLeft ? iconBounds.left : iconBounds.right; + float offset = onLeft ? (mDotCenterOffset / 2) : -(mDotCenterOffset / 2); + float badgeCenterX = x + offset; + float badgeCenterY = iconBounds.top + mDotCenterOffset / 2; + + canvas.translate(badgeCenterX + spaceForOffset.x, badgeCenterY - spaceForOffset.y); + + canvas.scale(badgeScale, badgeScale); + mCirclePaint.setColor(color); + canvas.drawCircle(0, 0, mCircleRadius, mCirclePaint); + canvas.restore(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java new file mode 100644 index 000000000000..92d3cc1ae34f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BadgedImageView.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.bubbles; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Point; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.android.systemui.R; + +/** + * View that circle crops its contents and supports displaying a coloured dot on a top corner. + */ +public class BadgedImageView extends ImageView { + + private BadgeRenderer mDotRenderer; + private int mIconSize; + private Rect mTempBounds = new Rect(); + private Point mTempPoint = new Point(); + private Path mClipPath = new Path(); + + private float mDotScale = 0f; + private int mUpdateDotColor; + private boolean mShowUpdateDot; + private boolean mOnLeft; + + public BadgedImageView(Context context) { + this(context, null); + } + + public BadgedImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + public BadgedImageView(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + setScaleType(ScaleType.CENTER_CROP); + mIconSize = getResources().getDimensionPixelSize(R.dimen.bubble_size); + mDotRenderer = new BadgeRenderer(mIconSize); + } + + // TODO: Clipping oval path isn't great: rerender image into a separate, rounded bitmap and + // then draw would be better + @Override + public void onDraw(Canvas canvas) { + canvas.save(); + // Circle crop + mClipPath.addOval(getPaddingStart(), getPaddingTop(), + getWidth() - getPaddingEnd(), getHeight() - getPaddingBottom(), Path.Direction.CW); + canvas.clipPath(mClipPath); + super.onDraw(canvas); + + // After we've circle cropped what we're showing, restore so we don't clip the badge + canvas.restore(); + + // Draw the badge + if (mShowUpdateDot) { + getDrawingRect(mTempBounds); + mTempPoint.set((getWidth() - mIconSize) / 2, getPaddingTop()); + mDotRenderer.draw(canvas, mUpdateDotColor, mTempBounds, mDotScale, mTempPoint, + mOnLeft); + } + } + + /** + * Set whether the dot should appear on left or right side of the view. + */ + public void setDotPosition(boolean onLeft) { + mOnLeft = onLeft; + invalidate(); + } + + /** + * Set whether the dot should show or not. + */ + public void setShowDot(boolean showBadge) { + mShowUpdateDot = showBadge; + invalidate(); + } + + /** + * @return whether the dot is being displayed. + */ + public boolean isShowingDot() { + return mShowUpdateDot; + } + + /** + * The colour to use for the dot. + */ + public void setDotColor(int color) { + mUpdateDotColor = color; + invalidate(); + } + + /** + * How big the dot should be, fraction from 0 to 1. + */ + public void setDotScale(float fraction) { + mDotScale = fraction; + invalidate(); + } + + public float getDotScale() { + return mDotScale; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java index 79ee4b85f887..d7bf77da1011 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java @@ -21,6 +21,8 @@ import static android.view.View.VISIBLE; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.systemui.bubbles.BubbleMovementHelper.EDGE_OVERLAP; +import static com.android.systemui.statusbar.StatusBarState.SHADE; +import static com.android.systemui.statusbar.notification.NotificationAlertingManager.alertAgain; import android.annotation.Nullable; import android.app.INotificationManager; @@ -35,21 +37,26 @@ import android.os.ServiceManager; import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.util.Log; +import android.view.LayoutInflater; import android.view.ViewGroup; import android.view.WindowManager; import android.widget.FrameLayout; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.statusbar.NotificationVisibility; import com.android.systemui.Dependency; import com.android.systemui.R; +import com.android.systemui.statusbar.StatusBarStateController; import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.NotificationInflater; import com.android.systemui.statusbar.phone.StatusBarWindowController; -import java.util.ArrayList; import java.util.HashMap; import java.util.Map; +import java.util.Set; import javax.inject.Inject; import javax.inject.Singleton; @@ -68,8 +75,6 @@ public class BubbleController { // Enables some subset of notifs to automatically become bubbles private static final boolean DEBUG_ENABLE_AUTO_BUBBLE = false; - // When a bubble is dismissed, recreate it as a notification - private static final boolean DEBUG_DEMOTE_TO_NOTIF = false; // Secure settings private static final String ENABLE_AUTO_BUBBLE_MESSAGES = "experiment_autobubble_messaging"; @@ -82,6 +87,7 @@ public class BubbleController { private final NotificationEntryManager mNotificationEntryManager; private BubbleStateChangeListener mStateChangeListener; private BubbleExpandListener mExpandListener; + private LayoutInflater mInflater; private final Map<String, BubbleView> mBubbles = new HashMap<>(); private BubbleStackView mStackView; @@ -89,6 +95,10 @@ public class BubbleController { // Bubbles get added to the status bar view private final StatusBarWindowController mStatusBarWindowController; + private StatusBarStateListener mStatusBarStateListener; + + private final NotificationInterruptionStateProvider mNotificationInterruptionStateProvider = + Dependency.get(NotificationInterruptionStateProvider.class); private INotificationManager mNotificationManagerService; @@ -111,22 +121,41 @@ public class BubbleController { public interface BubbleExpandListener { /** * Called when the expansion state of the bubble stack changes. - * * @param isExpanding whether it's expanding or collapsing - * @param amount fraction of how expanded or collapsed it is, 1 being fully, 0 at the start + * @param key the notification key associated with bubble being expanded + */ + void onBubbleExpandChanged(boolean isExpanding, String key); + } + + /** + * Listens for the current state of the status bar and updates the visibility state + * of bubbles as needed. + */ + private class StatusBarStateListener implements StatusBarStateController.StateListener { + private int mState; + /** + * Returns the current status bar state. */ - void onBubbleExpandChanged(boolean isExpanding, float amount); + public int getCurrentState() { + return mState; + } + + @Override + public void onStateChanged(int newState) { + mState = newState; + updateVisibility(); + } } @Inject public BubbleController(Context context, StatusBarWindowController statusBarWindowController) { mContext = context; - mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); mDisplaySize = new Point(); wm.getDefaultDisplay().getSize(mDisplaySize); - mStatusBarWindowController = statusBarWindowController; + mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + mNotificationEntryManager = Dependency.get(NotificationEntryManager.class); mNotificationEntryManager.addNotificationEntryListener(mEntryListener); try { @@ -135,6 +164,10 @@ public class BubbleController { } catch (ServiceManager.ServiceNotFoundException e) { e.printStackTrace(); } + + mStatusBarWindowController = statusBarWindowController; + mStatusBarStateListener = new StatusBarStateListener(); + Dependency.get(StatusBarStateController.class).addCallback(mStatusBarStateListener); } /** @@ -159,7 +192,12 @@ public class BubbleController { * screen (e.g. if on AOD). */ public boolean hasBubbles() { - return mBubbles.size() > 0; + for (BubbleView bv : mBubbles.values()) { + if (!bv.getEntry().isBubbleDismissed()) { + return true; + } + } + return false; } /** @@ -174,7 +212,7 @@ public class BubbleController { */ public void collapseStack() { if (mStackView != null) { - mStackView.animateExpansion(false); + mStackView.collapseStack(); } } @@ -185,33 +223,32 @@ public class BubbleController { if (mStackView == null) { return; } - Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize); + Set<String> keys = mBubbles.keySet(); + for (String key: keys) { + mBubbles.get(key).getEntry().setBubbleDismissed(true); + } + mStackView.stackDismissed(); + // Reset the position of the stack (TODO - or should we save / respect last user position?) + Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize); mStackView.setPosition(startPoint.x, startPoint.y); - for (String key: mBubbles.keySet()) { - removeBubble(key); - } + + updateVisibility(); mNotificationEntryManager.updateNotifications(); - updateBubblesShowing(); } /** - * Adds a bubble associated with the provided notification entry or updates it if it exists. + * Adds or updates a bubble associated with the provided notification entry. + * + * @param notif the notification associated with this bubble. + * @param updatePosition whether this update should promote the bubble to the top of the stack. */ - public void addBubble(NotificationEntry notif) { + public void updateBubble(NotificationEntry notif, boolean updatePosition) { if (mBubbles.containsKey(notif.key)) { // It's an update BubbleView bubble = mBubbles.get(notif.key); - mStackView.updateBubble(bubble, notif); + mStackView.updateBubble(bubble, notif, updatePosition); } else { - // It's new - BubbleView bubble = new BubbleView(mContext); - bubble.setNotif(notif); - if (shouldUseActivityView(mContext)) { - bubble.setAppOverlayIntent(getAppOverlayIntent(notif)); - } - mBubbles.put(bubble.getKey(), bubble); - boolean setPosition = mStackView != null && mStackView.getVisibility() != VISIBLE; if (mStackView == null) { setPosition = true; @@ -226,15 +263,22 @@ public class BubbleController { mStackView.setExpandListener(mExpandListener); } } + // It's new + BubbleView bubble = (BubbleView) mInflater.inflate( + R.layout.bubble_view, mStackView, false /* attachToRoot */); + bubble.setNotif(notif); + if (shouldUseActivityView(mContext)) { + bubble.setAppOverlayIntent(getAppOverlayIntent(notif)); + } + mBubbles.put(bubble.getKey(), bubble); mStackView.addBubble(bubble); if (setPosition) { // Need to add the bubble to the stack before we can know the width Point startPoint = getStartPoint(mStackView.getStackWidth(), mDisplaySize); mStackView.setPosition(startPoint.x, startPoint.y); - mStackView.setVisibility(VISIBLE); } - updateBubblesShowing(); } + updateVisibility(); } @Nullable @@ -256,23 +300,18 @@ public class BubbleController { * Removes the bubble associated with the {@param uri}. */ void removeBubble(String key) { - BubbleView bv = mBubbles.get(key); + BubbleView bv = mBubbles.remove(key); if (mStackView != null && bv != null) { mStackView.removeBubble(bv); bv.destroyActivityView(mStackView); - bv.getEntry().setBubbleDismissed(true); } - NotificationEntry entry = mNotificationEntryManager.getNotificationData().get(key); + NotificationEntry entry = bv != null ? bv.getEntry() : null; if (entry != null) { entry.setBubbleDismissed(true); - if (!DEBUG_DEMOTE_TO_NOTIF) { - mNotificationEntryManager.performRemoveNotification(entry.notification); - } + mNotificationEntryManager.updateNotifications(); } - mNotificationEntryManager.updateNotifications(); - - updateBubblesShowing(); + updateVisibility(); } @SuppressWarnings("FieldCanBeLocal") @@ -280,55 +319,77 @@ public class BubbleController { @Override public void onPendingEntryAdded(NotificationEntry entry) { if (shouldAutoBubble(mContext, entry) || shouldBubble(entry)) { + // TODO: handle group summaries + // It's a new notif, it shows in the shade and as a bubble entry.setIsBubble(true); + entry.setShowInShadeWhenBubble(true); + } + } + + @Override + public void onEntryInflated(NotificationEntry entry, + @NotificationInflater.InflationFlag int inflatedFlags) { + if (entry.isBubble() && mNotificationInterruptionStateProvider.shouldBubbleUp(entry)) { + updateBubble(entry, true /* updatePosition */); + } + } + + @Override + public void onPreEntryUpdated(NotificationEntry entry) { + if (mNotificationInterruptionStateProvider.shouldBubbleUp(entry) + && alertAgain(entry, entry.notification.getNotification())) { + entry.setShowInShadeWhenBubble(true); + entry.setBubbleDismissed(false); // updates come back as bubbles even if dismissed + if (mBubbles.containsKey(entry.key)) { + mBubbles.get(entry.key).updateDotVisibility(); + } + updateBubble(entry, true /* updatePosition */); + } + } + + @Override + public void onEntryRemoved(NotificationEntry entry, + @Nullable NotificationVisibility visibility, + boolean removedByUser) { + entry.setShowInShadeWhenBubble(false); + if (mBubbles.containsKey(entry.key)) { + mBubbles.get(entry.key).updateDotVisibility(); + } + if (!removedByUser) { + // This was a cancel so we should remove the bubble + removeBubble(entry.key); } } }; + /** + * Lets any listeners know if bubble state has changed. + */ private void updateBubblesShowing() { - boolean hasBubblesShowing = false; - for (BubbleView bv : mBubbles.values()) { - if (!bv.getEntry().isBubbleDismissed()) { - hasBubblesShowing = true; - break; - } + if (mStackView == null) { + return; } + boolean hadBubbles = mStatusBarWindowController.getBubblesShowing(); + boolean hasBubblesShowing = hasBubbles() && mStackView.getVisibility() == VISIBLE; mStatusBarWindowController.setBubblesShowing(hasBubblesShowing); - if (mStackView != null && !hasBubblesShowing) { - mStackView.setVisibility(INVISIBLE); - } if (mStateChangeListener != null && hadBubbles != hasBubblesShowing) { mStateChangeListener.onHasBubblesChanged(hasBubblesShowing); } } /** - * Sets the visibility of the bubbles, doesn't un-bubble them, just changes visibility. + * Updates the visibility of the bubbles based on current state. + * Does not un-bubble, just hides or un-hides. Will notify any + * {@link BubbleStateChangeListener}s if visibility changes. */ - public void updateVisibility(boolean visible) { - if (mStackView == null) { - return; - } - ArrayList<BubbleView> viewsToRemove = new ArrayList<>(); - for (BubbleView bv : mBubbles.values()) { - NotificationEntry entry = bv.getEntry(); - if (entry != null) { - if (entry.isRowRemoved() || entry.isBubbleDismissed() || entry.isRowDismissed()) { - viewsToRemove.add(bv); - } - } - } - for (BubbleView bubbleView : viewsToRemove) { - mBubbles.remove(bubbleView.getKey()); - mStackView.removeBubble(bubbleView); - bubbleView.destroyActivityView(mStackView); - } - if (mStackView != null) { - mStackView.setVisibility(visible ? VISIBLE : INVISIBLE); - if (!visible) { - collapseStack(); - } + public void updateVisibility() { + if (mStatusBarStateListener.getCurrentState() == SHADE && hasBubbles()) { + // Bubbles only appear in unlocked shade + mStackView.setVisibility(hasBubbles() ? VISIBLE : INVISIBLE); + } else if (mStackView != null) { + mStackView.setVisibility(INVISIBLE); + collapseStack(); } updateBubblesShowing(); } @@ -398,7 +459,11 @@ public class BubbleController { } /** - * Whether the notification should bubble or not. + * Whether the notification should bubble or not. Gated by debug flag. + * <p> + * If a notification has been set to bubble via proper bubble APIs or if it is an important + * message-like notification. + * </p> */ private boolean shouldAutoBubble(Context context, NotificationEntry entry) { if (entry.isBubbleDismissed()) { diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedViewContainer.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedViewContainer.java index badefe182bdd..71ae1f8620f6 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedViewContainer.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleExpandedViewContainer.java @@ -21,6 +21,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.ShapeDrawable; +import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; import android.widget.LinearLayout; @@ -88,6 +89,7 @@ public class BubbleExpandedViewContainer extends LinearLayout { */ public void setHeaderText(CharSequence text) { mHeaderView.setText(text); + mHeaderView.setVisibility(TextUtils.isEmpty(text) ? GONE : VISIBLE); } /** diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java index 3280a331a5c7..1539584d82b3 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java @@ -64,9 +64,9 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F private boolean mIsExpanded; private int mExpandedBubbleHeight; + private BubbleTouchHandler mTouchHandler; private BubbleView mExpandedBubble; private Point mCollapsedPosition; - private BubbleTouchHandler mTouchHandler; private BubbleController.BubbleExpandListener mExpandListener; private boolean mViewUpdatedRequested = false; @@ -211,13 +211,24 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F */ public void setExpandedBubble(BubbleView bubbleToExpand) { mExpandedBubble = bubbleToExpand; + boolean prevExpanded = mIsExpanded; mIsExpanded = true; - updateExpandedBubble(); - requestUpdate(); + if (!prevExpanded) { + // If we weren't previously expanded we should animate open. + animateExpansion(true /* expand */); + } else { + // If we were expanded just update the views + updateExpandedBubble(); + requestUpdate(); + } + mExpandedBubble.getEntry().setShowInShadeWhenBubble(false); + notifyExpansionChanged(mExpandedBubble, true /* expanded */); } /** - * Adds a bubble to the stack. + * Adds a bubble to the top of the stack. + * + * @param bubbleView the view to add to the stack. */ public void addBubble(BubbleView bubbleView) { mBubbleContainer.addView(bubbleView, 0, @@ -234,17 +245,26 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F mBubbleContainer.removeView(bubbleView); boolean wasExpanded = mIsExpanded; int bubbleCount = mBubbleContainer.getChildCount(); - if (bubbleView.equals(mExpandedBubble) && bubbleCount > 0) { + if (mIsExpanded && bubbleView.equals(mExpandedBubble) && bubbleCount > 0) { // If we have other bubbles and are expanded go to the next one or previous // if the bubble removed was last int nextIndex = bubbleCount > removedIndex ? removedIndex : bubbleCount - 1; - mExpandedBubble = (BubbleView) mBubbleContainer.getChildAt(nextIndex); + BubbleView expandedBubble = (BubbleView) mBubbleContainer.getChildAt(nextIndex); + setExpandedBubble(expandedBubble); } mIsExpanded = wasExpanded && mBubbleContainer.getChildCount() > 0; - requestUpdate(); - if (wasExpanded && !mIsExpanded && mExpandListener != null) { - mExpandListener.onBubbleExpandChanged(mIsExpanded, 1 /* amount */); + if (wasExpanded != mIsExpanded) { + notifyExpansionChanged(mExpandedBubble, mIsExpanded); } + requestUpdate(); + } + + /** + * Dismiss the stack of bubbles. + */ + public void stackDismissed() { + collapseStack(); + mBubbleContainer.removeAllViews(); } /** @@ -252,11 +272,19 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F * * @param bubbleView the view to update in the stack. * @param entry the entry to update it with. + * @param updatePosition whether this bubble should be moved to top of the stack. */ - public void updateBubble(BubbleView bubbleView, NotificationEntry entry) { - // TODO - move to top of bubble stack, make it show its update if it makes sense + public void updateBubble(BubbleView bubbleView, NotificationEntry entry, + boolean updatePosition) { bubbleView.update(entry); - if (bubbleView.equals(mExpandedBubble)) { + if (updatePosition && !mIsExpanded) { + // If alerting it gets promoted to top of the stack + mBubbleContainer.removeView(bubbleView); + mBubbleContainer.addView(bubbleView, 0); + requestUpdate(); + } + if (mIsExpanded && bubbleView.equals(mExpandedBubble)) { + entry.setShowInShadeWhenBubble(false); requestUpdate(); } } @@ -287,17 +315,36 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F } /** + * Collapses the stack of bubbles. + */ + public void collapseStack() { + if (mIsExpanded) { + // TODO: Save opened bubble & move it to top of stack + animateExpansion(false /* shouldExpand */); + notifyExpansionChanged(mExpandedBubble, mIsExpanded); + } + } + + /** + * Expands the stack fo bubbles. + */ + public void expandStack() { + if (!mIsExpanded) { + mExpandedBubble = getTopBubble(); + mExpandedBubble.getEntry().setShowInShadeWhenBubble(false); + animateExpansion(true /* shouldExpand */); + notifyExpansionChanged(mExpandedBubble, true /* expanded */); + } + } + + /** * Tell the stack to animate to collapsed or expanded state. */ - public void animateExpansion(boolean shouldExpand) { + private void animateExpansion(boolean shouldExpand) { if (mIsExpanded != shouldExpand) { mIsExpanded = shouldExpand; - mExpandedBubble = shouldExpand ? getTopBubble() : null; updateExpandedBubble(); - if (mExpandListener != null) { - mExpandListener.onBubbleExpandChanged(mIsExpanded, 1 /* amount */); - } if (shouldExpand) { // Save current position so that we might return there savePosition(); @@ -347,6 +394,13 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F mCollapsedPosition = getPosition(); } + private void notifyExpansionChanged(BubbleView bubbleView, boolean expanded) { + if (mExpandListener != null) { + NotificationEntry entry = bubbleView != null ? bubbleView.getEntry() : null; + mExpandListener.onBubbleExpandChanged(expanded, entry != null ? entry.key : null); + } + } + private BubbleView getTopBubble() { return getBubbleAt(0); } @@ -400,6 +454,7 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F } if (mExpandedBubble.hasAppOverlayIntent()) { + // Bubble with activity view expanded state ActivityView expandedView = mExpandedBubble.getActivityView(); expandedView.setLayoutParams(new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, mExpandedBubbleHeight)); @@ -423,13 +478,20 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F } }); } else { + // Bubble with notification view expanded state ExpandableNotificationRow row = mExpandedBubble.getRowView(); - if (!row.equals(mExpandedViewContainer.getExpandedView())) { - // Different expanded view than what we have + if (row.getParent() != null) { + // Row might still be in the shade when we expand + ((ViewGroup) row.getParent()).removeView(row); + } + if (mIsExpanded) { + mExpandedViewContainer.setExpandedView(row); + } else { mExpandedViewContainer.setExpandedView(null); } - mExpandedViewContainer.setExpandedView(row); + // Bubble with notification as expanded state doesn't need a header / title mExpandedViewContainer.setHeaderText(null); + } int pointerPosition = mExpandedBubble.getPosition().x + (mExpandedBubble.getWidth() / 2); @@ -456,7 +518,8 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F int bubbsCount = mBubbleContainer.getChildCount(); for (int i = 0; i < bubbsCount; i++) { BubbleView bv = (BubbleView) mBubbleContainer.getChildAt(i); - bv.setZ(bubbsCount - 1); + bv.updateDotVisibility(); + bv.setZ(bubbsCount - i); int transX = mIsExpanded ? (bv.getWidth() + mBubblePadding) * i : mBubblePadding * i; ViewState viewState = new ViewState(); @@ -510,6 +573,7 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F private void applyRowState(ExpandableNotificationRow view) { view.reset(); view.setHeadsUp(false); + view.resetTranslation(); view.setOnKeyguard(false); view.setOnAmbient(false); view.setClipBottomAmount(0); diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java index 96b2dbab9bdf..97784b0f4f93 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java @@ -110,7 +110,7 @@ class BubbleTouchHandler implements View.OnTouchListener { : stack.getTargetView(event); boolean isFloating = targetView instanceof FloatingView; if (!isFloating || targetView == null || action == MotionEvent.ACTION_OUTSIDE) { - stack.animateExpansion(false /* shouldExpand */); + stack.collapseStack(); cleanUpDismissTarget(); resetTouches(); return false; @@ -196,9 +196,13 @@ class BubbleTouchHandler implements View.OnTouchListener { mMovementHelper.getTranslateAnim(floatingView, toGoTo, 100, 0).start(); } } else if (floatingView.equals(stack.getExpandedBubble())) { - stack.animateExpansion(false /* shouldExpand */); + stack.collapseStack(); } else if (isBubbleStack) { - stack.animateExpansion(!stack.isExpanded() /* shouldExpand */); + if (stack.isExpanded()) { + stack.collapseStack(); + } else { + stack.expandStack(); + } } else { stack.setExpandedBubble((BubbleView) floatingView); } diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java index c1bbb9379e9c..91893ef3db00 100644 --- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java +++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2012 The Android Open Source Project + * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,40 +16,47 @@ package com.android.systemui.bubbles; +import android.annotation.Nullable; import android.app.ActivityView; import android.app.Notification; import android.app.PendingIntent; import android.content.Context; import android.graphics.Color; import android.graphics.Point; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.OvalShape; +import android.graphics.drawable.InsetDrawable; +import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; +import android.widget.FrameLayout; +import android.widget.TextView; -import com.android.internal.util.ContrastColorUtil; +import com.android.internal.graphics.ColorUtils; +import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; /** - * A floating object on the screen that has a collapsed and expanded state. + * A floating object on the screen that can post message updates. */ -class BubbleView extends LinearLayout implements BubbleTouchHandler.FloatingView { +public class BubbleView extends FrameLayout implements BubbleTouchHandler.FloatingView { private static final String TAG = "BubbleView"; + // Same value as Launcher3 badge code + private static final float WHITE_SCRIM_ALPHA = 0.54f; private Context mContext; - private View mIconView; + + private BadgedImageView mBadgedImageView; + private TextView mMessageView; + private int mPadding; + private int mIconInset; private NotificationEntry mEntry; - private int mBubbleSize; - private int mIconSize; private PendingIntent mAppOverlayIntent; private ActivityView mActivityView; @@ -67,66 +74,156 @@ class BubbleView extends LinearLayout implements BubbleTouchHandler.FloatingView public BubbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - setOrientation(LinearLayout.VERTICAL); mContext = context; - mBubbleSize = getResources().getDimensionPixelSize(R.dimen.bubble_size); - mIconSize = getResources().getDimensionPixelSize(R.dimen.bubble_icon_size); + // XXX: can this padding just be on the view and we look it up? + mPadding = getResources().getDimensionPixelSize(R.dimen.bubble_view_padding); + mIconInset = getResources().getDimensionPixelSize(R.dimen.bubble_icon_inset); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mBadgedImageView = (BadgedImageView) findViewById(R.id.bubble_image); + mMessageView = (TextView) findViewById(R.id.message_view); + mMessageView.setVisibility(GONE); + mMessageView.setPivotX(0); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + updateViews(); + } + + @Override + protected void onMeasure(int widthSpec, int heightSpec) { + measureChild(mBadgedImageView, widthSpec, heightSpec); + measureChild(mMessageView, widthSpec, heightSpec); + boolean messageGone = mMessageView.getVisibility() == GONE; + int imageHeight = mBadgedImageView.getMeasuredHeight(); + int imageWidth = mBadgedImageView.getMeasuredWidth(); + int messageHeight = messageGone ? 0 : mMessageView.getMeasuredHeight(); + int messageWidth = messageGone ? 0 : mMessageView.getMeasuredWidth(); + setMeasuredDimension( + getPaddingStart() + imageWidth + mPadding + messageWidth + getPaddingEnd(), + getPaddingTop() + Math.max(imageHeight, messageHeight) + getPaddingBottom()); + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + left = getPaddingStart(); + top = getPaddingTop(); + int imageWidth = mBadgedImageView.getMeasuredWidth(); + int imageHeight = mBadgedImageView.getMeasuredHeight(); + int messageWidth = mMessageView.getMeasuredWidth(); + int messageHeight = mMessageView.getMeasuredHeight(); + mBadgedImageView.layout(left, top, left + imageWidth, top + imageHeight); + mMessageView.layout(left + imageWidth + mPadding, top, + left + imageWidth + mPadding + messageWidth, top + messageHeight); } /** * Populates this view with a notification. + * <p> + * This should only be called when a new notification is being set on the view, updates to the + * current notification should use {@link #update(NotificationEntry)}. * * @param entry the notification to display as a bubble. */ public void setNotif(NotificationEntry entry) { - removeAllViews(); - // TODO: migrate to inflater - mIconView = new ImageView(mContext); - addView(mIconView); + mEntry = entry; + updateViews(); + } - LinearLayout.LayoutParams iconLp = (LinearLayout.LayoutParams) mIconView.getLayoutParams(); - iconLp.width = mBubbleSize; - iconLp.height = mBubbleSize; - mIconView.setLayoutParams(iconLp); + /** + * The {@link NotificationEntry} associated with this view, if one exists. + */ + @Nullable + public NotificationEntry getEntry() { + return mEntry; + } - update(entry); + /** + * The key for the {@link NotificationEntry} associated with this view, if one exists. + */ + @Nullable + public String getKey() { + return (mEntry != null) ? mEntry.key : null; } /** - * Updates the UI based on the entry. + * Updates the UI based on the entry, updates badge and animates messages as needed. */ public void update(NotificationEntry entry) { mEntry = entry; - Notification n = entry.notification.getNotification(); - Icon ic = n.getLargeIcon() != null ? n.getLargeIcon() : n.getSmallIcon(); - - if (n.getLargeIcon() == null) { - createCircledIcon(n.color, ic, ((ImageView) mIconView)); - } else { - ((ImageView) mIconView).setImageIcon(ic); - } + updateViews(); } + /** - * @return the key identifying this bubble / notification entry associated with this - * bubble, if it exists. + * @return the {@link ExpandableNotificationRow} view to display notification content when the + * bubble is expanded. */ - public String getKey() { - return mEntry == null ? null : mEntry.key; + @Nullable + public ExpandableNotificationRow getRowView() { + return (mEntry != null) ? mEntry.getRow() : null; } /** - * @return the notification entry associated with this bubble. + * Marks this bubble as "read", i.e. no badge should show. */ - public NotificationEntry getEntry() { - return mEntry; + public void updateDotVisibility() { + boolean showDot = getEntry().showInShadeWhenBubble(); + animateDot(showDot); } /** - * @return the view to display notification content when the bubble is expanded. + * Animates the badge to show or hide. */ - public ExpandableNotificationRow getRowView() { - return mEntry.getRow(); + private void animateDot(boolean showDot) { + if (mBadgedImageView.isShowingDot() != showDot) { + mBadgedImageView.setShowDot(showDot); + mBadgedImageView.clearAnimation(); + mBadgedImageView.animate().setDuration(200) + .setInterpolator(Interpolators.FAST_OUT_SLOW_IN) + .setUpdateListener((valueAnimator) -> { + float fraction = valueAnimator.getAnimatedFraction(); + fraction = showDot ? fraction : 1 - fraction; + mBadgedImageView.setDotScale(fraction); + }).withEndAction(() -> { + if (!showDot) { + mBadgedImageView.setShowDot(false); + } + }).start(); + } + } + + private void updateViews() { + if (mEntry == null) { + return; + } + Notification n = mEntry.notification.getNotification(); + boolean isLarge = n.getLargeIcon() != null; + Icon ic = isLarge ? n.getLargeIcon() : n.getSmallIcon(); + Drawable iconDrawable = ic.loadDrawable(mContext); + if (!isLarge) { + // Center icon on coloured background + iconDrawable.setTint(Color.WHITE); // TODO: dark mode + Drawable bg = new ColorDrawable(n.color); + InsetDrawable d = new InsetDrawable(iconDrawable, mIconInset); + Drawable[] layers = {bg, d}; + mBadgedImageView.setImageDrawable(new LayerDrawable(layers)); + } else { + mBadgedImageView.setImageDrawable(iconDrawable); + } + int badgeColor = determineDominateColor(iconDrawable, n.color); + mBadgedImageView.setDotColor(badgeColor); + animateDot(mEntry.showInShadeWhenBubble() /* showDot */); + } + + private int determineDominateColor(Drawable d, int defaultTint) { + // XXX: should we pull from the drawable, app icon, notif tint? + return ColorUtils.blendARGB(defaultTint, Color.WHITE, WHITE_SCRIM_ALPHA); } /** @@ -170,8 +267,8 @@ class BubbleView extends LinearLayout implements BubbleTouchHandler.FloatingView @Override public void setPosition(int x, int y) { - setTranslationX(x); - setTranslationY(y); + setPositionX(x); + setPositionY(y); } @Override @@ -189,25 +286,6 @@ class BubbleView extends LinearLayout implements BubbleTouchHandler.FloatingView return new Point((int) getTranslationX(), (int) getTranslationY()); } - // Seems sub optimal - private void createCircledIcon(int tint, Icon icon, ImageView v) { - // TODO: dark mode - icon.setTint(Color.WHITE); - icon.scaleDownIfNecessary(mIconSize, mIconSize); - v.setImageDrawable(icon.loadDrawable(mContext)); - v.setScaleType(ImageView.ScaleType.CENTER_INSIDE); - LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams(); - int color = ContrastColorUtil.ensureContrast(tint, Color.WHITE, - false /* isBgDarker */, 3); - Drawable d = new ShapeDrawable(new OvalShape()); - d.setTint(color); - v.setBackgroundDrawable(d); - - lp.width = mBubbleSize; - lp.height = mBubbleSize; - v.setLayoutParams(lp); - } - /** * @return whether an ActivityView should be used to display the content of this Bubble */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java index bf6caa010e9b..f2ff85bb226c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar; -import static com.android.systemui.statusbar.StatusBarState.SHADE; - import android.content.Context; import android.content.res.Resources; import android.os.Trace; @@ -26,7 +24,6 @@ import android.view.View; import android.view.ViewGroup; import com.android.systemui.R; -import com.android.systemui.bubbles.BubbleController; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -66,7 +63,6 @@ public class NotificationViewHierarchyManager { protected final VisualStabilityManager mVisualStabilityManager; private final StatusBarStateController mStatusBarStateController; private final NotificationEntryManager mEntryManager; - private final BubbleController mBubbleController; // Lazy private final Lazy<ShadeController> mShadeController; @@ -80,41 +76,6 @@ public class NotificationViewHierarchyManager { private NotificationPresenter mPresenter; private NotificationListContainer mListContainer; - private StatusBarStateListener mStatusBarStateListener; - - /** - * Listens for the current state of the status bar and updates the visibility state - * of bubbles as needed. - */ - public class StatusBarStateListener implements StatusBarStateController.StateListener { - private int mState; - private BubbleController mController; - - public StatusBarStateListener(BubbleController controller) { - mController = controller; - } - - /** - * Returns the current status bar state. - */ - public int getCurrentState() { - return mState; - } - - @Override - public void onStateChanged(int newState) { - mState = newState; - // Order here matters because we need to remove the expandable notification row - // from it's current parent (NSSL or bubble) before it can be added to the new parent - if (mState == SHADE) { - updateNotificationViews(); - mController.updateVisibility(true); - } else { - mController.updateVisibility(false); - updateNotificationViews(); - } - } - } @Inject public NotificationViewHierarchyManager(Context context, @@ -123,20 +84,16 @@ public class NotificationViewHierarchyManager { VisualStabilityManager visualStabilityManager, StatusBarStateController statusBarStateController, NotificationEntryManager notificationEntryManager, - BubbleController bubbleController, Lazy<ShadeController> shadeController) { mLockscreenUserManager = notificationLockscreenUserManager; mGroupManager = groupManager; mVisualStabilityManager = visualStabilityManager; mStatusBarStateController = statusBarStateController; mEntryManager = notificationEntryManager; - mBubbleController = bubbleController; mShadeController = shadeController; Resources res = context.getResources(); mAlwaysExpandNonGroupedNotification = res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications); - mStatusBarStateListener = new StatusBarStateListener(mBubbleController); - mStatusBarStateController.addCallback(mStatusBarStateListener); } public void setUpWithPresenter(NotificationPresenter presenter, @@ -153,7 +110,6 @@ public class NotificationViewHierarchyManager { ArrayList<NotificationEntry> activeNotifications = mEntryManager.getNotificationData() .getActiveNotifications(); ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size()); - ArrayList<NotificationEntry> toBubble = new ArrayList<>(); final int N = activeNotifications.size(); for (int i = 0; i < N; i++) { NotificationEntry ent = activeNotifications.get(i); @@ -162,13 +118,6 @@ public class NotificationViewHierarchyManager { // temporarily become children if they were isolated before. continue; } - ent.getRow().setStatusBarState(mStatusBarStateListener.getCurrentState()); - boolean showAsBubble = ent.isBubble() && !ent.isBubbleDismissed() - && mStatusBarStateListener.getCurrentState() == SHADE; - if (showAsBubble) { - toBubble.add(ent); - continue; - } int userId = ent.notification.getUserId(); @@ -269,12 +218,6 @@ public class NotificationViewHierarchyManager { } - for (int i = 0; i < toBubble.size(); i++) { - // TODO: might make sense to leave them in the shade and just reposition them - NotificationEntry ent = toBubble.get(i); - mBubbleController.addBubble(ent); - } - mVisualStabilityManager.onReorderingFinished(); // clear the map again for the next usage mTmpChildOrderMap.clear(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationAlertingManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationAlertingManager.java index 60d8cf460627..5605f3db90fb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationAlertingManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationAlertingManager.java @@ -150,7 +150,14 @@ public class NotificationAlertingManager { } } - private static boolean alertAgain( + /** + * Checks whether an update for a notification warrants an alert for the user. + * + * @param oldEntry the entry for this notification. + * @param newNotification the new notification for this entry. + * @return whether this notification should alert the user. + */ + public static boolean alertAgain( NotificationEntry oldEntry, Notification newNotification) { return oldEntry == null || !oldEntry.hasInterrupted() || (newNotification.flags & Notification.FLAG_ONLY_ALERT_ONCE) == 0; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java index e199ead18a68..154d7b356cd1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationFilter.java @@ -134,6 +134,10 @@ public class NotificationFilter { } } + if (entry.isBubble() && !entry.showInShadeWhenBubble()) { + return true; + } + return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java index fc7a2b37eca7..c50f10b55a71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInterruptionStateProvider.java @@ -135,6 +135,29 @@ public class NotificationInterruptionStateProvider { } /** + * Whether the notification should appear as a bubble with a fly-out on top of the screen. + * + * @param entry the entry to check + * @return true if the entry should bubble up, false otherwise + */ + public boolean shouldBubbleUp(NotificationEntry entry) { + StatusBarNotification sbn = entry.notification; + if (!entry.isBubble()) { + if (DEBUG) { + Log.d(TAG, "No bubble up: notification " + sbn.getKey() + + " is bubble? " + entry.isBubble()); + } + return false; + } + + if (!canHeadsUpCommon(entry)) { + return false; + } + + return true; + } + + /** * Whether the notification should peek in from the top and alert the user. * * @param entry the entry to check @@ -150,10 +173,12 @@ public class NotificationInterruptionStateProvider { return false; } - // TODO: need to changes this, e.g. should still heads up in expanded shade, might want - // message bubble from the bubble to go through heads up path boolean inShade = mStatusBarStateController.getState() == SHADE; - if (entry.isBubble() && !entry.isBubbleDismissed() && inShade) { + if (entry.isBubble() && inShade) { + if (DEBUG) { + Log.d(TAG, "No heads up: in unlocked shade where notification is shown as a " + + "bubble: " + sbn.getKey()); + } return false; } @@ -164,9 +189,13 @@ public class NotificationInterruptionStateProvider { return false; } - if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) { + if (!canHeadsUpCommon(entry)) { + return false; + } + + if (entry.importance < NotificationManager.IMPORTANCE_HIGH) { if (DEBUG) { - Log.d(TAG, "No heads up: no huns or vr mode"); + Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey()); } return false; } @@ -186,34 +215,6 @@ public class NotificationInterruptionStateProvider { return false; } - if (entry.shouldSuppressPeek()) { - if (DEBUG) { - Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey()); - } - return false; - } - - if (isSnoozedPackage(sbn)) { - if (DEBUG) { - Log.d(TAG, "No heads up: snoozed package: " + sbn.getKey()); - } - return false; - } - - if (entry.hasJustLaunchedFullScreenIntent()) { - if (DEBUG) { - Log.d(TAG, "No heads up: recent fullscreen: " + sbn.getKey()); - } - return false; - } - - if (entry.importance < NotificationManager.IMPORTANCE_HIGH) { - if (DEBUG) { - Log.d(TAG, "No heads up: unimportant notification: " + sbn.getKey()); - } - return false; - } - if (!mHeadsUpSuppressor.canHeadsUp(entry, sbn)) { return false; } @@ -302,6 +303,49 @@ public class NotificationInterruptionStateProvider { return true; } + /** + * Common checks between heads up alerting and bubble fly out alerting. See + * {@link #shouldHeadsUp(NotificationEntry)} and + * {@link #shouldBubbleUp(NotificationEntry)}. Notifications that fail any of these + * checks should not interrupt the user on screen. + * + * @param entry the entry to check + * @return true if these checks pass, false if the notification should not interrupt on screen + */ + public boolean canHeadsUpCommon(NotificationEntry entry) { + StatusBarNotification sbn = entry.notification; + + if (!mUseHeadsUp || mPresenter.isDeviceInVrMode()) { + if (DEBUG) { + Log.d(TAG, "No heads up: no huns or vr mode"); + } + return false; + } + + if (entry.shouldSuppressPeek()) { + if (DEBUG) { + Log.d(TAG, "No heads up: suppressed by DND: " + sbn.getKey()); + } + return false; + } + + if (isSnoozedPackage(sbn)) { + if (DEBUG) { + Log.d(TAG, "No heads up: snoozed package: " + sbn.getKey()); + } + return false; + } + + if (entry.hasJustLaunchedFullScreenIntent()) { + if (DEBUG) { + Log.d(TAG, "No heads up: recent fullscreen: " + sbn.getKey()); + } + return false; + } + + return true; + } + private boolean isSnoozedPackage(StatusBarNotification sbn) { return mHeadsUpManager.isSnoozed(sbn.getPackageName()); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index 58aa02ccf440..ee551ee96e7b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -141,6 +141,14 @@ public final class NotificationEntry { private boolean mIsBubble; /** + * Whether this notification should be shown in the shade when it is also displayed as a bubble. + * + * <p>When a notification is a bubble we don't show it in the shade once the bubble has been + * expanded</p> + */ + private boolean mShowInShadeWhenBubble; + + /** * Whether the user has dismissed this notification when it was in bubble form. */ private boolean mUserDismissedBubble; @@ -200,6 +208,23 @@ public final class NotificationEntry { } /** + * Sets whether this notification should be shown in the shade when it is also displayed as a + * bubble. + */ + public void setShowInShadeWhenBubble(boolean showInShade) { + mShowInShadeWhenBubble = showInShade; + } + + /** + * Whether this notification should be shown in the shade when it is also displayed as a + * bubble. + */ + public boolean showInShadeWhenBubble() { + // We always show it in the shade if non-clearable + return !isClearable() || mShowInShadeWhenBubble; + } + + /** * Resets the notification entry to be re-used. */ public void reset() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 95bd1ce1f9a1..df0189fc4c4a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.notification.row; -import static com.android.systemui.statusbar.StatusBarState.SHADE; import static com.android.systemui.statusbar.notification.ActivityLaunchAnimator.ExpandAnimationParameters; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_AMBIENT; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; @@ -2322,7 +2321,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } private boolean isShownAsBubble() { - return mEntry.isBubble() && (mStatusBarState == SHADE || mStatusBarState == -1); + return mEntry.isBubble() && !mEntry.showInShadeWhenBubble() && !mEntry.isBubbleDismissed(); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java index 9abd86d7088b..ad4aff574731 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java @@ -461,13 +461,6 @@ public class StatusBar extends SystemUI implements DemoMode, private NotificationMediaManager mMediaManager; protected NotificationLockscreenUserManager mLockscreenUserManager; protected NotificationRemoteInputManager mRemoteInputManager; - protected BubbleController mBubbleController; - private final BubbleController.BubbleExpandListener mBubbleExpandListener = - (isExpanding, amount) -> { - if (amount == 1) { - updateScrimController(); - } - }; private final BroadcastReceiver mWallpaperChangedReceiver = new BroadcastReceiver() { @Override @@ -589,6 +582,12 @@ public class StatusBar extends SystemUI implements DemoMode, private NotificationActivityStarter mNotificationActivityStarter; private boolean mPulsing; private ContentObserver mFeatureFlagObserver; + protected BubbleController mBubbleController; + private final BubbleController.BubbleExpandListener mBubbleExpandListener = + (isExpanding, key) -> { + mEntryManager.updateNotifications(); + updateScrimController(); + }; @Override public void onActiveStateChanged(int code, int uid, String packageName, boolean active) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index 4f61009095c7..04d24dc18f05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -346,7 +346,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit } private void handleFullScreenIntent(NotificationEntry entry) { - boolean isHeadsUped = mNotificationInterruptionStateProvider.shouldHeadsUp(entry); + boolean isHeadsUped = mNotificationInterruptionStateProvider.canHeadsUpCommon(entry); if (!isHeadsUped && entry.notification.getNotification().fullScreenIntent != null) { if (shouldSuppressFullScreenIntent(entry)) { if (DEBUG) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java index 702a64131ca1..fa5cf04d56f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java @@ -19,7 +19,6 @@ package com.android.systemui.bubbles; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -106,20 +105,20 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testAddBubble() { - mBubbleController.addBubble(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */); assertTrue(mBubbleController.hasBubbles()); } @Test public void testHasBubbles() { assertFalse(mBubbleController.hasBubbles()); - mBubbleController.addBubble(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */); assertTrue(mBubbleController.hasBubbles()); } @Test public void testRemoveBubble() { - mBubbleController.addBubble(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */); assertTrue(mBubbleController.hasBubbles()); mBubbleController.removeBubble(mRow.getEntry().key); @@ -130,35 +129,35 @@ public class BubbleControllerTest extends SysuiTestCase { @Test public void testDismissStack() { - mBubbleController.addBubble(mRow.getEntry()); - mBubbleController.addBubble(mRow2.getEntry()); + mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */); + mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */); assertTrue(mBubbleController.hasBubbles()); mBubbleController.dismissStack(); assertFalse(mStatusBarWindowController.getBubblesShowing()); - verify(mNotificationEntryManager, times(3)).updateNotifications(); + verify(mNotificationEntryManager).updateNotifications(); } @Test public void testIsStackExpanded() { assertFalse(mBubbleController.isStackExpanded()); - mBubbleController.addBubble(mRow.getEntry()); + mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */); BubbleStackView stackView = mBubbleController.getStackView(); - stackView.animateExpansion(true /* expanded */); + stackView.expandStack(); assertTrue(mBubbleController.isStackExpanded()); - stackView.animateExpansion(false /* expanded */); + stackView.collapseStack(); assertFalse(mBubbleController.isStackExpanded()); } @Test public void testCollapseStack() { - mBubbleController.addBubble(mRow.getEntry()); - mBubbleController.addBubble(mRow2.getEntry()); + mBubbleController.updateBubble(mRow.getEntry(), true /* updatePosition */); + mBubbleController.updateBubble(mRow2.getEntry(), true /* updatePosition */); BubbleStackView stackView = mBubbleController.getStackView(); - stackView.animateExpansion(true /* expanded */); + stackView.expandStack(); assertTrue(mBubbleController.isStackExpanded()); mBubbleController.collapseStack(); @@ -171,6 +170,12 @@ public class BubbleControllerTest extends SysuiTestCase { assertTrue(mRow.getEntry().isBubble()); } + @Test + public void testMarkNewNotificationAsShowInShade() { + mEntryListener.onPendingEntryAdded(mRow.getEntry()); + assertTrue(mRow.getEntry().showInShadeWhenBubble()); + } + static class TestableBubbleController extends BubbleController { TestableBubbleController(Context context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java index bf91305137e2..56e1fc6b70de 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java @@ -36,7 +36,6 @@ import android.widget.LinearLayout; import com.android.systemui.Dependency; import com.android.systemui.InitController; import com.android.systemui.SysuiTestCase; -import com.android.systemui.bubbles.BubbleController; import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.VisualStabilityManager; @@ -96,7 +95,7 @@ public class NotificationViewHierarchyManagerTest extends SysuiTestCase { mViewHierarchyManager = new NotificationViewHierarchyManager(mContext, mLockscreenUserManager, mGroupManager, mVisualStabilityManager, - mock(StatusBarStateController.class), mEntryManager, mock(BubbleController.class), + mock(StatusBarStateController.class), mEntryManager, () -> mShadeController); Dependency.get(InitController.class).executePostInitTasks(); mViewHierarchyManager.setUpWithPresenter(mPresenter, mListContainer); |