diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 34bf28aa741ea..ee0e4b34e9ff8 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -32,11 +32,11 @@ --> 48dp + @*android:dimen/quick_qs_offset_height 14dp - 0dp 12dp - 28dp 9dp 9dp diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml index 2f5e8eaa22635..942cb2bcafa0d 100644 --- a/packages/SystemUI/res/values-sw600dp/config.xml +++ b/packages/SystemUI/res/values-sw600dp/config.xml @@ -38,4 +38,6 @@ 4 + + 1 diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index da80b85b38bf0..527537b6155e4 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -95,4 +95,9 @@ 24dp 80dp + + + 48dp + 0dp diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 7c7f566589199..f20a2db75f2e7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -42,7 +42,6 @@ import com.android.systemui.settings.brightness.BrightnessSlider; import com.android.systemui.statusbar.policy.BrightnessMirrorController; import com.android.systemui.tuner.TunerService; import com.android.systemui.tuner.TunerService.Tunable; -import com.android.systemui.util.animation.UniqueObjectHostView; import java.util.ArrayList; import java.util.List; @@ -316,7 +315,6 @@ public class QSPanel extends LinearLayout implements Tunable { super.onConfigurationChanged(newConfig); mOnConfigurationChangedListeners.forEach( listener -> listener.onConfigurationChange(newConfig)); - switchSecurityFooter(); } @Override @@ -359,25 +357,21 @@ public class QSPanel extends LinearLayout implements Tunable { switchToParent(mFooter, parent, index); index++; } - - // The security footer is switched on orientation changes } - private void switchSecurityFooter() { - if (mSecurityFooter != null) { - if (mContext.getResources().getConfiguration().orientation - == Configuration.ORIENTATION_LANDSCAPE && mHeaderContainer != null) { - // Adding the security view to the header, that enables us to avoid scrolling - switchToParent(mSecurityFooter, mHeaderContainer, 0); - } else { - // Where should this go? If there's media, right before it. Otherwise, at the end. - View mediaView = findViewByPredicate(v -> v instanceof UniqueObjectHostView); - int index = -1; - if (mediaView != null) { - index = indexOfChild(mediaView); - } - switchToParent(mSecurityFooter, this, index); - } + /** Switch the security footer between top and bottom of QS depending on orientation. */ + public void switchSecurityFooter(boolean shouldUseSplitNotificationShade) { + if (mSecurityFooter == null) return; + + if (!shouldUseSplitNotificationShade + && mContext.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE && mHeaderContainer != null) { + // Adding the security view to the header, that enables us to avoid scrolling + switchToParent(mSecurityFooter, mHeaderContainer, 0); + } else { + // Add after the footer + int index = indexOfChild(mFooter); + switchToParent(mSecurityFooter, this, index + 1); } } @@ -652,9 +646,14 @@ public class QSPanel extends LinearLayout implements Tunable { return mListening; } - public void setSecurityFooter(View view) { + /** + * Set the security footer view and switch it into the right place + * @param view the view in question + * @param shouldUseSplitNotificationShade if QS is in split shade mode + */ + public void setSecurityFooter(View view, boolean shouldUseSplitNotificationShade) { mSecurityFooter = view; - switchSecurityFooter(); + switchSecurityFooter(shouldUseSplitNotificationShade); } protected void setPageMargin(int pageMargin) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index ae0f5104d20fe..7cbd45bd2af40 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -77,6 +77,7 @@ public class QSPanelController extends QSPanelControllerBase { refreshAllTiles(); } updateBrightnessMirror(); + mView.switchSecurityFooter(mShouldUseSplitNotificationShade); } }; @@ -141,7 +142,7 @@ public class QSPanelController extends QSPanelControllerBase { refreshAllTiles(); } mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener); - mView.setSecurityFooter(mQsSecurityFooter.getView()); + mView.setSecurityFooter(mQsSecurityFooter.getView(), mShouldUseSplitNotificationShade); switchTileLayout(true); if (mBrightnessMirrorController != null) { mBrightnessMirrorController.addCallback(mBrightnessMirrorListener); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 4739a3f4c7d69..64ef5cfdf7e2a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -69,7 +69,7 @@ public abstract class QSPanelControllerBase extends ViewContr private final DumpManager mDumpManager; private final FeatureFlags mFeatureFlags; protected final ArrayList mRecords = new ArrayList<>(); - private boolean mShouldUseSplitNotificationShade; + protected boolean mShouldUseSplitNotificationShade; @Nullable private Consumer mMediaVisibilityChangedListener; diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java deleted file mode 100644 index 4f8859927d06a..0000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * 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.qs; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.plugins.qs.QSTileView; -import com.android.systemui.qs.logging.QSLogger; -import com.android.systemui.qs.tileimpl.QSTileImpl; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.Collections; - -@RunWith(AndroidTestingRunner.class) -@RunWithLooper -@SmallTest -public class QSPanelTest extends SysuiTestCase { - - private TestableLooper mTestableLooper; - private QSPanel mQsPanel; - @Mock - private QSTileHost mHost; - @Mock - private QSTileImpl dndTile; - @Mock - private QSPanelControllerBase.TileRecord mDndTileRecord; - @Mock - private QSLogger mQSLogger; - private ViewGroup mParentView; - @Mock - private QSDetail.Callback mCallback; - @Mock - private QSTileView mQSTileView; - @Mock - private ActivityStarter mActivityStarter; - - @Before - public void setup() throws Exception { - MockitoAnnotations.initMocks(this); - mTestableLooper = TestableLooper.get(this); - -// // Dependencies for QSSecurityFooter -// mDependency.injectTestDependency(ActivityStarter.class, mActivityStarter); -// mDependency.injectMockDependency(SecurityController.class); -// mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper()); -// mContext.addMockSystemService(Context.USER_SERVICE, mock(UserManager.class)); - mDndTileRecord.tile = dndTile; - mDndTileRecord.tileView = mQSTileView; - - mTestableLooper.runWithLooper(() -> { - mQsPanel = new QSPanel(mContext, null); - mQsPanel.initialize(); - mQsPanel.onFinishInflate(); - // Provides a parent with non-zero size for QSPanel - mParentView = new FrameLayout(mContext); - mParentView.addView(mQsPanel); - - when(dndTile.getTileSpec()).thenReturn("dnd"); - when(mHost.getTiles()).thenReturn(Collections.emptyList()); - when(mHost.createTileView(any(), any(), anyBoolean())).thenReturn(mQSTileView); - mQsPanel.addTile(mDndTileRecord); - mQsPanel.setCallback(mCallback); - }); - } - - @Test - public void testOpenDetailsWithExistingTile_NoException() { - mTestableLooper.processAllMessages(); - mQsPanel.openDetails(dndTile); - mTestableLooper.processAllMessages(); - - verify(mCallback).onShowingDetail(any(), anyInt(), anyInt()); - } - - @Test - public void testOpenDetailsWithNullParameter_NoException() { - mTestableLooper.processAllMessages(); - mQsPanel.openDetails(null); - mTestableLooper.processAllMessages(); - - verify(mCallback, never()).onShowingDetail(any(), anyInt(), anyInt()); - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt new file mode 100644 index 0000000000000..a83a5e1f5484e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2021 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.qs + +import android.content.res.Configuration +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTileView +import com.android.systemui.qs.QSPanelControllerBase.TileRecord +import com.android.systemui.qs.logging.QSLogger +import com.android.systemui.qs.tileimpl.QSTileImpl +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import org.mockito.Mockito.`when` as whenever + +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +@SmallTest +class QSPanelTest : SysuiTestCase() { + private lateinit var mTestableLooper: TestableLooper + private lateinit var mQsPanel: QSPanel + + @Mock + private lateinit var mHost: QSTileHost + + @Mock + private lateinit var dndTile: QSTileImpl<*> + + @Mock + private lateinit var mDndTileRecord: TileRecord + + @Mock + private lateinit var mQSLogger: QSLogger + private lateinit var mParentView: ViewGroup + + @Mock + private lateinit var mCallback: QSDetail.Callback + + @Mock + private lateinit var mQSTileView: QSTileView + + @Before + @Throws(Exception::class) + fun setup() { + MockitoAnnotations.initMocks(this) + mTestableLooper = TestableLooper.get(this) + + mDndTileRecord.tile = dndTile + mDndTileRecord.tileView = mQSTileView + mTestableLooper.runWithLooper { + mQsPanel = QSPanel(mContext, null) + mQsPanel.initialize() + // QSPanel inflates a footer inside of it, mocking it here + mQsPanel.addView(LinearLayout(mContext).apply { id = R.id.qs_footer }) + mQsPanel.onFinishInflate() + mQsPanel.setSecurityFooter(View(mContext), false) + mQsPanel.setHeaderContainer(LinearLayout(mContext)) + // Provides a parent with non-zero size for QSPanel + mParentView = FrameLayout(mContext).apply { + addView(mQsPanel) + } + + whenever(dndTile.tileSpec).thenReturn("dnd") + whenever(mHost.tiles).thenReturn(emptyList()) + whenever(mHost.createTileView(any(), any(), anyBoolean())).thenReturn(mQSTileView) + mQsPanel.addTile(mDndTileRecord) + mQsPanel.setCallback(mCallback) + } + } + + @Test + fun testOpenDetailsWithExistingTile_NoException() { + mTestableLooper.runWithLooper { + mQsPanel.openDetails(dndTile) + } + + verify(mCallback).onShowingDetail(any(), anyInt(), anyInt()) + } + + @Test + fun testOpenDetailsWithNullParameter_NoException() { + mTestableLooper.runWithLooper { + mQsPanel.openDetails(null) + } + + verify(mCallback, never()).onShowingDetail(any(), anyInt(), anyInt()) + } + + @Test + fun testSecurityFooter_appearsOnBottomOnSplitShade() { + mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_LANDSCAPE)) + mQsPanel.switchSecurityFooter(true) + + mTestableLooper.runWithLooper { + mQsPanel.isExpanded = true + } + + assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(2) + } + + @Test + fun testSecurityFooter_appearsOnBottomIfPortrait() { + mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_PORTRAIT)) + mQsPanel.switchSecurityFooter(false) + + mTestableLooper.runWithLooper { + mQsPanel.isExpanded = true + } + + assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(2) + } + + @Test + fun testSecurityFooter_appearsOnTopIfSmallScreenAndLandscape() { + mQsPanel.onConfigurationChanged(getNewOrientationConfig(ORIENTATION_LANDSCAPE)) + mQsPanel.switchSecurityFooter(false) + + mTestableLooper.runWithLooper { + mQsPanel.isExpanded = true + } + + // -1 means that it is part of the mHeaderContainer + assertThat(mQsPanel.indexOfChild(mQsPanel.mSecurityFooter)).isEqualTo(-1) + } + + private fun getNewOrientationConfig(@Configuration.Orientation newOrientation: Int) = + context.resources.configuration.apply { orientation = newOrientation } +}