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