diff options
13 files changed, 336 insertions, 7 deletions
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index d72073d1ed74..777104d89c0b 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -158,6 +158,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. */ public static final String PROPERTY_LOCATION_INDICATORS_SMALL_ENABLED = diff --git a/packages/SystemUI/res/drawable/privacy_item_circle_media_projection.xml b/packages/SystemUI/res/drawable/privacy_item_circle_media_projection.xml new file mode 100644 index 000000000000..ac563dee2af5 --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_item_circle_media_projection.xml @@ -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>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 20e6a16b601f..6f33986a0a98 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2074,6 +2074,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> diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 5b6ddd8471da..aeda20f16515 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -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; @@ -321,6 +322,11 @@ public class FrameworkServicesModule { } @Provides + static MediaProjectionManager provideMediaProjectionManager(Context context) { + return context.getSystemService(MediaProjectionManager.class); + } + + @Provides static MediaRouter2Manager provideMediaRouter2Manager(Context context) { return MediaRouter2Manager.getInstance(context); } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/MediaProjectionPrivacyItemMonitor.kt b/packages/SystemUI/src/com/android/systemui/privacy/MediaProjectionPrivacyItemMonitor.kt new file mode 100644 index 000000000000..9b5a675371cc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/MediaProjectionPrivacyItemMonitor.kt @@ -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() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt index 6d29ba16837d..d652889f0082 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyConfig.kt @@ -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) {} } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt index d4e164208167..03145a714289 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialog.kt @@ -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 } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt index 76199bfab0e0..8b41000d2543 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItem.kt @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt index f72e022242d0..a676150f44a2 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyItemController.kt @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt index 1a268b5f94a9..1ea93474f954 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -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) diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java index 4685c148e7e5..9a19d8d11190 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java @@ -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); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt index 1b8564ca9cf8..272f149f379b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyConfigFlagsTest.kt @@ -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 @@ -90,6 +92,16 @@ class PrivacyConfigFlagsTest : SysuiTestCase() { } @Test + fun testMediaProjectionChanged() { + changeMediaProjection(false) // default is true + executor.runAllReady() + + verify(callback).onFlagMediaProjectionChanged(false) + + assertFalse(privacyConfig.mediaProjectionAvailable) + } + + @Test fun testLocationChanged() { changeLocation(true) executor.runAllReady() @@ -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( diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt index 41f8f049b4c5..d56363231827 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyItemControllerTest.kt @@ -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) @@ -393,6 +395,17 @@ class PrivacyItemControllerTest : SysuiTestCase() { } @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( PrivacyItem(PrivacyType.TYPE_MICROPHONE, |