1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License
15  */
16 
17 package com.android.server.telecom;
18 
19 import android.Manifest;
20 import android.app.AppOpsManager;
21 import android.content.Context;
22 import android.net.Uri;
23 import android.os.Build;
24 import android.os.IBinder;
25 import android.os.Looper;
26 import android.os.RemoteException;
27 import android.os.UserHandle;
28 import android.telecom.Connection;
29 import android.telecom.InCallService;
30 import android.telecom.Log;
31 import android.telecom.VideoProfile;
32 import android.text.TextUtils;
33 import android.view.Surface;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.telecom.IVideoCallback;
37 import com.android.internal.telecom.IVideoProvider;
38 
39 import java.util.Collections;
40 import java.util.Set;
41 import java.util.concurrent.ConcurrentHashMap;
42 
43 /**
44  * Proxies video provider messages from {@link InCallService.VideoCall}
45  * implementations to the underlying {@link Connection.VideoProvider} implementation.  Also proxies
46  * callbacks from the {@link Connection.VideoProvider} to {@link InCallService.VideoCall}
47  * implementations.
48  *
49  * Also provides a means for Telecom to send and receive these messages.
50  */
51 public class VideoProviderProxy extends Connection.VideoProvider {
52 
53     /**
54      * Listener for Telecom components interested in callbacks from the video provider.
55      */
56     public interface Listener {
onSessionModifyRequestReceived(Call call, VideoProfile videoProfile)57         void onSessionModifyRequestReceived(Call call, VideoProfile videoProfile);
58     }
59 
60     /**
61      * Set of listeners on this VideoProviderProxy.
62      *
63      * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
64      * load factor before resizing, 1 means we only expect a single thread to
65      * access the map so make only a single shard
66      */
67     private final Set<Listener> mListeners = Collections.newSetFromMap(
68             new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
69 
70     /** The TelecomSystem SyncRoot used for synchronized operations. */
71     private final TelecomSystem.SyncRoot mLock;
72 
73     /**
74      * The {@link android.telecom.Connection.VideoProvider} implementation residing with the
75      * {@link android.telecom.ConnectionService} which is being wrapped by this
76      * {@link VideoProviderProxy}.
77      */
78     private final IVideoProvider mConectionServiceVideoProvider;
79 
80     /**
81      * Binder used to bind to the {@link android.telecom.ConnectionService}'s
82      * {@link com.android.internal.telecom.IVideoCallback}.
83      */
84     private final VideoCallListenerBinder mVideoCallListenerBinder;
85 
86     /**
87      * The Telecom {@link Call} this {@link VideoProviderProxy} is associated with.
88      */
89     private Call mCall;
90 
91     /**
92      * Interface providing access to the currently logged in user.
93      */
94     private CurrentUserProxy mCurrentUserProxy;
95 
96     private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
97         @Override
98         public void binderDied() {
99             mConectionServiceVideoProvider.asBinder().unlinkToDeath(this, 0);
100         }
101     };
102 
103     /**
104      * Creates a new instance of the {@link VideoProviderProxy}, binding it to the passed in
105      * {@code videoProvider} residing with the {@link android.telecom.ConnectionService}.
106      *
107      *
108      * @param lock
109      * @param videoProvider The {@link android.telecom.ConnectionService}'s video provider.
110      * @param call The current call.
111      * @throws RemoteException Remote exception.
112      */
VideoProviderProxy(TelecomSystem.SyncRoot lock, IVideoProvider videoProvider, Call call, CurrentUserProxy currentUserProxy)113     public VideoProviderProxy(TelecomSystem.SyncRoot lock,
114             IVideoProvider videoProvider, Call call, CurrentUserProxy currentUserProxy)
115             throws RemoteException {
116 
117         super(Looper.getMainLooper());
118 
119         mLock = lock;
120 
121         mConectionServiceVideoProvider = videoProvider;
122         mConectionServiceVideoProvider.asBinder().linkToDeath(mDeathRecipient, 0);
123 
124         mVideoCallListenerBinder = new VideoCallListenerBinder();
125         mConectionServiceVideoProvider.addVideoCallback(mVideoCallListenerBinder);
126         mCall = call;
127         mCurrentUserProxy = currentUserProxy;
128     }
129 
clearVideoCallback()130     public void clearVideoCallback() {
131         try {
132             mConectionServiceVideoProvider.removeVideoCallback(mVideoCallListenerBinder);
133         } catch (RemoteException e) {
134         }
135     }
136 
137     @VisibleForTesting
getVideoCallListenerBinder()138     public VideoCallListenerBinder getVideoCallListenerBinder() {
139         return mVideoCallListenerBinder;
140     }
141 
142     /**
143      * IVideoCallback stub implementation.  An instance of this class receives callbacks from the
144      * {@code ConnectionService}'s video provider.
145      */
146     public final class VideoCallListenerBinder extends IVideoCallback.Stub {
147         /**
148          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
149          * {@link InCallService} when a session modification request is received.
150          *
151          * @param videoProfile The requested video profile.
152          */
153         @Override
receiveSessionModifyRequest(VideoProfile videoProfile)154         public void receiveSessionModifyRequest(VideoProfile videoProfile) {
155             try {
156                 Log.startSession("VPP.rSMR");
157                 synchronized (mLock) {
158                     logFromVideoProvider("receiveSessionModifyRequest: " + videoProfile);
159                     Log.addEvent(mCall, LogUtils.Events.RECEIVE_VIDEO_REQUEST,
160                             VideoProfile.videoStateToString(videoProfile.getVideoState()));
161 
162                     mCall.getAnalytics().addVideoEvent(
163                             Analytics.RECEIVE_REMOTE_SESSION_MODIFY_REQUEST,
164                             videoProfile.getVideoState());
165 
166                     if ((!mCall.isVideoCallingSupportedByPhoneAccount()
167                             || !mCall.isLocallyVideoCapable())
168                             && VideoProfile.isVideo(videoProfile.getVideoState())) {
169                         // If video calling is not supported by the phone account, or is not
170                         // locally video capable and we receive a request to upgrade to video,
171                         // automatically reject it without informing the InCallService.
172                         Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE,
173                                 "video not supported");
174                         VideoProfile responseProfile = new VideoProfile(
175                                 VideoProfile.STATE_AUDIO_ONLY);
176                         try {
177                             mConectionServiceVideoProvider.sendSessionModifyResponse(
178                                     responseProfile);
179                         } catch (RemoteException e) {
180                         }
181 
182                         // Don't want to inform listeners of the request as we've just rejected it.
183                         return;
184                     }
185 
186                     // Inform other Telecom components of the session modification request.
187                     for (Listener listener : mListeners) {
188                         listener.onSessionModifyRequestReceived(mCall, videoProfile);
189                     }
190 
191                     VideoProviderProxy.this.receiveSessionModifyRequest(videoProfile);
192                 }
193             } finally {
194                 Log.endSession();
195             }
196         }
197 
198         /**
199          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
200          * {@link InCallService} when a session modification response is received.
201          *
202          * @param status The status of the response.
203          * @param requestProfile The requested video profile.
204          * @param responseProfile The response video profile.
205          */
206         @Override
receiveSessionModifyResponse(int status, VideoProfile requestProfile, VideoProfile responseProfile)207         public void receiveSessionModifyResponse(int status, VideoProfile requestProfile,
208                 VideoProfile responseProfile) {
209             logFromVideoProvider("receiveSessionModifyResponse: status=" + status +
210                     " requestProfile=" + requestProfile + " responseProfile=" + responseProfile);
211             String eventMessage = "Status Code : " + status + " Video State: " +
212                     (responseProfile != null ? responseProfile.getVideoState() : "null");
213             Log.addEvent(mCall, LogUtils.Events.RECEIVE_VIDEO_RESPONSE, eventMessage);
214             synchronized (mLock) {
215                 if (status == Connection.VideoProvider.SESSION_MODIFY_REQUEST_SUCCESS) {
216                     mCall.getAnalytics().addVideoEvent(
217                             Analytics.RECEIVE_REMOTE_SESSION_MODIFY_RESPONSE,
218                             responseProfile == null ?
219                                     VideoProfile.STATE_AUDIO_ONLY :
220                                     responseProfile.getVideoState());
221                 }
222                 VideoProviderProxy.this.receiveSessionModifyResponse(status, requestProfile,
223                         responseProfile);
224             }
225         }
226 
227         /**
228          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
229          * {@link InCallService} when a call session event occurs.
230          *
231          * @param event The call session event.
232          */
233         @Override
handleCallSessionEvent(int event)234         public void handleCallSessionEvent(int event) {
235             synchronized (mLock) {
236                 logFromVideoProvider("handleCallSessionEvent: " +
237                         Connection.VideoProvider.sessionEventToString(event));
238                 VideoProviderProxy.this.handleCallSessionEvent(event);
239             }
240         }
241 
242         /**
243          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
244          * {@link InCallService} when the peer dimensions change.
245          *
246          * @param width The width of the peer's video.
247          * @param height The height of the peer's video.
248          */
249         @Override
changePeerDimensions(int width, int height)250         public void changePeerDimensions(int width, int height) {
251             synchronized (mLock) {
252                 logFromVideoProvider("changePeerDimensions: width=" + width + " height=" +
253                         height);
254                 VideoProviderProxy.this.changePeerDimensions(width, height);
255             }
256         }
257 
258         /**
259          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
260          * {@link InCallService} when the video quality changes.
261          *
262          * @param videoQuality The video quality.
263          */
264         @Override
changeVideoQuality(int videoQuality)265         public void changeVideoQuality(int videoQuality) {
266             synchronized (mLock) {
267                 logFromVideoProvider("changeVideoQuality: " + videoQuality);
268                 VideoProviderProxy.this.changeVideoQuality(videoQuality);
269             }
270         }
271 
272         /**
273          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
274          * {@link InCallService} when the call data usage changes.
275          *
276          * Also tracks the current call data usage on the {@link Call} for use when writing to the
277          * call log.
278          *
279          * @param dataUsage The data usage.
280          */
281         @Override
changeCallDataUsage(long dataUsage)282         public void changeCallDataUsage(long dataUsage) {
283             synchronized (mLock) {
284                 logFromVideoProvider("changeCallDataUsage: " + dataUsage);
285                 VideoProviderProxy.this.setCallDataUsage(dataUsage);
286                 mCall.setCallDataUsage(dataUsage);
287             }
288         }
289 
290         /**
291          * Proxies a request from the {@link #mConectionServiceVideoProvider} to the
292          * {@link InCallService} when the camera capabilities change.
293          *
294          * @param cameraCapabilities The camera capabilities.
295          */
296         @Override
changeCameraCapabilities(VideoProfile.CameraCapabilities cameraCapabilities)297         public void changeCameraCapabilities(VideoProfile.CameraCapabilities cameraCapabilities) {
298             synchronized (mLock) {
299                 logFromVideoProvider("changeCameraCapabilities: " + cameraCapabilities);
300                 VideoProviderProxy.this.changeCameraCapabilities(cameraCapabilities);
301             }
302         }
303     }
304 
305     @Override
onSetCamera(String cameraId)306     public void onSetCamera(String cameraId) {
307         // No-op.  We implement the other prototype of onSetCamera so that we can use the calling
308         // package, uid and pid to verify permission.
309     }
310 
311     /**
312      * Proxies a request from the {@link InCallService} to the
313      * {@link #mConectionServiceVideoProvider} to change the camera.
314      *
315      * @param cameraId The id of the camera.
316      * @param callingPackage The package calling in.
317      * @param callingUid The UID of the caller.
318      * @param callingPid The PID of the caller.
319      * @param targetSdkVersion The target SDK version of the calling InCallService where the camera
320      *      request originated.
321      */
322     @Override
onSetCamera(String cameraId, String callingPackage, int callingUid, int callingPid, int targetSdkVersion)323     public void onSetCamera(String cameraId, String callingPackage, int callingUid,
324             int callingPid, int targetSdkVersion) {
325         synchronized (mLock) {
326             logFromInCall("setCamera: " + cameraId + " callingPackage=" + callingPackage +
327                     "; callingUid=" + callingUid);
328 
329             if (!TextUtils.isEmpty(cameraId)) {
330                 if (!canUseCamera(mCall.getContext(), callingPackage, callingUid, callingPid)) {
331                     // Calling app is not permitted to use the camera.  Ignore the request and send
332                     // back a call session event indicating the error.
333                     Log.i(this, "onSetCamera: camera permission denied; package=%s, uid=%d, "
334                             + "pid=%d, targetSdkVersion=%d",
335                             callingPackage, callingUid, callingPid, targetSdkVersion);
336 
337                     // API 26 introduces a new camera permission error we can use here since the
338                     // caller supports that API version.
339                     if (targetSdkVersion > Build.VERSION_CODES.N_MR1) {
340                         VideoProviderProxy.this.handleCallSessionEvent(
341                                 Connection.VideoProvider.SESSION_EVENT_CAMERA_PERMISSION_ERROR);
342                     } else {
343                         VideoProviderProxy.this.handleCallSessionEvent(
344                                 Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE);
345                     }
346                     return;
347                 }
348             }
349             try {
350                 mConectionServiceVideoProvider.setCamera(cameraId, callingPackage,
351                         targetSdkVersion);
352             } catch (RemoteException e) {
353                 VideoProviderProxy.this.handleCallSessionEvent(
354                         Connection.VideoProvider.SESSION_EVENT_CAMERA_FAILURE);
355             }
356         }
357     }
358 
359     /**
360      * Proxies a request from the {@link InCallService} to the
361      * {@link #mConectionServiceVideoProvider} to set the preview surface.
362      *
363      * @param surface The surface.
364      */
365     @Override
onSetPreviewSurface(Surface surface)366     public void onSetPreviewSurface(Surface surface) {
367         synchronized (mLock) {
368             logFromInCall("setPreviewSurface");
369             try {
370                 mConectionServiceVideoProvider.setPreviewSurface(surface);
371             } catch (RemoteException e) {
372             }
373         }
374     }
375 
376     /**
377      * Proxies a request from the {@link InCallService} to the
378      * {@link #mConectionServiceVideoProvider} to change the display surface.
379      *
380      * @param surface The surface.
381      */
382     @Override
onSetDisplaySurface(Surface surface)383     public void onSetDisplaySurface(Surface surface) {
384         synchronized (mLock) {
385             logFromInCall("setDisplaySurface");
386             try {
387                 mConectionServiceVideoProvider.setDisplaySurface(surface);
388             } catch (RemoteException e) {
389             }
390         }
391     }
392 
393     /**
394      * Proxies a request from the {@link InCallService} to the
395      * {@link #mConectionServiceVideoProvider} to change the device orientation.
396      *
397      * @param rotation The device orientation, in degrees.
398      */
399     @Override
onSetDeviceOrientation(int rotation)400     public void onSetDeviceOrientation(int rotation) {
401         synchronized (mLock) {
402             logFromInCall("setDeviceOrientation: " + rotation);
403             try {
404                 mConectionServiceVideoProvider.setDeviceOrientation(rotation);
405             } catch (RemoteException e) {
406             }
407         }
408     }
409 
410     /**
411      * Proxies a request from the {@link InCallService} to the
412      * {@link #mConectionServiceVideoProvider} to change the camera zoom ratio.
413      *
414      * @param value The camera zoom ratio.
415      */
416     @Override
onSetZoom(float value)417     public void onSetZoom(float value) {
418         synchronized (mLock) {
419             logFromInCall("setZoom: " + value);
420             try {
421                 mConectionServiceVideoProvider.setZoom(value);
422             } catch (RemoteException e) {
423             }
424         }
425     }
426 
427     /**
428      * Proxies a request from the {@link InCallService} to the
429      * {@link #mConectionServiceVideoProvider} to provide a response to a session modification
430      * request.
431      *
432      * @param fromProfile The video properties prior to the request.
433      * @param toProfile The video properties with the requested changes made.
434      */
435     @Override
onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile)436     public void onSendSessionModifyRequest(VideoProfile fromProfile, VideoProfile toProfile) {
437         synchronized (mLock) {
438             logFromInCall("sendSessionModifyRequest: from=" + fromProfile + " to=" + toProfile);
439             Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_REQUEST,
440                     VideoProfile.videoStateToString(toProfile.getVideoState()));
441             if (!VideoProfile.isVideo(fromProfile.getVideoState())
442                     && VideoProfile.isVideo(toProfile.getVideoState())) {
443                 // Upgrading to video; change to speaker potentially.
444                 mCall.maybeEnableSpeakerForVideoUpgrade(toProfile.getVideoState());
445             }
446             mCall.getAnalytics().addVideoEvent(
447                     Analytics.SEND_LOCAL_SESSION_MODIFY_REQUEST,
448                     toProfile.getVideoState());
449             try {
450                 mConectionServiceVideoProvider.sendSessionModifyRequest(fromProfile, toProfile);
451             } catch (RemoteException e) {
452             }
453         }
454     }
455 
456     /**
457      * Proxies a request from the {@link InCallService} to the
458      * {@link #mConectionServiceVideoProvider} to send a session modification request.
459      *
460      * @param responseProfile The response connection video properties.
461      */
462     @Override
onSendSessionModifyResponse(VideoProfile responseProfile)463     public void onSendSessionModifyResponse(VideoProfile responseProfile) {
464         synchronized (mLock) {
465             logFromInCall("sendSessionModifyResponse: " + responseProfile);
466             Log.addEvent(mCall, LogUtils.Events.SEND_VIDEO_RESPONSE,
467                     VideoProfile.videoStateToString(responseProfile.getVideoState()));
468             mCall.getAnalytics().addVideoEvent(
469                     Analytics.SEND_LOCAL_SESSION_MODIFY_RESPONSE,
470                     responseProfile.getVideoState());
471             try {
472                 mConectionServiceVideoProvider.sendSessionModifyResponse(responseProfile);
473             } catch (RemoteException e) {
474             }
475         }
476     }
477 
478     /**
479      * Proxies a request from the {@link InCallService} to the
480      * {@link #mConectionServiceVideoProvider} to request the camera capabilities.
481      */
482     @Override
onRequestCameraCapabilities()483     public void onRequestCameraCapabilities() {
484         synchronized (mLock) {
485             logFromInCall("requestCameraCapabilities");
486             try {
487                 mConectionServiceVideoProvider.requestCameraCapabilities();
488             } catch (RemoteException e) {
489             }
490         }
491     }
492 
493     /**
494      * Proxies a request from the {@link InCallService} to the
495      * {@link #mConectionServiceVideoProvider} to request the connection data usage.
496      */
497     @Override
onRequestConnectionDataUsage()498     public void onRequestConnectionDataUsage() {
499         synchronized (mLock) {
500             logFromInCall("requestCallDataUsage");
501             try {
502                 mConectionServiceVideoProvider.requestCallDataUsage();
503             } catch (RemoteException e) {
504             }
505         }
506     }
507 
508     /**
509      * Proxies a request from the {@link InCallService} to the
510      * {@link #mConectionServiceVideoProvider} to set the pause image.
511      *
512      * @param uri URI of image to display.
513      */
514     @Override
onSetPauseImage(Uri uri)515     public void onSetPauseImage(Uri uri) {
516         synchronized (mLock) {
517             logFromInCall("setPauseImage: " + uri);
518             try {
519                 mConectionServiceVideoProvider.setPauseImage(uri);
520             } catch (RemoteException e) {
521             }
522         }
523     }
524 
525     /**
526      * Add a listener to this {@link VideoProviderProxy}.
527      *
528      * @param listener The listener.
529      */
addListener(Listener listener)530     public void addListener(Listener listener) {
531         mListeners.add(listener);
532     }
533 
534     /**
535      * Remove a listener from this {@link VideoProviderProxy}.
536      *
537      * @param listener The listener.
538      */
removeListener(Listener listener)539     public void removeListener(Listener listener) {
540         if (listener != null) {
541             mListeners.remove(listener);
542         }
543     }
544 
545     /**
546      * Logs a message originating from the {@link InCallService}.
547      *
548      * @param toLog The message to log.
549      */
logFromInCall(String toLog)550     private void logFromInCall(String toLog) {
551         Log.i(this, "IC->VP (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog);
552     }
553 
554     /**
555      * Logs a message originating from the {@link android.telecom.ConnectionService}'s
556      * {@link Connection.VideoProvider}.
557      *
558      * @param toLog The message to log.
559      */
logFromVideoProvider(String toLog)560     private void logFromVideoProvider(String toLog) {
561         Log.i(this, "VP->IC (callId=" + (mCall == null ? "?" : mCall.getId()) + "): " + toLog);
562     }
563 
564     /**
565      * Determines if the caller has permission to use the camera.
566      *
567      * @param context The context.
568      * @param callingPackage The package name of the caller (i.e. Dialer).
569      * @param callingUid The UID of the caller.
570      * @param callingPid The PID of the caller.
571      * @return {@code true} if the calling uid and package can use the camera, {@code false}
572      *      otherwise.
573      */
canUseCamera(Context context, String callingPackage, int callingUid, int callingPid)574     private boolean canUseCamera(Context context, String callingPackage, int callingUid,
575             int callingPid) {
576 
577         UserHandle callingUser = UserHandle.getUserHandleForUid(callingUid);
578         UserHandle currentUserHandle = mCurrentUserProxy.getCurrentUserHandle();
579         if (currentUserHandle != null && !currentUserHandle.equals(callingUser)) {
580             Log.w(this, "canUseCamera attempt to user camera by background user.");
581             return false;
582         }
583 
584         try {
585             context.enforcePermission(Manifest.permission.CAMERA, callingPid, callingUid,
586                     "Camera permission required.");
587         } catch (SecurityException se) {
588             return false;
589         }
590 
591         AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(
592                 Context.APP_OPS_SERVICE);
593 
594         try {
595             // Some apps that have the permission can be restricted via app ops.
596             return appOpsManager != null && appOpsManager.noteOp(AppOpsManager.OP_CAMERA,
597                     callingUid, callingPackage) == AppOpsManager.MODE_ALLOWED;
598         } catch (SecurityException se) {
599             Log.w(this, "canUseCamera got appOpps Exception " + se.toString());
600             return false;
601         }
602     }
603 
604 }
605