Loading src/com/android/server/telecom/VideoProviderProxy.java +66 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading Loading @@ -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); } } } Loading Loading @@ -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; } } } tests/src/com/android/server/telecom/tests/AnalyticsTests.java +2 −1 Original line number Diff line number Diff line Loading @@ -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); Loading tests/src/com/android/server/telecom/tests/ComponentContextFixture.java +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading tests/src/com/android/server/telecom/tests/MockVideoProvider.java +4 −0 Original line number Diff line number Diff line Loading @@ -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); } } Loading tests/src/com/android/server/telecom/tests/VideoProviderTest.java +94 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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", Loading @@ -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 Loading Loading @@ -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. Loading Loading
src/com/android/server/telecom/VideoProviderProxy.java +66 −3 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading Loading @@ -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); } } } Loading Loading @@ -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; } } }
tests/src/com/android/server/telecom/tests/AnalyticsTests.java +2 −1 Original line number Diff line number Diff line Loading @@ -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); Loading
tests/src/com/android/server/telecom/tests/ComponentContextFixture.java +6 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
tests/src/com/android/server/telecom/tests/MockVideoProvider.java +4 −0 Original line number Diff line number Diff line Loading @@ -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); } } Loading
tests/src/com/android/server/telecom/tests/VideoProviderTest.java +94 −1 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading @@ -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", Loading @@ -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 Loading Loading @@ -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. Loading