Loading core/res/res/values/strings.xml +4 −0 Original line number Diff line number Diff line Loading @@ -6266,4 +6266,8 @@ ul.</string> </string> <!-- Action label of notification for user to check background apps. [CHAR LIMIT=NONE] --> <string name="notification_action_check_bg_apps">Check active apps</string> <!-- Strings for VirtualDeviceManager --> <!-- Error message indicating the camera cannot be accessed when running on a virtual device. [CHAR LIMIT=NONE] --> <string name="vdm_camera_access_denied">Cannot access camera from this device</string> </resources> core/res/res/values/symbols.xml +3 −0 Original line number Diff line number Diff line Loading @@ -4724,5 +4724,8 @@ <java-symbol type="bool" name="config_lowPowerStandbyEnabledByDefault" /> <java-symbol type="integer" name="config_lowPowerStandbyNonInteractiveTimeout" /> <!-- For VirtualDeviceManager --> <java-symbol type="string" name="vdm_camera_access_denied" /> <java-symbol type="color" name="camera_privacy_light"/> </resources> services/companion/java/com/android/server/companion/virtual/CameraAccessController.java 0 → 100644 +212 −0 Original line number Diff line number Diff line /* * 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.server.companion.virtual; import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED; import android.annotation.NonNull; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraInjectionSession; import android.hardware.camera2.CameraManager; import android.util.ArrayMap; import android.util.Slog; import com.android.internal.annotations.GuardedBy; /** * Handles blocking access to the camera for apps running on virtual devices. */ class CameraAccessController extends CameraManager.AvailabilityCallback { private static final String TAG = "CameraAccessController"; private final Object mLock = new Object(); private final Context mContext; private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal; CameraAccessBlockedCallback mBlockedCallback; private CameraManager mCameraManager; private boolean mListeningForCameraEvents; private PackageManager mPackageManager; @GuardedBy("mLock") private ArrayMap<String, InjectionSessionData> mPackageToSessionData = new ArrayMap<>(); static class InjectionSessionData { public int appUid; public ArrayMap<String, CameraInjectionSession> cameraIdToSession = new ArrayMap<>(); } interface CameraAccessBlockedCallback { /** * Called whenever an app was blocked from accessing a camera. * @param appUid uid for the app which was blocked */ void onCameraAccessBlocked(int appUid); } CameraAccessController(Context context, VirtualDeviceManagerInternal virtualDeviceManagerInternal, CameraAccessBlockedCallback blockedCallback) { mContext = context; mVirtualDeviceManagerInternal = virtualDeviceManagerInternal; mBlockedCallback = blockedCallback; mCameraManager = mContext.getSystemService(CameraManager.class); mPackageManager = mContext.getPackageManager(); } /** * Starts watching for camera access by uids running on a virtual device, if we were not * already doing so. */ public void startObservingIfNeeded() { synchronized (mLock) { if (!mListeningForCameraEvents) { mCameraManager.registerAvailabilityCallback(mContext.getMainExecutor(), this); mListeningForCameraEvents = true; } } } /** * Stop watching for camera access. */ public void stopObserving() { synchronized (mLock) { mCameraManager.unregisterAvailabilityCallback(this); mListeningForCameraEvents = false; } } @Override public void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { synchronized (mLock) { try { final ApplicationInfo ainfo = mPackageManager.getApplicationInfo(packageName, 0); InjectionSessionData data = mPackageToSessionData.get(packageName); if (!mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(ainfo.uid)) { CameraInjectionSession existingSession = (data != null) ? data.cameraIdToSession.get(cameraId) : null; if (existingSession != null) { existingSession.close(); data.cameraIdToSession.remove(cameraId); if (data.cameraIdToSession.isEmpty()) { mPackageToSessionData.remove(packageName); } } return; } if (data == null) { data = new InjectionSessionData(); data.appUid = ainfo.uid; mPackageToSessionData.put(packageName, data); } if (data.cameraIdToSession.containsKey(cameraId)) { return; } startBlocking(packageName, cameraId); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "onCameraOpened - unknown package " + packageName, e); return; } } } @Override public void onCameraClosed(@NonNull String cameraId) { synchronized (mLock) { for (int i = mPackageToSessionData.size() - 1; i >= 0; i--) { InjectionSessionData data = mPackageToSessionData.valueAt(i); CameraInjectionSession session = data.cameraIdToSession.get(cameraId); if (session != null) { session.close(); data.cameraIdToSession.remove(cameraId); if (data.cameraIdToSession.isEmpty()) { mPackageToSessionData.removeAt(i); } } } } } /** * Turns on blocking for a particular camera and package. */ private void startBlocking(String packageName, String cameraId) { try { mCameraManager.injectCamera(packageName, cameraId, /* externalCamId */ "", mContext.getMainExecutor(), new CameraInjectionSession.InjectionStatusCallback() { @Override public void onInjectionSucceeded( @NonNull CameraInjectionSession session) { CameraAccessController.this.onInjectionSucceeded(cameraId, packageName, session); } @Override public void onInjectionError(@NonNull int errorCode) { CameraAccessController.this.onInjectionError(cameraId, packageName, errorCode); } }); } catch (CameraAccessException e) { Slog.e(TAG, "Failed to injectCamera for cameraId:" + cameraId + " package:" + packageName, e); } } private void onInjectionSucceeded(String cameraId, String packageName, @NonNull CameraInjectionSession session) { synchronized (mLock) { InjectionSessionData data = mPackageToSessionData.get(packageName); if (data == null) { Slog.e(TAG, "onInjectionSucceeded didn't find expected entry for package " + packageName); session.close(); return; } CameraInjectionSession existingSession = data.cameraIdToSession.put(cameraId, session); if (existingSession != null) { Slog.e(TAG, "onInjectionSucceeded found unexpected existing session for camera " + cameraId); existingSession.close(); } } } private void onInjectionError(String cameraId, String packageName, @NonNull int errorCode) { if (errorCode != ERROR_INJECTION_UNSUPPORTED) { // ERROR_INJECTION_UNSUPPORTED means that there wasn't an external camera to map to the // internal camera, which is expected when using the injection interface as we are in // this class to simply block camera access. Any other error is unexpected. Slog.e(TAG, "Unexpected injection error code:" + errorCode + " for camera:" + cameraId + " and package:" + packageName); return; } synchronized (mLock) { InjectionSessionData data = mPackageToSessionData.get(packageName); if (data != null) { mBlockedCallback.onCameraAccessBlocked(data.appUid); } } } } services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +23 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.StringRes; import android.app.Activity; import android.app.ActivityOptions; import android.app.PendingIntent; Loading Loading @@ -57,6 +58,8 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.widget.Toast; import android.window.DisplayWindowPolicyController; import com.android.internal.annotations.GuardedBy; Loading Loading @@ -583,6 +586,26 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return false; } /** * Shows a toast on virtual displays owned by this device which have a given uid running. */ void showToastWhereUidIsRunning(int uid, @StringRes int resId, @Toast.Duration int duration) { synchronized (mVirtualDeviceLock) { DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); final int size = mWindowPolicyControllers.size(); for (int i = 0; i < size; i++) { if (mWindowPolicyControllers.valueAt(i).containsUid(uid)) { int displayId = mWindowPolicyControllers.keyAt(i); Display display = displayManager.getDisplay(displayId); if (display != null && display.isValid()) { Toast.makeText(mContext.createDisplayContext(display), resId, duration).show(); } } } } } interface OnDeviceCloseListener { void onClose(int associationId); } Loading services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +21 −2 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.os.RemoteException; import android.util.ExceptionUtils; import android.util.Slog; import android.util.SparseArray; import android.widget.Toast; import android.window.DisplayWindowPolicyController; import com.android.internal.annotations.GuardedBy; Loading @@ -62,8 +63,10 @@ public class VirtualDeviceManagerService extends SystemService { private final Object mVirtualDeviceManagerLock = new Object(); private final VirtualDeviceManagerImpl mImpl; private VirtualDeviceManagerInternal mLocalService; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler); private final CameraAccessController mCameraAccessController; /** * Mapping from CDM association IDs to virtual devices. Only one virtual device is allowed for Loading @@ -90,6 +93,9 @@ public class VirtualDeviceManagerService extends SystemService { public VirtualDeviceManagerService(Context context) { super(context); mImpl = new VirtualDeviceManagerImpl(); mLocalService = new LocalService(); mCameraAccessController = new CameraAccessController(getContext(), mLocalService, this::onCameraAccessBlocked); } private final ActivityInterceptorCallback mActivityInterceptorCallback = Loading Loading @@ -118,8 +124,7 @@ public class VirtualDeviceManagerService extends SystemService { @Override public void onStart() { publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl); publishLocalService(VirtualDeviceManagerInternal.class, new LocalService()); publishLocalService(VirtualDeviceManagerInternal.class, mLocalService); ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService( ActivityTaskManagerInternal.class); activityTaskManagerInternal.registerActivityStartInterceptor( Loading Loading @@ -169,6 +174,16 @@ public class VirtualDeviceManagerService extends SystemService { } } void onCameraAccessBlocked(int appUid) { synchronized (mVirtualDeviceManagerLock) { int size = mVirtualDevices.size(); for (int i = 0; i < size; i++) { mVirtualDevices.valueAt(i).showToastWhereUidIsRunning(appUid, com.android.internal.R.string.vdm_camera_access_denied, Toast.LENGTH_LONG); } } } class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub implements VirtualDeviceImpl.PendingTrampolineCallback { Loading Loading @@ -205,10 +220,14 @@ public class VirtualDeviceManagerService extends SystemService { public void onClose(int associationId) { synchronized (mVirtualDeviceManagerLock) { mVirtualDevices.remove(associationId); if (mVirtualDevices.size() == 0) { mCameraAccessController.stopObserving(); } } } }, this, activityListener, params); mCameraAccessController.startObservingIfNeeded(); mVirtualDevices.put(associationInfo.getId(), virtualDevice); return virtualDevice; } Loading Loading
core/res/res/values/strings.xml +4 −0 Original line number Diff line number Diff line Loading @@ -6266,4 +6266,8 @@ ul.</string> </string> <!-- Action label of notification for user to check background apps. [CHAR LIMIT=NONE] --> <string name="notification_action_check_bg_apps">Check active apps</string> <!-- Strings for VirtualDeviceManager --> <!-- Error message indicating the camera cannot be accessed when running on a virtual device. [CHAR LIMIT=NONE] --> <string name="vdm_camera_access_denied">Cannot access camera from this device</string> </resources>
core/res/res/values/symbols.xml +3 −0 Original line number Diff line number Diff line Loading @@ -4724,5 +4724,8 @@ <java-symbol type="bool" name="config_lowPowerStandbyEnabledByDefault" /> <java-symbol type="integer" name="config_lowPowerStandbyNonInteractiveTimeout" /> <!-- For VirtualDeviceManager --> <java-symbol type="string" name="vdm_camera_access_denied" /> <java-symbol type="color" name="camera_privacy_light"/> </resources>
services/companion/java/com/android/server/companion/virtual/CameraAccessController.java 0 → 100644 +212 −0 Original line number Diff line number Diff line /* * 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.server.companion.virtual; import static android.hardware.camera2.CameraInjectionSession.InjectionStatusCallback.ERROR_INJECTION_UNSUPPORTED; import android.annotation.NonNull; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraInjectionSession; import android.hardware.camera2.CameraManager; import android.util.ArrayMap; import android.util.Slog; import com.android.internal.annotations.GuardedBy; /** * Handles blocking access to the camera for apps running on virtual devices. */ class CameraAccessController extends CameraManager.AvailabilityCallback { private static final String TAG = "CameraAccessController"; private final Object mLock = new Object(); private final Context mContext; private VirtualDeviceManagerInternal mVirtualDeviceManagerInternal; CameraAccessBlockedCallback mBlockedCallback; private CameraManager mCameraManager; private boolean mListeningForCameraEvents; private PackageManager mPackageManager; @GuardedBy("mLock") private ArrayMap<String, InjectionSessionData> mPackageToSessionData = new ArrayMap<>(); static class InjectionSessionData { public int appUid; public ArrayMap<String, CameraInjectionSession> cameraIdToSession = new ArrayMap<>(); } interface CameraAccessBlockedCallback { /** * Called whenever an app was blocked from accessing a camera. * @param appUid uid for the app which was blocked */ void onCameraAccessBlocked(int appUid); } CameraAccessController(Context context, VirtualDeviceManagerInternal virtualDeviceManagerInternal, CameraAccessBlockedCallback blockedCallback) { mContext = context; mVirtualDeviceManagerInternal = virtualDeviceManagerInternal; mBlockedCallback = blockedCallback; mCameraManager = mContext.getSystemService(CameraManager.class); mPackageManager = mContext.getPackageManager(); } /** * Starts watching for camera access by uids running on a virtual device, if we were not * already doing so. */ public void startObservingIfNeeded() { synchronized (mLock) { if (!mListeningForCameraEvents) { mCameraManager.registerAvailabilityCallback(mContext.getMainExecutor(), this); mListeningForCameraEvents = true; } } } /** * Stop watching for camera access. */ public void stopObserving() { synchronized (mLock) { mCameraManager.unregisterAvailabilityCallback(this); mListeningForCameraEvents = false; } } @Override public void onCameraOpened(@NonNull String cameraId, @NonNull String packageName) { synchronized (mLock) { try { final ApplicationInfo ainfo = mPackageManager.getApplicationInfo(packageName, 0); InjectionSessionData data = mPackageToSessionData.get(packageName); if (!mVirtualDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(ainfo.uid)) { CameraInjectionSession existingSession = (data != null) ? data.cameraIdToSession.get(cameraId) : null; if (existingSession != null) { existingSession.close(); data.cameraIdToSession.remove(cameraId); if (data.cameraIdToSession.isEmpty()) { mPackageToSessionData.remove(packageName); } } return; } if (data == null) { data = new InjectionSessionData(); data.appUid = ainfo.uid; mPackageToSessionData.put(packageName, data); } if (data.cameraIdToSession.containsKey(cameraId)) { return; } startBlocking(packageName, cameraId); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "onCameraOpened - unknown package " + packageName, e); return; } } } @Override public void onCameraClosed(@NonNull String cameraId) { synchronized (mLock) { for (int i = mPackageToSessionData.size() - 1; i >= 0; i--) { InjectionSessionData data = mPackageToSessionData.valueAt(i); CameraInjectionSession session = data.cameraIdToSession.get(cameraId); if (session != null) { session.close(); data.cameraIdToSession.remove(cameraId); if (data.cameraIdToSession.isEmpty()) { mPackageToSessionData.removeAt(i); } } } } } /** * Turns on blocking for a particular camera and package. */ private void startBlocking(String packageName, String cameraId) { try { mCameraManager.injectCamera(packageName, cameraId, /* externalCamId */ "", mContext.getMainExecutor(), new CameraInjectionSession.InjectionStatusCallback() { @Override public void onInjectionSucceeded( @NonNull CameraInjectionSession session) { CameraAccessController.this.onInjectionSucceeded(cameraId, packageName, session); } @Override public void onInjectionError(@NonNull int errorCode) { CameraAccessController.this.onInjectionError(cameraId, packageName, errorCode); } }); } catch (CameraAccessException e) { Slog.e(TAG, "Failed to injectCamera for cameraId:" + cameraId + " package:" + packageName, e); } } private void onInjectionSucceeded(String cameraId, String packageName, @NonNull CameraInjectionSession session) { synchronized (mLock) { InjectionSessionData data = mPackageToSessionData.get(packageName); if (data == null) { Slog.e(TAG, "onInjectionSucceeded didn't find expected entry for package " + packageName); session.close(); return; } CameraInjectionSession existingSession = data.cameraIdToSession.put(cameraId, session); if (existingSession != null) { Slog.e(TAG, "onInjectionSucceeded found unexpected existing session for camera " + cameraId); existingSession.close(); } } } private void onInjectionError(String cameraId, String packageName, @NonNull int errorCode) { if (errorCode != ERROR_INJECTION_UNSUPPORTED) { // ERROR_INJECTION_UNSUPPORTED means that there wasn't an external camera to map to the // internal camera, which is expected when using the injection interface as we are in // this class to simply block camera access. Any other error is unexpected. Slog.e(TAG, "Unexpected injection error code:" + errorCode + " for camera:" + cameraId + " and package:" + packageName); return; } synchronized (mLock) { InjectionSessionData data = mPackageToSessionData.get(packageName); if (data != null) { mBlockedCallback.onCameraAccessBlocked(data.appUid); } } } }
services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +23 −0 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.StringRes; import android.app.Activity; import android.app.ActivityOptions; import android.app.PendingIntent; Loading Loading @@ -57,6 +58,8 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import android.util.SparseArray; import android.view.Display; import android.widget.Toast; import android.window.DisplayWindowPolicyController; import com.android.internal.annotations.GuardedBy; Loading Loading @@ -583,6 +586,26 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub return false; } /** * Shows a toast on virtual displays owned by this device which have a given uid running. */ void showToastWhereUidIsRunning(int uid, @StringRes int resId, @Toast.Duration int duration) { synchronized (mVirtualDeviceLock) { DisplayManager displayManager = mContext.getSystemService(DisplayManager.class); final int size = mWindowPolicyControllers.size(); for (int i = 0; i < size; i++) { if (mWindowPolicyControllers.valueAt(i).containsUid(uid)) { int displayId = mWindowPolicyControllers.keyAt(i); Display display = displayManager.getDisplay(displayId); if (display != null && display.isValid()) { Toast.makeText(mContext.createDisplayContext(display), resId, duration).show(); } } } } } interface OnDeviceCloseListener { void onClose(int associationId); } Loading
services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +21 −2 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ import android.os.RemoteException; import android.util.ExceptionUtils; import android.util.Slog; import android.util.SparseArray; import android.widget.Toast; import android.window.DisplayWindowPolicyController; import com.android.internal.annotations.GuardedBy; Loading @@ -62,8 +63,10 @@ public class VirtualDeviceManagerService extends SystemService { private final Object mVirtualDeviceManagerLock = new Object(); private final VirtualDeviceManagerImpl mImpl; private VirtualDeviceManagerInternal mLocalService; private final Handler mHandler = new Handler(Looper.getMainLooper()); private final PendingTrampolineMap mPendingTrampolines = new PendingTrampolineMap(mHandler); private final CameraAccessController mCameraAccessController; /** * Mapping from CDM association IDs to virtual devices. Only one virtual device is allowed for Loading @@ -90,6 +93,9 @@ public class VirtualDeviceManagerService extends SystemService { public VirtualDeviceManagerService(Context context) { super(context); mImpl = new VirtualDeviceManagerImpl(); mLocalService = new LocalService(); mCameraAccessController = new CameraAccessController(getContext(), mLocalService, this::onCameraAccessBlocked); } private final ActivityInterceptorCallback mActivityInterceptorCallback = Loading Loading @@ -118,8 +124,7 @@ public class VirtualDeviceManagerService extends SystemService { @Override public void onStart() { publishBinderService(Context.VIRTUAL_DEVICE_SERVICE, mImpl); publishLocalService(VirtualDeviceManagerInternal.class, new LocalService()); publishLocalService(VirtualDeviceManagerInternal.class, mLocalService); ActivityTaskManagerInternal activityTaskManagerInternal = getLocalService( ActivityTaskManagerInternal.class); activityTaskManagerInternal.registerActivityStartInterceptor( Loading Loading @@ -169,6 +174,16 @@ public class VirtualDeviceManagerService extends SystemService { } } void onCameraAccessBlocked(int appUid) { synchronized (mVirtualDeviceManagerLock) { int size = mVirtualDevices.size(); for (int i = 0; i < size; i++) { mVirtualDevices.valueAt(i).showToastWhereUidIsRunning(appUid, com.android.internal.R.string.vdm_camera_access_denied, Toast.LENGTH_LONG); } } } class VirtualDeviceManagerImpl extends IVirtualDeviceManager.Stub implements VirtualDeviceImpl.PendingTrampolineCallback { Loading Loading @@ -205,10 +220,14 @@ public class VirtualDeviceManagerService extends SystemService { public void onClose(int associationId) { synchronized (mVirtualDeviceManagerLock) { mVirtualDevices.remove(associationId); if (mVirtualDevices.size() == 0) { mCameraAccessController.stopObserving(); } } } }, this, activityListener, params); mCameraAccessController.startObservingIfNeeded(); mVirtualDevices.put(associationInfo.getId(), virtualDevice); return virtualDevice; } Loading