Merge changes from topic "b215583339-AnimBigPic" into tm-qpr-dev

* changes:
  Animates drawables in Notifications
  Adds flag for Animated Big Picture notifications
This commit is contained in:
Alexander Roederer
2023-01-18 13:14:43 +00:00
committed by Android (Google) Code Review
15 changed files with 727 additions and 20 deletions

View File

@@ -102,6 +102,10 @@ object Flags {
val FILTER_UNSEEN_NOTIFS_ON_KEYGUARD =
unreleasedFlag(254647461, "filter_unseen_notifs_on_keyguard", teamfood = true)
// TODO(b/263414400): Tracking Bug
@JvmField
val NOTIFICATION_ANIMATE_BIG_PICTURE = unreleasedFlag(120, "notification_animate_big_picture")
// 200 - keyguard/lockscreen
// ** Flag retired **
// public static final BooleanFlag KEYGUARD_LAYOUT =

View File

@@ -79,6 +79,8 @@ import com.android.internal.widget.CallLayout;
import com.android.systemui.R;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.classifier.FalsingCollector;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
@@ -177,6 +179,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
private Optional<BubblesManager> mBubblesManagerOptional;
private MetricsLogger mMetricsLogger;
private FeatureFlags mFeatureFlags;
private int mIconTransformContentShift;
private int mMaxHeadsUpHeightBeforeN;
private int mMaxHeadsUpHeightBeforeP;
@@ -277,7 +280,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private boolean mChildIsExpanding;
private boolean mJustClicked;
private boolean mIconAnimationRunning;
private boolean mAnimationRunning;
private boolean mShowNoBackground;
private ExpandableNotificationRow mNotificationParent;
private OnExpandClickListener mOnExpandClickListener;
@@ -451,10 +454,26 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return mPublicLayout;
}
public void setIconAnimationRunning(boolean running) {
for (NotificationContentView l : mLayouts) {
setIconAnimationRunning(running, l);
/**
* Sets animations running in the layouts of this row, including public, private, and children.
* @param running whether the animations should be started running or stopped.
*/
public void setAnimationRunning(boolean running) {
// Sets animations running in the private/public layouts.
if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE)) {
for (NotificationContentView l : mLayouts) {
if (l != null) {
l.setContentAnimationRunning(running);
setIconAnimationRunning(running, l);
}
}
} else {
for (NotificationContentView l : mLayouts) {
setIconAnimationRunning(running, l);
}
}
// For groups summaries with children, we want to set the children containers
// animating as well.
if (mIsSummaryWithChildren) {
NotificationViewWrapper viewWrapper = mChildrenContainer.getNotificationViewWrapper();
if (viewWrapper != null) {
@@ -468,12 +487,18 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mChildrenContainer.getAttachedChildren();
for (int i = 0; i < notificationChildren.size(); i++) {
ExpandableNotificationRow child = notificationChildren.get(i);
child.setIconAnimationRunning(running);
child.setAnimationRunning(running);
}
}
mIconAnimationRunning = running;
mAnimationRunning = running;
}
/**
* Starts or stops animations of the icons in all potential content views (regardless of
* whether they're contracted, expanded, etc).
*
* @param running whether to start or stop the icon's animation.
*/
private void setIconAnimationRunning(boolean running, NotificationContentView layout) {
if (layout != null) {
View contractedChild = layout.getContractedChild();
@@ -485,16 +510,29 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
}
/**
* Starts or stops animations of the icon in the provided view's icon and right icon.
*
* @param running whether to start or stop the icon's animation.
* @param child the view with the icon to start or stop.
*/
private void setIconAnimationRunningForChild(boolean running, View child) {
if (child != null) {
ImageView icon = child.findViewById(com.android.internal.R.id.icon);
setIconRunning(icon, running);
setImageViewAnimationRunning(icon, running);
ImageView rightIcon = child.findViewById(com.android.internal.R.id.right_icon);
setIconRunning(rightIcon, running);
setImageViewAnimationRunning(rightIcon, running);
}
}
private void setIconRunning(ImageView imageView, boolean running) {
/**
* Starts or stops the animation of a provided image view if it's an AnimationDrawable or an
* AnimatedVectorDrawable.
*
* @param imageView the image view on which to start/stop animation.
* @param running whether to start or stop the view's animation.
*/
private void setImageViewAnimationRunning(ImageView imageView, boolean running) {
if (imageView != null) {
Drawable drawable = imageView.getDrawable();
if (drawable instanceof AnimationDrawable) {
@@ -561,8 +599,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mChildrenContainer.recreateNotificationHeader(mExpandClickListener, isConversation());
mChildrenContainer.onNotificationUpdated();
}
if (mIconAnimationRunning) {
setIconAnimationRunning(true);
if (mAnimationRunning) {
setAnimationRunning(true);
}
if (mLastChronometerRunning) {
setChronometerRunning(true);
@@ -1038,7 +1076,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
notifyHeightChanged(false /* needsAnimation */);
}
if (pinned) {
setIconAnimationRunning(true);
setAnimationRunning(true);
mExpandedWhenPinned = false;
} else if (mExpandedWhenPinned) {
setUserExpanded(true);
@@ -1627,6 +1665,11 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
);
}
/**
* Constructs an ExpandableNotificationRow.
* @param context context passed to image resolver
* @param attrs attributes used to initialize parent view
*/
public ExpandableNotificationRow(Context context, AttributeSet attrs) {
super(context, attrs);
mImageResolver = new NotificationInlineImageResolver(context,
@@ -1662,7 +1705,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
NotificationGutsManager gutsManager,
MetricsLogger metricsLogger,
SmartReplyConstants smartReplyConstants,
SmartReplyController smartReplyController) {
SmartReplyController smartReplyController,
FeatureFlags featureFlags) {
mEntry = entry;
mAppName = appName;
if (mMenuRow == null) {
@@ -1697,6 +1741,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mBubblesManagerOptional = bubblesManagerOptional;
mNotificationGutsManager = gutsManager;
mMetricsLogger = metricsLogger;
mFeatureFlags = featureFlags;
}
private void initDimens() {
@@ -3588,11 +3633,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
@VisibleForTesting
protected void setPrivateLayout(NotificationContentView privateLayout) {
mPrivateLayout = privateLayout;
mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
}
@VisibleForTesting
protected void setPublicLayout(NotificationContentView publicLayout) {
mPublicLayout = publicLayout;
mLayouts = new NotificationContentView[]{mPrivateLayout, mPublicLayout};
}
/**

View File

@@ -219,7 +219,8 @@ public class ExpandableNotificationRowController implements NotifViewController
mNotificationGutsManager,
mMetricsLogger,
mSmartReplyConstants,
mSmartReplyController
mSmartReplyController,
mFeatureFlags
);
mView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
if (mAllowLongPress) {

View File

@@ -184,6 +184,8 @@ public class NotificationContentView extends FrameLayout implements Notification
private boolean mRemoteInputVisible;
private int mUnrestrictedContentHeight;
private boolean mContentAnimating;
public NotificationContentView(Context context, AttributeSet attrs) {
super(context, attrs);
mHybridGroupManager = new HybridGroupManager(getContext());
@@ -2129,8 +2131,49 @@ public class NotificationContentView extends FrameLayout implements Notification
return false;
}
/**
* Starts and stops animations in the underlying views.
* Avoids restarting the animations by checking whether they're already running first.
* Return value is used for testing.
*
* @param running whether to start animations running, or stop them.
* @return true if the state of animations changed.
*/
public boolean setContentAnimationRunning(boolean running) {
boolean stateChangeRequired = (running != mContentAnimating);
if (stateChangeRequired) {
// Starts or stops the animations in the potential views.
if (mContractedWrapper != null) {
mContractedWrapper.setAnimationsRunning(running);
}
if (mExpandedWrapper != null) {
mExpandedWrapper.setAnimationsRunning(running);
}
if (mHeadsUpWrapper != null) {
mHeadsUpWrapper.setAnimationsRunning(running);
}
// Updates the state tracker.
mContentAnimating = running;
return true;
}
return false;
}
private static class RemoteInputViewData {
@Nullable RemoteInputView mView;
@Nullable RemoteInputViewController mController;
}
@VisibleForTesting
protected void setContractedWrapper(NotificationViewWrapper contractedWrapper) {
mContractedWrapper = contractedWrapper;
}
@VisibleForTesting
protected void setExpandedWrapper(NotificationViewWrapper expandedWrapper) {
mExpandedWrapper = expandedWrapper;
}
@VisibleForTesting
protected void setHeadsUpWrapper(NotificationViewWrapper headsUpWrapper) {
mHeadsUpWrapper = headsUpWrapper;
}
}

View File

@@ -18,11 +18,15 @@ package com.android.systemui.statusbar.notification.row.wrapper;
import android.app.Notification;
import android.content.Context;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.view.View;
import com.android.internal.R;
import com.android.internal.widget.BigPictureNotificationImageView;
import com.android.systemui.statusbar.notification.ImageTransformState;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -31,6 +35,8 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
*/
public class NotificationBigPictureTemplateViewWrapper extends NotificationTemplateViewWrapper {
private BigPictureNotificationImageView mImageView;
protected NotificationBigPictureTemplateViewWrapper(Context ctx, View view,
ExpandableNotificationRow row) {
super(ctx, view, row);
@@ -39,9 +45,14 @@ public class NotificationBigPictureTemplateViewWrapper extends NotificationTempl
@Override
public void onContentUpdated(ExpandableNotificationRow row) {
super.onContentUpdated(row);
resolveViews();
updateImageTag(row.getEntry().getSbn());
}
private void resolveViews() {
mImageView = mView.findViewById(R.id.big_picture);
}
private void updateImageTag(StatusBarNotification sbn) {
final Bundle extras = sbn.getNotification().extras;
Icon bigLargeIcon = extras.getParcelable(Notification.EXTRA_LARGE_ICON_BIG, Icon.class);
@@ -54,4 +65,25 @@ public class NotificationBigPictureTemplateViewWrapper extends NotificationTempl
mRightIcon.setTag(ImageTransformState.ICON_TAG, getLargeIcon(sbn.getNotification()));
}
}
/**
* Starts or stops the animations in any drawables contained in this BigPicture Notification.
*
* @param running Whether the animations should be set to run.
*/
@Override
public void setAnimationsRunning(boolean running) {
if (mImageView == null) {
return;
}
Drawable d = mImageView.getDrawable();
if (d instanceof AnimatedImageDrawable) {
AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d;
if (running) {
animatedImageDrawable.start();
} else {
animatedImageDrawable.stop();
}
}
}
}

View File

@@ -17,16 +17,20 @@
package com.android.systemui.statusbar.notification.row.wrapper
import android.content.Context
import android.graphics.drawable.AnimatedImageDrawable
import android.view.View
import android.view.ViewGroup
import com.android.internal.widget.CachingIconView
import com.android.internal.widget.ConversationLayout
import com.android.internal.widget.MessagingGroup
import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLinearLayout
import com.android.systemui.R
import com.android.systemui.statusbar.notification.NotificationFadeAware
import com.android.systemui.statusbar.notification.NotificationUtils
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform
import com.android.systemui.util.children
/**
* Wraps a notification containing a conversation template
@@ -49,6 +53,7 @@ class NotificationConversationTemplateViewWrapper constructor(
private lateinit var expandBtn: View
private lateinit var expandBtnContainer: View
private lateinit var imageMessageContainer: ViewGroup
private lateinit var messageContainers: ArrayList<MessagingGroup>
private lateinit var messagingLinearLayout: MessagingLinearLayout
private lateinit var conversationTitleView: View
private lateinit var importanceRing: View
@@ -60,6 +65,7 @@ class NotificationConversationTemplateViewWrapper constructor(
private fun resolveViews() {
messagingLinearLayout = conversationLayout.messagingLinearLayout
imageMessageContainer = conversationLayout.imageMessageContainer
messageContainers = conversationLayout.messagingGroups
with(conversationLayout) {
conversationIconContainer =
requireViewById(com.android.internal.R.id.conversation_icon_container)
@@ -146,4 +152,26 @@ class NotificationConversationTemplateViewWrapper constructor(
NotificationFadeAware.setLayerTypeForFaded(expandBtn, faded)
NotificationFadeAware.setLayerTypeForFaded(conversationIconContainer, faded)
}
// Starts or stops the animations in any drawables contained in this Conversation Notification.
override fun setAnimationsRunning(running: Boolean) {
// We apply to both the child message containers in a conversation group,
// and the top level image message container.
val containers = messageContainers.asSequence().map { it.messageContainer } +
sequenceOf(imageMessageContainer)
val drawables =
containers
.flatMap { it.children }
.mapNotNull { child ->
(child as? MessagingImageMessage)?.let { imageMessage ->
imageMessage.drawable as? AnimatedImageDrawable
}
}
drawables.toSet().forEach {
when {
running -> it.start()
!running -> it.stop()
}
}
}
}

View File

@@ -17,9 +17,13 @@
package com.android.systemui.statusbar.notification.row.wrapper;
import android.content.Context;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewGroup;
import com.android.internal.widget.MessagingGroup;
import com.android.internal.widget.MessagingImageMessage;
import com.android.internal.widget.MessagingLayout;
import com.android.internal.widget.MessagingLinearLayout;
import com.android.systemui.R;
@@ -127,4 +131,40 @@ public class NotificationMessagingTemplateViewWrapper extends NotificationTempla
}
return super.getMinLayoutHeight();
}
/**
* Starts or stops the animations in any drawables contained in this Messaging Notification.
*
* @param running Whether the animations should be set to run.
*/
@Override
public void setAnimationsRunning(boolean running) {
if (mMessagingLayout == null) {
return;
}
for (MessagingGroup group : mMessagingLayout.getMessagingGroups()) {
for (int i = 0; i < group.getMessageContainer().getChildCount(); i++) {
View view = group.getMessageContainer().getChildAt(i);
// We only need to set animations in MessagingImageMessages.
if (!(view instanceof MessagingImageMessage)) {
continue;
}
MessagingImageMessage imageMessage =
(com.android.internal.widget.MessagingImageMessage) view;
// If the drawable isn't an AnimatedImageDrawable, we can't set it to animate.
Drawable d = imageMessage.getDrawable();
if (!(d instanceof AnimatedImageDrawable)) {
continue;
}
AnimatedImageDrawable animatedImageDrawable = (AnimatedImageDrawable) d;
if (running) {
animatedImageDrawable.start();
} else {
animatedImageDrawable.stop();
}
}
}
}
}

View File

@@ -403,4 +403,12 @@ public abstract class NotificationViewWrapper implements TransformableView {
NotificationFadeAware.setLayerTypeForFaded(getIcon(), faded);
NotificationFadeAware.setLayerTypeForFaded(getExpandButton(), faded);
}
/**
* Starts or stops the animations in any drawables contained in this Notification.
*
* @param running Whether the animations should be set to run.
*/
public void setAnimationsRunning(boolean running) {
}
}

View File

@@ -3132,7 +3132,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
private void updateAnimationState(boolean running, View child) {
if (child instanceof ExpandableNotificationRow) {
ExpandableNotificationRow row = (ExpandableNotificationRow) child;
row.setIconAnimationRunning(running);
row.setAnimationRunning(running);
}
}

View File

@@ -44,16 +44,23 @@ import static org.mockito.Mockito.when;
import android.app.Notification;
import android.app.NotificationChannel;
import android.graphics.Color;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.util.DisplayMetrics;
import android.view.View;
import android.widget.ImageView;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
import com.android.internal.widget.CachingIconView;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FakeFeatureFlags;
import com.android.systemui.flags.Flags;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
@@ -61,6 +68,7 @@ import com.android.systemui.statusbar.notification.FeedbackIcon;
import com.android.systemui.statusbar.notification.SourceType;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.ExpandableView.OnHeightChangedListener;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
import org.junit.Assert;
@@ -72,6 +80,7 @@ import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.Arrays;
import java.util.List;
@SmallTest
@@ -96,6 +105,9 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
mDependency,
TestableLooper.get(this));
mNotificationTestHelper.setDefaultInflationFlags(FLAG_CONTENT_VIEW_ALL);
FakeFeatureFlags fakeFeatureFlags = new FakeFeatureFlags();
fakeFeatureFlags.set(Flags.NOTIFICATION_ANIMATE_BIG_PICTURE, true);
mNotificationTestHelper.setFeatureFlags(fakeFeatureFlags);
// create a standard private notification row
Notification normalNotif = mNotificationTestHelper.createNotification();
normalNotif.publicVersion = null;
@@ -559,4 +571,123 @@ public class ExpandableNotificationRowTest extends SysuiTestCase {
Assert.assertEquals(1f, mGroupRow.getBottomRoundness(), 0.001f);
Assert.assertEquals(1f, mGroupRow.getChildrenContainer().getBottomRoundness(), 0.001f);
}
@Test
public void testSetContentAnimationRunning_Run() throws Exception {
// Create views for the notification row.
NotificationContentView publicLayout = mock(NotificationContentView.class);
mNotifRow.setPublicLayout(publicLayout);
NotificationContentView privateLayout = mock(NotificationContentView.class);
mNotifRow.setPrivateLayout(privateLayout);
mNotifRow.setAnimationRunning(true);
verify(publicLayout, times(1)).setContentAnimationRunning(true);
verify(privateLayout, times(1)).setContentAnimationRunning(true);
}
@Test
public void testSetContentAnimationRunning_Stop() {
// Create views for the notification row.
NotificationContentView publicLayout = mock(NotificationContentView.class);
mNotifRow.setPublicLayout(publicLayout);
NotificationContentView privateLayout = mock(NotificationContentView.class);
mNotifRow.setPrivateLayout(privateLayout);
mNotifRow.setAnimationRunning(false);
verify(publicLayout, times(1)).setContentAnimationRunning(false);
verify(privateLayout, times(1)).setContentAnimationRunning(false);
}
@Test
public void testSetContentAnimationRunningInGroupChild_Run() {
// Creates parent views on mGroupRow.
NotificationContentView publicParentLayout = mock(NotificationContentView.class);
mGroupRow.setPublicLayout(publicParentLayout);
NotificationContentView privateParentLayout = mock(NotificationContentView.class);
mGroupRow.setPrivateLayout(privateParentLayout);
// Create child views on mNotifRow.
NotificationContentView publicChildLayout = mock(NotificationContentView.class);
mNotifRow.setPublicLayout(publicChildLayout);
NotificationContentView privateChildLayout = mock(NotificationContentView.class);
mNotifRow.setPrivateLayout(privateChildLayout);
when(mNotifRow.isGroupExpanded()).thenReturn(true);
setMockChildrenContainer(mGroupRow, mNotifRow);
mGroupRow.setAnimationRunning(true);
verify(publicParentLayout, times(1)).setContentAnimationRunning(true);
verify(privateParentLayout, times(1)).setContentAnimationRunning(true);
// The child layouts should be started too.
verify(publicChildLayout, times(1)).setContentAnimationRunning(true);
verify(privateChildLayout, times(1)).setContentAnimationRunning(true);
}
@Test
public void testSetIconAnimationRunningGroup_Run() {
// Create views for a group row.
NotificationContentView publicParentLayout = mock(NotificationContentView.class);
mGroupRow.setPublicLayout(publicParentLayout);
NotificationContentView privateParentLayout = mock(NotificationContentView.class);
mGroupRow.setPrivateLayout(privateParentLayout);
when(mGroupRow.isGroupExpanded()).thenReturn(true);
// Sets up mNotifRow as a child ExpandableNotificationRow.
NotificationContentView publicChildLayout = mock(NotificationContentView.class);
mNotifRow.setPublicLayout(publicChildLayout);
NotificationContentView privateChildLayout = mock(NotificationContentView.class);
mNotifRow.setPrivateLayout(privateChildLayout);
when(mNotifRow.isGroupExpanded()).thenReturn(true);
NotificationChildrenContainer mockContainer =
setMockChildrenContainer(mGroupRow, mNotifRow);
// Mock the children view wrappers, and give them each an icon.
NotificationViewWrapper mockViewWrapper = mock(NotificationViewWrapper.class);
when(mockContainer.getNotificationViewWrapper()).thenReturn(mockViewWrapper);
CachingIconView mockIcon = mock(CachingIconView.class);
when(mockViewWrapper.getIcon()).thenReturn(mockIcon);
NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class);
when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper);
CachingIconView mockLowPriorityIcon = mock(CachingIconView.class);
when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon);
// Give the icon image views drawables, so we can make sure they animate.
// We use both AnimationDrawables and AnimatedVectorDrawables to ensure both work.
AnimationDrawable drawable = mock(AnimationDrawable.class);
AnimatedVectorDrawable vectorDrawable = mock(AnimatedVectorDrawable.class);
setDrawableIconsInImageView(mockIcon, drawable, vectorDrawable);
AnimationDrawable lowPriDrawable = mock(AnimationDrawable.class);
AnimatedVectorDrawable lowPriVectorDrawable = mock(AnimatedVectorDrawable.class);
setDrawableIconsInImageView(mockLowPriorityIcon, lowPriDrawable, lowPriVectorDrawable);
mGroupRow.setAnimationRunning(true);
verify(drawable, times(1)).start();
verify(vectorDrawable, times(1)).start();
verify(lowPriDrawable, times(1)).start();
verify(lowPriVectorDrawable, times(1)).start();
}
private void setDrawableIconsInImageView(CachingIconView icon, Drawable iconDrawable,
Drawable rightIconDrawable) {
ImageView iconView = mock(ImageView.class);
when(icon.findViewById(com.android.internal.R.id.icon)).thenReturn(iconView);
when(iconView.getDrawable()).thenReturn(iconDrawable);
ImageView rightIconView = mock(ImageView.class);
when(icon.findViewById(com.android.internal.R.id.right_icon)).thenReturn(rightIconView);
when(rightIconView.getDrawable()).thenReturn(rightIconDrawable);
}
private NotificationChildrenContainer setMockChildrenContainer(
ExpandableNotificationRow parentRow, ExpandableNotificationRow childRow) {
List<ExpandableNotificationRow> rowList = Arrays.asList(childRow);
NotificationChildrenContainer mockContainer = mock(NotificationChildrenContainer.class);
when(mockContainer.getNotificationChildCount()).thenReturn(1);
when(mockContainer.getAttachedChildren()).thenReturn(rowList);
parentRow.setChildrenContainer(mockContainer);
return mockContainer;
}
}

