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