From 614b1df084ddebdfb43eded2900d30d686725811 Mon Sep 17 00:00:00 2001 From: Joshua Tsuji Date: Tue, 26 Mar 2019 13:57:05 -0400 Subject: [PATCH] Adds the flyout view. This moves the view itself from BubbleView to BubbleStackView, since there will never be multiple flyouts and it simplifies layout. This also adds getUpdateMessage to NotificationEntry which is used to generate the flyout text. Test: atest SystemUITests Change-Id: Ief2fcfb2b12b927fdd68f737d49080335c884bef --- .../SystemUI/res/drawable/bubble_flyout.xml | 29 +++++ .../SystemUI/res/layout/bubble_flyout.xml | 33 +++++ packages/SystemUI/res/layout/bubble_view.xml | 8 -- packages/SystemUI/res/values/dimens.xml | 6 + packages/SystemUI/res/values/strings.xml | 4 +- .../systemui/bubbles/BubbleStackView.java | 96 +++++++++++++- .../systemui/bubbles/BubbleTouchHandler.java | 10 ++ .../android/systemui/bubbles/BubbleView.java | 34 +---- .../animation/StackAnimationController.java | 9 ++ .../collection/NotificationEntry.java | 68 ++++++++++ .../systemui/bubbles/BubbleStackViewTest.java | 74 +++++++++++ .../collection/NotificationEntryTest.java | 121 ++++++++++++++++++ 12 files changed, 449 insertions(+), 43 deletions(-) create mode 100644 packages/SystemUI/res/drawable/bubble_flyout.xml create mode 100644 packages/SystemUI/res/layout/bubble_flyout.xml create mode 100644 packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleStackViewTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryTest.java 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)); + } +}