summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/res/res/values/strings.xml4
-rw-r--r--core/res/res/values/symbols.xml3
-rw-r--r--services/companion/java/com/android/server/companion/virtual/CameraAccessController.java212
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java23
-rw-r--r--services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java23
-rw-r--r--services/tests/mockingservicestests/Android.bp1
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java200
7 files changed, 464 insertions, 2 deletions
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6297ed90fcba..cb209ab15f03 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -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>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d731180bd56a..e10ed2434676 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -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>
diff --git a/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java b/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java
new file mode 100644
index 000000000000..2c42c910b9ab
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/virtual/CameraAccessController.java
@@ -0,0 +1,212 @@
+/*
+ * 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);
+ }
+ }
+ }
+}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 3fa1c865bda1..2f6a46a02338 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -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;
@@ -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;
@@ -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);
}
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index b507110d5473..7842697d6871 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -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;
@@ -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
@@ -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 =
@@ -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(
@@ -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 {
@@ -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;
}
diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp
index 75669d50c75d..3e60af33e408 100644
--- a/services/tests/mockingservicestests/Android.bp
+++ b/services/tests/mockingservicestests/Android.bp
@@ -53,6 +53,7 @@ android_test {
"service-jobscheduler",
"service-permission.impl",
"service-supplementalprocess.impl",
+ "services.companion",
"services.core",
"services.devicepolicy",
"services.net",
diff --git a/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
new file mode 100644
index 000000000000..1b9cb28dc8b2
--- /dev/null
+++ b/services/tests/mockingservicestests/src/com/android/server/companion/virtual/CameraAccessControllerTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+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.os.Process;
+import android.testing.TestableContext;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+public class CameraAccessControllerTest {
+ private static final String FRONT_CAMERA = "0";
+ private static final String REAR_CAMERA = "1";
+ private static final String TEST_APP_PACKAGE = "some.package";
+ private static final String OTHER_APP_PACKAGE = "other.package";
+
+ private CameraAccessController mController;
+
+ @Rule
+ public final TestableContext mContext = new TestableContext(
+ InstrumentationRegistry.getContext());
+
+ @Mock
+ private CameraManager mCameraManager;
+ @Mock
+ private PackageManager mPackageManager;
+ @Mock
+ private VirtualDeviceManagerInternal mDeviceManagerInternal;
+ @Mock
+ private CameraAccessController.CameraAccessBlockedCallback mBlockedCallback;
+
+ private ApplicationInfo mTestAppInfo = new ApplicationInfo();
+ private ApplicationInfo mOtherAppInfo = new ApplicationInfo();
+
+ @Captor
+ ArgumentCaptor<CameraInjectionSession.InjectionStatusCallback> mInjectionCallbackCaptor;
+
+
+ @Before
+ public void setUp() throws PackageManager.NameNotFoundException {
+ MockitoAnnotations.initMocks(this);
+ mContext.addMockSystemService(CameraManager.class, mCameraManager);
+ mContext.setMockPackageManager(mPackageManager);
+ LocalServices.removeServiceForTest(VirtualDeviceManagerInternal.class);
+ LocalServices.addService(VirtualDeviceManagerInternal.class, mDeviceManagerInternal);
+ mController = new CameraAccessController(mContext, mDeviceManagerInternal,
+ mBlockedCallback);
+ mTestAppInfo.uid = Process.FIRST_APPLICATION_UID;
+ mOtherAppInfo.uid = Process.FIRST_APPLICATION_UID + 1;
+ when(mPackageManager.getApplicationInfo(eq(TEST_APP_PACKAGE), anyInt())).thenReturn(
+ mTestAppInfo);
+ when(mPackageManager.getApplicationInfo(eq(OTHER_APP_PACKAGE), anyInt())).thenReturn(
+ mOtherAppInfo);
+ mController.startObservingIfNeeded();
+ }
+
+ @Test
+ public void onCameraOpened_uidNotRunning_noCameraBlocking() throws CameraAccessException {
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mTestAppInfo.uid))).thenReturn(false);
+ mController.onCameraOpened(FRONT_CAMERA, TEST_APP_PACKAGE);
+ verify(mCameraManager, never()).injectCamera(any(), any(), any(), any(), any());
+ }
+
+
+ @Test
+ public void onCameraOpened_uidRunning_cameraBlocked() throws CameraAccessException {
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mTestAppInfo.uid))).thenReturn(true);
+ mController.onCameraOpened(FRONT_CAMERA, TEST_APP_PACKAGE);
+ verify(mCameraManager).injectCamera(eq(TEST_APP_PACKAGE), eq(FRONT_CAMERA), anyString(),
+ any(), any());
+ }
+
+ @Test
+ public void onCameraClosed_injectionWasActive_cameraUnblocked() throws CameraAccessException {
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mTestAppInfo.uid))).thenReturn(true);
+ mController.onCameraOpened(FRONT_CAMERA, TEST_APP_PACKAGE);
+ verify(mCameraManager).injectCamera(eq(TEST_APP_PACKAGE), eq(FRONT_CAMERA), anyString(),
+ any(), mInjectionCallbackCaptor.capture());
+ CameraInjectionSession session = mock(CameraInjectionSession.class);
+ mInjectionCallbackCaptor.getValue().onInjectionSucceeded(session);
+
+ mController.onCameraClosed(FRONT_CAMERA);
+ verify(session).close();
+ }
+
+
+ @Test
+ public void onCameraClosed_otherCameraClosed_cameraNotUnblocked() throws CameraAccessException {
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mTestAppInfo.uid))).thenReturn(true);
+ mController.onCameraOpened(FRONT_CAMERA, TEST_APP_PACKAGE);
+ verify(mCameraManager).injectCamera(eq(TEST_APP_PACKAGE), eq(FRONT_CAMERA), anyString(),
+ any(), mInjectionCallbackCaptor.capture());
+ CameraInjectionSession session = mock(CameraInjectionSession.class);
+ mInjectionCallbackCaptor.getValue().onInjectionSucceeded(session);
+
+ mController.onCameraClosed(REAR_CAMERA);
+ verify(session, never()).close();
+ }
+
+ @Test
+ public void onCameraClosed_twoCamerasBlocked_correctCameraUnblocked()
+ throws CameraAccessException {
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mTestAppInfo.uid))).thenReturn(true);
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mOtherAppInfo.uid))).thenReturn(true);
+
+ mController.onCameraOpened(FRONT_CAMERA, TEST_APP_PACKAGE);
+ mController.onCameraOpened(REAR_CAMERA, OTHER_APP_PACKAGE);
+
+ verify(mCameraManager).injectCamera(eq(TEST_APP_PACKAGE), eq(FRONT_CAMERA), anyString(),
+ any(), mInjectionCallbackCaptor.capture());
+ CameraInjectionSession frontCameraSession = mock(CameraInjectionSession.class);
+ mInjectionCallbackCaptor.getValue().onInjectionSucceeded(frontCameraSession);
+
+ verify(mCameraManager).injectCamera(eq(OTHER_APP_PACKAGE), eq(REAR_CAMERA), anyString(),
+ any(), mInjectionCallbackCaptor.capture());
+ CameraInjectionSession rearCameraSession = mock(CameraInjectionSession.class);
+ mInjectionCallbackCaptor.getValue().onInjectionSucceeded(rearCameraSession);
+
+ mController.onCameraClosed(REAR_CAMERA);
+ verify(frontCameraSession, never()).close();
+ verify(rearCameraSession).close();
+ }
+
+ @Test
+ public void onInjectionError_callbackFires() throws CameraAccessException {
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mTestAppInfo.uid))).thenReturn(true);
+ mController.onCameraOpened(FRONT_CAMERA, TEST_APP_PACKAGE);
+ verify(mCameraManager).injectCamera(eq(TEST_APP_PACKAGE), eq(FRONT_CAMERA), anyString(),
+ any(), mInjectionCallbackCaptor.capture());
+ CameraInjectionSession session = mock(CameraInjectionSession.class);
+ mInjectionCallbackCaptor.getValue().onInjectionSucceeded(session);
+ mInjectionCallbackCaptor.getValue().onInjectionError(ERROR_INJECTION_UNSUPPORTED);
+ verify(mBlockedCallback).onCameraAccessBlocked(eq(mTestAppInfo.uid));
+ }
+
+ @Test
+ public void twoCameraAccesses_onlyOneOnVirtualDisplay_callbackFiresForCorrectUid()
+ throws CameraAccessException {
+ when(mDeviceManagerInternal.isAppRunningOnAnyVirtualDevice(
+ eq(mTestAppInfo.uid))).thenReturn(true);
+ mController.onCameraOpened(FRONT_CAMERA, TEST_APP_PACKAGE);
+ mController.onCameraOpened(REAR_CAMERA, OTHER_APP_PACKAGE);
+
+ verify(mCameraManager).injectCamera(eq(TEST_APP_PACKAGE), eq(FRONT_CAMERA), anyString(),
+ any(), mInjectionCallbackCaptor.capture());
+ CameraInjectionSession session = mock(CameraInjectionSession.class);
+ mInjectionCallbackCaptor.getValue().onInjectionSucceeded(session);
+ mInjectionCallbackCaptor.getValue().onInjectionError(ERROR_INJECTION_UNSUPPORTED);
+ verify(mBlockedCallback).onCameraAccessBlocked(eq(mTestAppInfo.uid));
+ }
+}