Fixing bottom part of QS not getting any touches in split shade

The main issue was that QS and notifications container had padding of the size of taskbar. It was because taskbar is included in window insets and container doesn't check where insets come from. If insets are coming from taskbar only they should be ignored because taskbar should disappear when shade is expanded. But if they're coming from taskabar and navigation buttons are visible, they shouldn't be ignored as then taskbar always stays on the screen - that's assessed with checking if gesture navigation is available (if not, it means button navigation). Also because buttons are on the right side only and taskbar might or might not be visible, QS is just taking full height and notifications (that are on the right) have bigger spacing from the bottom.

Because this change involved using a few extra objects not available in NotificationsQuickSettingsContainer and I wanted to avoid doing everything in NotificationPanelViewController (it's big enough) and just passing results to NotificationsQuickSettingsContainer, I introduced controller for it: NotificationsQSContainerController. Now this controller has all the logic for determining bottom spacing. There can be a few more things that can be moved from NPVC to NotificationsQSContainerController but it's a start. Also now QS views and its children views instead of talking directly to container, they talk to controller via QSContainerController interface, which is a nice side-effect improvement.

Fixes: 194989891
Fixes: 200291264
Test: Home screen (so taskbar is hidden) -> go to split shade -> use anything that is close to the bottom of QS side (music widget, rearranging QS tiles, etc)
I'll add unit tests if the change overall looks good

Change-Id: I91ab8ec26dd2ef60562ffb9fdbc0e5739745ca0b
This commit is contained in:
Michal Brzezinski
2021-09-24 15:23:48 +01:00
parent c4c480ab7e
commit 626b14e467
13 changed files with 515 additions and 123 deletions

View File

@@ -16,7 +16,6 @@ package com.android.systemui.plugins.qs;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import com.android.systemui.plugins.FragmentBase;
import com.android.systemui.plugins.annotations.DependsOn;
@@ -56,7 +55,7 @@ public interface QS extends FragmentBase {
void setQsExpansion(float qsExpansionFraction, float headerTranslation);
void setHeaderListening(boolean listening);
void notifyCustomizeChanged();
void setContainer(ViewGroup container);
void setContainerController(QSContainerController controller);
void setExpandClickListener(OnClickListener onClickListener);
View getHeader();

View File

@@ -0,0 +1,9 @@
package com.android.systemui.plugins.qs
interface QSContainerController {
fun setCustomizerAnimating(animating: Boolean)
fun setCustomizerShowing(showing: Boolean)
fun setDetailShowing(showing: Boolean)
}

View File

@@ -26,7 +26,6 @@ import android.graphics.Point;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageView;
@@ -58,7 +57,6 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
private int mSideMargins;
private boolean mQsDisabled;
private int mContentPadding = -1;
private int mNavBarInset = 0;
private boolean mClippingEnabled;
public QSContainerImpl(Context context, AttributeSet attrs) {
@@ -94,25 +92,14 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
return true;
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mNavBarInset = insets.getInsets(WindowInsets.Type.navigationBars()).bottom;
mQSPanelContainer.setPaddingRelative(
mQSPanelContainer.getPaddingStart(),
mQSPanelContainer.getPaddingTop(),
mQSPanelContainer.getPaddingEnd(),
mNavBarInset
);
return super.onApplyWindowInsets(insets);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the
// bottom and footer are inside the screen.
MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams();
int maxQs = getDisplayHeight() - layoutParams.topMargin - layoutParams.bottomMargin
int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec);
int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin
- getPaddingBottom();
int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin
+ layoutParams.rightMargin;
@@ -122,11 +109,11 @@ public class QSContainerImpl extends FrameLayout implements Dumpable {
MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST));
int width = mQSPanelContainer.getMeasuredWidth() + padding;
super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
// QSCustomizer will always be the height of the screen, but do this after
// other measuring to avoid changing the height of the QS.
mQSCustomizer.measure(widthMeasureSpec,
MeasureSpec.makeMeasureSpec(getDisplayHeight(), MeasureSpec.EXACTLY));
MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY));
}
@Override

View File

