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