View File

@@ -34,9 +34,12 @@ import com.android.systemui.media.dialog.MediaOutputDialogFactory
import com.android.systemui.statusbar.notification.FeedbackIcon
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -44,6 +47,7 @@ import org.mockito.Mock
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations.initMocks
@@ -305,6 +309,86 @@ class NotificationContentViewTest : SysuiTestCase() {
assertEquals(0, getMarginBottom(actionListMarginTarget))
}
@Test
fun onSetAnimationRunning() {
// Given: contractedWrapper, enpandedWrapper, and headsUpWrapper being set
val mockContracted = mock<NotificationViewWrapper>()
val mockExpanded = mock<NotificationViewWrapper>()
val mockHeadsUp = mock<NotificationViewWrapper>()
view.setContractedWrapper(mockContracted)
view.setExpandedWrapper(mockExpanded)
view.setHeadsUpWrapper(mockHeadsUp)
// When: we set content animation running.
assertTrue(view.setContentAnimationRunning(true))
// Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
// called on them.
verify(mockContracted, times(1)).setAnimationsRunning(true)
verify(mockExpanded, times(1)).setAnimationsRunning(true)
verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
// When: we set content animation running true _again_.
assertFalse(view.setContentAnimationRunning(true))
// Then: the children should not have setAnimationRunning called on them again.
// Verify counts number of calls so far on the object, so these still register as 1.
verify(mockContracted, times(1)).setAnimationsRunning(true)
verify(mockExpanded, times(1)).setAnimationsRunning(true)
verify(mockHeadsUp, times(1)).setAnimationsRunning(true)
}
@Test
fun onSetAnimationStopped() {
// Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
val mockContracted = mock<NotificationViewWrapper>()
val mockExpanded = mock<NotificationViewWrapper>()
val mockHeadsUp = mock<NotificationViewWrapper>()
view.setContractedWrapper(mockContracted)
view.setExpandedWrapper(mockExpanded)
view.setHeadsUpWrapper(mockHeadsUp)
// When: we set content animation running.
assertTrue(view.setContentAnimationRunning(true))
// Then: contractedChild, expandedChild, and headsUpChild should have setAnimationsRunning
// called on them.
verify(mockContracted).setAnimationsRunning(true)
verify(mockExpanded).setAnimationsRunning(true)
verify(mockHeadsUp).setAnimationsRunning(true)
// When: we set content animation running false, the state changes, so the function
// returns true.
assertTrue(view.setContentAnimationRunning(false))
// Then: the children have their animations stopped.
verify(mockContracted).setAnimationsRunning(false)
verify(mockExpanded).setAnimationsRunning(false)
verify(mockHeadsUp).setAnimationsRunning(false)
}
@Test
fun onSetAnimationInitStopped() {
// Given: contractedWrapper, expandedWrapper, and headsUpWrapper being set
val mockContracted = mock<NotificationViewWrapper>()
val mockExpanded = mock<NotificationViewWrapper>()
val mockHeadsUp = mock<NotificationViewWrapper>()
view.setContractedWrapper(mockContracted)
view.setExpandedWrapper(mockExpanded)
view.setHeadsUpWrapper(mockHeadsUp)
// When: we try to stop the animations before they've been started.
assertFalse(view.setContentAnimationRunning(false))
// Then: the children should not have setAnimationRunning called on them again.
verify(mockContracted, never()).setAnimationsRunning(false)
verify(mockExpanded, never()).setAnimationsRunning(false)
verify(mockHeadsUp, never()).setAnimationsRunning(false)
}
private fun createMockContainingNotification(notificationEntry: NotificationEntry) =
mock<ExpandableNotificationRow>().apply {
whenever(this.entry).thenReturn(notificationEntry)

View File

@@ -53,6 +53,7 @@ import com.android.internal.logging.UiEventLogger;
import com.android.systemui.TestableDependency;
import com.android.systemui.classifier.FalsingCollectorFake;
import com.android.systemui.classifier.FalsingManagerFake;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.media.controls.util.MediaFeatureFlag;
import com.android.systemui.media.dialog.MediaOutputDialogFactory;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -130,6 +131,7 @@ public class NotificationTestHelper {
public final OnUserInteractionCallback mOnUserInteractionCallback;
public final Runnable mFutureDismissalRunnable;
private @InflationFlag int mDefaultInflationFlags;
private FeatureFlags mFeatureFlags;
public NotificationTestHelper(
Context context,
@@ -193,12 +195,17 @@ public class NotificationTestHelper {
mFutureDismissalRunnable = mock(Runnable.class);
when(mOnUserInteractionCallback.registerFutureDismissal(any(), anyInt()))
.thenReturn(mFutureDismissalRunnable);
mFeatureFlags = mock(FeatureFlags.class);
}
public void setDefaultInflationFlags(@InflationFlag int defaultInflationFlags) {
mDefaultInflationFlags = defaultInflationFlags;
}
public void setFeatureFlags(FeatureFlags featureFlags) {
mFeatureFlags = featureFlags;
}
public ExpandableNotificationRowLogger getMockLogger() {
return mMockLogger;
}
@@ -561,7 +568,8 @@ public class NotificationTestHelper {
mock(NotificationGutsManager.class),
mock(MetricsLogger.class),
mock(SmartReplyConstants.class),
mock(SmartReplyController.class));
mock(SmartReplyController.class),
mFeatureFlags);
row.setAboveShelfChangedListener(aboveShelf -> { });
mBindStage.getStageParams(entry).requireContentViews(extraInflationFlags);

View File

@@ -16,11 +16,12 @@
package com.android.systemui.statusbar.notification.row.wrapper;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;
import android.app.Notification;
import android.content.Context;
import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.testing.AndroidTestingRunner;
@@ -28,11 +29,11 @@ import android.testing.TestableLooper;
import android.testing.TestableLooper.RunWithLooper;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
import com.android.internal.widget.BigPictureNotificationImageView;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
@@ -73,4 +74,38 @@ public class NotificationBigPictureTemplateViewWrapperTest extends SysuiTestCase
Notification.EXTRA_LARGE_ICON_BIG, new Bundle());
wrapper.onContentUpdated(mRow);
}
@Test
public void setAnimationsRunning_Run() {
BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture);
AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class);
assertNotNull(imageView);
imageView.setImageDrawable(mockDrawable);
NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext,
mView, mRow);
// Required to re-initialize the imageView to the imageView created above.
wrapper.onContentUpdated(mRow);
wrapper.setAnimationsRunning(true);
verify(mockDrawable).start();
}
@Test
public void setAnimationsRunning_Stop() {
BigPictureNotificationImageView imageView = mView.findViewById(R.id.big_picture);
AnimatedImageDrawable mockDrawable = mock(AnimatedImageDrawable.class);
assertNotNull(imageView);
imageView.setImageDrawable(mockDrawable);
NotificationViewWrapper wrapper = new NotificationBigPictureTemplateViewWrapper(mContext,
mView, mRow);
// Required to re-initialize the imageView to the imageView created above.
wrapper.onContentUpdated(mRow);
wrapper.setAnimationsRunning(false);
verify(mockDrawable).stop();
}
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (C) 2022 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.row.wrapper
import android.graphics.drawable.AnimatedImageDrawable
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.R
import com.android.internal.widget.CachingIconView
import com.android.internal.widget.ConversationLayout
import com.android.internal.widget.MessagingGroup
import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLinearLayout
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationTestHelper
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class NotificationConversationTemplateViewWrapperTest : SysuiTestCase() {
private lateinit var mRow: ExpandableNotificationRow
private lateinit var helper: NotificationTestHelper
@Before
fun setUp() {
allowTestableLooperAsMainThread()
helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
mRow = helper.createRow()
}
@Test
fun setAnimationsRunning_Run() {
// Creates a mocked out NotificationEntry of ConversationLayout type,
// with a mock imageMessage.drawable embedded in its MessagingImageMessages
// (both top level, and in a group).
val mockDrawable = mock<AnimatedImageDrawable>()
val mockDrawable2 = mock<AnimatedImageDrawable>()
val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2)
val wrapper: NotificationViewWrapper =
NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow)
wrapper.onContentUpdated(mRow)
wrapper.setAnimationsRunning(true)
// Verifies that each AnimatedImageDrawable is started animating.
verify(mockDrawable).start()
verify(mockDrawable2).start()
}
@Test
fun setAnimationsRunning_Stop() {
// Creates a mocked out NotificationEntry of ConversationLayout type,
// with a mock imageMessage.drawable embedded in its MessagingImageMessages
// (both top level, and in a group).
val mockDrawable = mock<AnimatedImageDrawable>()
val mockDrawable2 = mock<AnimatedImageDrawable>()
val mockLayoutView: View = fakeConversationLayout(mockDrawable, mockDrawable2)
val wrapper: NotificationViewWrapper =
NotificationConversationTemplateViewWrapper(mContext, mockLayoutView, mRow)
wrapper.onContentUpdated(mRow)
wrapper.setAnimationsRunning(false)
// Verifies that each AnimatedImageDrawable is started animating.
verify(mockDrawable).stop()
verify(mockDrawable2).stop()
}
private fun fakeConversationLayout(
mockDrawableGroupMessage: AnimatedImageDrawable,
mockDrawableImageMessage: AnimatedImageDrawable
): View {
val mockMessagingImageMessage: MessagingImageMessage =
mock<MessagingImageMessage>().apply {
whenever(drawable).thenReturn(mockDrawableImageMessage)
}
val mockImageMessageContainer: MessagingLinearLayout =
mock<MessagingLinearLayout>().apply {
whenever(childCount).thenReturn(1)
whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage)
}
val mockMessagingImageMessageForGroup: MessagingImageMessage =
mock<MessagingImageMessage>().apply {
whenever(drawable).thenReturn(mockDrawableGroupMessage)
}
val mockMessageContainer: MessagingLinearLayout =
mock<MessagingLinearLayout>().apply {
whenever(childCount).thenReturn(1)
whenever(getChildAt(any())).thenReturn(mockMessagingImageMessageForGroup)
}
val mockGroup: MessagingGroup =
mock<MessagingGroup>().apply {
whenever(messageContainer).thenReturn(mockMessageContainer)
}
val mockView: View =
mock<ConversationLayout>().apply {
whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup)))
whenever(imageMessageContainer).thenReturn(mockImageMessageContainer)
whenever(messagingLinearLayout).thenReturn(mockMessageContainer)
// These must be mocked as they're required to be nonnull.
whenever(requireViewById<View>(R.id.conversation_icon_container)).thenReturn(mock())
whenever(requireViewById<CachingIconView>(R.id.conversation_icon))
.thenReturn(mock())
whenever(findViewById<CachingIconView>(R.id.icon)).thenReturn(mock())
whenever(requireViewById<View>(R.id.conversation_icon_badge_bg)).thenReturn(mock())
whenever(requireViewById<View>(R.id.expand_button)).thenReturn(mock())
whenever(requireViewById<View>(R.id.expand_button_container)).thenReturn(mock())
whenever(requireViewById<View>(R.id.conversation_icon_badge_ring))
.thenReturn(mock())
whenever(requireViewById<View>(R.id.app_name_text)).thenReturn(mock())
whenever(requireViewById<View>(R.id.conversation_text)).thenReturn(mock())
}
return mockView
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2022 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.row.wrapper
import android.graphics.drawable.AnimatedImageDrawable
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import android.view.View
import androidx.test.filters.SmallTest
import com.android.internal.widget.MessagingGroup
import com.android.internal.widget.MessagingImageMessage
import com.android.internal.widget.MessagingLayout
import com.android.internal.widget.MessagingLinearLayout
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.NotificationTestHelper
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@RunWithLooper
class NotificationMessagingTemplateViewWrapperTest : SysuiTestCase() {
private lateinit var mRow: ExpandableNotificationRow
private lateinit var helper: NotificationTestHelper
@Before
fun setUp() {
allowTestableLooperAsMainThread()
helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this))
mRow = helper.createRow()
}
@Test
fun setAnimationsRunning_Run() {
// Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type,
// with a mock imageMessage.drawable embedded in its MessagingImageMessage.
val mockDrawable = mock<AnimatedImageDrawable>()
val mockLayoutView: View = fakeMessagingLayout(mockDrawable)
val wrapper: NotificationViewWrapper =
NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow)
wrapper.setAnimationsRunning(true)
// Verifies that each AnimatedImageDrawable is started animating.
verify(mockDrawable).start()
}
@Test
fun setAnimationsRunning_Stop() {
// Creates a mocked out NotificationEntry of MessagingLayout/MessagingStyle type,
// with a mock imageMessage.drawable embedded in its MessagingImageMessage.
val mockDrawable = mock<AnimatedImageDrawable>()
val mockLayoutView: View = fakeMessagingLayout(mockDrawable)
val wrapper: NotificationViewWrapper =
NotificationMessagingTemplateViewWrapper(mContext, mockLayoutView, mRow)
wrapper.setAnimationsRunning(false)
// Verifies that each AnimatedImageDrawable is started animating.
verify(mockDrawable).stop()
}
private fun fakeMessagingLayout(mockDrawable: AnimatedImageDrawable): View {
val mockMessagingImageMessage: MessagingImageMessage =
mock<MessagingImageMessage>().apply { whenever(drawable).thenReturn(mockDrawable) }
val mockMessageContainer: MessagingLinearLayout =
mock<MessagingLinearLayout>().apply {
whenever(childCount).thenReturn(1)
whenever(getChildAt(any())).thenReturn(mockMessagingImageMessage)
}
val mockGroup: MessagingGroup =
mock<MessagingGroup>().apply {
whenever(messageContainer).thenReturn(mockMessageContainer)
}
val mockView: View =
mock<MessagingLayout>().apply {
whenever(messagingGroups).thenReturn(ArrayList<MessagingGroup>(listOf(mockGroup)))
}
return mockView
}
}