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:
@@ -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>
|
||||
|
||||
38
packages/SystemUI/res/layout/bubble_view.xml
Normal file
38
packages/SystemUI/res/layout/bubble_view.xml
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -134,6 +134,10 @@ public class NotificationFilter {
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.isBubble() && !entry.showInShadeWhenBubble()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user