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
This commit is contained in:
Fabian Kozynski
2020-07-06 10:26:03 -04:00
parent fae3137fb9
commit d074a54435
19 changed files with 1235 additions and 11 deletions

View File

@@ -120,6 +120,13 @@ public final class SystemUiDeviceConfigFlags {
*/
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
/**

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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. -->

View File

@@ -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>

View File

@@ -54,6 +54,7 @@ import com.android.systemui.plugins.VolumeDialogController;
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 @@ public class Dependency {
@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 @@ public class Dependency {
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);

View File

@@ -57,7 +57,6 @@ public class AppOpsControllerImpl implements AppOpsController,
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 @@ public class AppOpsControllerImpl implements AppOpsController,
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;

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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))
}
}
}

View File

@@ -31,7 +31,9 @@ import android.graphics.Color;
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.ViewGroup;
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.Lifecycle;
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.R;
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
mRingerModeTextView.setSelected(true);
mNextAlarmTextView.setSelected(true);
mPermissionsHubEnabled = mPrivacyItemController.isPermissionsHubEnabled();
}
public QuickQSPanel getHeaderQsPanel() {
@@ -241,6 +289,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
}
}
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
updateStatusIconAlphaAnimator();
updateHeaderTextContainerAlphaAnimator();
updatePrivacyChipAlphaAnimator();
}
private void updateStatusIconAlphaAnimator() {
@@ -377,6 +444,12 @@ public class QuickStatusBarHeader extends RelativeLayout implements
.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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
});
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
setListening(false);
mRingerModeTracker.getRingerModeInternal().removeObservers(this);
mStatusBarIconController.removeIconGroup(mIconManager);
DeviceConfig.removeOnPropertiesChangedListener(mPropertiesListener);
super.onDetachedFromWindow();
}
@@ -513,10 +619,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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 @@ public class QuickStatusBarHeader extends RelativeLayout implements
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);

View File

@@ -47,6 +47,9 @@ import com.android.systemui.broadcast.BroadcastDispatcher;
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.statusbar.policy.ZenModeController;
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 @@ public class PhoneStatusBarPolicy
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 @@ public class PhoneStatusBarPolicy
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 @@ public class PhoneStatusBarPolicy
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 @@ public class PhoneStatusBarPolicy
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 @@ public class PhoneStatusBarPolicy
mProvisionedController = deviceProvisionedController;
mKeyguardStateController = keyguardStateController;
mLocationController = locationController;
mPrivacyItemController = privacyItemController;
mSensorPrivacyController = sensorPrivacyController;
mRecordingController = recordingController;
mUiBgExecutor = uiBgExecutor;
@@ -200,6 +211,8 @@ public class PhoneStatusBarPolicy
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 @@ public class PhoneStatusBarPolicy
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 @@ public class PhoneStatusBarPolicy
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 @@ public class PhoneStatusBarPolicy
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.

View File

@@ -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)
}
}

View File

@@ -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())
}
}