1 /* 2 * Copyright (C) 2022 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 static android.telecom.CallException.CODE_CALL_IS_NOT_BEING_TRACKED; 20 import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY; 21 import static android.telecom.TelecomManager.TELECOM_TRANSACTION_SUCCESS; 22 23 import android.content.ComponentName; 24 import android.os.Binder; 25 import android.os.Bundle; 26 import android.os.IBinder; 27 import android.os.OutcomeReceiver; 28 import android.os.RemoteException; 29 import android.os.ResultReceiver; 30 import android.telecom.CallEndpoint; 31 import android.telecom.CallException; 32 import android.telecom.CallStreamingService; 33 import android.telecom.DisconnectCause; 34 import android.telecom.Log; 35 import android.telecom.PhoneAccountHandle; 36 import android.text.TextUtils; 37 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.internal.telecom.ICallControl; 41 import com.android.internal.telecom.ICallEventCallback; 42 import com.android.server.telecom.callsequencing.TransactionalCallSequencingAdapter; 43 import com.android.server.telecom.callsequencing.voip.CallEventCallbackAckTransaction; 44 import com.android.server.telecom.callsequencing.voip.EndpointChangeTransaction; 45 import com.android.server.telecom.callsequencing.voip.SetMuteStateTransaction; 46 import com.android.server.telecom.callsequencing.voip.RequestVideoStateTransaction; 47 import com.android.server.telecom.callsequencing.TransactionManager; 48 import com.android.server.telecom.callsequencing.CallTransaction; 49 import com.android.server.telecom.callsequencing.CallTransactionResult; 50 import com.android.server.telecom.flags.FeatureFlags; 51 52 import java.util.Locale; 53 import java.util.Set; 54 import java.util.UUID; 55 import java.util.concurrent.CompletableFuture; 56 import java.util.concurrent.ConcurrentHashMap; 57 58 /** 59 * Implements {@link android.telecom.CallEventCallback} and {@link android.telecom.CallControl} 60 * on a per-client basis which is tied to a {@link PhoneAccountHandle} 61 */ 62 public class TransactionalServiceWrapper implements 63 ConnectionServiceFocusManager.ConnectionServiceFocus, CallSourceService { 64 private static final String TAG = TransactionalServiceWrapper.class.getSimpleName(); 65 66 // CallControl : Client (ex. voip app) --> Telecom 67 public static final String SET_ACTIVE = "SetActive"; 68 public static final String SET_INACTIVE = "SetInactive"; 69 public static final String ANSWER = "Answer"; 70 public static final String DISCONNECT = "Disconnect"; 71 public static final String START_STREAMING = "StartStreaming"; 72 public static final String REQUEST_VIDEO_STATE = "RequestVideoState"; 73 public static final String SET_MUTE_STATE = "SetMuteState"; 74 public static final String CALL_ENDPOINT_CHANGE = "CallEndpointChange"; 75 76 // CallEventCallback : Telecom --> Client (ex. voip app) 77 public static final String ON_SET_ACTIVE = "onSetActive"; 78 public static final String ON_SET_INACTIVE = "onSetInactive"; 79 public static final String ON_ANSWER = "onAnswer"; 80 public static final String ON_DISCONNECT = "onDisconnect"; 81 public static final String ON_STREAMING_STARTED = "onStreamingStarted"; 82 public static final String STOP_STREAMING = "stopStreaming"; 83 84 private final CallsManager mCallsManager; 85 private final ICallEventCallback mICallEventCallback; 86 private final PhoneAccountHandle mPhoneAccountHandle; 87 private final TransactionalServiceRepository mRepository; 88 private ConnectionServiceFocusManager.ConnectionServiceFocusListener mConnSvrFocusListener; 89 // init when constructor is called 90 private final ConcurrentHashMap<String, Call> mTrackedCalls = new ConcurrentHashMap<>(); 91 private final TelecomSystem.SyncRoot mLock; 92 private final String mPackageName; 93 // needs to be non-final for testing 94 private TransactionManager mTransactionManager; 95 private CallStreamingController mStreamingController; 96 private final TransactionalCallSequencingAdapter mCallSequencingAdapter; 97 private final FeatureFlags mFeatureFlags; 98 private final AnomalyReporterAdapter mAnomalyReporter; 99 public static final UUID CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID = 100 UUID.fromString("8187cd59-97a7-4e9f-a772-638dda4b69bb"); 101 public static final String CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG = 102 "A call update was attempted for a call no longer being tracked"; 103 104 // Each TransactionalServiceWrapper should have their own Binder.DeathRecipient to clean up 105 // any calls in the event the application crashes or is force stopped. 106 private final IBinder.DeathRecipient mAppDeathListener = new IBinder.DeathRecipient() { 107 @Override 108 public void binderDied() { 109 Log.i(TAG, "binderDied: for package=[%s]; cleaning calls", mPackageName); 110 cleanupTransactionalServiceWrapper(); 111 mICallEventCallback.asBinder().unlinkToDeath(this, 0); 112 } 113 }; 114 TransactionalServiceWrapper(ICallEventCallback callEventCallback, CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call, TransactionalServiceRepository repo, TransactionManager transactionManager, boolean isCallSequencingEnabled, FeatureFlags featureFlags, AnomalyReporterAdapter anomalyReporterAdapter)115 public TransactionalServiceWrapper(ICallEventCallback callEventCallback, 116 CallsManager callsManager, PhoneAccountHandle phoneAccountHandle, Call call, 117 TransactionalServiceRepository repo, TransactionManager transactionManager, 118 boolean isCallSequencingEnabled, FeatureFlags featureFlags, 119 AnomalyReporterAdapter anomalyReporterAdapter) { 120 // passed args 121 mICallEventCallback = callEventCallback; 122 mCallsManager = callsManager; 123 mPhoneAccountHandle = phoneAccountHandle; 124 mTrackedCalls.put(call.getId(), call); // service is now tracking its first call 125 mRepository = repo; 126 mTransactionManager = transactionManager; 127 // init instance vars 128 mPackageName = phoneAccountHandle.getComponentName().getPackageName(); 129 mStreamingController = mCallsManager.getCallStreamingController(); 130 mLock = mCallsManager.getLock(); 131 mCallSequencingAdapter = new TransactionalCallSequencingAdapter(mTransactionManager, 132 mCallsManager, isCallSequencingEnabled); 133 setDeathRecipient(callEventCallback); 134 mFeatureFlags = featureFlags; 135 mAnomalyReporter = anomalyReporterAdapter; 136 } 137 getTransactionManager()138 public TransactionManager getTransactionManager() { 139 return mTransactionManager; 140 } 141 142 @VisibleForTesting getPhoneAccountHandle()143 public PhoneAccountHandle getPhoneAccountHandle() { 144 return mPhoneAccountHandle; 145 } 146 trackCall(Call call)147 public void trackCall(Call call) { 148 synchronized (mLock) { 149 if (call != null) { 150 mTrackedCalls.put(call.getId(), call); 151 } 152 } 153 } 154 155 @VisibleForTesting untrackCall(Call call)156 public boolean untrackCall(Call call) { 157 Call removedCall = null; 158 synchronized (mLock) { 159 if (call != null) { 160 removedCall = mTrackedCalls.remove(call.getId()); 161 if (mTrackedCalls.size() == 0) { 162 mRepository.removeServiceWrapper(mPhoneAccountHandle); 163 } 164 } 165 } 166 Log.i(TAG, "removedCall call=" + removedCall); 167 return removedCall != null; 168 } 169 170 @VisibleForTesting getNumberOfTrackedCalls()171 public int getNumberOfTrackedCalls() { 172 int callCount = 0; 173 synchronized (mLock) { 174 callCount = mTrackedCalls.size(); 175 } 176 return callCount; 177 } 178 cleanupTransactionalServiceWrapper()179 private void cleanupTransactionalServiceWrapper() { 180 mCallSequencingAdapter.cleanup(mTrackedCalls.values()); 181 } 182 183 /*** 184 ********************************************************************************************* 185 ** ICallControl: Client --> Server ** 186 ********************************************************************************************** 187 */ 188 private final ICallControl mICallControl = new ICallControl.Stub() { 189 @Override 190 public void setActive(String callId, android.os.ResultReceiver callback) { 191 long token = Binder.clearCallingIdentity(); 192 try { 193 Log.startSession("TSW.sA"); 194 createTransactions(callId, callback, SET_ACTIVE); 195 } finally { 196 Binder.restoreCallingIdentity(token); 197 Log.endSession(); 198 } 199 } 200 201 @Override 202 203 public void answer(int videoState, String callId, android.os.ResultReceiver callback) { 204 long token = Binder.clearCallingIdentity(); 205 try { 206 Log.startSession("TSW.a"); 207 createTransactions(callId, callback, ANSWER, videoState); 208 } finally { 209 Binder.restoreCallingIdentity(token); 210 Log.endSession(); 211 } 212 } 213 214 @Override 215 public void setInactive(String callId, android.os.ResultReceiver callback) { 216 long token = Binder.clearCallingIdentity(); 217 try { 218 Log.startSession("TSW.sI"); 219 createTransactions(callId, callback, SET_INACTIVE); 220 } finally { 221 Binder.restoreCallingIdentity(token); 222 Log.endSession(); 223 } 224 } 225 226 @Override 227 public void disconnect(String callId, DisconnectCause disconnectCause, 228 android.os.ResultReceiver callback) { 229 long token = Binder.clearCallingIdentity(); 230 try { 231 Log.startSession("TSW.d"); 232 createTransactions(callId, callback, DISCONNECT, disconnectCause); 233 } finally { 234 Binder.restoreCallingIdentity(token); 235 Log.endSession(); 236 } 237 } 238 239 @Override 240 public void setMuteState(boolean isMuted, android.os.ResultReceiver callback) { 241 long token = Binder.clearCallingIdentity(); 242 try { 243 Log.startSession("TSW.sMS"); 244 addTransactionsToManager(SET_MUTE_STATE, 245 new SetMuteStateTransaction(mCallsManager, isMuted), callback); 246 } finally { 247 Binder.restoreCallingIdentity(token); 248 Log.endSession(); 249 } 250 } 251 252 @Override 253 public void startCallStreaming(String callId, android.os.ResultReceiver callback) { 254 long token = Binder.clearCallingIdentity(); 255 try { 256 Log.startSession("TSW.sCS"); 257 createTransactions(callId, callback, START_STREAMING); 258 } finally { 259 Binder.restoreCallingIdentity(token); 260 Log.endSession(); 261 } 262 } 263 264 @Override 265 public void requestVideoState(int videoState, String callId, ResultReceiver callback) { 266 long token = Binder.clearCallingIdentity(); 267 try { 268 Log.startSession("TSW.rVS"); 269 createTransactions(callId, callback, REQUEST_VIDEO_STATE, videoState); 270 } finally { 271 Binder.restoreCallingIdentity(token); 272 Log.endSession(); 273 } 274 } 275 276 private void createTransactions(String callId, ResultReceiver callback, String action, 277 Object... objects) { 278 Log.d(TAG, "createTransactions: callId=" + callId); 279 Call call = mTrackedCalls.get(callId); 280 if (call != null) { 281 switch (action) { 282 case SET_ACTIVE: 283 mCallSequencingAdapter.setActive(call, 284 getCompleteReceiver(action, callback)); 285 break; 286 case ANSWER: 287 mCallSequencingAdapter.setAnswered(call, (int) objects[0] /*VideoState*/, 288 getCompleteReceiver(action, callback)); 289 break; 290 case DISCONNECT: 291 DisconnectCause dc = (DisconnectCause) objects[0]; 292 mCallSequencingAdapter.setDisconnected(call, dc, 293 getCompleteReceiver(action, callback)); 294 break; 295 case SET_INACTIVE: 296 mCallSequencingAdapter.setInactive(call, 297 getCompleteReceiver(action,callback)); 298 break; 299 case START_STREAMING: 300 addTransactionsToManager(action, 301 mStreamingController.getStartStreamingTransaction(mCallsManager, 302 TransactionalServiceWrapper.this, call, mLock), callback); 303 break; 304 case REQUEST_VIDEO_STATE: 305 addTransactionsToManager(action, 306 new RequestVideoStateTransaction(mCallsManager, call, 307 (int) objects[0]), callback); 308 break; 309 } 310 } else { 311 Bundle exceptionBundle = new Bundle(); 312 exceptionBundle.putParcelable(TRANSACTION_EXCEPTION_KEY, 313 new CallException(TextUtils.formatSimple( 314 "Telecom cannot process [%s] because the call with id=[%s] is no longer " 315 + "being tracked. This is most likely a result of the call " 316 + "already being disconnected and removed. Try re-adding the call" 317 + " via TelecomManager#addCall", action, callId), 318 CODE_CALL_IS_NOT_BEING_TRACKED)); 319 callback.send(CODE_CALL_IS_NOT_BEING_TRACKED, exceptionBundle); 320 if (mFeatureFlags.enableCallExceptionAnomReports()) { 321 mAnomalyReporter.reportAnomaly( 322 CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_UUID, 323 CALL_IS_NO_LONGER_BEING_TRACKED_ERROR_MSG); 324 } 325 } 326 } 327 328 @Override 329 public void requestCallEndpointChange(CallEndpoint endpoint, ResultReceiver callback) { 330 long token = Binder.clearCallingIdentity(); 331 try { 332 Log.startSession("TSW.rCEC"); 333 addTransactionsToManager(CALL_ENDPOINT_CHANGE, 334 new EndpointChangeTransaction(endpoint, mCallsManager), callback); 335 } finally { 336 Binder.restoreCallingIdentity(token); 337 Log.endSession(); 338 } 339 } 340 341 /** 342 * Application would like to inform InCallServices of an event 343 */ 344 @Override 345 public void sendEvent(String callId, String event, Bundle extras) { 346 long token = Binder.clearCallingIdentity(); 347 try { 348 Log.startSession("TSW.sE"); 349 Call call = mTrackedCalls.get(callId); 350 if (call != null) { 351 call.onConnectionEvent(event, extras); 352 } else { 353 Log.i(TAG, 354 "sendEvent: was called but there is no call with id=[%s] cannot be " 355 + "found. Most likely the call has been disconnected"); 356 } 357 } finally { 358 Binder.restoreCallingIdentity(token); 359 Log.endSession(); 360 } 361 } 362 }; 363 addTransactionsToManager(String action, CallTransaction transaction, ResultReceiver callback)364 private void addTransactionsToManager(String action, CallTransaction transaction, 365 ResultReceiver callback) { 366 Log.d(TAG, "addTransactionsToManager"); 367 CompletableFuture<Boolean> transactionResult = mTransactionManager 368 .addTransaction(transaction, getCompleteReceiver(action, callback)); 369 } 370 getCompleteReceiver( String action, ResultReceiver callback)371 private OutcomeReceiver<CallTransactionResult, CallException> getCompleteReceiver( 372 String action, ResultReceiver callback) { 373 return new OutcomeReceiver<>() { 374 @Override 375 public void onResult(CallTransactionResult result) { 376 Log.d(TAG, "completeReceiver: onResult[" + action + "]:" + result); 377 callback.send(TELECOM_TRANSACTION_SUCCESS, new Bundle()); 378 } 379 380 @Override 381 public void onError(CallException exception) { 382 Log.d(TAG, "completeReceiver: onError[" + action + "]" + exception); 383 Bundle extras = new Bundle(); 384 extras.putParcelable(TRANSACTION_EXCEPTION_KEY, exception); 385 callback.send(exception == null ? CallException.CODE_ERROR_UNKNOWN : 386 exception.getCode(), extras); 387 } 388 }; 389 } 390 391 public ICallControl getICallControl() { 392 return mICallControl; 393 } 394 395 /*** 396 ********************************************************************************************* 397 ** ICallEventCallback: Server --> Client ** 398 ********************************************************************************************** 399 */ 400 401 public CompletableFuture<Boolean> onSetActive(Call call) { 402 CallTransaction callTransaction = new CallEventCallbackAckTransaction( 403 mICallEventCallback, ON_SET_ACTIVE, call.getId(), mLock); 404 CompletableFuture<Boolean> onSetActiveFuture; 405 try { 406 Log.startSession("TSW.oSA"); 407 Log.d(TAG, String.format(Locale.US, "onSetActive: callId=[%s]", call.getId())); 408 onSetActiveFuture = mCallSequencingAdapter.onSetActive(call, 409 callTransaction, result -> 410 Log.i(TAG, String.format(Locale.US, 411 "%s: onResult: callId=[%s], result=[%s]", ON_SET_ACTIVE, 412 call.getId(), result))); 413 } finally { 414 Log.endSession(); 415 } 416 return onSetActiveFuture; 417 } 418 419 public CompletableFuture<Boolean> onAnswer(Call call, int videoState) { 420 CompletableFuture<Boolean> onAnswerFuture; 421 try { 422 Log.startSession("TSW.oA"); 423 Log.d(TAG, String.format(Locale.US, "onAnswer: callId=[%s]", call.getId())); 424 onAnswerFuture = mCallSequencingAdapter.onSetAnswered(call, videoState, 425 new CallEventCallbackAckTransaction(mICallEventCallback, 426 ON_ANSWER, call.getId(), videoState, mLock), 427 result -> Log.i(TAG, String.format(Locale.US, 428 "%s: onResult: callId=[%s], result=[%s]", 429 ON_ANSWER, call.getId(), result))); 430 } finally { 431 Log.endSession(); 432 } 433 return onAnswerFuture; 434 } 435 436 public CompletableFuture<Boolean> onSetInactive(Call call) { 437 CallTransaction callTransaction = new CallEventCallbackAckTransaction( 438 mICallEventCallback, ON_SET_INACTIVE, call.getId(), mLock); 439 CompletableFuture<Boolean> onSetInactiveFuture; 440 try { 441 Log.startSession("TSW.oSI"); 442 Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]", call.getId())); 443 onSetInactiveFuture = mCallSequencingAdapter.onSetInactive(call, 444 callTransaction, new OutcomeReceiver<>() { 445 @Override 446 public void onResult(CallTransactionResult result) { 447 Log.i(TAG, String.format(Locale.US, "onSetInactive: callId=[%s]" 448 + ", result=[%s]", 449 call.getId(), result)); 450 } 451 452 @Override 453 public void onError(CallException exception) { 454 Log.w(TAG, "onSetInactive: onError: e.code=[%d], e.msg=[%s]", 455 exception.getCode(), exception.getMessage()); 456 } 457 }); 458 } finally { 459 Log.endSession(); 460 } 461 return onSetInactiveFuture; 462 } 463 464 public CompletableFuture<Boolean> onDisconnect(Call call, 465 DisconnectCause cause) { 466 CallTransaction callTransaction = new CallEventCallbackAckTransaction( 467 mICallEventCallback, ON_DISCONNECT, call.getId(), cause, mLock); 468 CompletableFuture<Boolean> onDisconnectFuture; 469 try { 470 Log.startSession("TSW.oD"); 471 Log.d(TAG, String.format(Locale.US, "onDisconnect: callId=[%s]", call.getId())); 472 onDisconnectFuture = mCallSequencingAdapter.onSetDisconnected(call, cause, 473 callTransaction, 474 result -> Log.i(TAG, String.format(Locale.US, 475 "%s: onResult: callId=[%s], result=[%s]", 476 ON_DISCONNECT, call.getId(), result))); 477 } finally { 478 Log.endSession(); 479 } 480 return onDisconnectFuture; 481 } 482 483 public void onCallStreamingStarted(Call call) { 484 try { 485 Log.startSession("TSW.oCSS"); 486 Log.d(TAG, String.format(Locale.US, "onCallStreamingStarted: callId=[%s]", 487 call.getId())); 488 489 mTransactionManager.addTransaction( 490 new CallEventCallbackAckTransaction(mICallEventCallback, ON_STREAMING_STARTED, 491 call.getId(), mLock), new OutcomeReceiver<>() { 492 @Override 493 public void onResult(CallTransactionResult result) { 494 } 495 496 @Override 497 public void onError(CallException exception) { 498 Log.w(TAG, "onCallStreamingStarted: onError: " 499 + "e.code=[%d], e.msg=[%s]", 500 exception.getCode(), exception.getMessage()); 501 stopCallStreaming(call); 502 } 503 } 504 ); 505 } finally { 506 Log.endSession(); 507 } 508 } 509 510 public void onCallStreamingFailed(Call call, 511 @CallStreamingService.StreamingFailedReason int streamingFailedReason) { 512 if (call != null) { 513 try { 514 mICallEventCallback.onCallStreamingFailed(call.getId(), streamingFailedReason); 515 } catch (RemoteException e) { 516 } 517 } 518 } 519 520 @Override 521 public void onCallEndpointChanged(Call call, CallEndpoint endpoint) { 522 if (call != null) { 523 try { 524 mICallEventCallback.onCallEndpointChanged(call.getId(), endpoint); 525 } catch (RemoteException e) { 526 } 527 } 528 } 529 530 @Override 531 public void onAvailableCallEndpointsChanged(Call call, Set<CallEndpoint> endpoints) { 532 if (call != null) { 533 try { 534 mICallEventCallback.onAvailableCallEndpointsChanged(call.getId(), 535 endpoints.stream().toList()); 536 } catch (RemoteException e) { 537 } 538 } 539 } 540 541 @Override 542 public void onMuteStateChanged(Call call, boolean isMuted) { 543 if (call != null) { 544 try { 545 mICallEventCallback.onMuteStateChanged(call.getId(), isMuted); 546 } catch (RemoteException e) { 547 } 548 } 549 } 550 551 @Override 552 public void onVideoStateChanged(Call call, int videoState) { 553 if (call != null) { 554 try { 555 mICallEventCallback.onVideoStateChanged(call.getId(), videoState); 556 } catch (RemoteException e) { 557 } 558 } 559 } 560 561 public void removeCallFromWrappers(Call call) { 562 if (call != null) { 563 try { 564 // remove the call from frameworks wrapper (client side) 565 mICallEventCallback.removeCallFromTransactionalServiceWrapper(call.getId()); 566 } catch (RemoteException e) { 567 } 568 // remove the call from this class/wrapper (server side) 569 untrackCall(call); 570 } 571 } 572 573 @Override 574 public void sendCallEvent(Call call, String event, Bundle extras) { 575 if (call != null) { 576 try { 577 mICallEventCallback.onEvent(call.getId(), event, extras); 578 } catch (RemoteException e) { 579 } 580 } 581 } 582 583 /*** 584 ********************************************************************************************* 585 ** Helpers ** 586 ********************************************************************************************** 587 */ 588 private void setDeathRecipient(ICallEventCallback callEventCallback) { 589 try { 590 callEventCallback.asBinder().linkToDeath(mAppDeathListener, 0); 591 } catch (Exception e) { 592 Log.w(TAG, "setDeathRecipient: hit exception=[%s] trying to link binder to death", 593 e.toString()); 594 } 595 } 596 597 /*** 598 ********************************************************************************************* 599 ** FocusManager ** 600 ********************************************************************************************** 601 */ 602 603 @Override 604 public void connectionServiceFocusLost() { 605 if (mConnSvrFocusListener != null) { 606 mConnSvrFocusListener.onConnectionServiceReleased(this); 607 } 608 Log.i(TAG, String.format(Locale.US, "connectionServiceFocusLost for package=[%s]", 609 mPackageName)); 610 } 611 612 @Override 613 public void connectionServiceFocusGained() { 614 Log.i(TAG, String.format(Locale.US, "connectionServiceFocusGained for package=[%s]", 615 mPackageName)); 616 } 617 618 @Override 619 public void setConnectionServiceFocusListener( 620 ConnectionServiceFocusManager.ConnectionServiceFocusListener listener) { 621 mConnSvrFocusListener = listener; 622 } 623 624 @Override 625 public ComponentName getComponentName() { 626 return mPhoneAccountHandle.getComponentName(); 627 } 628 629 /*** 630 ********************************************************************************************* 631 ** CallStreaming ** 632 ********************************************************************************************* 633 */ 634 635 public void stopCallStreaming(Call call) { 636 Log.i(this, "stopCallStreaming; callid=%s", call.getId()); 637 if (call != null && call.isStreaming()) { 638 CallTransaction stopStreamingTransaction = mStreamingController 639 .getStopStreamingTransaction(call, mLock); 640 addTransactionsToManager(STOP_STREAMING, stopStreamingTransaction, 641 new ResultReceiver(null)); 642 } 643 } 644 } 645