Merge changes from topic "privacy-type-media-projection" into tm-dev am: d59d17d74a

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/18375274

Change-Id: I7abbfc8bcdc5d2b5c89452a417c51b019fbb8d56
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
Robert Horvath
2022-05-30 18:08:34 +00:00
committed by Automerger Merge Worker
13 changed files with 336 additions and 7 deletions

View File

@@ -157,6 +157,12 @@ public final class SystemUiDeviceConfigFlags {
*/
public static final String PROPERTY_LOCATION_INDICATORS_ENABLED = "location_indicators_enabled";
/**
* Whether to show privacy chip for media projection.
*/
public static final String PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED =
"media_projection_indicators_enabled";
/**
* Whether to show old location indicator on all location accesses.
*/

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2022 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.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@id/background"
android:gravity="center"
>
<shape android:shape="oval">
<size
android:height="@dimen/ongoing_appops_dialog_circle_size"
android:width="@dimen/ongoing_appops_dialog_circle_size"
/>
<solid android:color="@color/privacy_chip_background" />
</shape>
</item>
<item android:id="@id/icon"
android:gravity="center"
android:width="@dimen/ongoing_appops_dialog_icon_size"
android:height="@dimen/ongoing_appops_dialog_icon_size"
android:drawable="@drawable/stat_sys_cast"
/>
</layer-list>

View File

@@ -2059,6 +2059,9 @@
<!-- Text for microphone app op [CHAR LIMIT=20]-->
<string name="privacy_type_microphone">microphone</string>
<!-- Text for media projection privacy type [CHAR LIMIT=20]-->
<string name="privacy_type_media_projection">screen recording</string>
<!-- What to show on the ambient display player when song doesn't have a title. [CHAR LIMIT=20] -->
<string name="music_controls_no_title">No title</string>

View File

@@ -54,6 +54,7 @@ import android.hardware.fingerprint.FingerprintManager;
import android.media.AudioManager;
import android.media.IAudioService;
import android.media.MediaRouter2Manager;
import android.media.projection.MediaProjectionManager;
import android.media.session.MediaSessionManager;
import android.net.ConnectivityManager;
import android.net.NetworkScoreManager;
@@ -320,6 +321,11 @@ public class FrameworkServicesModule {
return LayoutInflater.from(context);
}
@Provides
static MediaProjectionManager provideMediaProjectionManager(Context context) {
return context.getSystemService(MediaProjectionManager.class);
}
@Provides
static MediaRouter2Manager provideMediaRouter2Manager(Context context) {
return MediaRouter2Manager.getInstance(context);

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2022 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.pm.PackageManager
import android.media.projection.MediaProjectionInfo
import android.media.projection.MediaProjectionManager
import android.os.Handler
import android.util.Log
import androidx.annotation.WorkerThread
import com.android.internal.annotations.GuardedBy
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.privacy.logging.PrivacyLogger
import com.android.systemui.util.asIndenting
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.withIncreasedIndent
import java.io.PrintWriter
import javax.inject.Inject
/**
* Monitors the active media projection to update privacy items.
*/
@SysUISingleton
class MediaProjectionPrivacyItemMonitor @Inject constructor(
private val mediaProjectionManager: MediaProjectionManager,
private val packageManager: PackageManager,
private val privacyConfig: PrivacyConfig,
@Background private val bgHandler: Handler,
private val systemClock: SystemClock,
private val logger: PrivacyLogger
) : PrivacyItemMonitor {
companion object {
const val TAG = "MediaProjectionPrivacyItemMonitor"
const val DEBUG = false
}
private val lock = Any()
@GuardedBy("lock")
private var callback: PrivacyItemMonitor.Callback? = null
@GuardedBy("lock")
private var mediaProjectionAvailable = privacyConfig.mediaProjectionAvailable
@GuardedBy("lock")
private var listening = false
@GuardedBy("lock")
private val privacyItems = mutableListOf<PrivacyItem>()
private val optionsCallback = object : PrivacyConfig.Callback {
override fun onFlagMediaProjectionChanged(flag: Boolean) {
synchronized(lock) {
mediaProjectionAvailable = privacyConfig.mediaProjectionAvailable
setListeningStateLocked()
}
dispatchOnPrivacyItemsChanged()
}
}
private val mediaProjectionCallback = object : MediaProjectionManager.Callback() {
@WorkerThread
override fun onStart(info: MediaProjectionInfo) {
synchronized(lock) { onMediaProjectionStartedLocked(info) }
dispatchOnPrivacyItemsChanged()
}
@WorkerThread
override fun onStop(info: MediaProjectionInfo) {
synchronized(lock) { onMediaProjectionStoppedLocked(info) }
dispatchOnPrivacyItemsChanged()
}
}
init {
privacyConfig.addCallback(optionsCallback)
setListeningStateLocked()
}
override fun startListening(callback: PrivacyItemMonitor.Callback) {
synchronized(lock) {
this.callback = callback
}
}
override fun stopListening() {
synchronized(lock) {
this.callback = null
}
}
@GuardedBy("lock")
@WorkerThread
private fun onMediaProjectionStartedLocked(info: MediaProjectionInfo) {
if (DEBUG) Log.d(TAG, "onMediaProjectionStartedLocked: info=$info")
val item = makePrivacyItem(info)
privacyItems.add(item)
logItemActive(item, true)
}
@GuardedBy("lock")
@WorkerThread
private fun onMediaProjectionStoppedLocked(info: MediaProjectionInfo) {
if (DEBUG) Log.d(TAG, "onMediaProjectionStoppedLocked: info=$info")
val item = makePrivacyItem(info)
privacyItems.removeAt(privacyItems.indexOfFirst { it.application == item.application })
logItemActive(item, false)
}
@WorkerThread
private fun makePrivacyItem(info: MediaProjectionInfo): PrivacyItem {
val userId = info.userHandle.identifier
val uid = packageManager.getPackageUidAsUser(info.packageName, userId)
val app = PrivacyApplication(info.packageName, uid)
val now = systemClock.elapsedRealtime()
return PrivacyItem(PrivacyType.TYPE_MEDIA_PROJECTION, app, now)
}
private fun logItemActive(item: PrivacyItem, active: Boolean) {
logger.logUpdatedItemFromMediaProjection(
item.application.uid, item.application.packageName, active)
}
/**
* Updates listening status based on whether there are callbacks and the indicator is enabled.
*/
@GuardedBy("lock")
private fun setListeningStateLocked() {
val shouldListen = mediaProjectionAvailable
if (DEBUG) {
Log.d(TAG, "shouldListen=$shouldListen, " +
"mediaProjectionAvailable=$mediaProjectionAvailable")
}
if (listening == shouldListen) {
return
}
listening = shouldListen
if (shouldListen) {
if (DEBUG) Log.d(TAG, "Registering MediaProjectionManager callback")
mediaProjectionManager.addCallback(mediaProjectionCallback, bgHandler)
val activeProjection = mediaProjectionManager.activeProjectionInfo
if (activeProjection != null) {
onMediaProjectionStartedLocked(activeProjection)
dispatchOnPrivacyItemsChanged()
}
} else {
if (DEBUG) Log.d(TAG, "Unregistering MediaProjectionManager callback")
mediaProjectionManager.removeCallback(mediaProjectionCallback)
privacyItems.forEach { logItemActive(it, false) }
privacyItems.clear()
dispatchOnPrivacyItemsChanged()
}
}
override fun getActivePrivacyItems(): List<PrivacyItem> {
synchronized(lock) {
if (DEBUG) Log.d(TAG, "getActivePrivacyItems: privacyItems=$privacyItems")
return privacyItems.toList()
}
}
private fun dispatchOnPrivacyItemsChanged() {
if (DEBUG) Log.d(TAG, "dispatchOnPrivacyItemsChanged")
val cb = synchronized(lock) { callback }
if (cb != null) {
bgHandler.post {
cb.onPrivacyItemsChanged()
}
}
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
val ipw = pw.asIndenting()
ipw.println("MediaProjectionPrivacyItemMonitor:")
ipw.withIncreasedIndent {
synchronized(lock) {
ipw.println("Listening: $listening")
ipw.println("mediaProjectionAvailable: $mediaProjectionAvailable")
ipw.println("Callback: $callback")
ipw.println("Privacy Items: $privacyItems")
}
}
ipw.flush()
}
}

View File

@@ -43,8 +43,11 @@ class PrivacyConfig @Inject constructor(
const val TAG = "PrivacyConfig"
private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
private const val MEDIA_PROJECTION =
SystemUiDeviceConfigFlags.PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED
private const val DEFAULT_MIC_CAMERA = true
private const val DEFAULT_LOCATION = false
private const val DEFAULT_MEDIA_PROJECTION = true
}
private val callbacks = mutableListOf<WeakReference<Callback>>()
@@ -53,6 +56,8 @@ class PrivacyConfig @Inject constructor(
private set
var locationAvailable = isLocationEnabled()
private set
var mediaProjectionAvailable = isMediaProjectionEnabled()
private set
private val devicePropertiesChangedListener =
DeviceConfig.OnPropertiesChangedListener { properties ->
@@ -67,6 +72,14 @@ class PrivacyConfig @Inject constructor(
locationAvailable = properties.getBoolean(LOCATION, DEFAULT_LOCATION)
callbacks.forEach { it.get()?.onFlagLocationChanged(locationAvailable) }
}
if (properties.keyset.contains(MEDIA_PROJECTION)) {
mediaProjectionAvailable =
properties.getBoolean(MEDIA_PROJECTION, DEFAULT_MEDIA_PROJECTION)
callbacks.forEach {
it.get()?.onFlagMediaProjectionChanged(mediaProjectionAvailable)
}
}
}
}
@@ -88,6 +101,11 @@ class PrivacyConfig @Inject constructor(
LOCATION, DEFAULT_LOCATION)
}
private fun isMediaProjectionEnabled(): Boolean {
return deviceConfigProxy.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
MEDIA_PROJECTION, DEFAULT_MEDIA_PROJECTION)
}
fun addCallback(callback: Callback) {
addCallback(WeakReference(callback))
}
@@ -115,6 +133,7 @@ class PrivacyConfig @Inject constructor(
ipw.withIncreasedIndent {
ipw.println("micCameraAvailable: $micCameraAvailable")
ipw.println("locationAvailable: $locationAvailable")
ipw.println("mediaProjectionAvailable: $mediaProjectionAvailable")
ipw.println("Callbacks:")
ipw.withIncreasedIndent {
callbacks.forEach { callback ->
@@ -131,5 +150,8 @@ class PrivacyConfig @Inject constructor(
@JvmDefault
fun onFlagLocationChanged(flag: Boolean) {}
@JvmDefault
fun onFlagMediaProjectionChanged(flag: Boolean) {}
}
}

View File

@@ -165,6 +165,7 @@ class PrivacyDialog(
PrivacyType.TYPE_LOCATION -> R.drawable.privacy_item_circle_location
PrivacyType.TYPE_CAMERA -> R.drawable.privacy_item_circle_camera
PrivacyType.TYPE_MICROPHONE -> R.drawable.privacy_item_circle_microphone
PrivacyType.TYPE_MEDIA_PROJECTION -> R.drawable.privacy_item_circle_media_projection
}) as LayerDrawable
}

View File

@@ -43,6 +43,12 @@ enum class PrivacyType(
com.android.internal.R.drawable.perm_group_location,
android.Manifest.permission_group.LOCATION,
"location"
),
TYPE_MEDIA_PROJECTION(
R.string.privacy_type_media_projection,
R.drawable.stat_sys_cast,
android.Manifest.permission_group.UNDEFINED,
"media projection"
);
fun getName(context: Context) = context.resources.getString(nameId)

View File

@@ -65,7 +65,7 @@ class PrivacyItemController @Inject constructor(
val locationAvailable
get() = privacyConfig.locationAvailable
val allIndicatorsAvailable
get() = micCameraAvailable && locationAvailable
get() = micCameraAvailable && locationAvailable && privacyConfig.mediaProjectionAvailable
private val notifyChanges = Runnable {
val list = privacyList
@@ -85,6 +85,10 @@ class PrivacyItemController @Inject constructor(
override fun onFlagMicCameraChanged(flag: Boolean) {
callbacks.forEach { it.get()?.onFlagMicCameraChanged(flag) }
}
override fun onFlagMediaProjectionChanged(flag: Boolean) {
callbacks.forEach { it.get()?.onFlagMediaProjectionChanged(flag) }
}
}
private val privacyItemMonitorCallback = object : PrivacyItemMonitor.Callback {

View File

@@ -44,6 +44,16 @@ class PrivacyLogger @Inject constructor(
})
}
fun logUpdatedItemFromMediaProjection(uid: Int, packageName: String, active: Boolean) {
log(LogLevel.INFO, {
int1 = uid
str1 = packageName
bool1 = active
}, {
"MediaProjection: $str1($int1), active=$bool1"
})
}
fun logRetrievedPrivacyItemsList(list: List<PrivacyItem>) {
log(LogLevel.INFO, {
str1 = listToString(list)

View File

@@ -39,6 +39,8 @@ import com.android.systemui.plugins.qs.QSFactory;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.power.EnhancedEstimates;
import com.android.systemui.power.dagger.PowerModule;
import com.android.systemui.privacy.MediaProjectionPrivacyItemMonitor;
import com.android.systemui.privacy.PrivacyItemMonitor;
import com.android.systemui.qs.dagger.QSModule;
import com.android.systemui.qs.tileimpl.QSFactoryImpl;
import com.android.systemui.recents.Recents;
@@ -78,6 +80,7 @@ import javax.inject.Named;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
/**
* A dagger module for injecting default implementations of components of System UI that may be
@@ -212,4 +215,12 @@ public abstract class TvSystemUIModule {
NotificationListener notificationListener) {
return new TvNotificationHandler(context, notificationListener);
}
/**
* Binds {@link MediaProjectionPrivacyItemMonitor} into the set of {@link PrivacyItemMonitor}.
*/
@Binds
@IntoSet
abstract PrivacyItemMonitor bindMediaProjectionPrivacyItemMonitor(
MediaProjectionPrivacyItemMonitor mediaProjectionPrivacyItemMonitor);
}

View File

@@ -43,6 +43,8 @@ class PrivacyConfigFlagsTest : SysuiTestCase() {
companion object {
private const val MIC_CAMERA = SystemUiDeviceConfigFlags.PROPERTY_MIC_CAMERA_ENABLED
private const val LOCATION = SystemUiDeviceConfigFlags.PROPERTY_LOCATION_INDICATORS_ENABLED
private const val MEDIA_PROJECTION =
SystemUiDeviceConfigFlags.PROPERTY_MEDIA_PROJECTION_INDICATORS_ENABLED
}
private lateinit var privacyConfig: PrivacyConfig
@@ -89,6 +91,16 @@ class PrivacyConfigFlagsTest : SysuiTestCase() {
assertFalse(privacyConfig.micCameraAvailable)
}
@Test
fun testMediaProjectionChanged() {
changeMediaProjection(false) // default is true
executor.runAllReady()
verify(callback).onFlagMediaProjectionChanged(false)
assertFalse(privacyConfig.mediaProjectionAvailable)
}
@Test
fun testLocationChanged() {
changeLocation(true)
@@ -99,8 +111,8 @@ class PrivacyConfigFlagsTest : SysuiTestCase() {
}
@Test
fun testBothChanged() {
changeAll(true)
fun testMicCamAndLocationChanged() {
changeLocation(true)
changeMicCamera(false)
executor.runAllReady()
@@ -124,10 +136,7 @@ class PrivacyConfigFlagsTest : SysuiTestCase() {
private fun changeMicCamera(value: Boolean?) = changeProperty(MIC_CAMERA, value)
private fun changeLocation(value: Boolean?) = changeProperty(LOCATION, value)
private fun changeAll(value: Boolean?) {
changeMicCamera(value)
changeLocation(value)
}
private fun changeMediaProjection(value: Boolean?) = changeProperty(MEDIA_PROJECTION, value)
private fun changeProperty(name: String, value: Boolean?) {
deviceConfigProxy.setProperty(

View File

@@ -361,8 +361,10 @@ class PrivacyItemControllerTest : SysuiTestCase() {
privacyItemController.addCallback(callback)
`when`(privacyConfig.micCameraAvailable).thenReturn(true)
`when`(privacyConfig.locationAvailable).thenReturn(true)
`when`(privacyConfig.mediaProjectionAvailable).thenReturn(true)
argCaptorConfigCallback.value.onFlagMicCameraChanged(true)
argCaptorConfigCallback.value.onFlagLocationChanged(true)
argCaptorConfigCallback.value.onFlagMediaProjectionChanged(true)
executor.runAllReady()
assertTrue(privacyItemController.allIndicatorsAvailable)
@@ -392,6 +394,17 @@ class PrivacyItemControllerTest : SysuiTestCase() {
verify(callback).onFlagLocationChanged(true)
}
@Test
fun testFlags_onFlagMediaProjectionChanged() {
verify(privacyConfig).addCallback(capture(argCaptorConfigCallback))
privacyItemController.addCallback(callback)
`when`(privacyConfig.mediaProjectionAvailable).thenReturn(true)
argCaptorConfigCallback.value.onFlagMediaProjectionChanged(true)
executor.runAllReady()
verify(callback).onFlagMediaProjectionChanged(true)
}
@Test
fun testPausedElementsAreRemoved() {
doReturn(listOf(