| /* |
| * Copyright (C) 2015 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.telecom; |
| |
| import android.Manifest; |
| import android.app.AppOpsManager; |
| import android.content.Context; |
| import android.net.Uri; |
| import android.os.Binder; |
| import android.os.Build; |
| import android.os.IBinder; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| 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; |
| import com.android.internal.telecom.IVideoProvider; |
| |
| 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 |
| * callbacks from the {@link Connection.VideoProvider} to {@link InCallService.VideoCall} |
| * implementations. |
| * |
| * Also provides a means for Telecom to send and receive these messages. |
| */ |
| public class VideoProviderProxy extends Connection.VideoProvider { |
| |
| /** |
| * Listener for Telecom components interested in callbacks from the video provider. |
| */ |
| interface Listener { |
| void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile); |
| } |
| |
| /** |
| * Set of listeners on this VideoProviderProxy. |
| * |
| * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is |
| * load factor before resizing, 1 means we only expect a single thread to |
| * access the map so make only a single shard |
| */ |
| private final Set<Listener> mListeners = Collections.newSetFromMap( |
| new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1)); |
| |
| /** The TelecomSystem SyncRoot used for synchronized operations. */ |
| private final TelecomSystem.SyncRoot mLock; |
| |
| /** |
| * The {@link android.telecom.Connection.VideoProvider} implementation residing with the |
| * {@link android.telecom.ConnectionService} which is being wrapped by this |
| * {@link VideoProviderProxy}. |
| */ |
| private final IVideoProvider mConectionServiceVideoProvider; |
| |
| /** |
| * Binder used to bind to the {@link android.telecom.ConnectionService}'s |
| * {@link com.android.internal.telecom.IVideoCallback}. |
| */ |
| private final VideoCallListenerBinder mVideoCallListenerBinder; |
| |
| /** |
| * The Telecom {@link Call} this {@link VideoProviderProxy} is associated with. |
| */ |
| private Call mCall; |
| |
| /** |
| * Interface providing access to the currently logged in user. |
| */ |
| private CurrentUserProxy mCurrentUserProxy; |
| |
| private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() { |
| @Override |
| public void binderDied() { |
| mConectionServiceVideoProvider.asBinder().unlinkToDeath(this, 0); |
| } |
| }; |
| |
| /** |
| * Creates a new instance of the {@link VideoProviderProxy}, binding it to the passed in |
| * {@code videoProvider} residing with the {@link android.telecom.ConnectionService}. |
| * |
| * |
| * @param lock |
| * @param videoProvider The {@link android.telecom.ConnectionService}'s video provider. |
| * @param call The current call. |
| * @throws RemoteException Remote exception. |
| */ |
| VideoProviderProxy(TelecomSystem.SyncRoot lock, |
| IVideoProvider videoProvider, Call call, CurrentUserProxy currentUserProxy) |
| throws RemoteException { |
| |
| super(Looper.getMainLooper()); |
| |
| mLock = lock; |
| |
| mConectionServiceVideoProvider = videoProvider; |
| mConectionServiceVideoProvider.asBinder().linkToDeath(mDeathRecipient, 0); |
| |
| mVideoCallListenerBinder = new VideoCallListenerBinder(); |
| mConectionServiceVideoProvider.addVideoCallback(mVideoCallListenerBinder); |
| mCall = call; |
| mCurrentUserProxy = currentUserProxy; |
| } |
| |
| public void clearVideoCallback() { |
| try { |
| mConectionServiceVideoProvider.removeVideoCallback(mVideoCallListenerBinder); |
| } catch (RemoteException e) { |
| } |
| } |
| |
| /** |
| * IVideoCallback stub implementation. An instance of this class receives callbacks from the |
| * {@code ConnectionService}'s video provider. |
| */ |
| private final class VideoCallListenerBinder extends IVideoCallback.Stub { |
| /** |
| * Proxies a request from the {@link #mConectionServiceVideoProvider} to the |
| * {@link InCallService} when a session modification request is received. |
| * |
| * @param videoProfile The requested video profile. |
| */ |
| @Override |
| public void receiveSessionModifyRequest(VideoProfile videoProfile) { |
| try { |
| Log.startSession("VPP.rSMR"); |
| synchronized (mLock) { |
| logFromVideoProvider("receiveSessionModifyRequest: " + videoProfile); |
| Log.addEvent(mCall, LogUtils.Events.RECEIVE_VIDEO_REQUEST, |
| VideoProfile.videoStateToString(videoProfile.getVideoState())); |
| |
| mCall.getAnalytics().addVideoEvent( |
| Analytics.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST, |
| videoProfile.getVideoState()); |
| |
| if (!mCall.isVideoCallingSupported() && |
| VideoProfile.isVideo(videoProfile.getVideoState())) { |
| // If video calling is not supported by the phone account, and we receive |
| // a request to upgrade to video, automatically reject it without informing |
| // the InCallService. |
| |
| Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE, "video not supported"); |
| VideoProfile responseProfile = new VideoProfile( |
| VideoProfile.STATE_AUDIO_ONLY); |
| try { |
| mConectionServiceVideoProvider.sendSessionModifyResponse( |
| responseProfile); |
| } catch (RemoteException e) { |
| } |
| |
| // Don't want to inform listeners of the request as we've just rejected it. |
| return; |
| } |
| |
| // Inform other Telecom components of the session modification request. |
| for (Listener listener : mListeners) { |
| listener.onSessionModifyRequestReceived(mCall, videoProfile); |
| } |
| |
| VideoProviderProxy.this.receiveSessionModifyRequest(videoProfile); |
| } |
| } finally { |
| Log.endSession(); |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link #mConectionServiceVideoProvider} to the |
| * {@link InCallService} when a session modification response is received. |
| * |
| * @param status The status of the response. |
| * @param requestProfile The requested video profile. |
| * @param responseProfile The response video profile. |
| */ |
| @Override |
| public void receiveSessionModifyResponse(int status, VideoProfile requestProfile, |
| VideoProfile responseProfile) { |
| logFromVideoProvider("receiveSessionModifyResponse: status=" + status + |
| " requestProfile=" + requestProfile + " responseProfile=" + responseProfile); |
| String eventMessage = "Status Code : " + status + " Video State: " + |
| (responseProfile != null ? responseProfile.getVideoState() : "null"); |
| Log.addEvent(mCall, LogUtils.Events.RECEIVE_VIDEO_RESPONSE, eventMessage); |
| synchronized (mLock) { |
| if (status == Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) { |
| mCall.getAnalytics().addVideoEvent( |
| Analytics.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE, |
| responseProfile == null ? |
| VideoProfile.STATE_AUDIO_ONLY : |
| responseProfile.getVideoState()); |
| } |
| VideoProviderProxy.this.receiveSessionModifyResponse(status, requestProfile, |
| responseProfile); |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link #mConectionServiceVideoProvider} to the |
| * {@link InCallService} when a call session event occurs. |
| * |
| * @param event The call session event. |
| */ |
| @Override |
| public void handleCallSessionEvent(int event) { |
| synchronized (mLock) { |
| logFromVideoProvider("handleCallSessionEvent: " + |
| Connection.VideoProvider.sessionEventToString(event)); |
| VideoProviderProxy.this.handleCallSessionEvent(event); |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link #mConectionServiceVideoProvider} to the |
| * {@link InCallService} when the peer dimensions change. |
| * |
| * @param width The width of the peer's video. |
| * @param height The height of the peer's video. |
| */ |
| @Override |
| public void changePeerDimensions(int width, int height) { |
| synchronized (mLock) { |
| logFromVideoProvider("changePeerDimensions: width=" + width + " height=" + |
| height); |
| VideoProviderProxy.this.changePeerDimensions(width, height); |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link #mConectionServiceVideoProvider} to the |
| * {@link InCallService} when the video quality changes. |
| * |
| * @param videoQuality The video quality. |
| */ |
| @Override |
| public void changeVideoQuality(int videoQuality) { |
| synchronized (mLock) { |
| logFromVideoProvider("changeVideoQuality: " + videoQuality); |
| VideoProviderProxy.this.changeVideoQuality(videoQuality); |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link #mConectionServiceVideoProvider} to the |
| * {@link InCallService} when the call data usage changes. |
| * |
| * Also tracks the current call data usage on the {@link Call} for use when writing to the |
| * call log. |
| * |
| * @param dataUsage The data usage. |
| */ |
| @Override |
| public void changeCallDataUsage(long dataUsage) { |
| synchronized (mLock) { |
| logFromVideoProvider("changeCallDataUsage: " + dataUsage); |
| VideoProviderProxy.this.setCallDataUsage(dataUsage); |
| mCall.setCallDataUsage(dataUsage); |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link #mConectionServiceVideoProvider} to the |
| * {@link InCallService} when the camera capabilities change. |
| * |
| * @param cameraCapabilities The camera capabilities. |
| */ |
| @Override |
| public void changeCameraCapabilities(VideoProfile.CameraCapabilities cameraCapabilities) { |
| synchronized (mLock) { |
| logFromVideoProvider("changeCameraCapabilities: " + cameraCapabilities); |
| VideoProviderProxy.this.changeCameraCapabilities(cameraCapabilities); |
| } |
| } |
| } |
| |
| @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. |
| * @param targetSdkVersion The target SDK version of the calling InCallService where the camera |
| * request originated. |
| */ |
| @Override |
| public void onSetCamera(String cameraId, String callingPackage, int callingUid, |
| int callingPid, int targetSdkVersion) { |
| synchronized (mLock) { |
| logFromInCall("setCamera: " + cameraId + " callingPackage=" + callingPackage + |
| "; callingUid=" + callingUid); |
| |
| 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=%s, uid=%d, " |
| + "pid=%d, targetSdkVersion=%d", |
| callingPackage, callingUid, callingPid, targetSdkVersion); |
| |
| // API 26 introduces a new camera permission error we can use here since the |
| // caller supports that API version. |
| if (targetSdkVersion > Build.VERSION_CODES.N_MR1) { |
| VideoProviderProxy.this.handleCallSessionEvent( |
| Connection.VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR); |
| } else { |
| VideoProviderProxy.this.handleCallSessionEvent( |
| Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE); |
| } |
| return; |
| } |
| } |
| try { |
| mConectionServiceVideoProvider.setCamera(cameraId, callingPackage, |
| targetSdkVersion); |
| } catch (RemoteException e) { |
| VideoProviderProxy.this.handleCallSessionEvent( |
| Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE); |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to set the preview surface. |
| * |
| * @param surface The surface. |
| */ |
| @Override |
| public void onSetPreviewSurface(Surface surface) { |
| synchronized (mLock) { |
| logFromInCall("setPreviewSurface"); |
| try { |
| mConectionServiceVideoProvider.setPreviewSurface(surface); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to change the display surface. |
| * |
| * @param surface The surface. |
| */ |
| @Override |
| public void onSetDisplaySurface(Surface surface) { |
| synchronized (mLock) { |
| logFromInCall("setDisplaySurface"); |
| try { |
| mConectionServiceVideoProvider.setDisplaySurface(surface); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to change the device orientation. |
| * |
| * @param rotation The device orientation, in degrees. |
| */ |
| @Override |
| public void onSetDeviceOrientation(int rotation) { |
| synchronized (mLock) { |
| logFromInCall("setDeviceOrientation: " + rotation); |
| try { |
| mConectionServiceVideoProvider.setDeviceOrientation(rotation); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to change the camera zoom ratio. |
| * |
| * @param value The camera zoom ratio. |
| */ |
| @Override |
| public void onSetZoom(float value) { |
| synchronized (mLock) { |
| logFromInCall("setZoom: " + value); |
| try { |
| mConectionServiceVideoProvider.setZoom(value); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to provide a response to a session modification |
| * request. |
| * |
| * @param fromProfile The video properties prior to the request. |
| * @param toProfile The video properties with the requested changes made. |
| */ |
| @Override |
| public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) { |
| synchronized (mLock) { |
| logFromInCall("sendSessionModifyRequest: from=" + fromProfile + " to=" + toProfile); |
| Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_REQUEST, |
| VideoProfile.videoStateToString(toProfile.getVideoState())); |
| mCall.getAnalytics().addVideoEvent( |
| Analytics.SEND_LOCAL_SESSION_MODIFY_REQUEST, |
| toProfile.getVideoState()); |
| try { |
| mConectionServiceVideoProvider.sendSessionModifyRequest(fromProfile, toProfile); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to send a session modification request. |
| * |
| * @param responseProfile The response connection video properties. |
| */ |
| @Override |
| public void onSendSessionModifyResponse(VideoProfile responseProfile) { |
| synchronized (mLock) { |
| logFromInCall("sendSessionModifyResponse: " + responseProfile); |
| Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE, |
| VideoProfile.videoStateToString(responseProfile.getVideoState())); |
| mCall.getAnalytics().addVideoEvent( |
| Analytics.SEND_LOCAL_SESSION_MODIFY_RESPONSE, |
| responseProfile.getVideoState()); |
| try { |
| mConectionServiceVideoProvider.sendSessionModifyResponse(responseProfile); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to request the camera capabilities. |
| */ |
| @Override |
| public void onRequestCameraCapabilities() { |
| synchronized (mLock) { |
| logFromInCall("requestCameraCapabilities"); |
| try { |
| mConectionServiceVideoProvider.requestCameraCapabilities(); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to request the connection data usage. |
| */ |
| @Override |
| public void onRequestConnectionDataUsage() { |
| synchronized (mLock) { |
| logFromInCall("requestCallDataUsage"); |
| try { |
| mConectionServiceVideoProvider.requestCallDataUsage(); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Proxies a request from the {@link InCallService} to the |
| * {@link #mConectionServiceVideoProvider} to set the pause image. |
| * |
| * @param uri URI of image to display. |
| */ |
| @Override |
| public void onSetPauseImage(Uri uri) { |
| synchronized (mLock) { |
| logFromInCall("setPauseImage: " + uri); |
| try { |
| mConectionServiceVideoProvider.setPauseImage(uri); |
| } catch (RemoteException e) { |
| } |
| } |
| } |
| |
| /** |
| * Add a listener to this {@link VideoProviderProxy}. |
| * |
| * @param listener The listener. |
| */ |
| public void addListener(Listener listener) { |
| mListeners.add(listener); |
| } |
| |
| /** |
| * Remove a listener from this {@link VideoProviderProxy}. |
| * |
| * @param listener The listener. |
| */ |
| public void removeListener(Listener listener) { |
| if (listener != null) { |
| mListeners.remove(listener); |
| } |
| } |
| |
| /** |
| * Logs a message originating from the {@link InCallService}. |
| * |
| * @param toLog The message to log. |
| */ |
| private void logFromInCall(String toLog) { |
| Log.i(this, "IC->VP (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog); |
| } |
| |
| /** |
| * Logs a message originating from the {@link android.telecom.ConnectionService}'s |
| * {@link Connection.VideoProvider}. |
| * |
| * @param toLog The message to log. |
| */ |
| 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. |
| * @param callingPid The PID 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) { |
| |
| UserHandle callingUser = UserHandle.getUserHandleForUid(callingUid); |
| UserHandle currentUserHandle = mCurrentUserProxy.getCurrentUserHandle(); |
| if (currentUserHandle != null && !currentUserHandle.equals(callingUser)) { |
| Log.w(this, "canUseCamera attempt to user camera by background user."); |
| return false; |
| } |
| |
| 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, "canUseCamera got appOpps Exception " + se.toString()); |
| return false; |
| } |
| } |
| |
| } |