Merge "sysui: show security footer on the bottom when in split shade mode" into sc-v2-dev

This commit is contained in:
Thales Lima
2021-07-16 14:04:38 +00:00
committed by Android (Google) Code Review
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 }
}