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