sysui: show security footer on the bottom when in split shade mode

When showing the notification shade in split mode, the security footer
should always be shown after the footer. This also updates resources to
have a best usage of configs and dimensions.

Fixes: 188906516
Test: Added tests to QSPanelTest
Change-Id: Iea29490aa73bc4fb3fe3cce82e181a22adca2937
This commit is contained in:
Thales Lima
2021-06-10 18:52:35 +03:00
parent 540cfa9bbf
commit fc546a96c4
8 changed files with 190 additions and 140 deletions

View File

@@ -32,11 +32,11 @@
-->
<dimen name="qs_customize_header_min_height">48dp</dimen>
<!-- In landscape the security footer is actually part of the header,
and needs to be as short as the header -->
<dimen name="qs_security_footer_single_line_height">@*android:dimen/quick_qs_offset_height</dimen>
<dimen name="qs_footer_padding">14dp</dimen>
<dimen name="qs_footers_margin_bottom">0dp</dimen>
<dimen name="qs_security_footer_background_inset">12dp</dimen>
<dimen name="qs_security_footer_corner_radius">28dp</dimen>
<dimen name="battery_detail_graph_space_top">9dp</dimen>
<dimen name="battery_detail_graph_space_bottom">9dp</dimen>

View File

@@ -38,4 +38,6 @@
<!-- Max number of columns for quick controls area -->
<integer name="controls_max_columns">4</integer>
<!-- How many lines to show in the security footer -->
<integer name="qs_security_footer_maxLines">1</integer>
</resources>

View File

@@ -95,4 +95,9 @@
<dimen name="controls_top_margin">24dp</dimen>
<dimen name="global_actions_grid_item_layout_height">80dp</dimen>
<!-- For large screens the security footer appears below the footer,
same as phones in portrait -->
<dimen name="qs_security_footer_single_line_height">48dp</dimen>
<dimen name="qs_security_footer_background_inset">0dp</dimen>
</resources>

View File

@@ -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) {

View File

@@ -77,6 +77,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> {
refreshAllTiles();
}
updateBrightnessMirror();
mView.switchSecurityFooter(mShouldUseSplitNotificationShade);
}
};
@@ -141,7 +142,7 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> {
refreshAllTiles();
}
mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
mView.setSecurityFooter(mQsSecurityFooter.getView());
mView.setSecurityFooter(mQsSecurityFooter.getView(), mShouldUseSplitNotificationShade);
switchTileLayout(true);
if (mBrightnessMirrorController != null) {
mBrightnessMirrorController.addCallback(mBrightnessMirrorListener);

View File

@@ -69,7 +69,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr
private final DumpManager mDumpManager;
private final FeatureFlags mFeatureFlags;
protected final ArrayList<TileRecord> mRecords = new ArrayList<>();
private boolean mShouldUseSplitNotificationShade;
protected boolean mShouldUseSplitNotificationShade;
@Nullable
private Consumer<Boolean> mMediaVisibilityChangedListener;

View File

@@ -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());
}
}

View File

@@ -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 }
}