DO NOT MERGE Revert "DO NOT MERGE Remove Privacy Indicators"

This reverts commit ec3e0ecaca.

Reason for revert: Re-enable Privacy Indicators in qt-r1-dev
Bug: 133257910

Change-Id: I7c778dd76c0aff3f483cf06f1dd96fd067145c12
This commit is contained in:
Fabian Kozynski
2019-06-04 16:21:46 +00:00
parent ec3e0ecaca
commit 53ceaf677a
16 changed files with 1305 additions and 9 deletions

View File

@@ -106,6 +106,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_enabled";
// Flags related to Assistant Handles
/**

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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:gravity="center_vertical"
android:orientation="horizontal"
android:focusable="true" >
<FrameLayout
android:id="@+id/background"
android:layout_height="@dimen/ongoing_appops_chip_height"
android:minWidth="48dp"
android:layout_width="wrap_content" >
<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

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.QS.DetailItemPrimary"
/>

View File

@@ -22,11 +22,19 @@
android:layout_height="@*android:dimen/quick_qs_offset_height"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center"
android:orientation="horizontal"
android:clickable="true"
android:paddingStart="@dimen/status_bar_padding_start"
android:paddingEnd="@dimen/status_bar_padding_end" >
<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,4 +46,23 @@
android:singleLine="true"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
systemui:showDark="false" />
</LinearLayout>
<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

@@ -48,6 +48,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.shared.plugins.PluginManager;
import com.android.systemui.shared.system.ActivityManagerWrapper;
@@ -286,6 +287,7 @@ public class Dependency extends SystemUI {
@Inject Lazy<SensorPrivacyManager> mSensorPrivacyManager;
@Inject Lazy<AutoHideController> mAutoHideController;
@Inject Lazy<ForegroundServiceNotificationListener> mForegroundServiceNotificationListener;
@Inject Lazy<PrivacyItemController> mPrivacyItemController;
@Inject @Named(BG_LOOPER_NAME) Lazy<Looper> mBgLooper;
@Inject @Named(BG_HANDLER_NAME) Lazy<Handler> mBgHandler;
@Inject @Named(MAIN_HANDLER_NAME) Lazy<Handler> mMainHandler;
@@ -470,6 +472,7 @@ public class Dependency extends SystemUI {
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

@@ -20,6 +20,7 @@ import static com.android.systemui.Dependency.BG_LOOPER_NAME;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
@@ -209,6 +210,59 @@ public class AppOpsControllerImpl implements AppOpsController,
mBGHandler.scheduleRemoval(item, NOTED_OP_TIME_DELAY_MS);
}
/**
* Does the app-op code refer to a user sensitive permission for the specified user id
* and package. Only user sensitive permission should be shown to the user by default.
*
* @param appOpCode The code of the app-op.
* @param uid The uid of the user.
* @param packageName The name of the package.
*
* @return {@code true} iff the app-op item is user sensitive
*/
private boolean isUserSensitive(int appOpCode, int uid, String packageName) {
String permission = AppOpsManager.opToPermission(appOpCode);
if (permission == null) {
return false;
}
int permFlags = mContext.getPackageManager().getPermissionFlags(permission,
packageName, UserHandle.getUserHandleForUid(uid));
return (permFlags & PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED) != 0;
}
/**
* Does the app-op item refer to an operation that should be shown to the user.
* Only specficic ops (like SYSTEM_ALERT_WINDOW) or ops that refer to user sensitive
* permission should be shown to the user by default.
*
* @param item The item
*
* @return {@code true} iff the app-op item should be shown to the user
*/
private boolean isUserVisible(AppOpItem item) {
return isUserVisible(item.getCode(), item.getUid(), item.getPackageName());
}
/**
* Does the app-op, uid and package name, refer to an operation that should be shown to the
* user. Only specficic ops (like {@link AppOpsManager.OP_SYSTEM_ALERT_WINDOW}) or
* ops that refer to user sensitive permission should be shown to the user by default.
*
* @param item The item
*
* @return {@code true} iff the app-op for should be shown to the user
*/
private boolean isUserVisible(int appOpCode, int uid, String packageName) {
// currently OP_SYSTEM_ALERT_WINDOW does not correspond to a platform permission
// which may be user senstive, so for now always show it to the user.
if (appOpCode == AppOpsManager.OP_SYSTEM_ALERT_WINDOW) {
return true;
}
return isUserSensitive(appOpCode, uid, packageName);
}
/**
* Returns a copy of the list containing all the active AppOps that the controller tracks.
*
@@ -232,8 +286,8 @@ public class AppOpsControllerImpl implements AppOpsController,
final int numActiveItems = mActiveItems.size();
for (int i = 0; i < numActiveItems; i++) {
AppOpItem item = mActiveItems.get(i);
if ((userId == UserHandle.USER_ALL
|| UserHandle.getUserId(item.getUid()) == userId)) {
if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)
&& isUserVisible(item)) {
list.add(item);
}
}
@@ -242,8 +296,8 @@ public class AppOpsControllerImpl implements AppOpsController,
final int numNotedItems = mNotedItems.size();
for (int i = 0; i < numNotedItems; i++) {
AppOpItem item = mNotedItems.get(i);
if ((userId == UserHandle.USER_ALL
|| UserHandle.getUserId(item.getUid()) == userId)) {
if ((userId == UserHandle.USER_ALL || UserHandle.getUserId(item.getUid()) == userId)
&& isUserVisible(item)) {
list.add(item);
}
}
@@ -269,7 +323,8 @@ public class AppOpsControllerImpl implements AppOpsController,
}
private void notifySuscribers(int code, int uid, String packageName, boolean active) {
if (mCallbacksByCode.containsKey(code)) {
if (mCallbacksByCode.containsKey(code)
&& isUserVisible(code, uid, packageName)) {
for (Callback cb: mCallbacksByCode.get(code)) {
cb.onActiveStateChanged(code, uid, packageName, active);
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright (C) 2018 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
) : LinearLayout(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 = PrivacyDialogBuilder(context, emptyList<PrivacyItem>())
var privacyList = emptyList<PrivacyItem>()
set(value) {
field = value
builder = PrivacyDialogBuilder(context, value)
updateView()
}
override fun onFinishInflate() {
super.onFinishInflate()
back = findViewById(R.id.background)
iconsContainer = findViewById(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(dialogBuilder: PrivacyDialogBuilder, iconsContainer: ViewGroup) {
iconsContainer.removeAllViews()
dialogBuilder.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,56 @@
/*
* Copyright (C) 2018 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.graphics.drawable.Drawable
import com.android.systemui.R
class PrivacyDialogBuilder(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 generateIconsForApp(types: List<PrivacyType>): List<Drawable> {
return types.sorted().map { it.getIcon(context) }
}
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,80 @@
/*
* Copyright (C) 2018 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.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.UserHandle
import android.util.IconDrawableFactory
import com.android.systemui.R
typealias Privacy = PrivacyType
enum class PrivacyType(private 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, val context: Context)
: Comparable<PrivacyApplication> {
override fun compareTo(other: PrivacyApplication): Int {
return applicationName.compareTo(other.applicationName)
}
private val applicationInfo: ApplicationInfo? by lazy {
try {
val userHandle = UserHandle.getUserHandleForUid(uid)
context.createPackageContextAsUser(packageName, 0, userHandle).getPackageManager()
.getApplicationInfo(packageName, 0)
} catch (_: PackageManager.NameNotFoundException) {
null
}
}
val icon: Drawable by lazy {
applicationInfo?.let {
try {
val iconFactory = IconDrawableFactory.newInstance(context, true)
iconFactory.getBadgedIcon(it, UserHandle.getUserId(uid))
} catch (_: Exception) {
null
}
} ?: context.getDrawable(android.R.drawable.sym_def_app_icon)
}
val applicationName: String by lazy {
applicationInfo?.let {
context.packageManager.getApplicationLabel(it) as String
} ?: packageName
}
override fun toString() = "PrivacyApplication(packageName=$packageName, uid=$uid)"
}

View File

@@ -0,0 +1,294 @@
/*
* Copyright (C) 2018 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.Handler
import android.os.Looper
import android.os.Message
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.Dependency.BG_HANDLER_NAME
import com.android.systemui.Dependency.MAIN_HANDLER_NAME
import com.android.systemui.R
import com.android.systemui.appops.AppOpItem
import com.android.systemui.appops.AppOpsController
import com.android.systemui.Dumpable
import java.io.FileDescriptor
import java.io.PrintWriter
import java.lang.ref.WeakReference
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
fun isPermissionsHubEnabled() = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED, false)
@Singleton
class PrivacyItemController @Inject constructor(
val context: Context,
private val appOpsController: AppOpsController,
@Named(MAIN_HANDLER_NAME) private val uiHandler: Handler,
@Named(BG_HANDLER_NAME) private val bgHandler: Handler
) : Dumpable {
@VisibleForTesting
internal companion object {
val OPS = intArrayOf(AppOpsManager.OP_CAMERA,
AppOpsManager.OP_RECORD_AUDIO,
AppOpsManager.OP_COARSE_LOCATION,
AppOpsManager.OP_FINE_LOCATION)
val intents = listOf(Intent.ACTION_USER_FOREGROUND,
Intent.ACTION_MANAGED_PROFILE_ADDED,
Intent.ACTION_MANAGED_PROFILE_REMOVED)
const val TAG = "PrivacyItemController"
const val SYSTEM_UID = 1000
const val MSG_ADD_CALLBACK = 0
const val MSG_REMOVE_CALLBACK = 1
const val MSG_UPDATE_LISTENING_STATE = 2
}
@VisibleForTesting
internal var privacyList = emptyList<PrivacyItem>()
@Synchronized get() = field.toList() // Returns a shallow copy of the list
@Synchronized set
private val userManager = context.getSystemService(UserManager::class.java)
private var currentUserIds = emptyList<Int>()
private var listening = false
val systemApp =
PrivacyApplication(context.getString(R.string.device_services), SYSTEM_UID, context)
private val callbacks = mutableListOf<WeakReference<Callback>>()
private val messageHandler = H(WeakReference(this), uiHandler.looper)
private val notifyChanges = Runnable {
val list = privacyList
callbacks.forEach { it.get()?.privacyChanged(list) }
}
private val updateListAndNotifyChanges = Runnable {
updatePrivacyList()
uiHandler.post(notifyChanges)
}
private var indicatorsAvailable = isPermissionsHubEnabled()
@VisibleForTesting
internal val devicePropertyChangedListener =
object : DeviceConfig.OnPropertyChangedListener {
override fun onPropertyChanged(namespace: String, name: String, value: String?) {
if (DeviceConfig.NAMESPACE_PRIVACY.equals(namespace) &&
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED.equals(name)) {
indicatorsAvailable = java.lang.Boolean.parseBoolean(value)
messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
}
}
}
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) {
context.unregisterReceiver(field)
field = value
registerReceiver()
}
init {
DeviceConfig.addOnPropertyChangedListener(
DeviceConfig.NAMESPACE_PRIVACY, context.mainExecutor, devicePropertyChangedListener)
}
private fun unregisterReceiver() {
context.unregisterReceiver(userSwitcherReceiver)
}
private fun registerReceiver() {
context.registerReceiverAsUser(userSwitcherReceiver, UserHandle.ALL, IntentFilter().apply {
intents.forEach {
addAction(it)
}
}, null, null)
}
private fun update(updateUsers: Boolean) {
if (updateUsers) {
val currentUser = ActivityManager.getCurrentUser()
currentUserIds = userManager.getProfiles(currentUser).map { it.id }
}
bgHandler.post(updateListAndNotifyChanges)
}
/**
* 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) {
messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
}
// Notify this callback if we didn't set to listening
else if (listening) uiHandler.post(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()) {
messageHandler.removeMessages(MSG_UPDATE_LISTENING_STATE)
messageHandler.sendEmptyMessage(MSG_UPDATE_LISTENING_STATE)
}
}
fun addCallback(callback: Callback) {
messageHandler.obtainMessage(MSG_ADD_CALLBACK, callback).sendToTarget()
}
fun removeCallback(callback: Callback) {
messageHandler.obtainMessage(MSG_REMOVE_CALLBACK, callback).sendToTarget()
}
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
}
if (appOpItem.uid == SYSTEM_UID) return PrivacyItem(type, systemApp)
val app = PrivacyApplication(appOpItem.packageName, appOpItem.uid, context)
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 (intent?.action in intents) {
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 H(
private val outerClass: WeakReference<PrivacyItemController>,
looper: Looper
) : Handler(looper) {
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when (msg.what) {
MSG_UPDATE_LISTENING_STATE -> outerClass.get()?.setListeningState()
MSG_ADD_CALLBACK -> {
if (msg.obj !is PrivacyItemController.Callback) return
outerClass.get()?.addCallback(
WeakReference(msg.obj as PrivacyItemController.Callback))
}
MSG_REMOVE_CALLBACK -> {
if (msg.obj !is PrivacyItemController.Callback) return
outerClass.get()?.removeCallback(
WeakReference(msg.obj as PrivacyItemController.Callback))
}
else -> {}
}
}
}
}

View File

@@ -32,24 +32,30 @@ 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;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.util.StatsLog;
import android.view.ContextThemeWrapper;
import android.view.DisplayCutout;
import android.view.View;
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.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.settingslib.Utils;
import com.android.systemui.BatteryMeterView;
import com.android.systemui.DualToneHandler;
@@ -57,6 +63,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.PrivacyDialogBuilder;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.privacy.PrivacyItemControllerKt;
import com.android.systemui.qs.QSDetail.Callback;
import com.android.systemui.statusbar.phone.PhoneStatusBarView;
import com.android.systemui.statusbar.phone.StatusBarIconController;
@@ -67,6 +78,8 @@ import com.android.systemui.statusbar.policy.DateView;
import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.ZenModeController;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@@ -108,6 +121,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
private TintedIconManager mIconManager;
private TouchAnimator mStatusIconsAlphaAnimator;
private TouchAnimator mHeaderTextContainerAlphaAnimator;
private TouchAnimator mPrivacyChipAlphaAnimator;
private DualToneHandler mDualToneHandler;
private View mSystemIconsView;
@@ -127,7 +141,12 @@ 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 boolean mPermissionsHubEnabled;
private PrivacyItemController mPrivacyItemController;
private final BroadcastReceiver mRingerReceiver = new BroadcastReceiver() {
@Override
@@ -137,17 +156,41 @@ public class QuickStatusBarHeader extends RelativeLayout implements
}
};
private boolean mHasTopCutout = false;
private boolean mPrivacyChipLogged = false;
private final DeviceConfig.OnPropertyChangedListener mPropertyListener =
new DeviceConfig.OnPropertyChangedListener() {
@Override
public void onPropertyChanged(String namespace, String name, String value) {
if (DeviceConfig.NAMESPACE_PRIVACY.equals(namespace)
&& SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED.equals(
name)) {
mPermissionsHubEnabled = Boolean.valueOf(value);
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) {
ActivityStarter activityStarter, PrivacyItemController privacyItemController) {
super(context, attrs);
mAlarmController = nextAlarmController;
mZenController = zenModeController;
mStatusBarIconController = statusBarIconController;
mActivityStarter = activityStarter;
mPrivacyItemController = privacyItemController;
mDualToneHandler = new DualToneHandler(
new ContextThemeWrapper(context, R.style.QSHeaderTheme));
}
@@ -160,6 +203,8 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mSystemIconsView = findViewById(R.id.quick_status_bar_system_icons);
mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
StatusIconContainer iconContainer = findViewById(R.id.statusIcons);
// Ignore privacy icons because they show in the space above QQS
iconContainer.addIgnoredSlots(getIgnoredIconSlots());
iconContainer.setShouldRestrictIcons(false);
mIconManager = new TintedIconManager(iconContainer);
@@ -173,6 +218,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mRingerModeIcon = findViewById(R.id.ringer_mode_icon);
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);
@@ -195,6 +243,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);
@@ -205,6 +254,26 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mBatteryRemainingIcon.setPercentShowMode(BatteryMeterView.MODE_ESTIMATE);
mRingerModeTextView.setSelected(true);
mNextAlarmTextView.setSelected(true);
mPermissionsHubEnabled = PrivacyItemControllerKt.isPermissionsHubEnabled();
// Change the ignored slots when DeviceConfig flag changes
DeviceConfig.addOnPropertyChangedListener(DeviceConfig.NAMESPACE_PRIVACY,
mContext.getMainExecutor(), mPropertyListener);
}
private List<String> getIgnoredIconSlots() {
ArrayList<String> ignored = new ArrayList<>();
ignored.add(mContext.getResources().getString(
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;
}
private void updateStatusText() {
@@ -218,6 +287,21 @@ 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;
StatsLog.write(StatsLog.PRIVACY_INDICATORS_INTERACTED,
StatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__CHIP_VIEWED);
}
} else {
mPrivacyChip.setVisibility(View.GONE);
}
}
private boolean updateRingerStatus() {
boolean isOriginalVisible = mRingerModeTextView.getVisibility() == View.VISIBLE;
CharSequence originalRingerText = mRingerModeTextView.getText();
@@ -324,6 +408,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements
updateStatusIconAlphaAnimator();
updateHeaderTextContainerAlphaAnimator();
updatePrivacyChipAlphaAnimator();
}
private void updateStatusIconAlphaAnimator() {
@@ -338,6 +423,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;
@@ -376,6 +467,10 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mHeaderTextContainerView.setVisibility(INVISIBLE);
}
}
if (mPrivacyChipAlphaAnimator != null) {
mPrivacyChip.setExpanded(expansionFraction > 0.5);
mPrivacyChipAlphaAnimator.setPosition(keyguardExpansionFraction);
}
}
public void disable(int state1, int state2, boolean animate) {
@@ -408,6 +503,21 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mSystemIconsView.setPadding(padding.first, 0, padding.second, 0);
}
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSpace.getLayoutParams();
if (cutout != null) {
Rect topCutout = cutout.getBoundingRectTop();
if (topCutout.isEmpty()) {
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);
return super.onApplyWindowInsets(insets);
}
@@ -432,10 +542,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements
mAlarmController.addCallback(this);
mContext.registerReceiver(mRingerReceiver,
new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION));
mPrivacyItemController.addCallback(mPICCallback);
} else {
mZenController.removeCallback(this);
mAlarmController.removeCallback(this);
mPrivacyItemController.removeCallback(mPICCallback);
mContext.unregisterReceiver(mRingerReceiver);
mPrivacyChipLogged = false;
}
}
@@ -453,6 +566,18 @@ 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
PrivacyDialogBuilder builder = mPrivacyChip.getBuilder();
if (builder.getAppsAndTypes().size() == 0) return;
Handler mUiHandler = new Handler(Looper.getMainLooper());
StatsLog.write(StatsLog.PRIVACY_INDICATORS_INTERACTED,
StatsLog.PRIVACY_INDICATORS_INTERACTED__TYPE__CHIP_CLICKED);
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

@@ -42,6 +42,10 @@ import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.UiOffloadThread;
import com.android.systemui.privacy.PrivacyItem;
import com.android.systemui.privacy.PrivacyItemController;
import com.android.systemui.privacy.PrivacyItemControllerKt;
import com.android.systemui.privacy.PrivacyType;
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.statusbar.CommandQueue;
@@ -62,6 +66,9 @@ import com.android.systemui.statusbar.policy.SensorPrivacyController;
import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.statusbar.policy.ZenModeController;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import java.util.Locale;
/**
@@ -76,12 +83,12 @@ public class PhoneStatusBarPolicy
ZenModeController.Callback,
DeviceProvisionedListener,
KeyguardMonitor.Callback,
PrivacyItemController.Callback,
LocationController.LocationChangeCallback {
private static final String TAG = "PhoneStatusBarPolicy";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
public static final int LOCATION_STATUS_ICON_ID =
com.android.internal.R.drawable.perm_group_location;
public static final int LOCATION_STATUS_ICON_ID = PrivacyType.TYPE_LOCATION.getIconId();
private final String mSlotCast;
private final String mSlotHotspot;
@@ -95,6 +102,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 Context mContext;
@@ -112,6 +121,7 @@ public class PhoneStatusBarPolicy
private final DeviceProvisionedController mProvisionedController;
private final KeyguardMonitor mKeyguardMonitor;
private final LocationController mLocationController;
private final PrivacyItemController mPrivacyItemController;
private final UiOffloadThread mUiOffloadThread = Dependency.get(UiOffloadThread.class);
private final SensorPrivacyController mSensorPrivacyController;
@@ -144,6 +154,7 @@ public class PhoneStatusBarPolicy
mProvisionedController = Dependency.get(DeviceProvisionedController.class);
mKeyguardMonitor = Dependency.get(KeyguardMonitor.class);
mLocationController = Dependency.get(LocationController.class);
mPrivacyItemController = Dependency.get(PrivacyItemController.class);
mSensorPrivacyController = Dependency.get(SensorPrivacyController.class);
mSlotCast = context.getString(com.android.internal.R.string.status_bar_cast);
@@ -159,6 +170,8 @@ public class PhoneStatusBarPolicy
mSlotHeadset = context.getString(com.android.internal.R.string.status_bar_headset);
mSlotDataSaver = context.getString(com.android.internal.R.string.status_bar_data_saver);
mSlotLocation = context.getString(com.android.internal.R.string.status_bar_location);
mSlotMicrophone = context.getString(com.android.internal.R.string.status_bar_microphone);
mSlotCamera = context.getString(com.android.internal.R.string.status_bar_camera);
mSlotSensorsOff = context.getString(com.android.internal.R.string.status_bar_sensors_off);
// listen for broadcasts
@@ -218,6 +231,13 @@ public class PhoneStatusBarPolicy
context.getString(R.string.accessibility_data_saver_on));
mIconController.setIconVisibility(mSlotDataSaver, false);
// privacy items
mIconController.setIcon(mSlotMicrophone, PrivacyType.TYPE_MICROPHONE.getIconId(),
PrivacyType.TYPE_MICROPHONE.getName(mContext));
mIconController.setIconVisibility(mSlotMicrophone, false);
mIconController.setIcon(mSlotCamera, PrivacyType.TYPE_CAMERA.getIconId(),
PrivacyType.TYPE_CAMERA.getName(mContext));
mIconController.setIconVisibility(mSlotCamera, false);
mIconController.setIcon(mSlotLocation, LOCATION_STATUS_ICON_ID,
mContext.getString(R.string.accessibility_location_active));
mIconController.setIconVisibility(mSlotLocation, false);
@@ -237,6 +257,7 @@ public class PhoneStatusBarPolicy
mNextAlarmController.addCallback(mNextAlarmCallback);
mDataSaver.addCallback(this);
mKeyguardMonitor.addCallback(this);
mPrivacyItemController.addCallback(this);
mSensorPrivacyController.addCallback(mSensorPrivacyListener);
mLocationController.addCallback(this);
@@ -580,9 +601,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 (!PrivacyItemControllerKt.isPermissionsHubEnabled()) updateLocation();
}
// Updates the status view based on the current state of location requests.

View File

@@ -25,9 +25,11 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
import android.content.pm.PackageManager;
@@ -53,6 +55,7 @@ public class AppOpsControllerTest extends SysuiTestCase {
private static final String TEST_PACKAGE_NAME = "test";
private static final int TEST_UID = UserHandle.getUid(0, 0);
private static final int TEST_UID_OTHER = UserHandle.getUid(1, 0);
private static final int TEST_UID_NON_USER_SENSITIVE = UserHandle.getUid(2, 0);
@Mock
private AppOpsManager mAppOpsManager;
@@ -71,6 +74,18 @@ public class AppOpsControllerTest extends SysuiTestCase {
getContext().addMockSystemService(AppOpsManager.class, mAppOpsManager);
// All permissions of TEST_UID and TEST_UID_OTHER are user sensitive. None of
// TEST_UID_NON_USER_SENSITIVE are user sensitive.
getContext().setMockPackageManager(mPackageManager);
when(mPackageManager.getPermissionFlags(anyString(), anyString(),
eq(UserHandle.getUserHandleForUid(TEST_UID)))).thenReturn(
PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED);
when(mPackageManager.getPermissionFlags(anyString(), anyString(),
eq(UserHandle.getUserHandleForUid(TEST_UID_OTHER)))).thenReturn(
PackageManager.FLAG_PERMISSION_USER_SENSITIVE_WHEN_GRANTED);
when(mPackageManager.getPermissionFlags(anyString(), anyString(),
eq(UserHandle.getUserHandleForUid(TEST_UID_NON_USER_SENSITIVE)))).thenReturn(0);
mController = new AppOpsControllerImpl(mContext, Dependency.get(Dependency.BG_LOOPER));
}
@@ -162,6 +177,14 @@ public class AppOpsControllerTest extends SysuiTestCase {
mController.getActiveAppOpsForUser(UserHandle.getUserId(TEST_UID_OTHER)).size());
}
@Test
public void nonUserSensitiveOpsAreIgnored() {
mController.onOpActiveChanged(AppOpsManager.OP_RECORD_AUDIO,
TEST_UID_NON_USER_SENSITIVE, TEST_PACKAGE_NAME, true);
assertEquals(0, mController.getActiveAppOpsForUser(
UserHandle.getUserId(TEST_UID_NON_USER_SENSITIVE)).size());
}
@Test
public void opNotedScheduledForRemoval() {
mController.setBGHandler(mMockHandler);

View File

@@ -0,0 +1,77 @@
/*
* Copyright (C) 2018 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 PrivacyDialogBuilderTest : SysuiTestCase() {
companion object {
val TEST_UID = 1
}
@Test
fun testGenerateAppsList() {
val bar2 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
"Bar", TEST_UID, context))
val bar3 = PrivacyItem(Privacy.TYPE_LOCATION, PrivacyApplication(
"Bar", TEST_UID, context))
val foo0 = PrivacyItem(Privacy.TYPE_MICROPHONE, PrivacyApplication(
"Foo", TEST_UID, context))
val baz1 = PrivacyItem(Privacy.TYPE_CAMERA, PrivacyApplication(
"Baz", TEST_UID, context))
val items = listOf(bar2, foo0, baz1, bar3)
val textBuilder = PrivacyDialogBuilder(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, context))
val appMicrophone =
PrivacyItem(PrivacyType.TYPE_MICROPHONE,
PrivacyApplication("Microphone", TEST_UID, context))
val appLocation =
PrivacyItem(PrivacyType.TYPE_LOCATION,
PrivacyApplication("Location", TEST_UID, context))
val items = listOf(appLocation, appMicrophone, appCamera)
val textBuilder = PrivacyDialogBuilder(context, items)
val appList = textBuilder.appsAndTypes.map { it.first }.map { it.packageName }
assertEquals(listOf("Camera", "Microphone", "Location"), appList)
}
}

View File

@@ -0,0 +1,293 @@
/*
* Copyright (C) 2018 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.Handler
import android.os.UserHandle
import android.os.UserManager
import android.provider.DeviceConfig
import android.provider.Settings.RESET_MODE_PACKAGE_DEFAULTS
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.testing.TestableLooper.RunWithLooper
import androidx.test.filters.SmallTest
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
import com.android.systemui.Dependency
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.appops.AppOpItem
import com.android.systemui.appops.AppOpsController
import org.hamcrest.Matchers.hasItem
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.nullValue
import org.junit.After
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.any
import org.mockito.ArgumentMatchers.anyInt
import org.mockito.ArgumentMatchers.anyList
import org.mockito.ArgumentMatchers.eq
import org.mockito.Captor
import org.mockito.Mock
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.spy
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()
}
@Mock
private lateinit var appOpsController: AppOpsController
@Mock
private lateinit var callback: PrivacyItemController.Callback
@Mock
private lateinit var userManager: UserManager
@Captor
private lateinit var argCaptor: ArgumentCaptor<List<PrivacyItem>>
@Captor
private lateinit var argCaptorCallback: ArgumentCaptor<AppOpsController.Callback>
private lateinit var testableLooper: TestableLooper
private lateinit var privacyItemController: PrivacyItemController
private lateinit var handler: Handler
fun PrivacyItemController(context: Context) =
PrivacyItemController(context, appOpsController, handler, handler)
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
testableLooper = TestableLooper.get(this)
handler = Handler(testableLooper.looper)
appOpsController = mDependency.injectMockDependency(AppOpsController::class.java)
mDependency.injectTestDependency(Dependency.BG_HANDLER, handler)
mDependency.injectTestDependency(Dependency.MAIN_HANDLER, handler)
mContext.addMockSystemService(UserManager::class.java, userManager)
mContext.getOrCreateTestableResources().addOverride(R.string.device_services,
DEVICE_SERVICES_STRING)
DeviceConfig.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)
}
@After
fun tearDown() {
DeviceConfig.resetToDefaults(RESET_MODE_PACKAGE_DEFAULTS, DeviceConfig.NAMESPACE_PRIVACY)
}
@Test
fun testSetListeningTrueByAddingCallback() {
privacyItemController.addCallback(callback)
testableLooper.processAllMessages()
verify(appOpsController).addCallback(eq(PrivacyItemController.OPS),
any(AppOpsController.Callback::class.java))
testableLooper.processAllMessages()
verify(callback).privacyChanged(anyList())
}
@Test
fun testSetListeningFalseByRemovingLastCallback() {
privacyItemController.addCallback(callback)
testableLooper.processAllMessages()
verify(appOpsController, never()).removeCallback(any(IntArray::class.java),
any(AppOpsController.Callback::class.java))
privacyItemController.removeCallback(callback)
testableLooper.processAllMessages()
verify(appOpsController).removeCallback(eq(PrivacyItemController.OPS),
any(AppOpsController.Callback::class.java))
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)
testableLooper.processAllMessages()
verify(callback).privacyChanged(capture(argCaptor))
assertEquals(1, argCaptor.value.size)
}
@Test
fun testSystemApps() {
doReturn(listOf(AppOpItem(AppOpsManager.OP_COARSE_LOCATION, SYSTEM_UID, TEST_PACKAGE_NAME,
0))).`when`(appOpsController).getActiveAppOpsForUser(anyInt())
privacyItemController.addCallback(callback)
testableLooper.processAllMessages()
verify(callback).privacyChanged(capture(argCaptor))
assertEquals(1, argCaptor.value.size)
assertEquals(context.getString(R.string.device_services),
argCaptor.value[0].application.applicationName)
}
@Test
fun testRegisterReceiver_allUsers() {
val spiedContext = spy(mContext)
val itemController = PrivacyItemController(spiedContext)
itemController.addCallback(callback)
testableLooper.processAllMessages()
verify(spiedContext, atLeastOnce()).registerReceiverAsUser(
eq(itemController.userSwitcherReceiver), eq(UserHandle.ALL), any(), eq(null),
eq(null))
verify(spiedContext, never()).unregisterReceiver(eq(itemController.userSwitcherReceiver))
}
@Test
fun testReceiver_ACTION_USER_FOREGROUND() {
privacyItemController.userSwitcherReceiver.onReceive(context,
Intent(Intent.ACTION_USER_FOREGROUND))
verify(userManager).getProfiles(anyInt())
}
@Test
fun testReceiver_ACTION_MANAGED_PROFILE_ADDED() {
privacyItemController.userSwitcherReceiver.onReceive(context,
Intent(Intent.ACTION_MANAGED_PROFILE_ADDED))
verify(userManager).getProfiles(anyInt())
}
@Test
fun testReceiver_ACTION_MANAGED_PROFILE_REMOVED() {
privacyItemController.userSwitcherReceiver.onReceive(context,
Intent(Intent.ACTION_MANAGED_PROFILE_REMOVED))
verify(userManager).getProfiles(anyInt())
}
@Test
fun testAddMultipleCallbacks() {
val otherCallback = mock(PrivacyItemController.Callback::class.java)
privacyItemController.addCallback(callback)
testableLooper.processAllMessages()
verify(callback).privacyChanged(anyList())
privacyItemController.addCallback(otherCallback)
testableLooper.processAllMessages()
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)
testableLooper.processAllMessages()
reset(callback)
reset(otherCallback)
verify(appOpsController).addCallback(any<IntArray>(), capture(argCaptorCallback))
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
testableLooper.processAllMessages()
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)
testableLooper.processAllMessages()
reset(callback)
reset(otherCallback)
verify(appOpsController).addCallback(any<IntArray>(), capture(argCaptorCallback))
privacyItemController.removeCallback(callback)
argCaptorCallback.value.onActiveStateChanged(0, TEST_UID, "", true)
testableLooper.processAllMessages()
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)
testableLooper.processAllMessages()
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, mContext)))
privacyItemController.privacyList = list
val privacyList = privacyItemController.privacyList
assertEquals(list, privacyList)
assertTrue(list !== privacyList)
}
@Test
fun testNotListeningWhenIndicatorsDisabled() {
privacyItemController.devicePropertyChangedListener.onPropertyChanged(
DeviceConfig.NAMESPACE_PRIVACY,
SystemUiDeviceConfigFlags.PROPERTY_PERMISSIONS_HUB_ENABLED,
"false")
privacyItemController.addCallback(callback)
testableLooper.processAllMessages()
verify(appOpsController, never()).addCallback(eq(PrivacyItemController.OPS),
any(AppOpsController.Callback::class.java))
}
}