diff --git a/packages/SystemUI/res/drawable/bubble_flyout.xml b/packages/SystemUI/res/drawable/bubble_flyout.xml
new file mode 100644
index 0000000000000..5406aaa65372f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/bubble_flyout.xml
@@ -0,0 +1,29 @@
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bubble_flyout.xml b/packages/SystemUI/res/layout/bubble_flyout.xml
new file mode 100644
index 0000000000000..74c6c123479c2
--- /dev/null
+++ b/packages/SystemUI/res/layout/bubble_flyout.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/bubble_view.xml b/packages/SystemUI/res/layout/bubble_view.xml
index 13186fc6437c6..a8eb2914b0b26 100644
--- a/packages/SystemUI/res/layout/bubble_view.xml
+++ b/packages/SystemUI/res/layout/bubble_view.xml
@@ -27,12 +27,4 @@
android:padding="@dimen/bubble_view_padding"
android:clipToPadding="false"/>
-
-
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 6eb279affc4fe..e281b515745ae 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1027,6 +1027,12 @@
1dp
+
+ 4dp
+
+ 16dp
+
+ 200dp
0dp
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 0411d015fd638..cd286fccdf923 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -670,6 +670,9 @@
- %s more notifications inside.
+
+ %1$s: %2$s
+
Notification settings
@@ -2401,5 +2404,4 @@
Move bottom left
Move bottom right
-
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
index be55829869eb1..de4605b552722 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleStackView.java
@@ -42,7 +42,9 @@ import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
+import android.widget.TextView;
import androidx.annotation.MainThread;
import androidx.annotation.Nullable;
@@ -70,6 +72,13 @@ public class BubbleStackView extends FrameLayout {
private static final String TAG = "BubbleStackView";
private static final boolean DEBUG = false;
+ /** Duration of the flyout alpha animations. */
+ private static final int FLYOUT_ALPHA_ANIMATION_DURATION = 100;
+
+ /** How long to wait, in milliseconds, before hiding the flyout. */
+ @VisibleForTesting
+ static final int FLYOUT_HIDE_AFTER = 5000;
+
/**
* Interface to synchronize {@link View} state and the screen.
*
@@ -119,6 +128,14 @@ public class BubbleStackView extends FrameLayout {
private FrameLayout mExpandedViewContainer;
+ private View mFlyout;
+ private TextView mFlyoutText;
+ /** Spring animation for the flyout. */
+ private SpringAnimation mFlyoutSpring;
+ /** Runnable that fades out the flyout and then sets it to GONE. */
+ private Runnable mHideFlyout =
+ () -> mFlyout.animate().alpha(0f).withEndAction(() -> mFlyout.setVisibility(GONE));
+
private int mBubbleSize;
private int mBubblePadding;
private int mExpandedAnimateXDistance;
@@ -131,6 +148,9 @@ public class BubbleStackView extends FrameLayout {
private boolean mIsExpanded;
private boolean mImeVisible;
+ /** Whether the stack is currently being dragged. */
+ private boolean mIsDragging = false;
+
private BubbleTouchHandler mTouchHandler;
private BubbleController.BubbleExpandListener mExpandListener;
private BubbleExpandedView.OnBubbleBlockedListener mBlockedListener;
@@ -221,6 +241,17 @@ public class BubbleStackView extends FrameLayout {
mExpandedViewContainer.setClipChildren(false);
addView(mExpandedViewContainer);
+ mFlyout = mInflater.inflate(R.layout.bubble_flyout, this, false);
+ mFlyout.setVisibility(GONE);
+ mFlyout.animate()
+ .setDuration(FLYOUT_ALPHA_ANIMATION_DURATION)
+ .setInterpolator(new AccelerateDecelerateInterpolator());
+ addView(mFlyout);
+
+ mFlyoutText = mFlyout.findViewById(R.id.bubble_flyout_text);
+
+ mFlyoutSpring = new SpringAnimation(mFlyout, DynamicAnimation.TRANSLATION_X);
+
mExpandedViewXAnim =
new SpringAnimation(mExpandedViewContainer, DynamicAnimation.TRANSLATION_X);
mExpandedViewXAnim.setSpring(
@@ -448,6 +479,8 @@ public class BubbleStackView extends FrameLayout {
requestUpdate();
logBubbleEvent(b, StatsLog.BUBBLE_UICHANGED__ACTION__POSTED);
+
+ animateInFlyoutForBubble(b);
}
/**
@@ -549,6 +582,7 @@ public class BubbleStackView extends FrameLayout {
mBubbleContainer.moveViewTo(b.iconView, 0);
}
requestUpdate();
+ animateInFlyoutForBubble(b /* bubble */);
}
if (mIsExpanded && entry.equals(mExpandedBubble.entry)) {
entry.setShowInShadeWhenBubble(false);
@@ -577,11 +611,18 @@ public class BubbleStackView extends FrameLayout {
}
// Outside parts of view we care about.
return null;
+ } else if (isIntersecting(mFlyout, x, y)) {
+ return mFlyout;
}
- // If we're collapsed, the stack is always the target.
+
+ // If it wasn't an individual bubble in the expanded state, or the flyout, it's the stack.
return this;
}
+ public View getFlyoutView() {
+ return mFlyout;
+ }
+
/**
* Collapses the stack of bubbles.
*
@@ -622,6 +663,8 @@ public class BubbleStackView extends FrameLayout {
*/
private void animateExpansion(boolean shouldExpand) {
if (mIsExpanded != shouldExpand) {
+ hideFlyoutImmediate();
+
mIsExpanded = shouldExpand;
updateExpandedBubble();
applyCurrentState();
@@ -735,6 +778,9 @@ public class BubbleStackView extends FrameLayout {
mStackAnimationController.cancelStackPositionAnimations();
mBubbleContainer.setController(mStackAnimationController);
+ hideFlyoutImmediate();
+
+ mIsDragging = true;
}
void onDragged(float x, float y) {
@@ -747,6 +793,7 @@ public class BubbleStackView extends FrameLayout {
void onDragFinish(float x, float y, float velX, float velY) {
// TODO: Add fling to bottom to dismiss.
+ mIsDragging = false;
if (mIsExpanded || mIsExpansionAnimating) {
return;
@@ -797,6 +844,47 @@ public class BubbleStackView extends FrameLayout {
}
}
+ /**
+ * Animates in the flyout for the given bubble, if available, and then hides it after some time.
+ */
+ @VisibleForTesting
+ void animateInFlyoutForBubble(Bubble bubble) {
+ final CharSequence updateMessage = bubble.entry.getUpdateMessage(getContext());
+
+ // Show the message if one exists, and we're not expanded or animating expansion.
+ if (updateMessage != null && !isExpanded() && !mIsExpansionAnimating && !mIsDragging) {
+ final PointF stackPos = mStackAnimationController.getStackPosition();
+
+ mFlyoutText.setText(updateMessage);
+ mFlyout.measure(WRAP_CONTENT, WRAP_CONTENT);
+ mFlyout.post(() -> {
+ final boolean onLeft = mStackAnimationController.isStackOnLeftSide();
+ final float destinationX = onLeft
+ ? stackPos.x + mBubbleSize + mBubblePadding
+ : stackPos.x - mFlyout.getMeasuredWidth();
+
+ // Translate towards the stack slightly, then spring out from the stack.
+ mFlyout.setTranslationX(destinationX + (onLeft ? -mBubblePadding : mBubblePadding));
+ mFlyout.setTranslationY(stackPos.y);
+ mFlyout.setAlpha(0f);
+
+ mFlyout.setVisibility(VISIBLE);
+
+ mFlyout.animate().alpha(1f);
+ mFlyoutSpring.animateToFinalPosition(destinationX);
+
+ mFlyout.removeCallbacks(mHideFlyout);
+ mFlyout.postDelayed(mHideFlyout, FLYOUT_HIDE_AFTER);
+ });
+ }
+ }
+
+ /** Hide the flyout immediately and cancel any pending hide runnables. */
+ private void hideFlyoutImmediate() {
+ mFlyout.removeCallbacks(mHideFlyout);
+ mHideFlyout.run();
+ }
+
@Override
public void getBoundsOnScreen(Rect outRect) {
if (!mIsExpanded) {
@@ -806,6 +894,12 @@ public class BubbleStackView extends FrameLayout {
} else {
mBubbleContainer.getBoundsOnScreen(outRect);
}
+
+ if (mFlyout.getVisibility() == View.VISIBLE) {
+ final Rect flyoutBounds = new Rect();
+ mFlyout.getBoundsOnScreen(flyoutBounds);
+ outRect.union(flyoutBounds);
+ }
}
private int getStatusBarHeight() {
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
index a7170d0256e39..0d8cb6372c76c 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleTouchHandler.java
@@ -86,6 +86,7 @@ class BubbleTouchHandler implements View.OnTouchListener {
}
final boolean isStack = mStack.equals(mTouchedView);
+ final boolean isFlyout = mStack.getFlyoutView().equals(mTouchedView);
final float rawX = event.getRawX();
final float rawY = event.getRawY();
@@ -104,6 +105,8 @@ class BubbleTouchHandler implements View.OnTouchListener {
if (isStack) {
mViewPositionOnTouchDown.set(mStack.getStackPosition());
mStack.onDragStart();
+ } else if (isFlyout) {
+ // TODO(b/129768381): Make the flyout dismissable with a gesture.
} else {
mViewPositionOnTouchDown.set(
mTouchedView.getTranslationX(), mTouchedView.getTranslationY());
@@ -123,6 +126,8 @@ class BubbleTouchHandler implements View.OnTouchListener {
if (mMovedEnough) {
if (isStack) {
mStack.onDragged(viewX, viewY);
+ } else if (isFlyout) {
+ // TODO(b/129768381): Make the flyout dismissable with a gesture.
} else {
mStack.onBubbleDragged(mTouchedView, viewX, viewY);
}
@@ -141,6 +146,11 @@ class BubbleTouchHandler implements View.OnTouchListener {
trackMovement(event);
if (mInDismissTarget && isStack) {
mController.dismissStack(BubbleController.DISMISS_USER_GESTURE);
+ } else if (isFlyout) {
+ // TODO(b/129768381): Expand if tapped, dismiss if swiped away.
+ if (!mStack.isExpanded()) {
+ mStack.expandStack();
+ }
} else if (mMovedEnough) {
mVelocityTracker.computeCurrentVelocity(/* maxVelocity */ 1000);
final float velX = mVelocityTracker.getXVelocity();
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
index 3b9164d60c5cd..84b86bf9b69f6 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleView.java
@@ -27,7 +27,6 @@ import android.graphics.drawable.Icon;
import android.graphics.drawable.InsetDrawable;
import android.util.AttributeSet;
import android.widget.FrameLayout;
-import android.widget.TextView;
import com.android.internal.graphics.ColorUtils;
import com.android.systemui.Interpolators;
@@ -49,7 +48,6 @@ public class BubbleView extends FrameLayout {
private Context mContext;
private BadgedImageView mBadgedImageView;
- private TextView mMessageView;
private int mPadding;
private int mIconInset;
@@ -78,10 +76,7 @@ public class BubbleView extends FrameLayout {
@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);
+ mBadgedImageView = findViewById(R.id.bubble_image);
}
@Override
@@ -89,33 +84,6 @@ public class BubbleView extends FrameLayout {
super.onAttachedToWindow();
}
- @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.
*
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
index c395031294549..78c4fc17c6550 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/animation/StackAnimationController.java
@@ -157,6 +157,15 @@ public class StackAnimationController extends
return mStackPosition;
}
+ /** Whether the stack is on the left side of the screen. */
+ public boolean isStackOnLeftSide() {
+ if (mLayout != null) {
+ return mStackPosition.x - mIndividualBubbleSize / 2 < mLayout.getWidth() / 2;
+ } else {
+ return false;
+ }
+ }
+
/**
* Flings the stack starting with the given velocities, springing it to the nearest edge
* afterward.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index 4f4dcbc9ce922..f69356ea14a0b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -41,6 +41,7 @@ import android.os.SystemClock;
import android.service.notification.NotificationListenerService;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.view.View;
import android.widget.ImageView;
@@ -51,6 +52,7 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.StatusBarIcon;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ContrastColorUtil;
+import com.android.systemui.R;
import com.android.systemui.statusbar.InflationTask;
import com.android.systemui.statusbar.StatusBarIconView;
import com.android.systemui.statusbar.notification.InflationException;
@@ -392,6 +394,72 @@ public final class NotificationEntry {
return mCachedContrastColor;
}
+ /**
+ * Returns our best guess for the most relevant text summary of the latest update to this
+ * notification, based on its type. Returns null if there should not be an update message.
+ */
+ public CharSequence getUpdateMessage(Context context) {
+ final Notification underlyingNotif = notification.getNotification();
+ final Class extends Notification.Style> style = underlyingNotif.getNotificationStyle();
+
+ try {
+ if (Notification.BigTextStyle.class.equals(style)) {
+ // Return the big text, it is big so probably important. If it's not there use the
+ // normal text.
+ CharSequence bigText =
+ underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
+ return !TextUtils.isEmpty(bigText)
+ ? bigText
+ : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
+ } else if (Notification.MessagingStyle.class.equals(style)) {
+ final List messages =
+ Notification.MessagingStyle.Message.getMessagesFromBundleArray(
+ (Parcelable[]) underlyingNotif.extras.get(
+ Notification.EXTRA_MESSAGES));
+
+ final Notification.MessagingStyle.Message latestMessage =
+ Notification.MessagingStyle.findLatestIncomingMessage(messages);
+
+ if (latestMessage != null) {
+ final CharSequence personName = latestMessage.getSenderPerson() != null
+ ? latestMessage.getSenderPerson().getName()
+ : null;
+
+ // Prepend the sender name if available since group chats also use messaging
+ // style.
+ if (!TextUtils.isEmpty(personName)) {
+ return context.getResources().getString(
+ R.string.notification_summary_message_format,
+ personName,
+ latestMessage.getText());
+ } else {
+ return latestMessage.getText();
+ }
+ }
+ } else if (Notification.InboxStyle.class.equals(style)) {
+ CharSequence[] lines =
+ underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
+
+ // Return the last line since it should be the most recent.
+ if (lines != null && lines.length > 0) {
+ return lines[lines.length - 1];
+ }
+ } else if (Notification.MediaStyle.class.equals(style)) {
+ // Return nothing, media updates aren't typically useful as a text update.
+ return null;
+ } else {
+ // Default to text extra.
+ return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
+ }
+ } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
+ // No use crashing, we'll just return null and the caller will assume there's no update
+ // message.
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
/**
* Abort all existing inflation tasks
*/
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java
new file mode 100644
index 0000000000000..801308fc77daf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2019 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 org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+import android.view.View;
+import android.widget.TextView;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+public class BubbleStackViewTest extends SysuiTestCase {
+ private BubbleStackView mStackView;
+ @Mock private Bubble mBubble;
+ @Mock private NotificationEntry mNotifEntry;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ mStackView = new BubbleStackView(mContext, new BubbleData(), null);
+ mBubble.entry = mNotifEntry;
+ }
+
+ @Test
+ public void testAnimateInFlyoutForBubble() throws InterruptedException {
+ when(mNotifEntry.getUpdateMessage(any())).thenReturn("Test Flyout Message.");
+ mStackView.animateInFlyoutForBubble(mBubble);
+
+ // Wait for the fade in.
+ Thread.sleep(200);
+
+ // Flyout should be visible and showing our text.
+ assertEquals(1f, mStackView.findViewById(R.id.bubble_flyout).getAlpha(), .01f);
+ assertEquals("Test Flyout Message.",
+ ((TextView) mStackView.findViewById(R.id.bubble_flyout_text)).getText());
+
+ // Wait until it should have gone away.
+ Thread.sleep(BubbleStackView.FLYOUT_HIDE_AFTER + 200);
+
+ // Flyout should be gone.
+ assertEquals(View.GONE, mStackView.findViewById(R.id.bubble_flyout).getVisibility());
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
new file mode 100644
index 0000000000000..cca9f2834e93e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2019 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.statusbar.notification.collection;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.os.Bundle;
+import android.service.notification.StatusBarNotification;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+public class NotificationEntryTest extends SysuiTestCase {
+ @Mock
+ private StatusBarNotification mStatusBarNotification;
+ @Mock
+ private Notification mNotif;
+
+ private NotificationEntry mEntry;
+ private Bundle mExtras;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ when(mStatusBarNotification.getKey()).thenReturn("key");
+ when(mStatusBarNotification.getNotification()).thenReturn(mNotif);
+
+ mExtras = new Bundle();
+ mNotif.extras = mExtras;
+
+ mEntry = new NotificationEntry(mStatusBarNotification);
+ }
+
+ @Test
+ public void testGetUpdateMessage_default() {
+ final String msg = "Hello there!";
+ doReturn(Notification.Style.class).when(mNotif).getNotificationStyle();
+ mExtras.putCharSequence(Notification.EXTRA_TEXT, msg);
+ assertEquals(msg, mEntry.getUpdateMessage(mContext));
+ }
+
+ @Test
+ public void testGetUpdateMessage_bigText() {
+ final String msg = "A big hello there!";
+ doReturn(Notification.BigTextStyle.class).when(mNotif).getNotificationStyle();
+ mExtras.putCharSequence(Notification.EXTRA_TEXT, "A small hello there.");
+ mExtras.putCharSequence(Notification.EXTRA_BIG_TEXT, msg);
+
+ // Should be big text, not the small text.
+ assertEquals(msg, mEntry.getUpdateMessage(mContext));
+ }
+
+ @Test
+ public void testGetUpdateMessage_media() {
+ doReturn(Notification.MediaStyle.class).when(mNotif).getNotificationStyle();
+
+ // Media notifs don't get update messages.
+ assertNull(mEntry.getUpdateMessage(mContext));
+ }
+
+ @Test
+ public void testGetUpdateMessage_inboxStyle() {
+ doReturn(Notification.InboxStyle.class).when(mNotif).getNotificationStyle();
+ mExtras.putCharSequenceArray(
+ Notification.EXTRA_TEXT_LINES,
+ new CharSequence[]{
+ "How do you feel about tests?",
+ "They're okay, I guess.",
+ "I hate when they're flaky.",
+ "Really? I prefer them that way."});
+
+ // Should be the last one only.
+ assertEquals("Really? I prefer them that way.", mEntry.getUpdateMessage(mContext));
+ }
+
+ @Test
+ public void testGetUpdateMessage_messagingStyle() {
+ doReturn(Notification.MessagingStyle.class).when(mNotif).getNotificationStyle();
+ mExtras.putParcelableArray(
+ Notification.EXTRA_MESSAGES,
+ new Bundle[]{
+ new Notification.MessagingStyle.Message(
+ "Hello", 0, "Josh").toBundle(),
+ new Notification.MessagingStyle.Message(
+ "Oh, hello!", 0, "Mady").toBundle()});
+
+ // Should be the last one only.
+ assertEquals("Mady: Oh, hello!", mEntry.getUpdateMessage(mContext));
+ }
+}