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
This commit is contained in:
Mady Mellor
2018-11-21 11:30:45 -08:00
parent b4991e60db
commit 3f2efdbf5d
20 changed files with 806 additions and 288 deletions

View File

@@ -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;
@@ -674,6 +687,18 @@ public class ContrastColorUtil {
return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
}
/**
* 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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, float amount);
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.
*/
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);
// Reset the position of the stack (TODO - or should we save / respect last user position?)
mStackView.setPosition(startPoint.x, startPoint.y);
for (String key: mBubbles.keySet()) {
removeBubble(key);
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);
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()) {

View File

@@ -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);
}
/**

View File

@@ -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();
}
}
@@ -286,18 +314,37 @@ public class BubbleStackView extends FrameLayout implements BubbleTouchHandler.F
return this;
}
/**
* 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);

View File

@@ -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);
}

View File

@@ -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);
LinearLayout.LayoutParams iconLp = (LinearLayout.LayoutParams) mIconView.getLayoutParams();
iconLp.width = mBubbleSize;
iconLp.height = mBubbleSize;
mIconView.setLayoutParams(iconLp);
update(entry);
}
/**
* Updates the UI based on the entry.
*/
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.
*/
public String getKey() {
return mEntry == null ? null : mEntry.key;
}
/**
* @return the notification entry associated with this bubble.
* The {@link NotificationEntry} associated with this view, if one exists.
*/
@Nullable
public NotificationEntry getEntry() {
return mEntry;
}
/**
* @return the view to display notification content when the bubble is expanded.
* 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 badge and animates messages as needed.
*/
public void update(NotificationEntry entry) {
mEntry = entry;
updateViews();
}
/**
* @return the {@link ExpandableNotificationRow} view to display notification content when the
* bubble is expanded.
*/
@Nullable
public ExpandableNotificationRow getRowView() {
return mEntry.getRow();
return (mEntry != null) ? mEntry.getRow() : null;
}
/**
* Marks this bubble as "read", i.e. no badge should show.
*/
public void updateDotVisibility() {
boolean showDot = getEntry().showInShadeWhenBubble();
animateDot(showDot);
}
/**
* Animates the badge to show or hide.
*/
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
*/

View File

@@ -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();

View File

@@ -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;

View File

@@ -134,6 +134,10 @@ public class NotificationFilter {
}
}
if (entry.isBubble() && !entry.showInShadeWhenBubble()) {
return true;
}
return false;
}

View File

@@ -134,6 +134,29 @@ public class NotificationInterruptionStateProvider {
return mShadeController;
}
/**
* 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.
*
@@ -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());
}

View File

@@ -140,6 +140,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.
*/
@@ -199,6 +207,23 @@ public final class NotificationEntry {
return mUserDismissedBubble;
}
/**
* 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.
*/

View File

@@ -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();
}
/**

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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);