1 /* 2 * Copyright (C) 2013 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.incallui; 18 19 import android.app.ActivityManager.TaskDescription; 20 import android.app.FragmentManager; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.pm.ActivityInfo; 24 import android.content.res.Resources; 25 import android.graphics.Point; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.telecom.DisconnectCause; 29 import android.telecom.PhoneAccount; 30 import android.telecom.PhoneAccountHandle; 31 import android.telecom.TelecomManager; 32 import android.telecom.VideoProfile; 33 import android.telephony.PhoneNumberUtils; 34 import android.text.TextUtils; 35 import android.view.Surface; 36 import android.view.View; 37 import android.view.Window; 38 import android.view.WindowManager; 39 40 import com.android.contacts.common.interactions.TouchPointManager; 41 import com.android.contacts.common.testing.NeededForTesting; 42 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 43 import com.android.incalluibind.ObjectFactory; 44 import com.google.common.base.Preconditions; 45 46 import java.util.Collections; 47 import java.util.List; 48 import java.util.Locale; 49 import java.util.Set; 50 import java.util.concurrent.ConcurrentHashMap; 51 import java.util.concurrent.CopyOnWriteArrayList; 52 53 /** 54 * Takes updates from the CallList and notifies the InCallActivity (UI) 55 * of the changes. 56 * Responsible for starting the activity for a new call and finishing the activity when all calls 57 * are disconnected. 58 * Creates and manages the in-call state and provides a listener pattern for the presenters 59 * that want to listen in on the in-call state changes. 60 * TODO: This class has become more of a state machine at this point. Consider renaming. 61 */ 62 public class InCallPresenter implements CallList.Listener, 63 CircularRevealFragment.OnCircularRevealCompleteListener { 64 65 private static final String EXTRA_FIRST_TIME_SHOWN = 66 "com.android.incallui.intent.extra.FIRST_TIME_SHOWN"; 67 68 private static final Bundle EMPTY_EXTRAS = new Bundle(); 69 70 private static InCallPresenter sInCallPresenter; 71 72 /** 73 * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is 74 * load factor before resizing, 1 means we only expect a single thread to 75 * access the map so make only a single shard 76 */ 77 private final Set<InCallStateListener> mListeners = Collections.newSetFromMap( 78 new ConcurrentHashMap<InCallStateListener, Boolean>(8, 0.9f, 1)); 79 private final List<IncomingCallListener> mIncomingCallListeners = new CopyOnWriteArrayList<>(); 80 private final Set<InCallDetailsListener> mDetailsListeners = Collections.newSetFromMap( 81 new ConcurrentHashMap<InCallDetailsListener, Boolean>(8, 0.9f, 1)); 82 private final Set<CanAddCallListener> mCanAddCallListeners = Collections.newSetFromMap( 83 new ConcurrentHashMap<CanAddCallListener, Boolean>(8, 0.9f, 1)); 84 private final Set<InCallUiListener> mInCallUiListeners = Collections.newSetFromMap( 85 new ConcurrentHashMap<InCallUiListener, Boolean>(8, 0.9f, 1)); 86 private final Set<InCallOrientationListener> mOrientationListeners = Collections.newSetFromMap( 87 new ConcurrentHashMap<InCallOrientationListener, Boolean>(8, 0.9f, 1)); 88 private final Set<InCallEventListener> mInCallEventListeners = Collections.newSetFromMap( 89 new ConcurrentHashMap<InCallEventListener, Boolean>(8, 0.9f, 1)); 90 91 private AudioModeProvider mAudioModeProvider; 92 private StatusBarNotifier mStatusBarNotifier; 93 private ContactInfoCache mContactInfoCache; 94 private Context mContext; 95 private CallList mCallList; 96 private InCallActivity mInCallActivity; 97 private InCallState mInCallState = InCallState.NO_CALLS; 98 private ProximitySensor mProximitySensor; 99 private boolean mServiceConnected = false; 100 private boolean mAccountSelectionCancelled = false; 101 private InCallCameraManager mInCallCameraManager = null; 102 private AnswerPresenter mAnswerPresenter = new AnswerPresenter(); 103 104 /** 105 * Whether or not we are currently bound and waiting for Telecom to send us a new call. 106 */ 107 private boolean mBoundAndWaitingForOutgoingCall; 108 109 /** 110 * If there is no actual call currently in the call list, this will be used as a fallback 111 * to determine the theme color for InCallUI. 112 */ 113 private PhoneAccountHandle mPendingPhoneAccountHandle; 114 115 /** 116 * Determines if the InCall UI is in fullscreen mode or not. 117 */ 118 private boolean mIsFullScreen = false; 119 120 private final android.telecom.Call.Callback mCallCallback = 121 new android.telecom.Call.Callback() { 122 @Override 123 public void onPostDialWait(android.telecom.Call telecomCall, 124 String remainingPostDialSequence) { 125 final Call call = mCallList.getCallByTelecommCall(telecomCall); 126 if (call == null) { 127 Log.w(this, "Call not found in call list: " + telecomCall); 128 return; 129 } 130 onPostDialCharWait(call.getId(), remainingPostDialSequence); 131 } 132 133 @Override 134 public void onDetailsChanged(android.telecom.Call telecomCall, 135 android.telecom.Call.Details details) { 136 final Call call = mCallList.getCallByTelecommCall(telecomCall); 137 if (call == null) { 138 Log.w(this, "Call not found in call list: " + telecomCall); 139 return; 140 } 141 for (InCallDetailsListener listener : mDetailsListeners) { 142 listener.onDetailsChanged(call, details); 143 } 144 } 145 146 @Override 147 public void onConferenceableCallsChanged(android.telecom.Call telecomCall, 148 List<android.telecom.Call> conferenceableCalls) { 149 Log.i(this, "onConferenceableCallsChanged: " + telecomCall); 150 onDetailsChanged(telecomCall, telecomCall.getDetails()); 151 } 152 }; 153 154 /** 155 * Is true when the activity has been previously started. Some code needs to know not just if 156 * the activity is currently up, but if it had been previously shown in foreground for this 157 * in-call session (e.g., StatusBarNotifier). This gets reset when the session ends in the 158 * tear-down method. 159 */ 160 private boolean mIsActivityPreviouslyStarted = false; 161 162 /** 163 * Whether or not InCallService is bound to Telecom. 164 */ 165 private boolean mServiceBound = false; 166 167 /** 168 * When configuration changes Android kills the current activity and starts a new one. 169 * The flag is used to check if full clean up is necessary (activity is stopped and new 170 * activity won't be started), or if a new activity will be started right after the current one 171 * is destroyed, and therefore no need in release all resources. 172 */ 173 private boolean mIsChangingConfigurations = false; 174 175 /** Display colors for the UI. Consists of a primary color and secondary (darker) color */ 176 private MaterialPalette mThemeColors; 177 178 private TelecomManager mTelecomManager; 179 getInstance()180 public static synchronized InCallPresenter getInstance() { 181 if (sInCallPresenter == null) { 182 sInCallPresenter = new InCallPresenter(); 183 } 184 return sInCallPresenter; 185 } 186 187 @NeededForTesting setInstance(InCallPresenter inCallPresenter)188 static synchronized void setInstance(InCallPresenter inCallPresenter) { 189 sInCallPresenter = inCallPresenter; 190 } 191 getInCallState()192 public InCallState getInCallState() { 193 return mInCallState; 194 } 195 getCallList()196 public CallList getCallList() { 197 return mCallList; 198 } 199 setUp(Context context, CallList callList, AudioModeProvider audioModeProvider, StatusBarNotifier statusBarNotifier, ContactInfoCache contactInfoCache, ProximitySensor proximitySensor)200 public void setUp(Context context, 201 CallList callList, 202 AudioModeProvider audioModeProvider, 203 StatusBarNotifier statusBarNotifier, 204 ContactInfoCache contactInfoCache, 205 ProximitySensor proximitySensor) { 206 if (mServiceConnected) { 207 Log.i(this, "New service connection replacing existing one."); 208 // retain the current resources, no need to create new ones. 209 Preconditions.checkState(context == mContext); 210 Preconditions.checkState(callList == mCallList); 211 Preconditions.checkState(audioModeProvider == mAudioModeProvider); 212 return; 213 } 214 215 Preconditions.checkNotNull(context); 216 mContext = context; 217 218 mContactInfoCache = contactInfoCache; 219 220 mStatusBarNotifier = statusBarNotifier; 221 addListener(mStatusBarNotifier); 222 223 mAudioModeProvider = audioModeProvider; 224 225 mProximitySensor = proximitySensor; 226 addListener(mProximitySensor); 227 228 addIncomingCallListener(mAnswerPresenter); 229 addInCallUiListener(mAnswerPresenter); 230 231 mCallList = callList; 232 233 // This only gets called by the service so this is okay. 234 mServiceConnected = true; 235 236 // The final thing we do in this set up is add ourselves as a listener to CallList. This 237 // will kick off an update and the whole process can start. 238 mCallList.addListener(this); 239 240 VideoPauseController.getInstance().setUp(this); 241 242 Log.d(this, "Finished InCallPresenter.setUp"); 243 } 244 245 /** 246 * Called when the telephony service has disconnected from us. This will happen when there are 247 * no more active calls. However, we may still want to continue showing the UI for 248 * certain cases like showing "Call Ended". 249 * What we really want is to wait for the activity and the service to both disconnect before we 250 * tear things down. This method sets a serviceConnected boolean and calls a secondary method 251 * that performs the aforementioned logic. 252 */ tearDown()253 public void tearDown() { 254 Log.d(this, "tearDown"); 255 mServiceConnected = false; 256 attemptCleanup(); 257 258 VideoPauseController.getInstance().tearDown(); 259 } 260 attemptFinishActivity()261 private void attemptFinishActivity() { 262 final boolean doFinish = (mInCallActivity != null && isActivityStarted()); 263 Log.i(this, "Hide in call UI: " + doFinish); 264 if (doFinish) { 265 mInCallActivity.setExcludeFromRecents(true); 266 mInCallActivity.finish(); 267 268 if (mAccountSelectionCancelled) { 269 // This finish is a result of account selection cancellation 270 // do not include activity ending transition 271 mInCallActivity.overridePendingTransition(0, 0); 272 } 273 } 274 } 275 276 /** 277 * Called when the UI begins, and starts the callstate callbacks if necessary. 278 */ setActivity(InCallActivity inCallActivity)279 public void setActivity(InCallActivity inCallActivity) { 280 if (inCallActivity == null) { 281 throw new IllegalArgumentException("registerActivity cannot be called with null"); 282 } 283 if (mInCallActivity != null && mInCallActivity != inCallActivity) { 284 Log.w(this, "Setting a second activity before destroying the first."); 285 } 286 updateActivity(inCallActivity); 287 } 288 289 /** 290 * Called when the UI ends. Attempts to tear down everything if necessary. See 291 * {@link #tearDown()} for more insight on the tear-down process. 292 */ unsetActivity(InCallActivity inCallActivity)293 public void unsetActivity(InCallActivity inCallActivity) { 294 if (inCallActivity == null) { 295 throw new IllegalArgumentException("unregisterActivity cannot be called with null"); 296 } 297 if (mInCallActivity == null) { 298 Log.i(this, "No InCallActivity currently set, no need to unset."); 299 return; 300 } 301 if (mInCallActivity != inCallActivity) { 302 Log.w(this, "Second instance of InCallActivity is trying to unregister when another" 303 + " instance is active. Ignoring."); 304 return; 305 } 306 updateActivity(null); 307 } 308 309 /** 310 * Updates the current instance of {@link InCallActivity} with the provided one. If a 311 * {@code null} activity is provided, it means that the activity was finished and we should 312 * attempt to cleanup. 313 */ updateActivity(InCallActivity inCallActivity)314 private void updateActivity(InCallActivity inCallActivity) { 315 boolean updateListeners = false; 316 boolean doAttemptCleanup = false; 317 318 if (inCallActivity != null) { 319 if (mInCallActivity == null) { 320 updateListeners = true; 321 Log.i(this, "UI Initialized"); 322 } else { 323 // since setActivity is called onStart(), it can be called multiple times. 324 // This is fine and ignorable, but we do not want to update the world every time 325 // this happens (like going to/from background) so we do not set updateListeners. 326 } 327 328 mInCallActivity = inCallActivity; 329 mInCallActivity.setExcludeFromRecents(false); 330 331 // By the time the UI finally comes up, the call may already be disconnected. 332 // If that's the case, we may need to show an error dialog. 333 if (mCallList != null && mCallList.getDisconnectedCall() != null) { 334 maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall()); 335 } 336 337 // When the UI comes up, we need to first check the in-call state. 338 // If we are showing NO_CALLS, that means that a call probably connected and 339 // then immediately disconnected before the UI was able to come up. 340 // If we dont have any calls, start tearing down the UI instead. 341 // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after 342 // it has been set. 343 if (mInCallState == InCallState.NO_CALLS) { 344 Log.i(this, "UI Initialized, but no calls left. shut down."); 345 attemptFinishActivity(); 346 return; 347 } 348 } else { 349 Log.i(this, "UI Destroyed"); 350 updateListeners = true; 351 mInCallActivity = null; 352 353 // We attempt cleanup for the destroy case but only after we recalculate the state 354 // to see if we need to come back up or stay shut down. This is why we do the 355 // cleanup after the call to onCallListChange() instead of directly here. 356 doAttemptCleanup = true; 357 } 358 359 // Messages can come from the telephony layer while the activity is coming up 360 // and while the activity is going down. So in both cases we need to recalculate what 361 // state we should be in after they complete. 362 // Examples: (1) A new incoming call could come in and then get disconnected before 363 // the activity is created. 364 // (2) All calls could disconnect and then get a new incoming call before the 365 // activity is destroyed. 366 // 367 // b/1122139 - We previously had a check for mServiceConnected here as well, but there are 368 // cases where we need to recalculate the current state even if the service in not 369 // connected. In particular the case where startOrFinish() is called while the app is 370 // already finish()ing. In that case, we skip updating the state with the knowledge that 371 // we will check again once the activity has finished. That means we have to recalculate the 372 // state here even if the service is disconnected since we may not have finished a state 373 // transition while finish()ing. 374 if (updateListeners) { 375 onCallListChange(mCallList); 376 } 377 378 if (doAttemptCleanup) { 379 attemptCleanup(); 380 } 381 } 382 383 private boolean mAwaitingCallListUpdate = false; 384 onBringToForeground(boolean showDialpad)385 public void onBringToForeground(boolean showDialpad) { 386 Log.i(this, "Bringing UI to foreground."); 387 bringToForeground(showDialpad); 388 } 389 390 /** 391 * TODO: Consider listening to CallList callbacks to do this instead of receiving a direct 392 * method invocation from InCallService. 393 */ onCallAdded(android.telecom.Call call)394 public void onCallAdded(android.telecom.Call call) { 395 // Since a call has been added we are no longer waiting for Telecom to send us a 396 // call. 397 setBoundAndWaitingForOutgoingCall(false, null); 398 call.registerCallback(mCallCallback); 399 } 400 401 /** 402 * TODO: Consider listening to CallList callbacks to do this instead of receiving a direct 403 * method invocation from InCallService. 404 */ onCallRemoved(android.telecom.Call call)405 public void onCallRemoved(android.telecom.Call call) { 406 call.unregisterCallback(mCallCallback); 407 } 408 onCanAddCallChanged(boolean canAddCall)409 public void onCanAddCallChanged(boolean canAddCall) { 410 for (CanAddCallListener listener : mCanAddCallListeners) { 411 listener.onCanAddCallChanged(canAddCall); 412 } 413 } 414 415 /** 416 * Called when there is a change to the call list. 417 * Sets the In-Call state for the entire in-call app based on the information it gets from 418 * CallList. Dispatches the in-call state to all listeners. Can trigger the creation or 419 * destruction of the UI based on the states that is calculates. 420 */ 421 @Override onCallListChange(CallList callList)422 public void onCallListChange(CallList callList) { 423 if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null && 424 mInCallActivity.getCallCardFragment().isAnimating()) { 425 mAwaitingCallListUpdate = true; 426 return; 427 } 428 if (callList == null) { 429 return; 430 } 431 432 mAwaitingCallListUpdate = false; 433 434 InCallState newState = getPotentialStateFromCallList(callList); 435 InCallState oldState = mInCallState; 436 Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState); 437 newState = startOrFinishUi(newState); 438 Log.d(this, "onCallListChange newState changed to " + newState); 439 440 // Set the new state before announcing it to the world 441 Log.i(this, "Phone switching state: " + oldState + " -> " + newState); 442 mInCallState = newState; 443 444 // notify listeners of new state 445 for (InCallStateListener listener : mListeners) { 446 Log.d(this, "Notify " + listener + " of state " + mInCallState.toString()); 447 listener.onStateChange(oldState, mInCallState, callList); 448 } 449 450 if (isActivityStarted()) { 451 final boolean hasCall = callList.getActiveOrBackgroundCall() != null || 452 callList.getOutgoingCall() != null; 453 mInCallActivity.dismissKeyguard(hasCall); 454 } 455 } 456 457 /** 458 * Called when there is a new incoming call. 459 * 460 * @param call 461 */ 462 @Override onIncomingCall(Call call)463 public void onIncomingCall(Call call) { 464 InCallState newState = startOrFinishUi(InCallState.INCOMING); 465 InCallState oldState = mInCallState; 466 467 Log.i(this, "Phone switching state: " + oldState + " -> " + newState); 468 mInCallState = newState; 469 470 for (IncomingCallListener listener : mIncomingCallListeners) { 471 listener.onIncomingCall(oldState, mInCallState, call); 472 } 473 } 474 475 @Override onUpgradeToVideo(Call call)476 public void onUpgradeToVideo(Call call) { 477 //NO-OP 478 } 479 /** 480 * Called when a call becomes disconnected. Called everytime an existing call 481 * changes from being connected (incoming/outgoing/active) to disconnected. 482 */ 483 @Override onDisconnect(Call call)484 public void onDisconnect(Call call) { 485 maybeShowErrorDialogOnDisconnect(call); 486 487 // We need to do the run the same code as onCallListChange. 488 onCallListChange(mCallList); 489 490 if (isActivityStarted()) { 491 mInCallActivity.dismissKeyguard(false); 492 } 493 } 494 495 /** 496 * Given the call list, return the state in which the in-call screen should be. 497 */ getPotentialStateFromCallList(CallList callList)498 public InCallState getPotentialStateFromCallList(CallList callList) { 499 500 InCallState newState = InCallState.NO_CALLS; 501 502 if (callList == null) { 503 return newState; 504 } 505 if (callList.getIncomingCall() != null) { 506 newState = InCallState.INCOMING; 507 } else if (callList.getWaitingForAccountCall() != null) { 508 newState = InCallState.WAITING_FOR_ACCOUNT; 509 } else if (callList.getPendingOutgoingCall() != null) { 510 newState = InCallState.PENDING_OUTGOING; 511 } else if (callList.getOutgoingCall() != null) { 512 newState = InCallState.OUTGOING; 513 } else if (callList.getActiveCall() != null || 514 callList.getBackgroundCall() != null || 515 callList.getDisconnectedCall() != null || 516 callList.getDisconnectingCall() != null) { 517 newState = InCallState.INCALL; 518 } 519 520 if (newState == InCallState.NO_CALLS) { 521 if (mBoundAndWaitingForOutgoingCall) { 522 return InCallState.OUTGOING; 523 } 524 } 525 526 return newState; 527 } 528 isBoundAndWaitingForOutgoingCall()529 public boolean isBoundAndWaitingForOutgoingCall() { 530 return mBoundAndWaitingForOutgoingCall; 531 } 532 setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle)533 public void setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle) { 534 // NOTE: It is possible for there to be a race and have handle become null before 535 // the circular reveal starts. This should not cause any problems because CallCardFragment 536 // should fallback to the actual call in the CallList at that point in time to determine 537 // the theme color. 538 Log.i(this, "setBoundAndWaitingForOutgoingCall: " + isBound); 539 mBoundAndWaitingForOutgoingCall = isBound; 540 mPendingPhoneAccountHandle = handle; 541 if (isBound && mInCallState == InCallState.NO_CALLS) { 542 mInCallState = InCallState.OUTGOING; 543 } 544 } 545 546 @Override onCircularRevealComplete(FragmentManager fm)547 public void onCircularRevealComplete(FragmentManager fm) { 548 if (mInCallActivity != null) { 549 mInCallActivity.showCallCardFragment(true); 550 mInCallActivity.getCallCardFragment().animateForNewOutgoingCall(); 551 CircularRevealFragment.endCircularReveal(mInCallActivity.getFragmentManager()); 552 } 553 } 554 onShrinkAnimationComplete()555 public void onShrinkAnimationComplete() { 556 if (mAwaitingCallListUpdate) { 557 onCallListChange(mCallList); 558 } 559 } 560 addIncomingCallListener(IncomingCallListener listener)561 public void addIncomingCallListener(IncomingCallListener listener) { 562 Preconditions.checkNotNull(listener); 563 mIncomingCallListeners.add(listener); 564 } 565 removeIncomingCallListener(IncomingCallListener listener)566 public void removeIncomingCallListener(IncomingCallListener listener) { 567 if (listener != null) { 568 mIncomingCallListeners.remove(listener); 569 } 570 } 571 addListener(InCallStateListener listener)572 public void addListener(InCallStateListener listener) { 573 Preconditions.checkNotNull(listener); 574 mListeners.add(listener); 575 } 576 removeListener(InCallStateListener listener)577 public void removeListener(InCallStateListener listener) { 578 if (listener != null) { 579 mListeners.remove(listener); 580 } 581 } 582 addDetailsListener(InCallDetailsListener listener)583 public void addDetailsListener(InCallDetailsListener listener) { 584 Preconditions.checkNotNull(listener); 585 mDetailsListeners.add(listener); 586 } 587 removeDetailsListener(InCallDetailsListener listener)588 public void removeDetailsListener(InCallDetailsListener listener) { 589 if (listener != null) { 590 mDetailsListeners.remove(listener); 591 } 592 } 593 addCanAddCallListener(CanAddCallListener listener)594 public void addCanAddCallListener(CanAddCallListener listener) { 595 Preconditions.checkNotNull(listener); 596 mCanAddCallListeners.add(listener); 597 } 598 removeCanAddCallListener(CanAddCallListener listener)599 public void removeCanAddCallListener(CanAddCallListener listener) { 600 if (listener != null) { 601 mCanAddCallListeners.remove(listener); 602 } 603 } 604 addOrientationListener(InCallOrientationListener listener)605 public void addOrientationListener(InCallOrientationListener listener) { 606 Preconditions.checkNotNull(listener); 607 mOrientationListeners.add(listener); 608 } 609 removeOrientationListener(InCallOrientationListener listener)610 public void removeOrientationListener(InCallOrientationListener listener) { 611 if (listener != null) { 612 mOrientationListeners.remove(listener); 613 } 614 } 615 addInCallEventListener(InCallEventListener listener)616 public void addInCallEventListener(InCallEventListener listener) { 617 Preconditions.checkNotNull(listener); 618 mInCallEventListeners.add(listener); 619 } 620 removeInCallEventListener(InCallEventListener listener)621 public void removeInCallEventListener(InCallEventListener listener) { 622 if (listener != null) { 623 mInCallEventListeners.remove(listener); 624 } 625 } 626 getProximitySensor()627 public ProximitySensor getProximitySensor() { 628 return mProximitySensor; 629 } 630 handleAccountSelection(PhoneAccountHandle accountHandle, boolean setDefault)631 public void handleAccountSelection(PhoneAccountHandle accountHandle, boolean setDefault) { 632 if (mCallList != null) { 633 Call call = mCallList.getWaitingForAccountCall(); 634 if (call != null) { 635 String callId = call.getId(); 636 TelecomAdapter.getInstance().phoneAccountSelected(callId, accountHandle, setDefault); 637 } 638 } 639 } 640 cancelAccountSelection()641 public void cancelAccountSelection() { 642 mAccountSelectionCancelled = true; 643 if (mCallList != null) { 644 Call call = mCallList.getWaitingForAccountCall(); 645 if (call != null) { 646 String callId = call.getId(); 647 TelecomAdapter.getInstance().disconnectCall(callId); 648 } 649 } 650 } 651 652 /** 653 * Hangs up any active or outgoing calls. 654 */ hangUpOngoingCall(Context context)655 public void hangUpOngoingCall(Context context) { 656 // By the time we receive this intent, we could be shut down and call list 657 // could be null. Bail in those cases. 658 if (mCallList == null) { 659 if (mStatusBarNotifier == null) { 660 // The In Call UI has crashed but the notification still stayed up. We should not 661 // come to this stage. 662 StatusBarNotifier.clearAllCallNotifications(context); 663 } 664 return; 665 } 666 667 Call call = mCallList.getOutgoingCall(); 668 if (call == null) { 669 call = mCallList.getActiveOrBackgroundCall(); 670 } 671 672 if (call != null) { 673 TelecomAdapter.getInstance().disconnectCall(call.getId()); 674 call.setState(Call.State.DISCONNECTING); 675 mCallList.onUpdate(call); 676 } 677 } 678 679 /** 680 * Answers any incoming call. 681 */ answerIncomingCall(Context context, int videoState)682 public void answerIncomingCall(Context context, int videoState) { 683 // By the time we receive this intent, we could be shut down and call list 684 // could be null. Bail in those cases. 685 if (mCallList == null) { 686 StatusBarNotifier.clearAllCallNotifications(context); 687 return; 688 } 689 690 Call call = mCallList.getIncomingCall(); 691 if (call != null) { 692 TelecomAdapter.getInstance().answerCall(call.getId(), videoState); 693 showInCall(false, false/* newOutgoingCall */); 694 } 695 } 696 697 /** 698 * Declines any incoming call. 699 */ declineIncomingCall(Context context)700 public void declineIncomingCall(Context context) { 701 // By the time we receive this intent, we could be shut down and call list 702 // could be null. Bail in those cases. 703 if (mCallList == null) { 704 StatusBarNotifier.clearAllCallNotifications(context); 705 return; 706 } 707 708 Call call = mCallList.getIncomingCall(); 709 if (call != null) { 710 TelecomAdapter.getInstance().rejectCall(call.getId(), false, null); 711 } 712 } 713 acceptUpgradeRequest(int videoState, Context context)714 public void acceptUpgradeRequest(int videoState, Context context) { 715 Log.d(this, " acceptUpgradeRequest videoState " + videoState); 716 // Bail if we have been shut down and the call list is null. 717 if (mCallList == null) { 718 StatusBarNotifier.clearAllCallNotifications(context); 719 Log.e(this, " acceptUpgradeRequest mCallList is empty so returning"); 720 return; 721 } 722 723 Call call = mCallList.getVideoUpgradeRequestCall(); 724 if (call != null) { 725 VideoProfile videoProfile = new VideoProfile(videoState); 726 call.getVideoCall().sendSessionModifyResponse(videoProfile); 727 call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); 728 } 729 } 730 declineUpgradeRequest(Context context)731 public void declineUpgradeRequest(Context context) { 732 Log.d(this, " declineUpgradeRequest"); 733 // Bail if we have been shut down and the call list is null. 734 if (mCallList == null) { 735 StatusBarNotifier.clearAllCallNotifications(context); 736 Log.e(this, " declineUpgradeRequest mCallList is empty so returning"); 737 return; 738 } 739 740 Call call = mCallList.getVideoUpgradeRequestCall(); 741 if (call != null) { 742 VideoProfile videoProfile = 743 new VideoProfile(call.getVideoState()); 744 call.getVideoCall().sendSessionModifyResponse(videoProfile); 745 call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST); 746 } 747 } 748 749 /** 750 * Returns true if the incall app is the foreground application. 751 */ isShowingInCallUi()752 public boolean isShowingInCallUi() { 753 return (isActivityStarted() && mInCallActivity.isVisible()); 754 } 755 756 /** 757 * Returns true if the activity has been created and is running. 758 * Returns true as long as activity is not destroyed or finishing. This ensures that we return 759 * true even if the activity is paused (not in foreground). 760 */ isActivityStarted()761 public boolean isActivityStarted() { 762 return (mInCallActivity != null && 763 !mInCallActivity.isDestroyed() && 764 !mInCallActivity.isFinishing()); 765 } 766 isActivityPreviouslyStarted()767 public boolean isActivityPreviouslyStarted() { 768 return mIsActivityPreviouslyStarted; 769 } 770 isChangingConfigurations()771 public boolean isChangingConfigurations() { 772 return mIsChangingConfigurations; 773 } 774 775 /*package*/ updateIsChangingConfigurations()776 void updateIsChangingConfigurations() { 777 mIsChangingConfigurations = false; 778 if (mInCallActivity != null) { 779 mIsChangingConfigurations = mInCallActivity.isChangingConfigurations(); 780 } 781 Log.d(this, "IsChangingConfigurations=" + mIsChangingConfigurations); 782 } 783 784 785 /** 786 * Called when the activity goes in/out of the foreground. 787 */ onUiShowing(boolean showing)788 public void onUiShowing(boolean showing) { 789 // We need to update the notification bar when we leave the UI because that 790 // could trigger it to show again. 791 if (mStatusBarNotifier != null) { 792 mStatusBarNotifier.updateNotification(mInCallState, mCallList); 793 } 794 795 if (mProximitySensor != null) { 796 mProximitySensor.onInCallShowing(showing); 797 } 798 799 Intent broadcastIntent = ObjectFactory.getUiReadyBroadcastIntent(mContext); 800 if (broadcastIntent != null) { 801 broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted); 802 803 if (showing) { 804 Log.d(this, "Sending sticky broadcast: ", broadcastIntent); 805 mContext.sendStickyBroadcast(broadcastIntent); 806 } else { 807 Log.d(this, "Removing sticky broadcast: ", broadcastIntent); 808 mContext.removeStickyBroadcast(broadcastIntent); 809 } 810 } 811 812 if (showing) { 813 mIsActivityPreviouslyStarted = true; 814 } else { 815 updateIsChangingConfigurations(); 816 } 817 818 for (InCallUiListener listener : mInCallUiListeners) { 819 listener.onUiShowing(showing); 820 } 821 } 822 addInCallUiListener(InCallUiListener listener)823 public void addInCallUiListener(InCallUiListener listener) { 824 mInCallUiListeners.add(listener); 825 } 826 removeInCallUiListener(InCallUiListener listener)827 public boolean removeInCallUiListener(InCallUiListener listener) { 828 return mInCallUiListeners.remove(listener); 829 } 830 831 /*package*/ onActivityStarted()832 void onActivityStarted() { 833 Log.d(this, "onActivityStarted"); 834 notifyVideoPauseController(true); 835 } 836 837 /*package*/ onActivityStopped()838 void onActivityStopped() { 839 Log.d(this, "onActivityStopped"); 840 notifyVideoPauseController(false); 841 } 842 notifyVideoPauseController(boolean showing)843 private void notifyVideoPauseController(boolean showing) { 844 Log.d(this, "notifyVideoPauseController: mIsChangingConfigurations=" + 845 mIsChangingConfigurations); 846 if (!mIsChangingConfigurations) { 847 VideoPauseController.getInstance().onUiShowing(showing); 848 } 849 } 850 851 /** 852 * Brings the app into the foreground if possible. 853 */ bringToForeground(boolean showDialpad)854 public void bringToForeground(boolean showDialpad) { 855 // Before we bring the incall UI to the foreground, we check to see if: 856 // 1. It is not currently in the foreground 857 // 2. We are in a state where we want to show the incall ui (i.e. there are calls to 858 // be displayed) 859 // If the activity hadn't actually been started previously, yet there are still calls 860 // present (e.g. a call was accepted by a bluetooth or wired headset), we want to 861 // bring it up the UI regardless. 862 if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) { 863 showInCall(showDialpad, false /* newOutgoingCall */); 864 } 865 } 866 onPostDialCharWait(String callId, String chars)867 public void onPostDialCharWait(String callId, String chars) { 868 if (isActivityStarted()) { 869 mInCallActivity.showPostCharWaitDialog(callId, chars); 870 } 871 } 872 873 /** 874 * Handles the green CALL key while in-call. 875 * @return true if we consumed the event. 876 */ handleCallKey()877 public boolean handleCallKey() { 878 Log.v(this, "handleCallKey"); 879 880 // The green CALL button means either "Answer", "Unhold", or 881 // "Swap calls", or can be a no-op, depending on the current state 882 // of the Phone. 883 884 /** 885 * INCOMING CALL 886 */ 887 final CallList calls = mCallList; 888 final Call incomingCall = calls.getIncomingCall(); 889 Log.v(this, "incomingCall: " + incomingCall); 890 891 // (1) Attempt to answer a call 892 if (incomingCall != null) { 893 TelecomAdapter.getInstance().answerCall( 894 incomingCall.getId(), VideoProfile.STATE_AUDIO_ONLY); 895 return true; 896 } 897 898 /** 899 * STATE_ACTIVE CALL 900 */ 901 final Call activeCall = calls.getActiveCall(); 902 if (activeCall != null) { 903 // TODO: This logic is repeated from CallButtonPresenter.java. We should 904 // consolidate this logic. 905 final boolean canMerge = activeCall.can( 906 android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); 907 final boolean canSwap = activeCall.can( 908 android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); 909 910 Log.v(this, "activeCall: " + activeCall + ", canMerge: " + canMerge + 911 ", canSwap: " + canSwap); 912 913 // (2) Attempt actions on conference calls 914 if (canMerge) { 915 TelecomAdapter.getInstance().merge(activeCall.getId()); 916 return true; 917 } else if (canSwap) { 918 TelecomAdapter.getInstance().swap(activeCall.getId()); 919 return true; 920 } 921 } 922 923 /** 924 * BACKGROUND CALL 925 */ 926 final Call heldCall = calls.getBackgroundCall(); 927 if (heldCall != null) { 928 // We have a hold call so presumeable it will always support HOLD...but 929 // there is no harm in double checking. 930 final boolean canHold = heldCall.can(android.telecom.Call.Details.CAPABILITY_HOLD); 931 932 Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold); 933 934 // (4) unhold call 935 if (heldCall.getState() == Call.State.ONHOLD && canHold) { 936 TelecomAdapter.getInstance().unholdCall(heldCall.getId()); 937 return true; 938 } 939 } 940 941 // Always consume hard keys 942 return true; 943 } 944 945 /** 946 * A dialog could have prevented in-call screen from being previously finished. 947 * This function checks to see if there should be any UI left and if not attempts 948 * to tear down the UI. 949 */ onDismissDialog()950 public void onDismissDialog() { 951 Log.i(this, "Dialog dismissed"); 952 if (mInCallState == InCallState.NO_CALLS) { 953 attemptFinishActivity(); 954 attemptCleanup(); 955 } 956 } 957 958 /** 959 * Toggles whether the application is in fullscreen mode or not. 960 * 961 * @return {@code true} if in-call is now in fullscreen mode. 962 */ toggleFullscreenMode()963 public boolean toggleFullscreenMode() { 964 mIsFullScreen = !mIsFullScreen; 965 Log.v(this, "toggleFullscreenMode = " + mIsFullScreen); 966 notifyFullscreenModeChange(mIsFullScreen); 967 return mIsFullScreen; 968 } 969 970 /** 971 * Changes the fullscreen mode of the in-call UI. 972 * 973 * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false} 974 * otherwise. 975 */ setFullScreen(boolean isFullScreen)976 public void setFullScreen(boolean isFullScreen) { 977 Log.v(this, "setFullScreen = " + isFullScreen); 978 if (mIsFullScreen == isFullScreen) { 979 Log.v(this, "setFullScreen ignored as already in that state."); 980 return; 981 } 982 mIsFullScreen = isFullScreen; 983 notifyFullscreenModeChange(mIsFullScreen); 984 } 985 986 /** 987 * @return {@code true} if the in-call ui is currently in fullscreen mode, {@code false} 988 * otherwise. 989 */ isFullscreen()990 public boolean isFullscreen() { 991 return mIsFullScreen; 992 } 993 994 995 /** 996 * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status. 997 * 998 * @param isFullscreenMode {@code True} if entering full screen mode. 999 */ notifyFullscreenModeChange(boolean isFullscreenMode)1000 public void notifyFullscreenModeChange(boolean isFullscreenMode) { 1001 for (InCallEventListener listener : mInCallEventListeners) { 1002 listener.onFullscreenModeChanged(isFullscreenMode); 1003 } 1004 } 1005 1006 /** 1007 * For some disconnected causes, we show a dialog. This calls into the activity to show 1008 * the dialog if appropriate for the call. 1009 */ maybeShowErrorDialogOnDisconnect(Call call)1010 private void maybeShowErrorDialogOnDisconnect(Call call) { 1011 // For newly disconnected calls, we may want to show a dialog on specific error conditions 1012 if (isActivityStarted() && call.getState() == Call.State.DISCONNECTED) { 1013 if (call.getAccountHandle() == null && !call.isConferenceCall()) { 1014 setDisconnectCauseForMissingAccounts(call); 1015 } 1016 mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause()); 1017 } 1018 } 1019 1020 /** 1021 * When the state of in-call changes, this is the first method to get called. It determines if 1022 * the UI needs to be started or finished depending on the new state and does it. 1023 */ startOrFinishUi(InCallState newState)1024 private InCallState startOrFinishUi(InCallState newState) { 1025 Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState); 1026 1027 // TODO: Consider a proper state machine implementation 1028 1029 // If the state isn't changing we have already done any starting/stopping of activities in 1030 // a previous pass...so lets cut out early 1031 if (newState == mInCallState) { 1032 return newState; 1033 } 1034 1035 // A new Incoming call means that the user needs to be notified of the the call (since 1036 // it wasn't them who initiated it). We do this through full screen notifications and 1037 // happens indirectly through {@link StatusBarNotifier}. 1038 // 1039 // The process for incoming calls is as follows: 1040 // 1041 // 1) CallList - Announces existence of new INCOMING call 1042 // 2) InCallPresenter - Gets announcement and calculates that the new InCallState 1043 // - should be set to INCOMING. 1044 // 3) InCallPresenter - This method is called to see if we need to start or finish 1045 // the app given the new state. 1046 // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls 1047 // StatusBarNotifier explicitly to issue a FullScreen Notification 1048 // that will either start the InCallActivity or show the user a 1049 // top-level notification dialog if the user is in an immersive app. 1050 // That notification can also start the InCallActivity. 1051 // 5) InCallActivity - Main activity starts up and at the end of its onCreate will 1052 // call InCallPresenter::setActivity() to let the presenter 1053 // know that start-up is complete. 1054 // 1055 // [ AND NOW YOU'RE IN THE CALL. voila! ] 1056 // 1057 // Our app is started using a fullScreen notification. We need to do this whenever 1058 // we get an incoming call. Depending on the current context of the device, either a 1059 // incoming call HUN or the actual InCallActivity will be shown. 1060 final boolean startIncomingCallSequence = (InCallState.INCOMING == newState); 1061 1062 // A dialog to show on top of the InCallUI to select a PhoneAccount 1063 final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState); 1064 1065 // A new outgoing call indicates that the user just now dialed a number and when that 1066 // happens we need to display the screen immediately or show an account picker dialog if 1067 // no default is set. However, if the main InCallUI is already visible, we do not want to 1068 // re-initiate the start-up animation, so we do not need to do anything here. 1069 // 1070 // It is also possible to go into an intermediate state where the call has been initiated 1071 // but Telecomm has not yet returned with the details of the call (handle, gateway, etc.). 1072 // This pending outgoing state can also launch the call screen. 1073 // 1074 // This is different from the incoming call sequence because we do not need to shock the 1075 // user with a top-level notification. Just show the call UI normally. 1076 final boolean mainUiNotVisible = !isShowingInCallUi() || !getCallCardFragmentVisible(); 1077 boolean showCallUi = InCallState.OUTGOING == newState && mainUiNotVisible; 1078 1079 // Direct transition from PENDING_OUTGOING -> INCALL means that there was an error in the 1080 // outgoing call process, so the UI should be brought up to show an error dialog. 1081 showCallUi |= (InCallState.PENDING_OUTGOING == mInCallState 1082 && InCallState.INCALL == newState && !isActivityStarted()); 1083 1084 // Another exception - InCallActivity is in charge of disconnecting a call with no 1085 // valid accounts set. Bring the UI up if this is true for the current pending outgoing 1086 // call so that: 1087 // 1) The call can be disconnected correctly 1088 // 2) The UI comes up and correctly displays the error dialog. 1089 // TODO: Remove these special case conditions by making InCallPresenter a true state 1090 // machine. Telecom should also be the component responsible for disconnecting a call 1091 // with no valid accounts. 1092 showCallUi |= InCallState.PENDING_OUTGOING == newState && mainUiNotVisible 1093 && isCallWithNoValidAccounts(mCallList.getPendingOutgoingCall()); 1094 1095 // The only time that we have an instance of mInCallActivity and it isn't started is 1096 // when it is being destroyed. In that case, lets avoid bringing up another instance of 1097 // the activity. When it is finally destroyed, we double check if we should bring it back 1098 // up so we aren't going to lose anything by avoiding a second startup here. 1099 boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted(); 1100 if (activityIsFinishing) { 1101 Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState); 1102 return mInCallState; 1103 } 1104 1105 if (showCallUi || showAccountPicker) { 1106 Log.i(this, "Start in call UI"); 1107 showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */); 1108 } else if (startIncomingCallSequence) { 1109 Log.i(this, "Start Full Screen in call UI"); 1110 1111 // We're about the bring up the in-call UI for an incoming call. If we still have 1112 // dialogs up, we need to clear them out before showing incoming screen. 1113 if (isActivityStarted()) { 1114 mInCallActivity.dismissPendingDialogs(); 1115 } 1116 if (!startUi(newState)) { 1117 // startUI refused to start the UI. This indicates that it needed to restart the 1118 // activity. When it finally restarts, it will call us back, so we do not actually 1119 // change the state yet (we return mInCallState instead of newState). 1120 return mInCallState; 1121 } 1122 } else if (newState == InCallState.NO_CALLS) { 1123 // The new state is the no calls state. Tear everything down. 1124 attemptFinishActivity(); 1125 attemptCleanup(); 1126 } 1127 1128 return newState; 1129 } 1130 1131 /** 1132 * Determines whether or not a call has no valid phone accounts that can be used to make the 1133 * call with. Emergency calls do not require a phone account. 1134 * 1135 * @param call to check accounts for. 1136 * @return {@code true} if the call has no call capable phone accounts set, {@code false} if 1137 * the call contains a phone account that could be used to initiate it with, or is an emergency 1138 * call. 1139 */ isCallWithNoValidAccounts(Call call)1140 public static boolean isCallWithNoValidAccounts(Call call) { 1141 if (call != null && !call.isEmergencyCall()) { 1142 Bundle extras = call.getIntentExtras(); 1143 1144 if (extras == null) { 1145 extras = EMPTY_EXTRAS; 1146 } 1147 1148 final List<PhoneAccountHandle> phoneAccountHandles = extras 1149 .getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); 1150 1151 if ((call.getAccountHandle() == null && 1152 (phoneAccountHandles == null || phoneAccountHandles.isEmpty()))) { 1153 Log.i(InCallPresenter.getInstance(), "No valid accounts for call " + call); 1154 return true; 1155 } 1156 } 1157 return false; 1158 } 1159 1160 /** 1161 * Sets the DisconnectCause for a call that was disconnected because it was missing a 1162 * PhoneAccount or PhoneAccounts to select from. 1163 * @param call 1164 */ setDisconnectCauseForMissingAccounts(Call call)1165 private void setDisconnectCauseForMissingAccounts(Call call) { 1166 android.telecom.Call telecomCall = call.getTelecommCall(); 1167 1168 Bundle extras = telecomCall.getDetails().getIntentExtras(); 1169 // Initialize the extras bundle to avoid NPE 1170 if (extras == null) { 1171 extras = new Bundle(); 1172 } 1173 1174 final List<PhoneAccountHandle> phoneAccountHandles = extras.getParcelableArrayList( 1175 android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); 1176 1177 if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) { 1178 String scheme = telecomCall.getDetails().getHandle().getScheme(); 1179 final String errorMsg = PhoneAccount.SCHEME_TEL.equals(scheme) ? 1180 mContext.getString(R.string.callFailed_simError) : 1181 mContext.getString(R.string.incall_error_supp_service_unknown); 1182 DisconnectCause disconnectCause = 1183 new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg); 1184 call.setDisconnectCause(disconnectCause); 1185 } 1186 } 1187 startUi(InCallState inCallState)1188 private boolean startUi(InCallState inCallState) { 1189 boolean isCallWaiting = mCallList.getActiveCall() != null && 1190 mCallList.getIncomingCall() != null; 1191 1192 // If the screen is off, we need to make sure it gets turned on for incoming calls. 1193 // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works 1194 // when the activity is first created. Therefore, to ensure the screen is turned on 1195 // for the call waiting case, we finish() the current activity and start a new one. 1196 // There should be no jank from this since the screen is already off and will remain so 1197 // until our new activity is up. 1198 1199 if (isCallWaiting) { 1200 if (mProximitySensor.isScreenReallyOff() && isActivityStarted()) { 1201 Log.i(this, "Restarting InCallActivity to turn screen on for call waiting"); 1202 mInCallActivity.finish(); 1203 // When the activity actually finishes, we will start it again if there are 1204 // any active calls, so we do not need to start it explicitly here. Note, we 1205 // actually get called back on this function to restart it. 1206 1207 // We return false to indicate that we did not actually start the UI. 1208 return false; 1209 } else { 1210 showInCall(false, false); 1211 } 1212 } else { 1213 mStatusBarNotifier.updateNotification(inCallState, mCallList); 1214 } 1215 return true; 1216 } 1217 1218 /** 1219 * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all 1220 * down. 1221 */ attemptCleanup()1222 private void attemptCleanup() { 1223 boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected && 1224 mInCallState == InCallState.NO_CALLS); 1225 Log.i(this, "attemptCleanup? " + shouldCleanup); 1226 1227 if (shouldCleanup) { 1228 mIsActivityPreviouslyStarted = false; 1229 mIsChangingConfigurations = false; 1230 1231 // blow away stale contact info so that we get fresh data on 1232 // the next set of calls 1233 if (mContactInfoCache != null) { 1234 mContactInfoCache.clearCache(); 1235 } 1236 mContactInfoCache = null; 1237 1238 if (mProximitySensor != null) { 1239 removeListener(mProximitySensor); 1240 mProximitySensor.tearDown(); 1241 } 1242 mProximitySensor = null; 1243 1244 mAudioModeProvider = null; 1245 1246 if (mStatusBarNotifier != null) { 1247 removeListener(mStatusBarNotifier); 1248 } 1249 mStatusBarNotifier = null; 1250 1251 if (mCallList != null) { 1252 mCallList.removeListener(this); 1253 } 1254 mCallList = null; 1255 1256 mContext = null; 1257 mInCallActivity = null; 1258 1259 mListeners.clear(); 1260 mIncomingCallListeners.clear(); 1261 mDetailsListeners.clear(); 1262 mCanAddCallListeners.clear(); 1263 mOrientationListeners.clear(); 1264 mInCallEventListeners.clear(); 1265 1266 Log.d(this, "Finished InCallPresenter.CleanUp"); 1267 } 1268 } 1269 showInCall(final boolean showDialpad, final boolean newOutgoingCall)1270 public void showInCall(final boolean showDialpad, final boolean newOutgoingCall) { 1271 Log.i(this, "Showing InCallActivity"); 1272 mContext.startActivity(getInCallIntent(showDialpad, newOutgoingCall)); 1273 } 1274 onServiceBind()1275 public void onServiceBind() { 1276 mServiceBound = true; 1277 } 1278 onServiceUnbind()1279 public void onServiceUnbind() { 1280 InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(false, null); 1281 mServiceBound = false; 1282 } 1283 isServiceBound()1284 public boolean isServiceBound() { 1285 return mServiceBound; 1286 } 1287 maybeStartRevealAnimation(Intent intent)1288 public void maybeStartRevealAnimation(Intent intent) { 1289 if (intent == null || mInCallActivity != null) { 1290 return; 1291 } 1292 final Bundle extras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); 1293 if (extras == null) { 1294 // Incoming call, just show the in-call UI directly. 1295 return; 1296 } 1297 1298 if (extras.containsKey(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS)) { 1299 // Account selection dialog will show up so don't show the animation. 1300 return; 1301 } 1302 1303 final PhoneAccountHandle accountHandle = 1304 intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); 1305 final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT); 1306 1307 InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle); 1308 1309 final Intent incallIntent = getInCallIntent(false, true); 1310 incallIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint); 1311 mContext.startActivity(incallIntent); 1312 } 1313 getInCallIntent(boolean showDialpad, boolean newOutgoingCall)1314 public Intent getInCallIntent(boolean showDialpad, boolean newOutgoingCall) { 1315 final Intent intent = new Intent(Intent.ACTION_MAIN, null); 1316 intent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION | Intent.FLAG_ACTIVITY_NEW_TASK); 1317 1318 intent.setClass(mContext, InCallActivity.class); 1319 if (showDialpad) { 1320 intent.putExtra(InCallActivity.SHOW_DIALPAD_EXTRA, true); 1321 } 1322 intent.putExtra(InCallActivity.NEW_OUTGOING_CALL_EXTRA, newOutgoingCall); 1323 return intent; 1324 } 1325 1326 /** 1327 * Retrieves the current in-call camera manager instance, creating if necessary. 1328 * 1329 * @return The {@link InCallCameraManager}. 1330 */ getInCallCameraManager()1331 public InCallCameraManager getInCallCameraManager() { 1332 synchronized(this) { 1333 if (mInCallCameraManager == null) { 1334 mInCallCameraManager = new InCallCameraManager(mContext); 1335 } 1336 1337 return mInCallCameraManager; 1338 } 1339 } 1340 1341 /** 1342 * Handles changes to the device rotation. 1343 * 1344 * @param rotation The device rotation (one of: {@link Surface#ROTATION_0}, 1345 * {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180}, 1346 * {@link Surface#ROTATION_270}). 1347 */ onDeviceRotationChange(int rotation)1348 public void onDeviceRotationChange(int rotation) { 1349 Log.d(this, "onDeviceRotationChange: rotation=" + rotation); 1350 // First translate to rotation in degrees. 1351 if (mCallList != null) { 1352 mCallList.notifyCallsOfDeviceRotation(toRotationAngle(rotation)); 1353 } else { 1354 Log.w(this, "onDeviceRotationChange: CallList is null."); 1355 } 1356 } 1357 1358 /** 1359 * Converts rotation constants to rotation in degrees. 1360 * @param rotation Rotation constants (one of: {@link Surface#ROTATION_0}, 1361 * {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180}, 1362 * {@link Surface#ROTATION_270}). 1363 */ toRotationAngle(int rotation)1364 public static int toRotationAngle(int rotation) { 1365 int rotationAngle; 1366 switch (rotation) { 1367 case Surface.ROTATION_0: 1368 rotationAngle = 0; 1369 break; 1370 case Surface.ROTATION_90: 1371 rotationAngle = 90; 1372 break; 1373 case Surface.ROTATION_180: 1374 rotationAngle = 180; 1375 break; 1376 case Surface.ROTATION_270: 1377 rotationAngle = 270; 1378 break; 1379 default: 1380 rotationAngle = 0; 1381 } 1382 return rotationAngle; 1383 } 1384 1385 /** 1386 * Notifies listeners of changes in orientation (e.g. portrait/landscape). 1387 * 1388 * @param orientation The orientation of the device (one of: {@link Surface#ROTATION_0}, 1389 * {@link Surface#ROTATION_90}, {@link Surface#ROTATION_180}, 1390 * {@link Surface#ROTATION_270}). 1391 */ onDeviceOrientationChange(int orientation)1392 public void onDeviceOrientationChange(int orientation) { 1393 for (InCallOrientationListener listener : mOrientationListeners) { 1394 listener.onDeviceOrientationChanged(orientation); 1395 } 1396 } 1397 1398 /** 1399 * Configures the in-call UI activity so it can change orientations or not. 1400 * 1401 * @param allowOrientationChange {@code True} if the in-call UI can change between portrait 1402 * and landscape. {@Code False} if the in-call UI should be locked in portrait. 1403 */ setInCallAllowsOrientationChange(boolean allowOrientationChange)1404 public void setInCallAllowsOrientationChange(boolean allowOrientationChange) { 1405 if (mInCallActivity == null) { 1406 Log.e(this, "InCallActivity is null. Can't set requested orientation."); 1407 return; 1408 } 1409 1410 if (!allowOrientationChange) { 1411 mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR); 1412 } else { 1413 mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR); 1414 } 1415 } 1416 enableScreenTimeout(boolean enable)1417 public void enableScreenTimeout(boolean enable) { 1418 Log.v(this, "enableScreenTimeout: value=" + enable); 1419 if (mInCallActivity == null) { 1420 Log.e(this, "enableScreenTimeout: InCallActivity is null."); 1421 return; 1422 } 1423 1424 final Window window = mInCallActivity.getWindow(); 1425 if (enable) { 1426 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1427 } else { 1428 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1429 } 1430 } 1431 1432 /** 1433 * Returns the space available beside the call card. 1434 * 1435 * @return The space beside the call card. 1436 */ getSpaceBesideCallCard()1437 public float getSpaceBesideCallCard() { 1438 if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) { 1439 return mInCallActivity.getCallCardFragment().getSpaceBesideCallCard(); 1440 } 1441 return 0; 1442 } 1443 1444 /** 1445 * Returns whether the call card fragment is currently visible. 1446 * 1447 * @return True if the call card fragment is visible. 1448 */ getCallCardFragmentVisible()1449 public boolean getCallCardFragmentVisible() { 1450 if (mInCallActivity != null && mInCallActivity.getCallCardFragment() != null) { 1451 return mInCallActivity.getCallCardFragment().isVisible(); 1452 } 1453 return false; 1454 } 1455 1456 /** 1457 * Hides or shows the conference manager fragment. 1458 * 1459 * @param show {@code true} if the conference manager should be shown, {@code false} if it 1460 * should be hidden. 1461 */ showConferenceCallManager(boolean show)1462 public void showConferenceCallManager(boolean show) { 1463 if (mInCallActivity == null) { 1464 return; 1465 } 1466 1467 mInCallActivity.showConferenceFragment(show); 1468 } 1469 1470 /** 1471 * @return True if the application is currently running in a right-to-left locale. 1472 */ isRtl()1473 public static boolean isRtl() { 1474 return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == 1475 View.LAYOUT_DIRECTION_RTL; 1476 } 1477 1478 /** 1479 * Extract background color from call object. The theme colors will include a primary color 1480 * and a secondary color. 1481 */ setThemeColors()1482 public void setThemeColors() { 1483 // This method will set the background to default if the color is PhoneAccount.NO_COLOR. 1484 mThemeColors = getColorsFromCall(mCallList.getFirstCall()); 1485 1486 if (mInCallActivity == null) { 1487 return; 1488 } 1489 1490 final Resources resources = mInCallActivity.getResources(); 1491 final int color; 1492 if (resources.getBoolean(R.bool.is_layout_landscape)) { 1493 color = resources.getColor(R.color.statusbar_background_color, null); 1494 } else { 1495 color = mThemeColors.mSecondaryColor; 1496 } 1497 1498 mInCallActivity.getWindow().setStatusBarColor(color); 1499 final TaskDescription td = new TaskDescription( 1500 resources.getString(R.string.notification_ongoing_call), null, color); 1501 mInCallActivity.setTaskDescription(td); 1502 } 1503 1504 /** 1505 * @return A palette for colors to display in the UI. 1506 */ getThemeColors()1507 public MaterialPalette getThemeColors() { 1508 return mThemeColors; 1509 } 1510 getColorsFromCall(Call call)1511 private MaterialPalette getColorsFromCall(Call call) { 1512 if (call == null) { 1513 return getColorsFromPhoneAccountHandle(mPendingPhoneAccountHandle); 1514 } else { 1515 return getColorsFromPhoneAccountHandle(call.getAccountHandle()); 1516 } 1517 } 1518 getColorsFromPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle)1519 private MaterialPalette getColorsFromPhoneAccountHandle(PhoneAccountHandle phoneAccountHandle) { 1520 int highlightColor = PhoneAccount.NO_HIGHLIGHT_COLOR; 1521 if (phoneAccountHandle != null) { 1522 final TelecomManager tm = getTelecomManager(); 1523 1524 if (tm != null) { 1525 final PhoneAccount account = tm.getPhoneAccount(phoneAccountHandle); 1526 // For single-sim devices, there will be no selected highlight color, so the phone 1527 // account will default to NO_HIGHLIGHT_COLOR. 1528 if (account != null) { 1529 highlightColor = account.getHighlightColor(); 1530 } 1531 } 1532 } 1533 return new InCallUIMaterialColorMapUtils( 1534 mContext.getResources()).calculatePrimaryAndSecondaryColor(highlightColor); 1535 } 1536 1537 /** 1538 * @return An instance of TelecomManager. 1539 */ getTelecomManager()1540 public TelecomManager getTelecomManager() { 1541 if (mTelecomManager == null) { 1542 mTelecomManager = (TelecomManager) 1543 mContext.getSystemService(Context.TELECOM_SERVICE); 1544 } 1545 return mTelecomManager; 1546 } 1547 getActivity()1548 InCallActivity getActivity() { 1549 return mInCallActivity; 1550 } 1551 getAnswerPresenter()1552 AnswerPresenter getAnswerPresenter() { 1553 return mAnswerPresenter; 1554 } 1555 1556 /** 1557 * Private constructor. Must use getInstance() to get this singleton. 1558 */ InCallPresenter()1559 private InCallPresenter() { 1560 } 1561 1562 /** 1563 * All the main states of InCallActivity. 1564 */ 1565 public enum InCallState { 1566 // InCall Screen is off and there are no calls 1567 NO_CALLS, 1568 1569 // Incoming-call screen is up 1570 INCOMING, 1571 1572 // In-call experience is showing 1573 INCALL, 1574 1575 // Waiting for user input before placing outgoing call 1576 WAITING_FOR_ACCOUNT, 1577 1578 // UI is starting up but no call has been initiated yet. 1579 // The UI is waiting for Telecomm to respond. 1580 PENDING_OUTGOING, 1581 1582 // User is dialing out 1583 OUTGOING; 1584 isIncoming()1585 public boolean isIncoming() { 1586 return (this == INCOMING); 1587 } 1588 isConnectingOrConnected()1589 public boolean isConnectingOrConnected() { 1590 return (this == INCOMING || 1591 this == OUTGOING || 1592 this == INCALL); 1593 } 1594 } 1595 1596 /** 1597 * Interface implemented by classes that need to know about the InCall State. 1598 */ 1599 public interface InCallStateListener { 1600 // TODO: Enhance state to contain the call objects instead of passing CallList onStateChange(InCallState oldState, InCallState newState, CallList callList)1601 public void onStateChange(InCallState oldState, InCallState newState, CallList callList); 1602 } 1603 1604 public interface IncomingCallListener { onIncomingCall(InCallState oldState, InCallState newState, Call call)1605 public void onIncomingCall(InCallState oldState, InCallState newState, Call call); 1606 } 1607 1608 public interface CanAddCallListener { onCanAddCallChanged(boolean canAddCall)1609 public void onCanAddCallChanged(boolean canAddCall); 1610 } 1611 1612 public interface InCallDetailsListener { onDetailsChanged(Call call, android.telecom.Call.Details details)1613 public void onDetailsChanged(Call call, android.telecom.Call.Details details); 1614 } 1615 1616 public interface InCallOrientationListener { onDeviceOrientationChanged(int orientation)1617 public void onDeviceOrientationChanged(int orientation); 1618 } 1619 1620 /** 1621 * Interface implemented by classes that need to know about events which occur within the 1622 * In-Call UI. Used as a means of communicating between fragments that make up the UI. 1623 */ 1624 public interface InCallEventListener { onFullscreenModeChanged(boolean isFullscreenMode)1625 public void onFullscreenModeChanged(boolean isFullscreenMode); 1626 } 1627 1628 public interface InCallUiListener { onUiShowing(boolean showing)1629 void onUiShowing(boolean showing); 1630 } 1631 } 1632