@@ -46,8 +46,8 @@ import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.DetailAdapter;
import com.android.systemui.plugins.qs.QSContainerController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
public class QSDetail extends LinearLayout {
@@ -86,7 +86,7 @@ public class QSDetail extends LinearLayout {
private boolean mSwitchState;
private QSFooter mFooter;
private NotificationsQuickSettingsContainer mContainer;
private QSContainerController mQsContainerController;
public QSDetail(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -120,8 +120,8 @@ public class QSDetail extends LinearLayout {
mClipper = new QSDetailClipper(this);
}
public void setContainer(NotificationsQuickSettingsContainer container) {
mContainer = container;
public void setContainerController(QSContainerController controller) {
mQsContainerController = controller;
}
/** */
@@ -262,8 +262,8 @@ public class QSDetail extends LinearLayout {
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
animateDetailVisibleDiff(x, y, visibleDiff, listener);
if (mContainer != null) {
mContainer.setDetailShowing(showingDetail);
if (mQsContainerController != null) {
mQsContainerController.setDetailShowing(showingDetail);
}
}

View File

@@ -43,6 +43,7 @@ import com.android.systemui.dump.DumpManager;
import com.android.systemui.media.MediaHost;
import com.android.systemui.plugins.FalsingManager;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.plugins.qs.QSContainerController;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.qs.customize.QSCustomizerController;
import com.android.systemui.qs.dagger.QSFragmentComponent;
@@ -50,7 +51,6 @@ import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
import com.android.systemui.statusbar.policy.BrightnessMirrorController;
import com.android.systemui.statusbar.policy.RemoteInputQuickSettingsDisabler;
import com.android.systemui.util.InjectionInflationController;
@@ -336,11 +336,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca
}
@Override
public void setContainer(ViewGroup container) {
if (container instanceof NotificationsQuickSettingsContainer) {
mQSCustomizerController.setContainer((NotificationsQuickSettingsContainer) container);
mQSDetail.setContainer((NotificationsQuickSettingsContainer) container);
}
public void setContainerController(QSContainerController controller) {
mQSCustomizerController.setContainerController(controller);
mQSDetail.setContainerController(controller);
}
@Override

View File

@@ -33,9 +33,9 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.systemui.R;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.plugins.qs.QSContainerController;
import com.android.systemui.qs.QSDetailClipper;
import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
/**
* Allows full-screen customization of QS, through show() and hide().
@@ -54,7 +54,7 @@ public class QSCustomizer extends LinearLayout {
private boolean isShown;
private final RecyclerView mRecyclerView;
private boolean mCustomizing;
private NotificationsQuickSettingsContainer mNotifQsContainer;
private QSContainerController mQsContainerController;
private QS mQs;
private int mX;
private int mY;
@@ -103,8 +103,8 @@ public class QSCustomizer extends LinearLayout {
lightBarController.setQsCustomizing(mIsShowingNavBackdrop && isShown);
}
public void setContainer(NotificationsQuickSettingsContainer notificationsQsContainer) {
mNotifQsContainer = notificationsQsContainer;
public void setContainerController(QSContainerController controller) {
mQsContainerController = controller;
}
public void setQs(QS qs) {
@@ -123,8 +123,8 @@ public class QSCustomizer extends LinearLayout {
mOpening = true;
setVisibility(View.VISIBLE);
mClipper.animateCircularClip(mX, mY, true, new ExpandAnimatorListener(tileAdapter));
mNotifQsContainer.setCustomizerAnimating(true);
mNotifQsContainer.setCustomizerShowing(true);
mQsContainerController.setCustomizerAnimating(true);
mQsContainerController.setCustomizerShowing(true);
}
}
@@ -136,8 +136,8 @@ public class QSCustomizer extends LinearLayout {
mClipper.showBackground();
isShown = true;
setCustomizing(true);
mNotifQsContainer.setCustomizerAnimating(false);
mNotifQsContainer.setCustomizerShowing(true);
mQsContainerController.setCustomizerAnimating(false);
mQsContainerController.setCustomizerShowing(true);
}
}
@@ -154,8 +154,8 @@ public class QSCustomizer extends LinearLayout {
} else {
setVisibility(View.GONE);
}
mNotifQsContainer.setCustomizerAnimating(animate);
mNotifQsContainer.setCustomizerShowing(false);
mQsContainerController.setCustomizerAnimating(animate);
mQsContainerController.setCustomizerShowing(false);
}
}
@@ -193,7 +193,7 @@ public class QSCustomizer extends LinearLayout {
setCustomizing(true);
}
mOpening = false;
mNotifQsContainer.setCustomizerAnimating(false);
mQsContainerController.setCustomizerAnimating(false);
mRecyclerView.setAdapter(mTileAdapter);
}
@@ -201,7 +201,7 @@ public class QSCustomizer extends LinearLayout {
public void onAnimationCancel(Animator animation) {
mOpening = false;
mQs.notifyCustomizeChanged();
mNotifQsContainer.setCustomizerAnimating(false);
mQsContainerController.setCustomizerAnimating(false);
}
}
@@ -211,7 +211,7 @@ public class QSCustomizer extends LinearLayout {
if (!isShown) {
setVisibility(View.GONE);
}
mNotifQsContainer.setCustomizerAnimating(false);
mQsContainerController.setCustomizerAnimating(false);
}
@Override
@@ -219,7 +219,7 @@ public class QSCustomizer extends LinearLayout {
if (!isShown) {
setVisibility(View.GONE);
}
mNotifQsContainer.setCustomizerAnimating(false);
mQsContainerController.setCustomizerAnimating(false);
}
};

View File

@@ -35,13 +35,13 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.R;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.plugins.qs.QSContainerController;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSEditEvent;
import com.android.systemui.qs.QSFragment;
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.dagger.QSScope;
import com.android.systemui.statusbar.phone.LightBarController;
import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -233,8 +233,8 @@ public class QSCustomizerController extends ViewController<QSCustomizer> {
}
/** */
public void setContainer(NotificationsQuickSettingsContainer container) {
mView.setContainer(container);
public void setContainerController(QSContainerController controller) {
mView.setContainerController(controller);
}
public boolean isShown() {

View File

@@ -310,6 +310,7 @@ public class NotificationPanelViewController extends PanelViewController {
private KeyguardStatusViewController mKeyguardStatusViewController;
private LockIconViewController mLockIconViewController;
private NotificationsQuickSettingsContainer mNotificationContainerParent;
private NotificationsQSContainerController mNotificationsQSContainerController;
private boolean mAnimateNextPositionUpdate;
private float mQuickQsOffsetHeight;
private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
@@ -653,6 +654,7 @@ public class NotificationPanelViewController extends PanelViewController {
ConversationNotificationManager conversationNotificationManager,
MediaHierarchyManager mediaHierarchyManager,
StatusBarKeyguardViewManager statusBarKeyguardViewManager,
NotificationsQSContainerController notificationsQSContainerController,
NotificationStackScrollLayoutController notificationStackScrollLayoutController,
KeyguardStatusViewComponent.Factory keyguardStatusViewComponentFactory,
KeyguardQsUserSwitchComponent.Factory keyguardQsUserSwitchComponentFactory,
@@ -708,6 +710,8 @@ public class NotificationPanelViewController extends PanelViewController {
mFlingAnimationUtilsBuilder = flingAnimationUtilsBuilder;
mMediaHierarchyManager = mediaHierarchyManager;
mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
mNotificationsQSContainerController = notificationsQSContainerController;
mNotificationsQSContainerController.init();
mNotificationStackScrollLayoutController = notificationStackScrollLayoutController;
mGroupManager = groupManager;
mNotificationIconAreaController = notificationIconAreaController;
@@ -1005,7 +1009,7 @@ public class NotificationPanelViewController extends PanelViewController {
constraintSet.setMargin(R.id.notification_stack_scroller, TOP, topMargin);
constraintSet.setMargin(R.id.qs_frame, TOP, topMargin);
constraintSet.applyTo(mNotificationContainerParent);
mNotificationContainerParent.setSplitShadeEnabled(mShouldUseSplitNotificationShade);
mNotificationsQSContainerController.setSplitShadeEnabled(mShouldUseSplitNotificationShade);
updateKeyguardStatusViewAlignment(/* animate= */false);
@@ -2106,7 +2110,7 @@ public class NotificationPanelViewController extends PanelViewController {
requestPanelHeightUpdate();
mFalsingCollector.setQsExpanded(expanded);
mStatusBar.setQsExpanded(expanded);
mNotificationContainerParent.setQsExpanded(expanded);
mNotificationsQSContainerController.setQsExpanded(expanded);
mPulseExpansionHandler.setQsExpanded(expanded);
mKeyguardBypassController.setQSExpanded(expanded);
mStatusBarKeyguardViewManager.setQsExpanded(expanded);

View File

@@ -0,0 +1,141 @@
package com.android.systemui.statusbar.phone
import android.view.WindowInsets
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.plugins.qs.QS
import com.android.systemui.plugins.qs.QSContainerController
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import com.android.systemui.shared.system.QuickStepContract
import com.android.systemui.util.ViewController
import java.util.function.Consumer
import javax.inject.Inject
class NotificationsQSContainerController @Inject constructor(
view: NotificationsQuickSettingsContainer,
private val navigationModeController: NavigationModeController,
private val overviewProxyService: OverviewProxyService
) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController {
var qsExpanded = false
set(value) {
if (field != value) {
field = value
mView.invalidate()
}
}
var splitShadeEnabled = false
set(value) {
if (field != value) {
field = value
// in case device configuration changed while showing QS details/customizer
updateBottomSpacing()
}
}
private var isQSDetailShowing = false
private var isQSCustomizing = false
private var isQSCustomizerAnimating = false
private var notificationsBottomMargin = 0
private var bottomStableInsets = 0
private var bottomCutoutInsets = 0
private var isGestureNavigation = true
private var taskbarVisible = false
private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener {
override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) {
taskbarVisible = visible
}
}
private val windowInsetsListener: Consumer<WindowInsets> = Consumer { insets ->
// when taskbar is visible, stableInsetBottom will include its height
bottomStableInsets = insets.stableInsetBottom
bottomCutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0
updateBottomSpacing()
}
override fun onInit() {
val currentMode: Int = navigationModeController.addListener { mode: Int ->
isGestureNavigation = QuickStepContract.isGesturalMode(mode)
}
isGestureNavigation = QuickStepContract.isGesturalMode(currentMode)
}
public override fun onViewAttached() {
notificationsBottomMargin = mView.defaultNotificationsMarginBottom
overviewProxyService.addCallback(taskbarVisibilityListener)
mView.setInsetsChangedListener(windowInsetsListener)
mView.setQSFragmentAttachedListener { qs: QS -> qs.setContainerController(this) }
}
override fun onViewDetached() {
overviewProxyService.removeCallback(taskbarVisibilityListener)
mView.removeOnInsetsChangedListener()
mView.removeQSFragmentAttachedListener()
}
override fun setCustomizerAnimating(animating: Boolean) {
if (isQSCustomizerAnimating != animating) {
isQSCustomizerAnimating = animating
mView.invalidate()
}
}
override fun setCustomizerShowing(showing: Boolean) {
isQSCustomizing = showing
updateBottomSpacing()
}
override fun setDetailShowing(showing: Boolean) {
isQSDetailShowing = showing
updateBottomSpacing()
}
private fun updateBottomSpacing() {
val (containerPadding, notificationsMargin) = calculateBottomSpacing()
var qsScrollPaddingBottom = 0
if (!(splitShadeEnabled || isQSCustomizing || isQSDetailShowing || isGestureNavigation ||
taskbarVisible)) {
// no taskbar, portrait, navigation buttons enabled:
// padding is needed so QS can scroll up over bottom insets - to reach the point when
// the whole QS is above bottom insets
qsScrollPaddingBottom = bottomStableInsets
}
mView.setPadding(0, 0, 0, containerPadding)
mView.setNotificationsMarginBottom(notificationsMargin)
mView.setQSScrollPaddingBottom(qsScrollPaddingBottom)
}
private fun calculateBottomSpacing(): Pair<Int, Int> {
val containerPadding: Int
var stackScrollMargin = notificationsBottomMargin
if (splitShadeEnabled) {
if (isGestureNavigation) {
// only default cutout padding, taskbar always hides
containerPadding = bottomCutoutInsets
} else if (taskbarVisible) {
// navigation buttons + visible taskbar means we're NOT on homescreen
containerPadding = bottomStableInsets
} else {
// navigation buttons + hidden taskbar means we're on homescreen
containerPadding = 0
// we need extra margin for notifications as navigation buttons are below them
stackScrollMargin = bottomStableInsets + notificationsBottomMargin
}
} else {
if (isQSCustomizing || isQSDetailShowing) {
// Clear out bottom paddings/margins so the qs customization can be full height.
containerPadding = 0
stackScrollMargin = 0
} else if (isGestureNavigation) {
containerPadding = bottomCutoutInsets
} else if (taskbarVisible) {
containerPadding = bottomStableInsets
} else {
containerPadding = 0
}
}
return containerPadding to stackScrollMargin
}
}

View File

@@ -33,6 +33,7 @@ import com.android.systemui.statusbar.notification.AboveShelfObserver;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.function.Consumer;
/**
* The container with notification stack scroller and quick settings inside.
@@ -43,17 +44,15 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
private View mQsFrame;
private View mStackScroller;
private View mKeyguardStatusBar;
private boolean mQsExpanded;
private boolean mCustomizerAnimating;
private boolean mCustomizing;
private boolean mDetailShowing;
private int mBottomPadding;
private int mStackScrollerMargin;
private ArrayList<View> mDrawingOrderedChildren = new ArrayList<>();
private ArrayList<View> mLayoutDrawingOrder = new ArrayList<>();
private final Comparator<View> mIndexComparator = Comparator.comparingInt(this::indexOfChild);
private boolean mSplitShadeEnabled;
private Consumer<WindowInsets> mInsetsChangedListener = insets -> {};
private Consumer<QS> mQSFragmentAttachedListener = qs -> {};
private QS mQs;
private View mQSScrollView;
public NotificationsQuickSettingsContainer(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -68,6 +67,59 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
mKeyguardStatusBar = findViewById(R.id.keyguard_header);
}
@Override
public void onFragmentViewCreated(String tag, Fragment fragment) {
mQs = (QS) fragment;
mQSFragmentAttachedListener.accept(mQs);
mQSScrollView = mQs.getView().findViewById(R.id.expanded_qs_scroll_view);
}
@Override
public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
invalidate();
}
public void setNotificationsMarginBottom(int margin) {
LayoutParams params = (LayoutParams) mStackScroller.getLayoutParams();
params.bottomMargin = margin;
mStackScroller.setLayoutParams(params);
}
public void setQSScrollPaddingBottom(int paddingBottom) {
if (mQSScrollView != null) {
mQSScrollView.setPaddingRelative(
mQSScrollView.getPaddingLeft(),
mQSScrollView.getPaddingTop(),
mQSScrollView.getPaddingRight(),
paddingBottom
);
}
}
public int getDefaultNotificationsMarginBottom() {
return mStackScrollerMargin;
}
public void setInsetsChangedListener(Consumer<WindowInsets> onInsetsChangedListener) {
mInsetsChangedListener = onInsetsChangedListener;
}
public void removeOnInsetsChangedListener() {
mInsetsChangedListener = insets -> {};
}
public void setQSFragmentAttachedListener(Consumer<QS> qsFragmentAttachedListener) {
mQSFragmentAttachedListener = qsFragmentAttachedListener;
// listener might be attached after fragment is attached
if (mQs != null) {
mQSFragmentAttachedListener.accept(mQs);
}
}
public void removeQSFragmentAttachedListener() {
mQSFragmentAttachedListener = qs -> {};
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -82,8 +134,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mBottomPadding = insets.getStableInsetBottom();
setPadding(0, 0, 0, mBottomPadding);
mInsetsChangedListener.accept(insets);
return insets;
}
@@ -119,66 +170,4 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout
}
}
@Override
public void onFragmentViewCreated(String tag, Fragment fragment) {
QS container = (QS) fragment;
container.setContainer(this);
}
public void setQsExpanded(boolean expanded) {
if (mQsExpanded != expanded) {
mQsExpanded = expanded;
invalidate();
}
}
public void setCustomizerAnimating(boolean isAnimating) {
if (mCustomizerAnimating != isAnimating) {
mCustomizerAnimating = isAnimating;
invalidate();
}
}
public void setCustomizerShowing(boolean isShowing) {
mCustomizing = isShowing;
updateBottomMargin();
}
public void setDetailShowing(boolean isShowing) {
mDetailShowing = isShowing;
updateBottomMargin();
}
/**
* Sets if split shade is enabled and adjusts margins/paddings depending on QS details and
* customizer state
*/
public void setSplitShadeEnabled(boolean splitShadeEnabled) {
mSplitShadeEnabled = splitShadeEnabled;
// in case device was rotated while showing QS details/customizer
updateBottomMargin();
}
private void updateBottomMargin() {
// in split shade, QS state changes should not influence notifications panel
if (!mSplitShadeEnabled && (mCustomizing || mDetailShowing)) {
// Clear out bottom paddings/margins so the qs customization can be full height.
setPadding(0, 0, 0, 0);
setBottomMargin(mStackScroller, 0);
} else {
setPadding(0, 0, 0, mBottomPadding);
setBottomMargin(mStackScroller, mStackScrollerMargin);
}
}
private void setBottomMargin(View v, int bottomMargin) {
LayoutParams params = (LayoutParams) v.getLayoutParams();
params.bottomMargin = bottomMargin;
v.setLayoutParams(params);
}
@Override
public void onHasViewsAboveShelfChanged(boolean hasViewsAboveShelf) {
invalidate();
}
}

View File

@@ -29,9 +29,9 @@ import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.NotificationShelfController;
import com.android.systemui.statusbar.notification.row.dagger.NotificationShelfComponent;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.phone.NotificationPanelView;
import com.android.systemui.statusbar.phone.NotificationShadeWindowView;
import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
import com.android.systemui.statusbar.phone.TapAgainView;
import com.android.systemui.util.InjectionInflationController;
@@ -67,8 +67,8 @@ public abstract class StatusBarViewModule {
@Provides
@StatusBarComponent.StatusBarScope
public static NotificationStackScrollLayout providesNotificationStackScrollLayout(
NotificationStackScrollLayoutController notificationStackScrollLayoutController) {
return notificationStackScrollLayoutController.getView();
NotificationShadeWindowView notificationShadeWindowView) {
return notificationShadeWindowView.findViewById(R.id.notification_stack_scroller);
}
/** */
@@ -149,4 +149,12 @@ public abstract class StatusBarViewModule {
public static TapAgainView getTapAgainView(NotificationPanelView npv) {
return npv.getTapAgainView();
}
/** */
@Provides
@StatusBarComponent.StatusBarScope
public static NotificationsQuickSettingsContainer getNotificationsQuickSettingsContainer(
NotificationShadeWindowView notificationShadeWindowView) {
return notificationShadeWindowView.findViewById(R.id.notification_container_parent);
}
}

View File

@@ -300,6 +300,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
private LockscreenGestureLogger mLockscreenGestureLogger;
@Mock
private DumpManager mDumpManager;
@Mock
private NotificationsQSContainerController mNotificationsQSContainerController;
private SysuiStatusBarStateController mStatusBarStateController;
private NotificationPanelViewController mNotificationPanelViewController;
@@ -414,6 +416,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
() -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
mConversationNotificationManager, mMediaHiearchyManager,
mStatusBarKeyguardViewManager,
mNotificationsQSContainerController,
mNotificationStackScrollLayoutController,
mKeyguardStatusViewComponentFactory,
mKeyguardQsUserSwitchComponentFactory,

View File

@@ -0,0 +1,254 @@
package com.android.systemui.statusbar.phone
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.WindowInsets
import android.view.WindowManagerPolicyConstants
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.navigationbar.NavigationModeController
import com.android.systemui.navigationbar.NavigationModeController.ModeChangedListener
import com.android.systemui.recents.OverviewProxyService
import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.RETURNS_DEEP_STUBS
import org.mockito.Mockito.anyInt
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.function.Consumer
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper
class NotificationQSContainerControllerTest : SysuiTestCase() {
companion object {
const val STABLE_INSET_BOTTOM = 100
const val CUTOUT_HEIGHT = 50
const val GESTURES_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL
const val BUTTONS_NAVIGATION = WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON
const val NOTIFICATIONS_MARGIN = 50
}
@Mock
private lateinit var navigationModeController: NavigationModeController
@Mock
private lateinit var overviewProxyService: OverviewProxyService
@Mock
private lateinit var notificationsQSContainer: NotificationsQuickSettingsContainer
@Captor
lateinit var navigationModeCaptor: ArgumentCaptor<ModeChangedListener>
@Captor
lateinit var taskbarVisibilityCaptor: ArgumentCaptor<OverviewProxyListener>
@Captor
lateinit var windowInsetsCallbackCaptor: ArgumentCaptor<Consumer<WindowInsets>>
private lateinit var notificationsQSContainerController: NotificationsQSContainerController
private lateinit var navigationModeCallback: ModeChangedListener
private lateinit var taskbarVisibilityCallback: OverviewProxyListener
private lateinit var windowInsetsCallback: Consumer<WindowInsets>
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
notificationsQSContainerController = NotificationsQSContainerController(
notificationsQSContainer,
navigationModeController,
overviewProxyService
)
whenever(notificationsQSContainer.defaultNotificationsMarginBottom)
.thenReturn(NOTIFICATIONS_MARGIN)
whenever(navigationModeController.addListener(navigationModeCaptor.capture()))
.thenReturn(GESTURES_NAVIGATION)
doNothing().`when`(overviewProxyService).addCallback(taskbarVisibilityCaptor.capture())
doNothing().`when`(notificationsQSContainer)
.setInsetsChangedListener(windowInsetsCallbackCaptor.capture())
notificationsQSContainerController.init()
notificationsQSContainerController.onViewAttached()
navigationModeCallback = navigationModeCaptor.value
taskbarVisibilityCallback = taskbarVisibilityCaptor.value
windowInsetsCallback = windowInsetsCallbackCaptor.value
}
@Test
fun testTaskbarVisibleInSplitShade() {
notificationsQSContainerController.splitShadeEnabled = true
given(taskbarVisible = true,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0, // taskbar should disappear when shade is expanded
expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
given(taskbarVisible = true,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = STABLE_INSET_BOTTOM,
expectedNotificationsMargin = NOTIFICATIONS_MARGIN)
}
@Test
fun testTaskbarNotVisibleInSplitShade() {
// when taskbar is not visible, it means we're on the home screen
notificationsQSContainerController.splitShadeEnabled = true
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons
expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
}
@Test
fun testTaskbarNotVisibleInSplitShadeWithCutout() {
notificationsQSContainerController.splitShadeEnabled = true
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withCutout())
then(expectedContainerPadding = CUTOUT_HEIGHT)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withCutout().withStableBottom())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN)
}
@Test
fun testTaskbarVisibleInSinglePaneShade() {
notificationsQSContainerController.splitShadeEnabled = false
given(taskbarVisible = true,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0)
given(taskbarVisible = true,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = STABLE_INSET_BOTTOM)
}
@Test
fun testTaskbarNotVisibleInSinglePaneShade() {
notificationsQSContainerController.splitShadeEnabled = false
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0)
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withCutout().withStableBottom())
then(expectedContainerPadding = CUTOUT_HEIGHT)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedQsPadding = STABLE_INSET_BOTTOM)
}
@Test
fun testCustomizingInSinglePaneShade() {
notificationsQSContainerController.splitShadeEnabled = false
notificationsQSContainerController.setCustomizerShowing(true)
// always sets spacings to 0
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
}
@Test
fun testDetailShowingInSinglePaneShade() {
notificationsQSContainerController.splitShadeEnabled = false
notificationsQSContainerController.setDetailShowing(true)
// always sets spacings to 0
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0,
expectedNotificationsMargin = 0)
}
@Test
fun testDetailShowingInSplitShade() {
notificationsQSContainerController.splitShadeEnabled = true
given(taskbarVisible = false,
navigationMode = GESTURES_NAVIGATION,
insets = windowInsets().withStableBottom())
then(expectedContainerPadding = 0)
notificationsQSContainerController.setDetailShowing(true)
// should not influence spacing
given(taskbarVisible = false,
navigationMode = BUTTONS_NAVIGATION,
insets = emptyInsets())
then(expectedContainerPadding = 0)
}
private fun given(
taskbarVisible: Boolean,
navigationMode: Int,
insets: WindowInsets
) {
Mockito.clearInvocations(notificationsQSContainer)
taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false)
navigationModeCallback.onNavigationModeChanged(navigationMode)
windowInsetsCallback.accept(insets)
}
fun then(
expectedContainerPadding: Int,
expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN,
expectedQsPadding: Int = 0
) {
verify(notificationsQSContainer)
.setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding))
verify(notificationsQSContainer).setNotificationsMarginBottom(expectedNotificationsMargin)
verify(notificationsQSContainer).setQSScrollPaddingBottom(expectedQsPadding)
Mockito.clearInvocations(notificationsQSContainer)
}
private fun windowInsets() = mock(WindowInsets::class.java, RETURNS_DEEP_STUBS)
private fun emptyInsets() = mock(WindowInsets::class.java)
private fun WindowInsets.withCutout(): WindowInsets {
whenever(displayCutout.safeInsetBottom).thenReturn(CUTOUT_HEIGHT)
return this
}
private fun WindowInsets.withStableBottom(): WindowInsets {
whenever(stableInsetBottom).thenReturn(STABLE_INSET_BOTTOM)
return this
}
}