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.
      */