Add back privacy chip
This adds back the privacy chip classes (Controller and view).
Change to using Executors and DeviceConfigProxy, also fix tests that
were flaky before.
Test: SystemUITests
Bug: 160966908
Change-Id: Id3e5981a87c33a8cabe7ce348f9512d81ad2b1d8
Merged-In: Id3e5981a87c33a8cabe7ce348f9512d81ad2b1d8
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index d238d0e..ea3d2de 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -120,6 +120,13 @@
*/
public static final String HASH_SALT_MAX_DAYS = "hash_salt_max_days";
+ // Flag related to Privacy Indicators
+
+ /**
+ * Whether the Permissions Hub is showing.
+ */
+ public static final String PROPERTY_PERMISSIONS_HUB_ENABLED = "permissions_hub_2_enabled";
+
// Flags related to Assistant
/**
diff --git a/packages/CarSystemUI/res/values/dimens.xml b/packages/CarSystemUI/res/values/dimens.xml
index cb321cd..8359dac 100644
--- a/packages/CarSystemUI/res/values/dimens.xml
+++ b/packages/CarSystemUI/res/values/dimens.xml
@@ -81,6 +81,21 @@
<dimen name="car_keyline_2">96dp</dimen>
<dimen name="car_keyline_3">128dp</dimen>
+ <!-- Height of icons in Ongoing App Ops dialog. Both App Op icon and application icon -->
+ <dimen name="ongoing_appops_dialog_icon_height">48dp</dimen>
+ <!-- Margin between text lines in Ongoing App Ops dialog -->
+ <dimen name="ongoing_appops_dialog_text_margin">15dp</dimen>
+ <!-- Padding around Ongoing App Ops dialog content -->
+ <dimen name="ongoing_appops_dialog_content_padding">24dp</dimen>
+ <!-- Margins around the Ongoing App Ops chip. In landscape, the side margins are 0 -->
+ <dimen name="ongoing_appops_chip_margin">12dp</dimen>
+ <!-- Start and End padding for Ongoing App Ops chip -->
+ <dimen name="ongoing_appops_chip_side_padding">6dp</dimen>
+ <!-- Padding between background of Ongoing App Ops chip and content -->
+ <dimen name="ongoing_appops_chip_bg_padding">4dp</dimen>
+ <!-- Radius of Ongoing App Ops chip corners -->
+ <dimen name="ongoing_appops_chip_bg_corner_radius">12dp</dimen>
+
<!-- Car volume dimens. -->
<dimen name="car_volume_item_icon_size">@dimen/car_primary_icon_size</dimen>
<dimen name="car_volume_item_height">@*android:dimen/car_single_line_list_item_height</dimen>
diff --git a/packages/SystemUI/res/drawable/privacy_chip_bg.xml b/packages/SystemUI/res/drawable/privacy_chip_bg.xml
new file mode 100644
index 0000000..827cf4a9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/privacy_chip_bg.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <solid android:color="#242424" /> <!-- 14% of white -->
+ <padding android:paddingTop="@dimen/ongoing_appops_chip_bg_padding"
+ android:paddingBottom="@dimen/ongoing_appops_chip_bg_padding" />
+ <corners android:radius="@dimen/ongoing_appops_chip_bg_corner_radius" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
new file mode 100644
index 0000000..3c30632
--- /dev/null
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2020 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.
+-->
+
+
+<com.android.systemui.privacy.OngoingPrivacyChip
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/privacy_chip"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:layout_gravity="center_vertical|end"
+ android:focusable="true" >
+
+ <FrameLayout
+ android:id="@+id/background"
+ android:layout_height="@dimen/ongoing_appops_chip_height"
+ android:layout_width="wrap_content"
+ android:minWidth="48dp"
+ android:layout_gravity="center_vertical">
+ <LinearLayout
+ android:id="@+id/icons_container"
+ android:layout_height="match_parent"
+ android:layout_width="wrap_content"
+ android:gravity="center_vertical"
+ />
+ </FrameLayout>
+</com.android.systemui.privacy.OngoingPrivacyChip>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
index be86e5f..3c74801 100644
--- a/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
+++ b/packages/SystemUI/res/layout/quick_status_bar_header_system_icons.xml
@@ -14,7 +14,7 @@
** See the License for the specific language governing permissions and
** limitations under the License.
-->
-<FrameLayout
+<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/quick_status_bar_system_icons"
@@ -27,6 +27,13 @@
android:clickable="true"
android:paddingTop="@dimen/status_bar_padding_top" >
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:gravity="center_vertical|start" >
+
<com.android.systemui.statusbar.policy.Clock
android:id="@+id/clock"
android:layout_width="wrap_content"
@@ -38,5 +45,23 @@
android:singleLine="true"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
systemui:showDark="false" />
+ </LinearLayout>
-</FrameLayout>
+ <android.widget.Space
+ android:id="@+id/space"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_vertical|center_horizontal"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_weight="1"
+ android:orientation="horizontal"
+ android:gravity="center_vertical|end" >
+
+ <include layout="@layout/ongoing_privacy_chip" />
+
+ </LinearLayout>
+</LinearLayout>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 72f623e..5e5df6b 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -510,6 +510,8 @@
<item>com.android.systemui</item>
</string-array>
+ <integer name="ongoing_appops_dialog_max_apps">5</integer>
+
<!-- Launcher package name for overlaying icons. -->
<string name="launcher_overlayable_package" translatable="false">com.android.launcher3</string>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index eb8758c..5984d8d 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1175,6 +1175,23 @@
<!-- How much into a DisplayCutout's bounds we can go, on each side -->
<dimen name="display_cutout_margin_consumption">0px</dimen>
+
+ <!-- Height of the Ongoing App Ops chip -->
+ <dimen name="ongoing_appops_chip_height">32dp</dimen>
+ <!-- Padding between background of Ongoing App Ops chip and content -->
+ <dimen name="ongoing_appops_chip_bg_padding">8dp</dimen>
+ <!-- Side padding between background of Ongoing App Ops chip and content -->
+ <dimen name="ongoing_appops_chip_side_padding">8dp</dimen>
+ <!-- Margin between icons of Ongoing App Ops chip when QQS-->
+ <dimen name="ongoing_appops_chip_icon_margin_collapsed">0dp</dimen>
+ <!-- Margin between icons of Ongoing App Ops chip when QS-->
+ <dimen name="ongoing_appops_chip_icon_margin_expanded">2dp</dimen>
+ <!-- Icon size of Ongoing App Ops chip -->
+ <dimen name="ongoing_appops_chip_icon_size">@dimen/status_bar_icon_drawing_size</dimen>
+ <!-- Radius of Ongoing App Ops chip corners -->
+ <dimen name="ongoing_appops_chip_bg_corner_radius">16dp</dimen>
+
+
<!-- How much each bubble is elevated. -->
<dimen name="bubble_elevation">1dp</dimen>
<!-- How much the bubble flyout text container is elevated. -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index db45a60..16688b4 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2608,6 +2608,27 @@
app for debugging. Will not be seen by users. [CHAR LIMIT=20] -->
<string name="heap_dump_tile_name">Dump SysUI Heap</string>
+ <!-- Content description for ongoing privacy chip. Use with a single app [CHAR LIMIT=NONE]-->
+ <string name="ongoing_privacy_chip_content_single_app"><xliff:g id="app" example="Example App">%1$s</xliff:g> is using your <xliff:g id="types_list" example="camera, location">%2$s</xliff:g>.</string>
+
+ <!-- Content description for ongoing privacy chip. Use with multiple apps [CHAR LIMIT=NONE]-->
+ <string name="ongoing_privacy_chip_content_multiple_apps">Applications are using your <xliff:g id="types_list" example="camera, location">%s</xliff:g>.</string>
+
+ <!-- Separator for types. Include spaces before and after if needed [CHAR LIMIT=10] -->
+ <string name="ongoing_privacy_dialog_separator">,\u0020</string>
+
+ <!-- Separator for types, before last type. Include spaces before and after if needed [CHAR LIMIT=10] -->
+ <string name="ongoing_privacy_dialog_last_separator">\u0020and\u0020</string>
+
+ <!-- Text for camera app op [CHAR LIMIT=20]-->
+ <string name="privacy_type_camera">camera</string>
+
+ <!-- Text for location app op [CHAR LIMIT=20]-->
+ <string name="privacy_type_location">location</string>
+
+ <!-- Text for microphone app op [CHAR LIMIT=20]-->
+ <string name="privacy_type_microphone">microphone</string>
+
<!-- Text for the quick setting tile for sensor privacy [CHAR LIMIT=30] -->
<string name="sensor_privacy_mode">Sensors off</string>
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 02d2b8e..59580bb 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -54,6 +54,7 @@
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.power.EnhancedEstimates;
import com.android.systemui.power.PowerUI;
+import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.recents.Recents;
import com.android.systemui.screenrecord.RecordingController;
@@ -294,6 +295,7 @@
@Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager;
@Inject Lazy<AutoHideController> mAutoHideController;
@Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener;
+ @Inject Lazy<PrivacyItemController> mPrivacyItemController;
@Inject @Background Lazy<Looper> mBgLooper;
@Inject @Background Lazy<Handler> mBgHandler;
@Inject @Main Lazy<Looper> mMainLooper;
@@ -491,6 +493,7 @@
mProviders.put(ForegroundServiceNotificationListener.class,
mForegroundServiceNotificationListener::get);
mProviders.put(ClockManager.class, mClockManager::get);
+ mProviders.put(PrivacyItemController.class, mPrivacyItemController::get);
mProviders.put(ActivityManagerWrapper.class, mActivityManagerWrapper::get);
mProviders.put(DevicePolicyManagerWrapper.class, mDevicePolicyManagerWrapper::get);
mProviders.put(PackageManagerWrapper.class, mPackageManagerWrapper::get);
diff --git a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
index 941de2d..fc7cc7e 100644
--- a/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/appops/AppOpsControllerImpl.java
@@ -57,7 +57,6 @@
private static final long NOTED_OP_TIME_DELAY_MS = 5000;
private static final String TAG = "AppOpsControllerImpl";
private static final boolean DEBUG = false;
- private final Context mContext;
private final AppOpsManager mAppOps;
private H mBGHandler;
@@ -83,7 +82,6 @@
Context context,
@Background Looper bgLooper,
DumpManager dumpManager) {
- mContext = context;
mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
mBGHandler = new H(bgLooper);
final int numOps = OPS.length;
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
new file mode 100644
index 0000000..48769cd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.LinearLayout
+import com.android.systemui.R
+
+class OngoingPrivacyChip @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttrs: Int = 0,
+ defStyleRes: Int = 0
+) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes) {
+
+ private val iconMarginExpanded = context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_appops_chip_icon_margin_expanded)
+ private val iconMarginCollapsed = context.resources.getDimensionPixelSize(
+ R.dimen.ongoing_appops_chip_icon_margin_collapsed)
+ private val iconSize =
+ context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_icon_size)
+ private val iconColor = context.resources.getColor(
+ R.color.status_bar_clock_color, context.theme)
+ private val sidePadding =
+ context.resources.getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
+ private val backgroundDrawable = context.getDrawable(R.drawable.privacy_chip_bg)
+ private lateinit var iconsContainer: LinearLayout
+ private lateinit var back: FrameLayout
+ var expanded = false
+ set(value) {
+ if (value != field) {
+ field = value
+ updateView()
+ }
+ }
+
+ var builder = PrivacyChipBuilder(context, emptyList<PrivacyItem>())
+ var privacyList = emptyList<PrivacyItem>()
+ set(value) {
+ field = value
+ builder = PrivacyChipBuilder(context, value)
+ updateView()
+ }
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+
+ back = requireViewById(R.id.background)
+ iconsContainer = requireViewById(R.id.icons_container)
+ }
+
+ // Should only be called if the builder icons or app changed
+ private fun updateView() {
+ back.background = if (expanded) backgroundDrawable else null
+ val padding = if (expanded) sidePadding else 0
+ back.setPaddingRelative(padding, 0, padding, 0)
+ fun setIcons(chipBuilder: PrivacyChipBuilder, iconsContainer: ViewGroup) {
+ iconsContainer.removeAllViews()
+ chipBuilder.generateIcons().forEachIndexed { i, it ->
+ it.mutate()
+ it.setTint(iconColor)
+ val image = ImageView(context).apply {
+ setImageDrawable(it)
+ scaleType = ImageView.ScaleType.CENTER_INSIDE
+ }
+ iconsContainer.addView(image, iconSize, iconSize)
+ if (i != 0) {
+ val lp = image.layoutParams as MarginLayoutParams
+ lp.marginStart = if (expanded) iconMarginExpanded else iconMarginCollapsed
+ image.layoutParams = lp
+ }
+ }
+ }
+
+ if (!privacyList.isEmpty()) {
+ generateContentDescription()
+ setIcons(builder, iconsContainer)
+ val lp = iconsContainer.layoutParams as FrameLayout.LayoutParams
+ lp.gravity = Gravity.CENTER_VERTICAL or
+ (if (expanded) Gravity.CENTER_HORIZONTAL else Gravity.END)
+ iconsContainer.layoutParams = lp
+ } else {
+ iconsContainer.removeAllViews()
+ }
+ requestLayout()
+ }
+
+ private fun generateContentDescription() {
+ val typesText = builder.joinTypes()
+ contentDescription = context.getString(
+ R.string.ongoing_privacy_chip_content_multiple_apps, typesText)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt
new file mode 100644
index 0000000..1d2e747
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipBuilder.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import android.content.Context
+import com.android.systemui.R
+
+class PrivacyChipBuilder(private val context: Context, itemsList: List<PrivacyItem>) {
+
+ val appsAndTypes: List<Pair<PrivacyApplication, List<PrivacyType>>>
+ val types: List<PrivacyType>
+ private val separator = context.getString(R.string.ongoing_privacy_dialog_separator)
+ private val lastSeparator = context.getString(R.string.ongoing_privacy_dialog_last_separator)
+
+ init {
+ appsAndTypes = itemsList.groupBy({ it.application }, { it.privacyType })
+ .toList()
+ .sortedWith(compareBy({ -it.second.size }, // Sort by number of AppOps
+ { it.second.min() })) // Sort by "smallest" AppOpp (Location is largest)
+ types = itemsList.map { it.privacyType }.distinct().sorted()
+ }
+
+ fun generateIcons() = types.map { it.getIcon(context) }
+
+ private fun <T> List<T>.joinWithAnd(): StringBuilder {
+ return subList(0, size - 1).joinTo(StringBuilder(), separator = separator).apply {
+ append(lastSeparator)
+ append(this@joinWithAnd.last())
+ }
+ }
+
+ fun joinTypes(): String {
+ return when (types.size) {
+ 0 -> ""
+ 1 -> types[0].getName(context)
+ else -> types.map { it.getName(context) }.joinWithAnd().toString()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt
new file mode 100644
index 0000000..1f24fde
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyChipEvent.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import com.android.internal.logging.UiEvent
+import com.android.internal.logging.UiEventLogger
+
+enum class PrivacyChipEvent(private val _id: Int) : UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "Privacy chip is viewed by the user. Logged at most once per time QS is visible")
+ ONGOING_INDICATORS_CHIP_VIEW(601),
+
+ @UiEvent(doc = "Privacy chip is clicked")
+ ONGOING_INDICATORS_CHIP_CLICK(602);
+
+ override fun getId() = _id
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
new file mode 100644
index 0000000..3da1363
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import android.content.Context
+import com.android.systemui.R
+
+typealias Privacy = PrivacyType
+
+enum class PrivacyType(val nameId: Int, val iconId: Int) {
+ // This is uses the icons used by the corresponding permission groups in the AndroidManifest
+ TYPE_CAMERA(R.string.privacy_type_camera,
+ com.android.internal.R.drawable.perm_group_camera),
+ TYPE_MICROPHONE(R.string.privacy_type_microphone,
+ com.android.internal.R.drawable.perm_group_microphone),
+ TYPE_LOCATION(R.string.privacy_type_location,
+ com.android.internal.R.drawable.perm_group_location);
+
+ fun getName(context: Context) = context.resources.getString(nameId)
+
+ fun getIcon(context: Context) = context.resources.getDrawable(iconId, context.theme)
+}
+
+data class PrivacyItem(val privacyType: PrivacyType, val application: PrivacyApplication)
+
+data class PrivacyApplication(val packageName: String, val uid: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
new file mode 100644
index 0000000..8001ecc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import android.app.ActivityManager
+import android.app.AppOpsManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.DeviceConfig
+import com.android.internal.annotations.VisibleForTesting
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.systemui.Dumpable
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.DeviceConfigProxy
+import com.android.systemui.util.concurrency.DelayableExecutor
+import java.io.FileDescriptor
+import java.io.PrintWriter
+import java.lang.ref.WeakReference
+import java.util.concurrent.Executor
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class PrivacyItemController @Inject constructor(
+ context: Context,
+ private val appOpsController: AppOpsController,
+ @Main uiExecutor: DelayableExecutor,
+ @Background private val bgExecutor: Executor,
+ private val broadcastDispatcher: BroadcastDispatcher,
+ private val deviceConfigProxy: DeviceConfigProxy,
+ dumpManager: DumpManager
+) : Dumpable {
+
+ @VisibleForTesting
+ internal companion object {
+ val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
+ AppOpsManager.OP_RECORD_AUDIO,
+ AppOpsManager.OP_COARSE_LOCATION,
+ AppOpsManager.OP_FINE_LOCATION)
+ val intentFilter = IntentFilter().apply {
+ addAction(Intent.ACTION_USER_SWITCHED)
+ addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)
+ addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
+ }
+ const val TAG = "PrivacyItemController"
+ }
+
+ @VisibleForTesting
+ internal var privacyList = emptyList<PrivacyItem>()
+ @Synchronized get() = field.toList() // Returns a shallow copy of the list
+ @Synchronized set
+
+ fun isPermissionsHubEnabled(): Boolean {
+ return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
+ }
+
+ private val userManager = context.getSystemService(UserManager::class.java)
+ private var currentUserIds = emptyList<Int>()
+ private var listening = false
+ private val callbacks = mutableListOf<WeakReference<Callback>>()
+ private val internalUiExecutor = MyExecutor(WeakReference(this), uiExecutor)
+
+ private val notifyChanges = Runnable {
+ val list = privacyList
+ callbacks.forEach { it.get()?.privacyChanged(list) }
+ }
+
+ private val updateListAndNotifyChanges = Runnable {
+ updatePrivacyList()
+ uiExecutor.execute(notifyChanges)
+ }
+
+ private var indicatorsAvailable = isPermissionsHubEnabled()
+ @VisibleForTesting
+ internal val devicePropertiesChangedListener =
+ object : DeviceConfig.OnPropertiesChangedListener {
+ override fun onPropertiesChanged(properties: DeviceConfig.Properties) {
+ if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace()) &&
+ properties.getKeyset().contains(
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) {
+ indicatorsAvailable = properties.getBoolean(
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
+ internalUiExecutor.updateListeningState()
+ }
+ }
+ }
+
+ private val cb = object : AppOpsController.Callback {
+ override fun onActiveStateChanged(
+ code: Int,
+ uid: Int,
+ packageName: String,
+ active: Boolean
+ ) {
+ val userId = UserHandle.getUserId(uid)
+ if (userId in currentUserIds) {
+ update(false)
+ }
+ }
+ }
+
+ @VisibleForTesting
+ internal var userSwitcherReceiver = Receiver()
+ set(value) {
+ unregisterReceiver()
+ field = value
+ if (listening) registerReceiver()
+ }
+
+ init {
+ deviceConfigProxy.addOnPropertiesChangedListener(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ uiExecutor,
+ devicePropertiesChangedListener)
+ dumpManager.registerDumpable(TAG, this)
+ }
+
+ private fun unregisterReceiver() {
+ broadcastDispatcher.unregisterReceiver(userSwitcherReceiver)
+ }
+
+ private fun registerReceiver() {
+ broadcastDispatcher.registerReceiver(userSwitcherReceiver, intentFilter,
+ null /* handler */, UserHandle.ALL)
+ }
+
+ private fun update(updateUsers: Boolean) {
+ bgExecutor.execute {
+ if (updateUsers) {
+ val currentUser = ActivityManager.getCurrentUser()
+ currentUserIds = userManager.getProfiles(currentUser).map { it.id }
+ }
+ updateListAndNotifyChanges.run()
+ }
+ }
+
+ /**
+ * Updates listening status based on whether there are callbacks and the indicators are enabled
+ *
+ * This is only called from private (add/remove)Callback and from the config listener, all in
+ * main thread.
+ */
+ private fun setListeningState() {
+ val listen = !callbacks.isEmpty() and indicatorsAvailable
+ if (listening == listen) return
+ listening = listen
+ if (listening) {
+ appOpsController.addCallback(OPS, cb)
+ registerReceiver()
+ update(true)
+ } else {
+ appOpsController.removeCallback(OPS, cb)
+ unregisterReceiver()
+ // Make sure that we remove all indicators and notify listeners if we are not
+ // listening anymore due to indicators being disabled
+ update(false)
+ }
+ }
+
+ private fun addCallback(callback: WeakReference<Callback>) {
+ callbacks.add(callback)
+ if (callbacks.isNotEmpty() && !listening) {
+ internalUiExecutor.updateListeningState()
+ }
+ // Notify this callback if we didn't set to listening
+ else if (listening) {
+ internalUiExecutor.execute(NotifyChangesToCallback(callback.get(), privacyList))
+ }
+ }
+
+ private fun removeCallback(callback: WeakReference<Callback>) {
+ // Removes also if the callback is null
+ callbacks.removeIf { it.get()?.equals(callback.get()) ?: true }
+ if (callbacks.isEmpty()) {
+ internalUiExecutor.updateListeningState()
+ }
+ }
+
+ fun addCallback(callback: Callback) {
+ internalUiExecutor.addCallback(callback)
+ }
+
+ fun removeCallback(callback: Callback) {
+ internalUiExecutor.removeCallback(callback)
+ }
+
+ private fun updatePrivacyList() {
+ if (!listening) {
+ privacyList = emptyList()
+ return
+ }
+ val list = currentUserIds.flatMap { appOpsController.getActiveAppOpsForUser(it) }
+ .mapNotNull { toPrivacyItem(it) }.distinct()
+ privacyList = list
+ }
+
+ private fun toPrivacyItem(appOpItem: AppOpItem): PrivacyItem? {
+ val type: PrivacyType = when (appOpItem.code) {
+ AppOpsManager.OP_CAMERA -> PrivacyType.TYPE_CAMERA
+ AppOpsManager.OP_COARSE_LOCATION -> PrivacyType.TYPE_LOCATION
+ AppOpsManager.OP_FINE_LOCATION -> PrivacyType.TYPE_LOCATION
+ AppOpsManager.OP_RECORD_AUDIO -> PrivacyType.TYPE_MICROPHONE
+ else -> return null
+ }
+ val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid)
+ return PrivacyItem(type, app)
+ }
+
+ // Used by containing class to get notified of changes
+ interface Callback {
+ fun privacyChanged(privacyItems: List<PrivacyItem>)
+ }
+
+ internal inner class Receiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intentFilter.hasAction(intent.action)) {
+ update(true)
+ }
+ }
+ }
+
+ private class NotifyChangesToCallback(
+ private val callback: Callback?,
+ private val list: List<PrivacyItem>
+ ) : Runnable {
+ override fun run() {
+ callback?.privacyChanged(list)
+ }
+ }
+
+ override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
+ pw.println("PrivacyItemController state:")
+ pw.println(" Listening: $listening")
+ pw.println(" Current user ids: $currentUserIds")
+ pw.println(" Privacy Items:")
+ privacyList.forEach {
+ pw.print(" ")
+ pw.println(it.toString())
+ }
+ pw.println(" Callbacks:")
+ callbacks.forEach {
+ it.get()?.let {
+ pw.print(" ")
+ pw.println(it.toString())
+ }
+ }
+ }
+
+ private class MyExecutor(
+ private val outerClass: WeakReference<PrivacyItemController>,
+ private val delegate: DelayableExecutor
+ ) : Executor {
+
+ private var listeningCanceller: Runnable? = null
+
+ override fun execute(command: Runnable) {
+ delegate.execute(command)
+ }
+
+ fun updateListeningState() {
+ listeningCanceller?.run()
+ listeningCanceller = delegate.executeDelayed({
+ outerClass.get()?.setListeningState()
+ }, 0L)
+ }
+
+ fun addCallback(callback: Callback) {
+ outerClass.get()?.addCallback(WeakReference(callback))
+ }
+
+ fun removeCallback(callback: Callback) {
+ outerClass.get()?.removeCallback(WeakReference(callback))
+ }
+ }
+}
\ 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 b07b1a9..a559a54 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -31,7 +31,9 @@
import android.graphics.Rect;
import android.media.AudioManager;
import android.os.Handler;
+import android.os.Looper;
import android.provider.AlarmClock;
+import android.provider.DeviceConfig;
import android.provider.Settings;
import android.service.notification.ZenModeConfig;
import android.text.format.DateUtils;
@@ -46,7 +48,9 @@
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.RelativeLayout;
+import android.widget.Space;
import android.widget.TextView;
import androidx.annotation.NonNull;
@@ -55,6 +59,8 @@
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LifecycleRegistry;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.logging.UiEventLogger;
import com.android.settingslib.Utils;
import com.android.systemui.BatteryMeterView;
import com.android.systemui.DualToneHandler;
@@ -63,6 +69,11 @@
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.DarkIconDispatcher;
import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.privacy.OngoingPrivacyChip;
+import com.android.systemui.privacy.PrivacyChipBuilder;
+import com.android.systemui.privacy.PrivacyChipEvent;
+import com.android.systemui.privacy.PrivacyItem;
+import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.qs.QSDetail.Callback;
import com.android.systemui.qs.carrier.QSCarrierGroup;
import com.android.systemui.statusbar.CommandQueue;
@@ -101,7 +112,6 @@
private static final int TOOLTIP_NOT_YET_SHOWN_COUNT = 0;
public static final int MAX_TOOLTIP_SHOWN_COUNT = 2;
- private final Handler mHandler = new Handler();
private final NextAlarmController mAlarmController;
private final ZenModeController mZenController;
private final StatusBarIconController mStatusBarIconController;
@@ -140,9 +150,14 @@
private View mRingerContainer;
private Clock mClockView;
private DateView mDateView;
+ private OngoingPrivacyChip mPrivacyChip;
+ private Space mSpace;
private BatteryMeterView mBatteryRemainingIcon;
private RingerModeTracker mRingerModeTracker;
+ private boolean mPermissionsHubEnabled;
+ private PrivacyItemController mPrivacyItemController;
+ private final UiEventLogger mUiEventLogger;
// Used for RingerModeTracker
private final LifecycleRegistry mLifecycle = new LifecycleRegistry(this);
@@ -156,22 +171,49 @@
private int mCutOutPaddingRight;
private float mExpandedHeaderAlpha = 1.0f;
private float mKeyguardExpansionFraction;
+ private boolean mPrivacyChipLogged = false;
+
+ private final DeviceConfig.OnPropertiesChangedListener mPropertiesListener =
+ new DeviceConfig.OnPropertiesChangedListener() {
+ @Override
+ public void onPropertiesChanged(DeviceConfig.Properties properties) {
+ if (DeviceConfig.NAMESPACE_PRIVACY.equals(properties.getNamespace())
+ && properties.getKeyset()
+ .contains(SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED)) {
+ mPermissionsHubEnabled = properties.getBoolean(
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false);
+ StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
+ iconContainer.setIgnoredSlots(getIgnoredIconSlots());
+ }
+ }
+ };
+
+ private PrivacyItemController.Callback mPICCallback = new PrivacyItemController.Callback() {
+ @Override
+ public void privacyChanged(List<PrivacyItem> privacyItems) {
+ mPrivacyChip.setPrivacyList(privacyItems);
+ setChipVisibility(!privacyItems.isEmpty());
+ }
+ };
@Inject
public QuickStatusBarHeader(@Named(VIEW_CONTEXT) Context context, AttributeSet attrs,
NextAlarmController nextAlarmController, ZenModeController zenModeController,
StatusBarIconController statusBarIconController,
- ActivityStarter activityStarter,
- CommandQueue commandQueue, RingerModeTracker ringerModeTracker) {
+ ActivityStarter activityStarter, PrivacyItemController privacyItemController,
+ CommandQueue commandQueue, RingerModeTracker ringerModeTracker,
+ UiEventLogger uiEventLogger) {
super(context, attrs);
mAlarmController = nextAlarmController;
mZenController = zenModeController;
mStatusBarIconController = statusBarIconController;
mActivityStarter = activityStarter;
+ mPrivacyItemController = privacyItemController;
mDualToneHandler = new DualToneHandler(
new ContextThemeWrapper(context, R.style.QSHeaderTheme));
mCommandQueue = commandQueue;
mRingerModeTracker = ringerModeTracker;
+ mUiEventLogger = uiEventLogger;
}
@Override
@@ -198,8 +240,11 @@
mRingerModeTextView = findViewById(R.id.ringer_mode_text);
mRingerContainer = findViewById(R.id.ringer_container);
mRingerContainer.setOnClickListener(this::onClick);
+ mPrivacyChip = findViewById(R.id.privacy_chip);
+ mPrivacyChip.setOnClickListener(this::onClick);
mCarrierGroup = findViewById(R.id.carrier_group);
+
updateResources();
Rect tintArea = new Rect(0, 0, 0, 0);
@@ -219,6 +264,7 @@
mClockView = findViewById(R.id.clock);
mClockView.setOnClickListener(this);
mDateView = findViewById(R.id.date);
+ mSpace = findViewById(R.id.space);
// Tint for the battery icons are handled in setupHost()
mBatteryRemainingIcon = findViewById(R.id.batteryRemainingIcon);
@@ -229,6 +275,8 @@
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
mRingerModeTextView.setSelected(true);
mNextAlarmTextView.setSelected(true);
+
+ mPermissionsHubEnabled = mPrivacyItemController.isPermissionsHubEnabled();
}
public QuickQSPanel getHeaderQsPanel() {
@@ -241,6 +289,10 @@
com.android.internal.R.string.status_bar_camera));
ignored.add(mContext.getResources().getString(
com.android.internal.R.string.status_bar_microphone));
+ if (mPermissionsHubEnabled) {
+ ignored.add(mContext.getResources().getString(
+ com.android.internal.R.string.status_bar_location));
+ }
return ignored;
}
@@ -256,6 +308,20 @@
}
}
+ private void setChipVisibility(boolean chipVisible) {
+ if (chipVisible && mPermissionsHubEnabled) {
+ mPrivacyChip.setVisibility(View.VISIBLE);
+ // 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 (!mPrivacyChipLogged && mListening) {
+ mPrivacyChipLogged = true;
+ mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_VIEW);
+ }
+ } else {
+ mPrivacyChip.setVisibility(View.GONE);
+ }
+ }
+
private boolean updateRingerStatus() {
boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
CharSequence originalRingerText = mRingerModeTextView.getText();
@@ -363,6 +429,7 @@
updateStatusIconAlphaAnimator();
updateHeaderTextContainerAlphaAnimator();
+ updatePrivacyChipAlphaAnimator();
}
private void updateStatusIconAlphaAnimator() {
@@ -377,6 +444,12 @@
.build();
}
+ private void updatePrivacyChipAlphaAnimator() {
+ mPrivacyChipAlphaAnimator = new TouchAnimator.Builder()
+ .addFloat(mPrivacyChip, "alpha", 1, 0, 1)
+ .build();
+ }
+
public void setExpanded(boolean expanded) {
if (mExpanded == expanded) return;
mExpanded = expanded;
@@ -415,6 +488,10 @@
mHeaderTextContainerView.setVisibility(INVISIBLE);
}
}
+ if (mPrivacyChipAlphaAnimator != null) {
+ mPrivacyChip.setExpanded(expansionFraction > 0.5);
+ mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction);
+ }
if (expansionFraction < 1 && expansionFraction > 0.99) {
if (mHeaderQsPanel.switchTileLayout()) {
updateResources();
@@ -442,6 +519,9 @@
});
mStatusBarIconController.addIconGroup(mIconManager);
requestApplyInsets();
+ // Change the ignored slots when DeviceConfig flag changes
+ DeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
+ mContext.getMainExecutor(), mPropertiesListener);
}
@Override
@@ -453,6 +533,31 @@
Pair<Integer, Integer> padding =
StatusBarWindowView.paddingNeededForCutoutAndRoundedCorner(
cutout, cornerCutoutPadding, -1);
+ if (padding == null) {
+ mSystemIconsView.setPaddingRelative(
+ getResources().getDimensionPixelSize(R.dimen.status_bar_padding_start), 0,
+ getResources().getDimensionPixelSize(R.dimen.status_bar_padding_end), 0);
+ } else {
+ mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
+
+ }
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpace.getLayoutParams();
+ boolean cornerCutout = cornerCutoutPadding != null
+ && (cornerCutoutPadding.first == 0 || cornerCutoutPadding.second == 0);
+ if (cutout != null) {
+ Rect topCutout = cutout.getBoundingRectTop();
+ if (topCutout.isEmpty() || cornerCutout) {
+ mHasTopCutout = false;
+ lp.width = 0;
+ mSpace.setVisibility(View.GONE);
+ } else {
+ mHasTopCutout = true;
+ lp.width = topCutout.width();
+ mSpace.setVisibility(View.VISIBLE);
+ }
+ }
+ mSpace.setLayoutParams(lp);
+ setChipVisibility(mPrivacyChip.getVisibility() == View.VISIBLE);
mCutOutPaddingLeft = padding.first;
mCutOutPaddingRight = padding.second;
mWaterfallTopInset = cutout == null ? 0 : cutout.getWaterfallInsets().top;
@@ -496,6 +601,7 @@
setListening(false);
mRingerModeTracker.getRingerModeInternal().removeObservers(this);
mStatusBarIconController.removeIconGroup(mIconManager);
+ DeviceConfig.removeOnPropertiesChangedListener(mPropertiesListener);
super.onDetachedFromWindow();
}
@@ -513,10 +619,13 @@
mZenController.addCallback(this);
mAlarmController.addCallback(this);
mLifecycle.setCurrentState(Lifecycle.State.RESUMED);
+ mPrivacyItemController.addCallback(mPICCallback);
} else {
mZenController.removeCallback(this);
mAlarmController.removeCallback(this);
mLifecycle.setCurrentState(Lifecycle.State.CREATED);
+ mPrivacyItemController.removeCallback(mPICCallback);
+ mPrivacyChipLogged = false;
}
}
@@ -534,6 +643,17 @@
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
AlarmClock.ACTION_SHOW_ALARMS), 0);
}
+ } else if (v == mPrivacyChip) {
+ // Makes sure that the builder is grabbed as soon as the chip is pressed
+ PrivacyChipBuilder builder = mPrivacyChip.getBuilder();
+ if (builder.getAppsAndTypes().size() == 0) return;
+ Handler mUiHandler = new Handler(Looper.getMainLooper());
+ mUiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK);
+ mUiHandler.post(() -> {
+ mActivityStarter.postStartActivityDismissingKeyguard(
+ new Intent(Intent.ACTION_REVIEW_ONGOING_PERMISSION_USAGE), 0);
+ mHost.collapsePanels();
+ });
} else if (v == mRingerContainer && mRingerContainer.isVisibleToUser()) {
mActivityStarter.postStartActivityDismissingKeyguard(new Intent(
Settings.ACTION_SOUND_SETTINGS), 0);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 5bb8fab..01b6fbf 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -47,6 +47,9 @@
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.privacy.PrivacyItem;
+import com.android.systemui.privacy.PrivacyItemController;
+import com.android.systemui.privacy.PrivacyType;
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.screenrecord.RecordingController;
@@ -70,6 +73,9 @@
import com.android.systemui.util.RingerModeTracker;
import com.android.systemui.util.time.DateFormatUtil;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
import java.util.Locale;
import java.util.concurrent.Executor;
@@ -87,13 +93,13 @@
ZenModeController.Callback,
DeviceProvisionedListener,
KeyguardStateController.Callback,
+ PrivacyItemController.Callback,
LocationController.LocationChangeCallback,
RecordingController.RecordingStateChangeCallback {
private static final String TAG = "PhoneStatusBarPolicy";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- static final int LOCATION_STATUS_ICON_ID =
- com.android.internal.R.drawable.perm_group_location;
+ static final int LOCATION_STATUS_ICON_ID = PrivacyType.TYPE_LOCATION.getIconId();
private final String mSlotCast;
private final String mSlotHotspot;
@@ -107,6 +113,8 @@
private final String mSlotHeadset;
private final String mSlotDataSaver;
private final String mSlotLocation;
+ private final String mSlotMicrophone;
+ private final String mSlotCamera;
private final String mSlotSensorsOff;
private final String mSlotScreenRecord;
private final int mDisplayId;
@@ -132,6 +140,7 @@
private final DeviceProvisionedController mProvisionedController;
private final KeyguardStateController mKeyguardStateController;
private final LocationController mLocationController;
+ private final PrivacyItemController mPrivacyItemController;
private final Executor mUiBgExecutor;
private final SensorPrivacyController mSensorPrivacyController;
private final RecordingController mRecordingController;
@@ -162,7 +171,8 @@
RecordingController recordingController,
@Nullable TelecomManager telecomManager, @DisplayId int displayId,
@Main SharedPreferences sharedPreferences, DateFormatUtil dateFormatUtil,
- RingerModeTracker ringerModeTracker) {
+ RingerModeTracker ringerModeTracker,
+ PrivacyItemController privacyItemController) {
mIconController = iconController;
mCommandQueue = commandQueue;
mBroadcastDispatcher = broadcastDispatcher;
@@ -181,6 +191,7 @@
mProvisionedController = deviceProvisionedController;
mKeyguardStateController = keyguardStateController;
mLocationController = locationController;
+ mPrivacyItemController = privacyItemController;
mSensorPrivacyController = sensorPrivacyController;
mRecordingController = recordingController;
mUiBgExecutor = uiBgExecutor;
@@ -200,6 +211,8 @@
mSlotHeadset = resources.getString(com.android.internal.R.string.status_bar_headset);
mSlotDataSaver = resources.getString(com.android.internal.R.string.status_bar_data_saver);
mSlotLocation = resources.getString(com.android.internal.R.string.status_bar_location);
+ mSlotMicrophone = resources.getString(com.android.internal.R.string.status_bar_microphone);
+ mSlotCamera = resources.getString(com.android.internal.R.string.status_bar_camera);
mSlotSensorsOff = resources.getString(com.android.internal.R.string.status_bar_sensors_off);
mSlotScreenRecord = resources.getString(
com.android.internal.R.string.status_bar_screen_record);
@@ -271,6 +284,13 @@
mResources.getString(R.string.accessibility_data_saver_on));
mIconController.setIconVisibility(mSlotDataSaver, false);
+ // privacy items
+ mIconController.setIcon(mSlotMicrophone, PrivacyType.TYPE_MICROPHONE.getIconId(),
+ mResources.getString(PrivacyType.TYPE_MICROPHONE.getNameId()));
+ mIconController.setIconVisibility(mSlotMicrophone, false);
+ mIconController.setIcon(mSlotCamera, PrivacyType.TYPE_CAMERA.getIconId(),
+ mResources.getString(PrivacyType.TYPE_CAMERA.getNameId()));
+ mIconController.setIconVisibility(mSlotCamera, false);
mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
mResources.getString(R.string.accessibility_location_active));
mIconController.setIconVisibility(mSlotLocation, false);
@@ -294,6 +314,7 @@
mNextAlarmController.addCallback(mNextAlarmCallback);
mDataSaver.addCallback(this);
mKeyguardStateController.addCallback(this);
+ mPrivacyItemController.addCallback(this);
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
mLocationController.addCallback(this);
mRecordingController.addCallback(this);
@@ -609,9 +630,46 @@
mIconController.setIconVisibility(mSlotDataSaver, isDataSaving);
}
+ @Override // PrivacyItemController.Callback
+ public void privacyChanged(List<PrivacyItem> privacyItems) {
+ updatePrivacyItems(privacyItems);
+ }
+
+ private void updatePrivacyItems(List<PrivacyItem> items) {
+ boolean showCamera = false;
+ boolean showMicrophone = false;
+ boolean showLocation = false;
+ for (PrivacyItem item : items) {
+ if (item == null /* b/124234367 */) {
+ if (DEBUG) {
+ Log.e(TAG, "updatePrivacyItems - null item found");
+ StringWriter out = new StringWriter();
+ mPrivacyItemController.dump(null, new PrintWriter(out), null);
+ Log.e(TAG, out.toString());
+ }
+ continue;
+ }
+ switch (item.getPrivacyType()) {
+ case TYPE_CAMERA:
+ showCamera = true;
+ break;
+ case TYPE_LOCATION:
+ showLocation = true;
+ break;
+ case TYPE_MICROPHONE:
+ showMicrophone = true;
+ break;
+ }
+ }
+
+ mIconController.setIconVisibility(mSlotCamera, showCamera);
+ mIconController.setIconVisibility(mSlotMicrophone, showMicrophone);
+ mIconController.setIconVisibility(mSlotLocation, showLocation);
+ }
+
@Override
public void onLocationActiveChanged(boolean active) {
- updateLocation();
+ if (!mPrivacyItemController.isPermissionsHubEnabled()) updateLocation();
}
// Updates the status view based on the current state of location requests.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
new file mode 100644
index 0000000..dcee5a716
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyChipBuilderTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class PrivacyChipBuilderTest : SysuiTestCase() {
+
+ companion object {
+ val TEST_UID = 1
+ }
+
+ @Test
+ fun testGenerateAppsList() {
+ val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Bar", TEST_UID))
+ val bar3 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
+ "Bar", TEST_UID))
+ val foo0 = PrivacyItem(Privacy.TYPE_MICROPHONE, PrivacyApplication(
+ "Foo", TEST_UID))
+ val baz1 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
+ "Baz", TEST_UID))
+
+ val items = listOf(bar2, foo0, baz1, bar3)
+
+ val textBuilder = PrivacyChipBuilder(context, items)
+
+ val list = textBuilder.appsAndTypes
+ assertEquals(3, list.size)
+ val appsList = list.map { it.first }
+ val typesList = list.map { it.second }
+ // List is sorted by number of types and then by types
+ assertEquals(listOf("Bar", "Baz", "Foo"), appsList.map { it.packageName })
+ assertEquals(listOf(Privacy.TYPE_CAMERA, Privacy.TYPE_LOCATION), typesList[0])
+ assertEquals(listOf(Privacy.TYPE_CAMERA), typesList[1])
+ assertEquals(listOf(Privacy.TYPE_MICROPHONE), typesList[2])
+ }
+
+ @Test
+ fun testOrder() {
+ // We want location to always go last, so it will go in the "+ other apps"
+ val appCamera = PrivacyItem(PrivacyType.TYPE_CAMERA,
+ PrivacyApplication("Camera", TEST_UID))
+ val appMicrophone =
+ PrivacyItem(PrivacyType.TYPE_MICROPHONE,
+ PrivacyApplication("Microphone", TEST_UID))
+ val appLocation =
+ PrivacyItem(PrivacyType.TYPE_LOCATION,
+ PrivacyApplication("Location", TEST_UID))
+
+ val items = listOf(appLocation, appMicrophone, appCamera)
+ val textBuilder = PrivacyChipBuilder(context, items)
+ val appList = textBuilder.appsAndTypes.map { it.first }.map { it.packageName }
+ assertEquals(listOf("Camera", "Microphone", "Location"), appList)
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt
new file mode 100644
index 0000000..1f7baa9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2020 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.privacy
+
+import android.app.ActivityManager
+import android.app.AppOpsManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.DeviceConfig
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.appops.AppOpItem
+import com.android.systemui.appops.AppOpsController
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.util.DeviceConfigProxy
+import com.android.systemui.util.DeviceConfigProxyFake
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.time.FakeSystemClock
+import org.hamcrest.Matchers.hasItem
+import org.hamcrest.Matchers.not
+import org.hamcrest.Matchers.nullValue
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertThat
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.reset
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper
+class PrivacyItemControllerTest : SysuiTestCase() {
+
+ companion object {
+ val CURRENT_USER_ID = ActivityManager.getCurrentUser()
+ val TEST_UID = CURRENT_USER_ID * UserHandle.PER_USER_RANGE
+ const val SYSTEM_UID = 1000
+ const val TEST_PACKAGE_NAME = "test"
+ const val DEVICE_SERVICES_STRING = "Device services"
+ const val TAG = "PrivacyItemControllerTest"
+ fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+ fun <T> eq(value: T): T = Mockito.eq(value) ?: value
+ fun <T> any(): T = Mockito.any<T>()
+ }
+
+ @Mock
+ private lateinit var appOpsController: AppOpsController
+ @Mock
+ private lateinit var callback: PrivacyItemController.Callback
+ @Mock
+ private lateinit var userManager: UserManager
+ @Mock
+ private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock
+ private lateinit var dumpManager: DumpManager
+ @Captor
+ private lateinit var argCaptor: ArgumentCaptor<List<PrivacyItem>>
+ @Captor
+ private lateinit var argCaptorCallback: ArgumentCaptor<AppOpsController.Callback>
+
+ private lateinit var privacyItemController: PrivacyItemController
+ private lateinit var executor: FakeExecutor
+ private lateinit var deviceConfigProxy: DeviceConfigProxy
+
+ fun PrivacyItemController(context: Context): PrivacyItemController {
+ return PrivacyItemController(
+ context,
+ appOpsController,
+ executor,
+ executor,
+ broadcastDispatcher,
+ deviceConfigProxy,
+ dumpManager
+ )
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+ executor = FakeExecutor(FakeSystemClock())
+ deviceConfigProxy = DeviceConfigProxyFake()
+
+ appOpsController = mDependency.injectMockDependency(AppOpsController::class.java)
+ mContext.addMockSystemService(UserManager::class.java, userManager)
+
+ deviceConfigProxy.setProperty(DeviceConfig.NAMESPACE_PRIVACY,
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
+ "true", false)
+
+ doReturn(listOf(object : UserInfo() {
+ init {
+ id = CURRENT_USER_ID
+ }
+ })).`when`(userManager).getProfiles(anyInt())
+
+ privacyItemController = PrivacyItemController(mContext)
+ }
+
+ @Test
+ fun testSetListeningTrueByAddingCallback() {
+ privacyItemController.addCallback(callback)
+ executor.runAllReady()
+ verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
+ any())
+ verify(callback).privacyChanged(anyList())
+ }
+
+ @Test
+ fun testSetListeningFalseByRemovingLastCallback() {
+ privacyItemController.addCallback(callback)
+ executor.runAllReady()
+ verify(appOpsController, never()).removeCallback(any(),
+ any())
+ privacyItemController.removeCallback(callback)
+ executor.runAllReady()
+ verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
+ any())
+ verify(callback).privacyChanged(emptyList())
+ }
+
+ @Test
+ fun testDistinctItems() {
+ doReturn(listOf(AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 0),
+ AppOpItem(AppOpsManager.OP_CAMERA, TEST_UID, "", 1)))
+ .`when`(appOpsController).getActiveAppOpsForUser(anyInt())
+
+ privacyItemController.addCallback(callback)
+ executor.runAllReady()
+ verify(callback).privacyChanged(capture(argCaptor))
+ assertEquals(1, argCaptor.value.size)
+ }
+
+ @Test
+ fun testRegisterReceiver_allUsers() {
+ privacyItemController.addCallback(callback)
+ executor.runAllReady()
+ verify(broadcastDispatcher, atLeastOnce()).registerReceiver(
+ eq(privacyItemController.userSwitcherReceiver), any(), eq(null), eq(UserHandle.ALL))
+ verify(broadcastDispatcher, never())
+ .unregisterReceiver(eq(privacyItemController.userSwitcherReceiver))
+ }
+
+ @Test
+ fun testReceiver_ACTION_USER_FOREGROUND() {
+ privacyItemController.userSwitcherReceiver.onReceive(context,
+ Intent(Intent.ACTION_USER_SWITCHED))
+ executor.runAllReady()
+ verify(userManager).getProfiles(anyInt())
+ }
+
+ @Test
+ fun testReceiver_ACTION_MANAGED_PROFILE_ADDED() {
+ privacyItemController.userSwitcherReceiver.onReceive(context,
+ Intent(Intent.ACTION_MANAGED_PROFILE_AVAILABLE))
+ executor.runAllReady()
+ verify(userManager).getProfiles(anyInt())
+ }
+
+ @Test
+ fun testReceiver_ACTION_MANAGED_PROFILE_REMOVED() {
+ privacyItemController.userSwitcherReceiver.onReceive(context,
+ Intent(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE))
+ executor.runAllReady()
+ verify(userManager).getProfiles(anyInt())
+ }
+
+ @Test
+ fun testAddMultipleCallbacks() {
+ val otherCallback = mock(PrivacyItemController.Callback::class.java)
+ privacyItemController.addCallback(callback)
+ executor.runAllReady()
+ verify(callback).privacyChanged(anyList())
+
+ privacyItemController.addCallback(otherCallback)
+ executor.runAllReady()
+ verify(otherCallback).privacyChanged(anyList())
+ // Adding a callback should not unnecessarily call previous ones
+ verifyNoMoreInteractions(callback)
+ }
+
+ @Test
+ fun testMultipleCallbacksAreUpdated() {
+ doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOpsForUser(anyInt())
+
+ val otherCallback = mock(PrivacyItemController.Callback::class.java)
+ privacyItemController.addCallback(callback)
+ privacyItemController.addCallback(otherCallback)
+ executor.runAllReady()
+ reset(callback)
+ reset(otherCallback)
+
+ verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
+ argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
+ executor.runAllReady()
+ verify(callback).privacyChanged(anyList())
+ verify(otherCallback).privacyChanged(anyList())
+ }
+
+ @Test
+ fun testRemoveCallback() {
+ doReturn(emptyList<AppOpItem>()).`when`(appOpsController).getActiveAppOpsForUser(anyInt())
+ val otherCallback = mock(PrivacyItemController.Callback::class.java)
+ privacyItemController.addCallback(callback)
+ privacyItemController.addCallback(otherCallback)
+ executor.runAllReady()
+ executor.runAllReady()
+ reset(callback)
+ reset(otherCallback)
+
+ verify(appOpsController).addCallback(any(), capture(argCaptorCallback))
+ privacyItemController.removeCallback(callback)
+ argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
+ executor.runAllReady()
+ verify(callback, never()).privacyChanged(anyList())
+ verify(otherCallback).privacyChanged(anyList())
+ }
+
+ @Test
+ fun testListShouldNotHaveNull() {
+ doReturn(listOf(AppOpItem(AppOpsManager.OP_ACTIVATE_VPN, TEST_UID, "", 0),
+ AppOpItem(AppOpsManager.OP_COARSE_LOCATION, TEST_UID, "", 0)))
+ .`when`(appOpsController).getActiveAppOpsForUser(anyInt())
+ privacyItemController.addCallback(callback)
+ executor.runAllReady()
+ executor.runAllReady()
+
+ verify(callback).privacyChanged(capture(argCaptor))
+ assertEquals(1, argCaptor.value.size)
+ assertThat(argCaptor.value, not(hasItem(nullValue())))
+ }
+
+ @Test
+ fun testListShouldBeCopy() {
+ val list = listOf(PrivacyItem(PrivacyType.TYPE_CAMERA,
+ PrivacyApplication("", TEST_UID)))
+ privacyItemController.privacyList = list
+ val privacyList = privacyItemController.privacyList
+ assertEquals(list, privacyList)
+ assertTrue(list !== privacyList)
+ }
+
+ @Test
+ fun testNotListeningWhenIndicatorsDisabled() {
+ deviceConfigProxy.setProperty(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
+ "false",
+ false
+ )
+ privacyItemController.addCallback(callback)
+ executor.runAllReady()
+ verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS),
+ any())
+ }
+}
\ No newline at end of file