Perform camera permission and app ops check when setting camera for VT.
When a calling InCallService attempts to use the setCamera API on the
VideoCall, Telecom will perform a permission check to ensure that the
caller has the correct camera permission and passes the app-ops camera
check. A failure to set the camera will result in a callback via the
call session event API.
This got a little messy as the app ops package name needs to come from the
InCallService, and handler usage in the VideoProvider API means we had to
pass around the uid/pid of the caller, obtained before we trampoline onto
the handler.
Test: Unit tests added, plus manual tests.
Bug: 32747443
Change-Id: Ib96114502fe459b0429a87c5d13640b68ae6a2f7
diff --git a/src/com/android/server/telecom/VideoProviderProxy.java b/src/com/android/server/telecom/VideoProviderProxy.java
index fe3bbea..4e33482 100644
--- a/src/com/android/server/telecom/VideoProviderProxy.java
+++ b/src/com/android/server/telecom/VideoProviderProxy.java
@@ -16,7 +16,11 @@
package com.android.server.telecom;
+import android.Manifest;
+import android.app.AppOpsManager;
+import android.content.Context;
import android.net.Uri;
+import android.os.Binder;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
@@ -24,6 +28,7 @@
import android.telecom.InCallService;
import android.telecom.Log;
import android.telecom.VideoProfile;
+import android.text.TextUtils;
import android.view.Surface;
import com.android.internal.telecom.IVideoCallback;
@@ -33,6 +38,8 @@
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import static android.Manifest.permission.CALL_PHONE;
+
/**
* Proxies video provider messages from {@link InCallService.VideoCall}
* implementations to the underlying {@link Connection.VideoProvider} implementation. Also proxies
@@ -275,19 +282,43 @@
}
}
+ @Override
+ public void onSetCamera(String cameraId) {
+ // No-op. We implement the other prototype of onSetCamera so that we can use the calling
+ // package, uid and pid to verify permission.
+ }
+
/**
* Proxies a request from the {@link InCallService} to the
* {@link #mConectionServiceVideoProvider} to change the camera.
*
* @param cameraId The id of the camera.
+ * @param callingPackage The package calling in.
+ * @param callingUid The UID of the caller.
+ * @param callingPid The PID of the caller.
*/
@Override
- public void onSetCamera(String cameraId) {
+ public void onSetCamera(String cameraId, String callingPackage, int callingUid,
+ int callingPid) {
synchronized (mLock) {
- logFromInCall("setCamera: " + cameraId);
+ logFromInCall("setCamera: " + cameraId + " callingPackage=" + callingPackage);
+
+ if (!TextUtils.isEmpty(cameraId)) {
+ if (!canUseCamera(mCall.getContext(), callingPackage, callingUid, callingPid)) {
+ // Calling app is not permitted to use the camera. Ignore the request and send
+ // back a call session event indicating the error.
+ Log.i(this, "onSetCamera: camera permission denied; package=%d, uid=%d, pid=%d",
+ callingPackage, callingUid, callingPid);
+ VideoProviderProxy.this.handleCallSessionEvent(
+ Connection.VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR);
+ return;
+ }
+ }
try {
- mConectionServiceVideoProvider.setCamera(cameraId);
+ mConectionServiceVideoProvider.setCamera(cameraId, callingPackage);
} catch (RemoteException e) {
+ VideoProviderProxy.this.handleCallSessionEvent(
+ Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE);
}
}
}
@@ -491,4 +522,36 @@
private void logFromVideoProvider(String toLog) {
Log.i(this, "VP->IC (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog);
}
+
+ /**
+ * Determines if the caller has permission to use the camera.
+ *
+ * @param context The context.
+ * @param callingPackage The package name of the caller (i.e. Dialer).
+ * @param callingUid The UID of the caller.
+ * @return {@code true} if the calling uid and package can use the camera, {@code false}
+ * otherwise.
+ */
+ private boolean canUseCamera(Context context, String callingPackage, int callingUid,
+ int callingPid) {
+ try {
+ context.enforcePermission(Manifest.permission.CAMERA, callingPid, callingUid,
+ "Camera permission required.");
+ } catch (SecurityException se) {
+ return false;
+ }
+
+ AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(
+ Context.APP_OPS_SERVICE);
+
+ try {
+ // Some apps that have the permission can be restricted via app ops.
+ return appOpsManager != null && appOpsManager.noteOp(AppOpsManager.OP_CAMERA,
+ callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED;
+ } catch (SecurityException se) {
+ Log.w(this, "canUserCamera got appOpps Exception " + se.toString());
+ return false;
+ }
+ }
+
}
diff --git a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
index b22ae82..3d69ff0 100644
--- a/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
+++ b/tests/src/com/android/server/telecom/tests/AnalyticsTests.java
@@ -206,7 +206,8 @@
mConnectionServiceFixtureA.sendSetVideoProvider(
mConnectionServiceFixtureA.mLatestConnectionId);
InCallService.VideoCall videoCall =
- mInCallServiceFixtureX.getCall(callIds.mCallId).getVideoCallImpl();
+ mInCallServiceFixtureX.getCall(callIds.mCallId).getVideoCallImpl(
+ mInCallServiceComponentNameX.getPackageName());
videoCall.registerCallback(callback);
((VideoCallImpl) videoCall).setVideoState(VideoProfile.STATE_BIDIRECTIONAL);
diff --git a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
index 65352f9..76929e7 100644
--- a/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
+++ b/tests/src/com/android/server/telecom/tests/ComponentContextFixture.java
@@ -303,6 +303,12 @@
}
@Override
+ public void enforcePermission(
+ String permission, int pid, int uid, String message) {
+ // By default, don't enforce anything in mock.
+ }
+
+ @Override
public void startActivityAsUser(Intent intent, UserHandle userHandle) {
// For capturing
}
diff --git a/tests/src/com/android/server/telecom/tests/MockVideoProvider.java b/tests/src/com/android/server/telecom/tests/MockVideoProvider.java
index 147d232..fe852ec 100644
--- a/tests/src/com/android/server/telecom/tests/MockVideoProvider.java
+++ b/tests/src/com/android/server/telecom/tests/MockVideoProvider.java
@@ -165,6 +165,10 @@
} else if (CAMERA_BACK.equals(cameraId)) {
super.changeCameraCapabilities(new VideoProfile.CameraCapabilities(
CAMERA_BACK_DIMENSIONS, CAMERA_BACK_DIMENSIONS));
+ } else {
+ // If the camera is nulled, we will send back a "camera ready" event so that the unit
+ // test has something to wait for.
+ super.handleCallSessionEvent(VideoProvider.SESSION_EVENT_CAMERA_READY);
}
}
diff --git a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
index cca6446..da0f9b1 100644
--- a/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
+++ b/tests/src/com/android/server/telecom/tests/VideoProviderTest.java
@@ -23,6 +23,8 @@
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import android.app.AppOpsManager;
+import android.content.Context;
import android.graphics.SurfaceTexture;
import android.net.Uri;
import android.os.Handler;
@@ -46,9 +48,12 @@
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
+import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.mock;
@@ -71,6 +76,7 @@
private VideoCallImpl mVideoCallImpl;
private ConnectionServiceFixture.ConnectionInfo mConnectionInfo;
private CountDownLatch mVerificationLock;
+ private AppOpsManager mAppOpsManager;
private Answer mVerification = new Answer() {
@Override
@@ -83,6 +89,8 @@
@Override
public void setUp() throws Exception {
super.setUp();
+ mContext = mComponentContextFixture.getTestDouble().getApplicationContext();
+ mAppOpsManager = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mCallIds = startAndMakeActiveOutgoingCall(
"650-555-1212",
@@ -96,13 +104,18 @@
// Provide a mocked VideoCall.Callback to receive callbacks via.
mVideoCallCallback = mock(InCallService.VideoCall.Callback.class);
- mVideoCall = mInCallServiceFixtureX.getCall(mCallIds.mCallId).getVideoCallImpl();
+ mVideoCall = mInCallServiceFixtureX.getCall(mCallIds.mCallId).getVideoCallImpl(
+ mInCallServiceComponentNameX.getPackageName());
mVideoCallImpl = (VideoCallImpl) mVideoCall;
mVideoCall.registerCallback(mVideoCallCallback);
mConnectionInfo = mConnectionServiceFixtureA.mConnectionById.get(mCallIds.mConnectionId);
mVerificationLock = new CountDownLatch(1);
waitForHandlerAction(new Handler(Looper.getMainLooper()), TEST_TIMEOUT);
+
+ doNothing().when(mContext).enforcePermission(anyString(), anyInt(), anyInt(), anyString());
+ doReturn(AppOpsManager.MODE_ALLOWED).when(mAppOpsManager).noteOp(anyInt(), anyInt(),
+ anyString());
}
@Override
@@ -146,6 +159,86 @@
}
/**
+ * Tests the caller permission check in {@link VideoCall#setCamera(String)} to ensure a camera
+ * change from a non-permitted caller is ignored.
+ */
+ @MediumTest
+ public void testCameraChangePermissionFail() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback).onCallSessionEvent(anyInt());
+
+ // ensure permission check fails.
+ doThrow(new SecurityException()).when(mContext)
+ .enforcePermission(anyString(), anyInt(), anyInt(), anyString());
+
+ // Make a request to change the camera
+ mVideoCall.setCamera(MockVideoProvider.CAMERA_FRONT);
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // Capture the session event reported via the callback.
+ ArgumentCaptor<Integer> sessionEventCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT)).onCallSessionEvent(
+ sessionEventCaptor.capture());
+
+ assertEquals(VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR,
+ sessionEventCaptor.getValue().intValue());
+ }
+
+ /**
+ * Tests the caller app ops check in {@link VideoCall#setCamera(String)} to ensure a camera
+ * change from a non-permitted caller is ignored.
+ */
+ @MediumTest
+ public void testCameraChangeAppOpsFail() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback).onCallSessionEvent(anyInt());
+
+ // ensure app ops check fails.
+ doReturn(AppOpsManager.MODE_ERRORED).when(mAppOpsManager).noteOp(anyInt(), anyInt(),
+ anyString());
+
+ // Make a request to change the camera
+ mVideoCall.setCamera(MockVideoProvider.CAMERA_FRONT);
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // Capture the session event reported via the callback.
+ ArgumentCaptor<Integer> sessionEventCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT)).onCallSessionEvent(
+ sessionEventCaptor.capture());
+
+ assertEquals(VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR,
+ sessionEventCaptor.getValue().intValue());
+ }
+
+ /**
+ * Tests the caller permission check in {@link VideoCall#setCamera(String)} to ensure the
+ * caller can null out the camera, even if they do not have camera permission.
+ */
+ @MediumTest
+ public void testCameraChangeNullNoPermission() throws Exception {
+ // Wait until the callback has been received before performing verification.
+ doAnswer(mVerification).when(mVideoCallCallback).onCallSessionEvent(anyInt());
+
+ // ensure permission check fails.
+ doThrow(new SecurityException()).when(mContext)
+ .enforcePermission(anyString(), anyInt(), anyInt(), anyString());
+
+ // Make a request to null the camera; we expect the permission check won't happen.
+ mVideoCall.setCamera(null);
+ mVerificationLock.await(TEST_TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // Capture the session event reported via the callback.
+ ArgumentCaptor<Integer> sessionEventCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mVideoCallCallback, timeout(TEST_TIMEOUT)).onCallSessionEvent(
+ sessionEventCaptor.capture());
+
+ // See the MockVideoProvider class; for convenience when the camera is nulled we just send
+ // back a "camera ready" event.
+ assertEquals(VideoProvider.SESSION_EVENT_CAMERA_READY,
+ sessionEventCaptor.getValue().intValue());
+ }
+
+ /**
* Tests the {@link VideoCall#setPreviewSurface(Surface)} and
* {@link VideoProvider#onSetPreviewSurface(Surface)} APIs.
*/