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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user