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