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