From be9380bef22e5d2a869b3a6c46e1653cc6412f1b Mon Sep 17 00:00:00 2001 From: Michal Brzezinski Date: Thu, 18 Nov 2021 12:39:59 +0000 Subject: [PATCH] Adding privacy indicators to split shade header This change extracts privacy indicators logic from QuickStatusBarHeaderController to a separate class - HeaderPrivacyIconsController - and then uses that controller in SplitShadeHeaderController. Analogically, privacy-related tests are moved to HeaderPrivacyIconsControllerTest. Fixes: 195271863 Test: All tests in com.android.systemui.qs Merged-In: I80e1e808d54c4da1b91a38e8140430edf5a57504 Change-Id: I80e1e808d54c4da1b91a38e8140430edf5a57504 --- .../res/layout/split_shade_header.xml | 11 ++ .../qs/HeaderPrivacyIconsController.kt | 146 +++++++++++++++++ .../systemui/qs/QuickStatusBarHeader.java | 1 - .../qs/QuickStatusBarHeaderController.java | 148 ++---------------- .../systemui/qs/dagger/QSFragmentModule.java | 16 ++ .../phone/SplitShadeHeaderController.kt | 30 +++- .../phone/dagger/StatusBarViewModule.java | 17 ++ .../qs/HeaderPrivacyIconsControllerTest.kt | 128 +++++++++++++++ .../qs/QuickStatusBarHeaderControllerTest.kt | 99 +----------- .../phone/SplitShadeHeaderControllerTest.kt | 12 +- 10 files changed, 369 insertions(+), 239 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt diff --git a/packages/SystemUI/res/layout/split_shade_header.xml b/packages/SystemUI/res/layout/split_shade_header.xml index f2c5b7bd491cd..b6e96ceeaf201 100644 --- a/packages/SystemUI/res/layout/split_shade_header.xml +++ b/packages/SystemUI/res/layout/split_shade_header.xml @@ -83,6 +83,17 @@ android:layout_width="wrap_content" android:layout_height="match_parent" systemui:textAppearance="@style/TextAppearance.QS.Status" /> + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt new file mode 100644 index 0000000000000..3b305bb01cbc8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt @@ -0,0 +1,146 @@ +package com.android.systemui.qs + +import android.view.View +import com.android.internal.R +import com.android.internal.logging.UiEventLogger +import com.android.systemui.privacy.OngoingPrivacyChip +import com.android.systemui.privacy.PrivacyChipEvent +import com.android.systemui.privacy.PrivacyDialogController +import com.android.systemui.privacy.PrivacyItem +import com.android.systemui.privacy.PrivacyItemController +import com.android.systemui.privacy.logging.PrivacyLogger +import com.android.systemui.statusbar.phone.StatusIconContainer +import javax.inject.Inject + +interface ChipVisibilityListener { + fun onChipVisibilityRefreshed(visible: Boolean) +} + +/** + * Controls privacy icons/chip residing in QS header which show up when app is using camera, + * microphone or location. + * Manages their visibility depending on privacy signals coming from [PrivacyItemController]. + * + * Unlike typical controller extending [com.android.systemui.util.ViewController] this view doesn't + * observe its attachment state because depending on where it is used, it might be never detached. + * Instead, parent controller should use [onParentVisible] and [onParentInvisible] to "activate" or + * "deactivate" this controller. + */ +class HeaderPrivacyIconsController @Inject constructor( + private val privacyItemController: PrivacyItemController, + private val uiEventLogger: UiEventLogger, + private val privacyChip: OngoingPrivacyChip, + private val privacyDialogController: PrivacyDialogController, + private val privacyLogger: PrivacyLogger, + private val iconContainer: StatusIconContainer +) { + + var chipVisibilityListener: ChipVisibilityListener? = null + private var listening = false + private var micCameraIndicatorsEnabled = false + private var locationIndicatorsEnabled = false + private var privacyChipLogged = false + private val cameraSlot = privacyChip.resources.getString(R.string.status_bar_camera) + private val micSlot = privacyChip.resources.getString(R.string.status_bar_microphone) + private val locationSlot = privacyChip.resources.getString(R.string.status_bar_location) + + private val picCallback: PrivacyItemController.Callback = + object : PrivacyItemController.Callback { + override fun onPrivacyItemsChanged(privacyItems: List) { + privacyChip.privacyList = privacyItems + setChipVisibility(privacyItems.isNotEmpty()) + } + + override fun onFlagMicCameraChanged(flag: Boolean) { + if (micCameraIndicatorsEnabled != flag) { + micCameraIndicatorsEnabled = flag + update() + } + } + + override fun onFlagLocationChanged(flag: Boolean) { + if (locationIndicatorsEnabled != flag) { + locationIndicatorsEnabled = flag + update() + } + } + + private fun update() { + updatePrivacyIconSlots() + setChipVisibility(privacyChip.privacyList.isNotEmpty()) + } + } + + private fun getChipEnabled() = micCameraIndicatorsEnabled || locationIndicatorsEnabled + + fun onParentVisible() { + privacyChip.setOnClickListener { + // If the privacy chip is visible, it means there were some indicators + uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK) + privacyDialogController.showDialog(privacyChip.context) + } + setChipVisibility(privacyChip.visibility == View.VISIBLE) + micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable + locationIndicatorsEnabled = privacyItemController.locationAvailable + + // Ignore privacy icons because they show in the space above QQS + updatePrivacyIconSlots() + } + + fun onParentInvisible() { + chipVisibilityListener = null + privacyChip.setOnClickListener(null) + } + + fun startListening() { + listening = true + // Get the most up to date info + micCameraIndicatorsEnabled = privacyItemController.micCameraAvailable + locationIndicatorsEnabled = privacyItemController.locationAvailable + privacyItemController.addCallback(picCallback) + } + + fun stopListening() { + listening = false + privacyItemController.removeCallback(picCallback) + privacyChipLogged = false + } + + private fun setChipVisibility(visible: Boolean) { + if (visible && getChipEnabled()) { + privacyLogger.logChipVisible(true) + // Makes sure that the chip is logged as viewed at most once each time QS is opened + // mListening makes sure that the callback didn't return after the user closed QS + if (!privacyChipLogged && listening) { + privacyChipLogged = true + uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW) + } + } else { + privacyLogger.logChipVisible(false) + } + + privacyChip.visibility = if (visible) View.VISIBLE else View.GONE + chipVisibilityListener?.onChipVisibilityRefreshed(visible) + } + + private fun updatePrivacyIconSlots() { + if (getChipEnabled()) { + if (micCameraIndicatorsEnabled) { + iconContainer.addIgnoredSlot(cameraSlot) + iconContainer.addIgnoredSlot(micSlot) + } else { + iconContainer.removeIgnoredSlot(cameraSlot) + iconContainer.removeIgnoredSlot(micSlot) + } + if (locationIndicatorsEnabled) { + iconContainer.addIgnoredSlot(locationSlot) + } else { + iconContainer.removeIgnoredSlot(locationSlot) + } + } else { + iconContainer.removeIgnoredSlot(cameraSlot) + iconContainer.removeIgnoredSlot(micSlot) + iconContainer.removeIgnoredSlot(locationSlot) + } + } +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java index a923effea1e18..3bf8bee881d17 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java @@ -373,7 +373,6 @@ public class QuickStatusBarHeader extends FrameLayout { } void setChipVisibility(boolean visibility) { - mPrivacyChip.setVisibility(visibility ? View.VISIBLE : View.GONE); if (visibility) { // Animates the icons and battery indicator from alpha 0 to 1, when the chip is visible mIconsAlphaAnimator = mIconsAlphaAnimatorFixed; diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java index 3a80764d4c257..2dc4ee4e00644 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeaderController.java @@ -17,26 +17,14 @@ package com.android.systemui.qs; import android.os.Bundle; -import android.view.View; -import android.view.View.OnClickListener; - -import androidx.annotation.NonNull; import com.android.internal.colorextraction.ColorExtractor; -import com.android.internal.logging.UiEventLogger; import com.android.systemui.R; import com.android.systemui.battery.BatteryMeterViewController; import com.android.systemui.colorextraction.SysuiColorExtractor; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.privacy.OngoingPrivacyChip; -import com.android.systemui.privacy.PrivacyChipEvent; -import com.android.systemui.privacy.PrivacyDialogController; -import com.android.systemui.privacy.PrivacyItem; -import com.android.systemui.privacy.PrivacyItemController; -import com.android.systemui.privacy.logging.PrivacyLogger; import com.android.systemui.qs.carrier.QSCarrierGroupController; import com.android.systemui.qs.dagger.QSScope; import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider; @@ -54,23 +42,17 @@ import javax.inject.Inject; * Controller for {@link QuickStatusBarHeader}. */ @QSScope -class QuickStatusBarHeaderController extends ViewController { - private static final String TAG = "QuickStatusBarHeader"; +class QuickStatusBarHeaderController extends ViewController implements + ChipVisibilityListener { - private final PrivacyItemController mPrivacyItemController; - private final ActivityStarter mActivityStarter; - private final UiEventLogger mUiEventLogger; private final QSCarrierGroupController mQSCarrierGroupController; private final QuickQSPanelController mQuickQSPanelController; - private final OngoingPrivacyChip mPrivacyChip; private final Clock mClockView; private final StatusBarIconController mStatusBarIconController; private final DemoModeController mDemoModeController; private final StatusIconContainer mIconContainer; private final StatusBarIconController.TintedIconManager mIconManager; private final DemoMode mDemoModeReceiver; - private final PrivacyLogger mPrivacyLogger; - private final PrivacyDialogController mPrivacyDialogController; private final QSExpansionPathInterpolator mQSExpansionPathInterpolator; private final BatteryMeterViewController mBatteryMeterViewController; private final FeatureFlags mFeatureFlags; @@ -78,83 +60,31 @@ class QuickStatusBarHeaderController extends ViewController privacyItems) { - mPrivacyChip.setPrivacyList(privacyItems); - setChipVisibility(!privacyItems.isEmpty()); - } - - @Override - public void onFlagMicCameraChanged(boolean flag) { - if (mMicCameraIndicatorsEnabled != flag) { - mMicCameraIndicatorsEnabled = flag; - update(); - } - } - - @Override - public void onFlagLocationChanged(boolean flag) { - if (mLocationIndicatorsEnabled != flag) { - mLocationIndicatorsEnabled = flag; - update(); - } - } - - private void update() { - updatePrivacyIconSlots(); - setChipVisibility(!mPrivacyChip.getPrivacyList().isEmpty()); - } - }; - - private View.OnClickListener mOnClickListener = new OnClickListener() { - @Override - public void onClick(View v) { - if (v == mPrivacyChip) { - // If the privacy chip is visible, it means there were some indicators - mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK); - mPrivacyDialogController.showDialog(getContext()); - } - } - }; - @Inject QuickStatusBarHeaderController(QuickStatusBarHeader view, - PrivacyItemController privacyItemController, - ActivityStarter activityStarter, UiEventLogger uiEventLogger, + HeaderPrivacyIconsController headerPrivacyIconsController, StatusBarIconController statusBarIconController, DemoModeController demoModeController, QuickQSPanelController quickQSPanelController, QSCarrierGroupController.Builder qsCarrierGroupControllerBuilder, - PrivacyLogger privacyLogger, SysuiColorExtractor colorExtractor, - PrivacyDialogController privacyDialogController, QSExpansionPathInterpolator qsExpansionPathInterpolator, BatteryMeterViewController batteryMeterViewController, FeatureFlags featureFlags, VariableDateViewController.Factory variableDateViewControllerFactory, StatusBarContentInsetsProvider statusBarContentInsetsProvider) { super(view); - mPrivacyItemController = privacyItemController; - mActivityStarter = activityStarter; - mUiEventLogger = uiEventLogger; + mPrivacyIconsController = headerPrivacyIconsController; mStatusBarIconController = statusBarIconController; mDemoModeController = demoModeController; mQuickQSPanelController = quickQSPanelController; - mPrivacyLogger = privacyLogger; - mPrivacyDialogController = privacyDialogController; mQSExpansionPathInterpolator = qsExpansionPathInterpolator; mBatteryMeterViewController = batteryMeterViewController; mFeatureFlags = featureFlags; @@ -163,8 +93,6 @@ class QuickStatusBarHeaderController extends ViewController() + verify(privacyChip).setOnClickListener(capture(captor)) + + captor.value.onClick(privacyChip) + + verify(privacyDialogController).showDialog(any(Context::class.java)) + } + + private fun setPrivacyController(micCamera: Boolean, location: Boolean) { + whenever(privacyItemController.micCameraAvailable).thenReturn(micCamera) + whenever(privacyItemController.locationAvailable).thenReturn(location) + } +} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt index f85167e6aa638..62b6725cab38e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickStatusBarHeaderControllerTest.kt @@ -20,18 +20,12 @@ import android.content.Context import android.testing.AndroidTestingRunner import android.view.View import androidx.test.filters.SmallTest -import com.android.internal.logging.UiEventLogger import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.colorextraction.SysuiColorExtractor import com.android.systemui.demomode.DemoModeController import com.android.systemui.flags.FeatureFlags -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.privacy.OngoingPrivacyChip -import com.android.systemui.privacy.PrivacyDialogController -import com.android.systemui.privacy.PrivacyItemController -import com.android.systemui.privacy.logging.PrivacyLogger import com.android.systemui.qs.carrier.QSCarrierGroup import com.android.systemui.qs.carrier.QSCarrierGroupController import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider @@ -51,10 +45,10 @@ import org.junit.runner.RunWith import org.mockito.Answers import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.`when` import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -64,11 +58,7 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() { @Mock private lateinit var view: QuickStatusBarHeader @Mock - private lateinit var privacyItemController: PrivacyItemController - @Mock - private lateinit var activityStarter: ActivityStarter - @Mock - private lateinit var uiEventLogger: UiEventLogger + private lateinit var privacyIconsController: HeaderPrivacyIconsController @Mock private lateinit var statusBarIconController: StatusBarIconController @Mock @@ -80,18 +70,12 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() { @Mock private lateinit var qsCarrierGroupController: QSCarrierGroupController @Mock - private lateinit var privacyLogger: PrivacyLogger - @Mock private lateinit var colorExtractor: SysuiColorExtractor @Mock private lateinit var iconContainer: StatusIconContainer @Mock private lateinit var qsCarrierGroup: QSCarrierGroup @Mock - private lateinit var privacyChip: OngoingPrivacyChip - @Mock - private lateinit var privacyDialogController: PrivacyDialogController - @Mock private lateinit var variableDateViewControllerFactory: VariableDateViewController.Factory @Mock private lateinit var variableDateViewController: VariableDateViewController @@ -114,10 +98,6 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() { private lateinit var controller: QuickStatusBarHeaderController - private lateinit var cameraSlotName: String - private lateinit var microphoneSlotName: String - private lateinit var locationSlotName: String - @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -130,25 +110,14 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() { `when`(view.isAttachedToWindow).thenReturn(true) `when`(view.context).thenReturn(context) - cameraSlotName = mContext.resources.getString( - com.android.internal.R.string.status_bar_camera) - microphoneSlotName = mContext.resources.getString( - com.android.internal.R.string.status_bar_microphone) - locationSlotName = mContext.resources.getString( - com.android.internal.R.string.status_bar_location) - controller = QuickStatusBarHeaderController( view, - privacyItemController, - activityStarter, - uiEventLogger, + privacyIconsController, statusBarIconController, demoModeController, quickQSPanelController, qsCarrierGroupControllerBuilder, - privacyLogger, colorExtractor, - privacyDialogController, qsExpansionPathInterpolator, batteryMeterViewController, featureFlags, @@ -167,62 +136,6 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() { assertThat(clock.isClickable).isFalse() } - @Test - fun testIgnoredSlotsOnAttached_noIndicators() { - setPrivacyController(micCamera = false, location = false) - - controller.init() - - verify(iconContainer).removeIgnoredSlot(cameraSlotName) - verify(iconContainer).removeIgnoredSlot(microphoneSlotName) - verify(iconContainer).removeIgnoredSlot(locationSlotName) - } - - @Test - fun testIgnoredSlotsOnAttached_onlyMicCamera() { - setPrivacyController(micCamera = true, location = false) - - controller.init() - - verify(iconContainer).addIgnoredSlot(cameraSlotName) - verify(iconContainer).addIgnoredSlot(microphoneSlotName) - verify(iconContainer).removeIgnoredSlot(locationSlotName) - } - - @Test - fun testIgnoredSlotsOnAttached_onlyLocation() { - setPrivacyController(micCamera = false, location = true) - - controller.init() - - verify(iconContainer).removeIgnoredSlot(cameraSlotName) - verify(iconContainer).removeIgnoredSlot(microphoneSlotName) - verify(iconContainer).addIgnoredSlot(locationSlotName) - } - - @Test - fun testIgnoredSlotsOnAttached_locationMicCamera() { - setPrivacyController(micCamera = true, location = true) - - controller.init() - - verify(iconContainer).addIgnoredSlot(cameraSlotName) - verify(iconContainer).addIgnoredSlot(microphoneSlotName) - verify(iconContainer).addIgnoredSlot(locationSlotName) - } - - @Test - fun testPrivacyChipClicked() { - controller.init() - - val captor = argumentCaptor() - verify(privacyChip).setOnClickListener(capture(captor)) - - captor.value.onClick(privacyChip) - - verify(privacyDialogController).showDialog(any(Context::class.java)) - } - @Test fun testSingleCarrierListenerAttachedOnInit() { controller.init() @@ -292,14 +205,8 @@ class QuickStatusBarHeaderControllerTest : SysuiTestCase() { `when`(view.findViewById(anyInt())).thenReturn(mockView) `when`(view.findViewById(R.id.carrier_group)).thenReturn(qsCarrierGroup) `when`(view.findViewById(R.id.statusIcons)).thenReturn(iconContainer) - `when`(view.findViewById(R.id.privacy_chip)).thenReturn(privacyChip) `when`(view.findViewById(R.id.clock)).thenReturn(clock) `when`(view.requireViewById(R.id.date)).thenReturn(variableDateView) `when`(view.requireViewById(R.id.date_clock)).thenReturn(variableDateView) } - - private fun setPrivacyController(micCamera: Boolean, location: Boolean) { - `when`(privacyItemController.micCameraAvailable).thenReturn(micCamera) - `when`(privacyItemController.locationAvailable).thenReturn(location) - } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt index 0cf0bd300419d..2e7f8a2897f2c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SplitShadeHeaderControllerTest.kt @@ -9,6 +9,7 @@ import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.flags.FeatureFlags +import com.android.systemui.qs.HeaderPrivacyIconsController import com.android.systemui.qs.carrier.QSCarrierGroupController import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -34,6 +35,7 @@ class SplitShadeHeaderControllerTest : SysuiTestCase() { @Mock private lateinit var featureFlags: FeatureFlags @Mock private lateinit var batteryMeterView: BatteryMeterView @Mock private lateinit var batteryMeterViewController: BatteryMeterViewController + @Mock private lateinit var privacyIconsController: HeaderPrivacyIconsController @JvmField @Rule val mockitoRule = MockitoJUnit.rule() var viewVisibility = View.GONE @@ -56,8 +58,14 @@ class SplitShadeHeaderControllerTest : SysuiTestCase() { } whenever(view.visibility).thenAnswer { _ -> viewVisibility } whenever(featureFlags.useCombinedQSHeaders()).thenReturn(false) - splitShadeHeaderController = SplitShadeHeaderController(view, statusBarIconController, - qsCarrierGroupControllerBuilder, featureFlags, batteryMeterViewController) + splitShadeHeaderController = SplitShadeHeaderController( + view, + statusBarIconController, + privacyIconsController, + qsCarrierGroupControllerBuilder, + featureFlags, + batteryMeterViewController + ) } @Test