import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin;
import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem;
import com.android.systemui.statusbar.NotificationGuts.GutsContent;
+import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.HybridNotificationView;
import com.android.systemui.statusbar.notification.NotificationInflater;
import com.android.systemui.statusbar.notification.NotificationUtils;
private boolean mIsSystemChildExpanded;
private boolean mIsPinned;
private FalsingManager mFalsingManager;
+ private AboveShelfChangedListener mAboveShelfChangedListener;
private HeadsUpManager mHeadsUpManager;
private boolean mJustClicked;
expandedIcon.setStaticDrawableColor(color);
}
+ public void setAboveShelfChangedListener(AboveShelfChangedListener aboveShelfChangedListener) {
+ mAboveShelfChangedListener = aboveShelfChangedListener;
+ }
+
@Override
public boolean isDimmable() {
if (!getShowingLayout().isDimmable()) {
}
public void setHeadsUp(boolean isHeadsUp) {
+ boolean wasAboveShelf = isAboveShelf();
int intrinsicBefore = getIntrinsicHeight();
mIsHeadsUp = isHeadsUp;
mPrivateLayout.setHeadsUp(isHeadsUp);
}
if (isHeadsUp) {
setAboveShelf(true);
+ } else if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
}
}
*/
public void setPinned(boolean pinned) {
int intrinsicHeight = getIntrinsicHeight();
+ boolean wasAboveShelf = isAboveShelf();
mIsPinned = pinned;
if (intrinsicHeight != getIntrinsicHeight()) {
notifyHeightChanged(false /* needsAnimation */);
setUserExpanded(true);
}
setChronometerRunning(mLastChronometerRunning);
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
}
public boolean isPinned() {
}
public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {
+ boolean wasAboveShelf = isAboveShelf();
mHeadsupDisappearRunning = headsUpAnimatingAway;
mPrivateLayout.setHeadsUpAnimatingAway(headsUpAnimatingAway);
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
}
/**
*/
public void setOnKeyguard(boolean onKeyguard) {
if (onKeyguard != mOnKeyguard) {
+ boolean wasAboveShelf = isAboveShelf();
final boolean wasExpanded = isExpanded();
mOnKeyguard = onKeyguard;
onExpansionChanged(false /* userAction */, wasExpanded);
}
notifyHeightChanged(false /* needsAnimation */);
}
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
}
}
}
public void setAboveShelf(boolean aboveShelf) {
+ boolean wasAboveShelf = isAboveShelf();
mAboveShelf = aboveShelf;
+ if (isAboveShelf() != wasAboveShelf) {
+ mAboveShelfChangedListener.onAboveShelfStateChanged(!wasAboveShelf);
+ }
}
public static class NotificationViewState extends ExpandableViewState {
mShelfState.notGoneIndex = Math.min(mShelfState.notGoneIndex, mNotGoneIndex);
}
mShelfState.hasItemsInStableShelf = lastViewState.inShelf;
- mShelfState.hidden = !mAmbientState.isShadeExpanded();
+ mShelfState.hidden = !mAmbientState.isShadeExpanded()
+ || mAmbientState.isQsCustomizerShowing();
mShelfState.maxShelfEnd = maxShelfEnd;
} else {
mShelfState.hidden = true;
--- /dev/null
+/*
+ * Copyright (C) 2017 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;
+
+/**
+ * A listener for when the above shelf state of notification changes
+ */
+public interface AboveShelfChangedListener {
+
+ /**
+ * Notifies a listener that the above shelf state changed
+ */
+ void onAboveShelfStateChanged(boolean aboveShelf);
+}
--- /dev/null
+/*
+ * Copyright (C) 2017 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;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+
+/**
+ * An observer that listens to the above shelf state and can notify listeners
+ */
+public class AboveShelfObserver implements AboveShelfChangedListener {
+
+ private final ViewGroup mHostLayout;
+ private boolean mHasViewsAboveShelf = false;
+ private HasViewAboveShelfChangedListener mListener;
+
+ public AboveShelfObserver(ViewGroup hostLayout) {
+ mHostLayout = hostLayout;
+ }
+
+ public void setListener(HasViewAboveShelfChangedListener listener) {
+ mListener = listener;
+ }
+
+ @Override
+ public void onAboveShelfStateChanged(boolean aboveShelf) {
+ boolean hasViewsAboveShelf = aboveShelf;
+ if (!hasViewsAboveShelf && mHostLayout != null) {
+ int n = mHostLayout.getChildCount();
+ for (int i = 0; i < n; i++) {
+ View child = mHostLayout.getChildAt(i);
+ if (child instanceof ExpandableNotificationRow) {
+ if (((ExpandableNotificationRow) child).isAboveShelf()) {
+ hasViewsAboveShelf = true;
+ break;
+ }
+ }
+ }
+ }
+ if (mHasViewsAboveShelf != hasViewsAboveShelf) {
+ mHasViewsAboveShelf = hasViewsAboveShelf;
+ if (mListener != null) {
+ mListener.onHasViewsAboveShelfChanged(hasViewsAboveShelf);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ boolean hasViewsAboveShelf() {
+ return mHasViewsAboveShelf;
+ }
+
+ public interface HasViewAboveShelfChangedListener {
+ void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf);
+ }
+}
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.statusbar.NotificationData.Entry;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
-import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
+import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
/**
* The container with notification stack scroller and quick settings inside.
*/
public class NotificationsQuickSettingsContainer extends FrameLayout
- implements OnInflateListener, FragmentListener, OnHeadsUpChangedListener {
+ implements OnInflateListener, FragmentListener,
+ AboveShelfObserver.HasViewAboveShelfChangedListener {
private FrameLayout mQsFrame;
private View mUserSwitcher;
- private View mStackScroller;
+ private NotificationStackScrollLayout mStackScroller;
private View mKeyguardStatusBar;
private boolean mInflated;
private boolean mQsExpanded;
private int mBottomPadding;
private int mStackScrollerMargin;
- private boolean mHeadsUp;
- private HeadsUpManager mHeadsUpManager;
+ private boolean mHasViewsAboveShelf;
public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
super(context, attrs);
protected void onAttachedToWindow() {
super.onAttachedToWindow();
FragmentHostManager.get(this).addTagListener(QS.TAG, this);
- mHeadsUpManager = SysUiServiceProvider.getComponent(getContext(), StatusBar.class)
- .mHeadsUpManager;
- mHeadsUpManager.addListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
FragmentHostManager.get(this).removeTagListener(QS.TAG, this);
- mHeadsUpManager.removeListener(this);
}
@Override
boolean userSwitcherVisible = mInflated && mUserSwitcher.getVisibility() == View.VISIBLE;
boolean statusBarVisible = mKeyguardStatusBar.getVisibility() == View.VISIBLE;
- final boolean qsBottom = mHeadsUp;
+ final boolean qsBottom = mHasViewsAboveShelf;
View stackQsTop = qsBottom ? mStackScroller : mQsFrame;
View stackQsBottom = !qsBottom ? mStackScroller : mQsFrame;
// Invert the order of the scroll view and user switcher such that the notifications receive
setPadding(0, 0, 0, mBottomPadding);
setBottomMargin(mStackScroller, mStackScrollerMargin);
}
-
+ mStackScroller.setQsCustomizerShowing(isShowing);
}
private void setBottomMargin(View v, int bottomMargin) {
}
@Override
- public void onHeadsUpStateChanged(Entry entry, boolean isHeadsUp) {
- boolean hasHeadsUp = mHeadsUpManager.getAllEntries().size() != 0;
- if (mHeadsUp == hasHeadsUp) {
- return;
- }
- mHeadsUp = hasHeadsUp;
+ public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
+ mHasViewsAboveShelf = hasViewsAboveShelf;
invalidate();
}
}
import com.android.systemui.statusbar.ScrimView;
import com.android.systemui.statusbar.SignalClusterView;
import com.android.systemui.statusbar.StatusBarState;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.RowInflaterTask;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
R.id.notification_stack_scroller);
mNotificationPanel.setStatusBar(this);
mNotificationPanel.setGroupManager(mGroupManager);
+ mAboveShelfObserver = new AboveShelfObserver(mStackScroller);
+ mAboveShelfObserver.setListener(mStatusBarWindow.findViewById(
+ R.id.notification_container_parent));
mKeyguardStatusBar = (KeyguardStatusBarView) mStatusBarWindow.findViewById(R.id.keyguard_header);
mNotificationIconAreaController = SystemUIFactory.getInstance()
// for heads up notifications
protected HeadsUpManager mHeadsUpManager;
+ private AboveShelfObserver mAboveShelfObserver;
+
// handling reordering
protected VisualStabilityManager mVisualStabilityManager = new VisualStabilityManager();
row.setExpansionLogger(this, entry.notification.getKey());
row.setGroupManager(mGroupManager);
row.setHeadsUpManager(mHeadsUpManager);
+ row.setAboveShelfChangedListener(mAboveShelfObserver);
row.setRemoteInputController(mRemoteInputController);
row.setOnExpandClickListener(this);
row.setRemoteViewClickHandler(mOnClickHandler);
import android.content.res.Resources;
import android.database.ContentObserver;
import android.os.Handler;
+import android.os.Looper;
import android.os.SystemClock;
import android.provider.Settings;
import android.support.v4.util.ArraySet;
private final ArrayMap<String, Long> mSnoozedPackages;
private final HashSet<OnHeadsUpChangedListener> mListeners = new HashSet<>();
private final int mDefaultSnoozeLengthMs;
- private final Handler mHandler = new Handler();
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
private final Pools.Pool<HeadsUpEntry> mEntryPool = new Pools.Pool<HeadsUpEntry>() {
private Stack<HeadsUpEntry> mPoolObjects = new Stack<>();
@Override
public void onReorderingAllowed() {
+ mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(false);
for (NotificationData.Entry entry : mEntriesToRemoveWhenReorderingAllowed) {
if (isHeadsUp(entry.key)) {
// Maybe the heads-up was removed already
}
}
mEntriesToRemoveWhenReorderingAllowed.clear();
+ mBar.getNotificationScrollLayout().setHeadsUpGoingAwayAnimationsAllowed(true);
}
public void setVisualStabilityManager(VisualStabilityManager visualStabilityManager) {
private boolean mPanelFullWidth;
private boolean mHasPulsingNotifications;
private boolean mUnlockHintRunning;
+ private boolean mQsCustomizerShowing;
public AmbientState(Context context) {
reload(context);
public boolean isUnlockHintRunning() {
return mUnlockHintRunning;
}
+
+ public boolean isQsCustomizerShowing() {
+ return mQsCustomizerShowing;
+ }
+
+ public void setQsCustomizerShowing(boolean qsCustomizerShowing) {
+ mQsCustomizerShowing = qsCustomizerShowing;
+ }
}
private boolean mHeadsUpAnimatingAway;
private int mStatusBarState;
private int mCachedBackgroundColor;
+ private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
private Runnable mAnimateScroll = this::animateScroll;
public NotificationStackScrollLayout(Context context) {
}
public void generateHeadsUpAnimation(ExpandableNotificationRow row, boolean isHeadsUp) {
- if (mAnimationsEnabled) {
+ if (mAnimationsEnabled && (isHeadsUp || mHeadsUpGoingAwayAnimationsAllowed)) {
mHeadsUpChangeAnimations.add(new Pair<>(row, isHeadsUp));
mNeedsAnimation = true;
if (!mIsExpanded && !isHeadsUp) {
mAmbientState.setUnlockHintRunning(running);
}
+ public void setQsCustomizerShowing(boolean isShowing) {
+ mAmbientState.setQsCustomizerShowing(isShowing);
+ requestChildrenUpdate();
+ }
+
+ public void setHeadsUpGoingAwayAnimationsAllowed(boolean headsUpGoingAwayAnimationsAllowed) {
+ mHeadsUpGoingAwayAnimationsAllowed = headsUpGoingAwayAnimationsAllowed;
+ }
+
/**
* A listener that is notified when some child locations might have changed.
*/
import android.support.test.runner.AndroidJUnit4;
import android.view.View;
+import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
import com.android.systemui.statusbar.stack.NotificationChildrenContainer;
import com.android.systemui.SysuiTestCase;
row.setHideSensitive(true, false, 0, 0);
verify(row).updateShelfIconColor();
}
+
+ @Test
+ public void testAboveShelfChangedListenerCalled() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ AboveShelfChangedListener listener = mock(AboveShelfChangedListener.class);
+ row.setAboveShelfChangedListener(listener);
+ row.setHeadsUp(true);
+ verify(listener).onAboveShelfStateChanged(true);
+ }
+
+ @Test
+ public void testAboveShelfChangedListenerCalledPinned() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ AboveShelfChangedListener listener = mock(AboveShelfChangedListener.class);
+ row.setAboveShelfChangedListener(listener);
+ row.setPinned(true);
+ verify(listener).onAboveShelfStateChanged(true);
+ }
+
+ @Test
+ public void testAboveShelfChangedListenerCalledHeadsUpGoingAway() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ AboveShelfChangedListener listener = mock(AboveShelfChangedListener.class);
+ row.setAboveShelfChangedListener(listener);
+ row.setHeadsUpAnimatingAway(true);
+ verify(listener).onAboveShelfStateChanged(true);
+ }
+ @Test
+ public void testAboveShelfChangedListenerCalledWhenGoingBelow() throws Exception {
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setHeadsUp(true);
+ AboveShelfChangedListener listener = mock(AboveShelfChangedListener.class);
+ row.setAboveShelfChangedListener(listener);
+ row.setAboveShelf(false);
+ verify(listener).onAboveShelfStateChanged(false);
+ }
}
import android.service.notification.StatusBarNotification;
import android.support.test.InstrumentationRegistry;
import android.view.LayoutInflater;
+import android.widget.FrameLayout;
import android.widget.RemoteViews;
import com.android.systemui.R;
+import com.android.systemui.statusbar.notification.AboveShelfChangedListener;
+import com.android.systemui.statusbar.notification.AboveShelfObserver;
import com.android.systemui.statusbar.notification.InflationException;
import com.android.systemui.statusbar.notification.NotificationInflaterTest;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
+import com.android.systemui.statusbar.policy.HeadsUpManager;
/**
* A helper class to create {@link ExpandableNotificationRow}
private final NotificationGroupManager mGroupManager = new NotificationGroupManager();
private ExpandableNotificationRow mRow;
private InflationException mException;
+ private HeadsUpManager mHeadsUpManager;
public NotificationTestHelper(Context context) {
mContext = context;
mInstrumentation = InstrumentationRegistry.getInstrumentation();
+ mHeadsUpManager = new HeadsUpManager(mContext, null, mGroupManager);
}
public ExpandableNotificationRow createRow() throws Exception {
});
ExpandableNotificationRow row = mRow;
row.setGroupManager(mGroupManager);
+ row.setHeadsUpManager(mHeadsUpManager);
+ row.setAboveShelfChangedListener(aboveShelf -> {});
UserHandle mUser = UserHandle.of(ActivityManager.getCurrentUser());
StatusBarNotification sbn = new StatusBarNotification("com.android.systemui",
"com.android.systemui", mId++, null, 1000,
--- /dev/null
+/*
+ * Copyright (C) 2017 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;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.widget.FrameLayout;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.NotificationTestHelper;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AboveShelfObserverTest extends SysuiTestCase {
+
+ private AboveShelfObserver mObserver;
+ private FrameLayout mHostLayout;
+ private NotificationTestHelper mNotificationTestHelper;
+ private AboveShelfObserver.HasViewAboveShelfChangedListener mListener;
+
+ @Before
+ public void setUp() throws Exception {
+ mNotificationTestHelper = new NotificationTestHelper(getContext());
+ mHostLayout = new FrameLayout(getContext());
+ mObserver = new AboveShelfObserver(mHostLayout);
+ ExpandableNotificationRow row = mNotificationTestHelper.createRow();
+ row.setAboveShelfChangedListener(mObserver);
+ mHostLayout.addView(row);
+ row = mNotificationTestHelper.createRow();
+ row.setAboveShelfChangedListener(mObserver);
+ mHostLayout.addView(row);
+ mListener = mock(AboveShelfObserver.HasViewAboveShelfChangedListener.class);
+ }
+
+ @Test
+ public void testObserverChangesWhenGoingAbove() {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) mHostLayout.getChildAt(0);
+ mObserver.setListener(mListener);
+ row.setHeadsUp(true);
+ verify(mListener).onHasViewsAboveShelfChanged(true);
+ }
+
+ @Test
+ public void testObserverChangesWhenGoingBelow() {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) mHostLayout.getChildAt(0);
+ row.setHeadsUp(true);
+ mObserver.setListener(mListener);
+ row.setHeadsUp(false);
+ verify(mListener).onHasViewsAboveShelfChanged(false);
+ }
+
+ @Test
+ public void testStaysAboveWhenOneGoesAway() {
+ ExpandableNotificationRow row = (ExpandableNotificationRow) mHostLayout.getChildAt(0);
+ row.setHeadsUp(true);
+ row = (ExpandableNotificationRow) mHostLayout.getChildAt(1);
+ row.setHeadsUp(true);
+ row.setHeadsUp(false);
+ Assert.assertTrue("There are still views above the shelf but removing one cleared it",
+ mObserver.hasViewsAboveShelf());
+ }
+}
+