Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f1dcfdb9 authored by Tyler Gunn's avatar Tyler Gunn Committed by Brad Ebinger
Browse files

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
parent afb17686
Loading
Loading
Loading
Loading
+66 −3
Original line number Diff line number Diff line
@@ -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.Connection;
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.Collections;
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
@@ -274,19 +281,43 @@ public class VideoProviderProxy extends Connection.VideoProvider {
        }
    }

    @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);
            }
        }
    }
@@ -490,4 +521,36 @@ public class VideoProviderProxy extends Connection.VideoProvider {
    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;
        }
    }

}
+2 −1
Original line number Diff line number Diff line
@@ -207,7 +207,8 @@ public class AnalyticsTests extends TelecomSystemTest {
        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);

+6 −0
Original line number Diff line number Diff line
@@ -308,6 +308,12 @@ public class ComponentContextFixture implements TestFixture<Context> {
            // Don't bother enforcing anything in mock.
        }

        @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
+4 −0
Original line number Diff line number Diff line
@@ -165,6 +165,10 @@ public class MockVideoProvider extends VideoProvider {
        } 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);
        }
    }

+94 −1
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ import org.mockito.internal.exceptions.ExceptionIncludingMockitoWarnings;
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 android.test.MoreAsserts.assertEquals;
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 @@ public class VideoProviderTest extends TelecomSystemTest {
    private VideoCallImpl mVideoCallImpl;
    private ConnectionServiceFixture.ConnectionInfo mConnectionInfo;
    private CountDownLatch mVerificationLock;
    private AppOpsManager mAppOpsManager;

    private Answer mVerification = new Answer() {
        @Override
@@ -83,6 +89,8 @@ public class VideoProviderTest extends TelecomSystemTest {
    @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 @@ public class VideoProviderTest extends TelecomSystemTest {
        // 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
@@ -145,6 +158,86 @@ public class VideoProviderTest extends TelecomSystemTest {
                cameraCapabilities.get(1).getHeight());
    }

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