1 /* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.incallui; 18 19 import android.content.Context; 20 import android.content.Intent; 21 import android.graphics.Point; 22 import android.os.Bundle; 23 import android.os.Handler; 24 import android.support.annotation.NonNull; 25 import android.support.annotation.Nullable; 26 import android.support.annotation.VisibleForTesting; 27 import android.support.v4.os.UserManagerCompat; 28 import android.telecom.Call.Details; 29 import android.telecom.DisconnectCause; 30 import android.telecom.PhoneAccount; 31 import android.telecom.PhoneAccountHandle; 32 import android.telecom.TelecomManager; 33 import android.telecom.VideoProfile; 34 import android.telephony.PhoneStateListener; 35 import android.telephony.TelephonyManager; 36 import android.view.Window; 37 import android.view.WindowManager; 38 import com.android.contacts.common.compat.CallCompat; 39 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; 40 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler.OnCheckBlockedListener; 41 import com.android.dialer.blocking.FilteredNumberCompat; 42 import com.android.dialer.blocking.FilteredNumbersUtil; 43 import com.android.dialer.common.LogUtil; 44 import com.android.dialer.enrichedcall.EnrichedCallComponent; 45 import com.android.dialer.location.GeoUtil; 46 import com.android.dialer.logging.InteractionEvent; 47 import com.android.dialer.logging.Logger; 48 import com.android.dialer.postcall.PostCall; 49 import com.android.dialer.telecom.TelecomUtil; 50 import com.android.dialer.util.TouchPointManager; 51 import com.android.incallui.InCallOrientationEventListener.ScreenOrientation; 52 import com.android.incallui.answerproximitysensor.PseudoScreenState; 53 import com.android.incallui.call.CallList; 54 import com.android.incallui.call.DialerCall; 55 import com.android.incallui.call.ExternalCallList; 56 import com.android.incallui.call.TelecomAdapter; 57 import com.android.incallui.latencyreport.LatencyReport; 58 import com.android.incallui.legacyblocking.BlockedNumberContentObserver; 59 import com.android.incallui.spam.SpamCallListListener; 60 import com.android.incallui.util.TelecomCallUtil; 61 import com.android.incallui.videosurface.bindings.VideoSurfaceBindings; 62 import com.android.incallui.videosurface.protocol.VideoSurfaceTexture; 63 import com.android.incallui.videotech.utils.VideoUtils; 64 import java.util.Collections; 65 import java.util.List; 66 import java.util.Objects; 67 import java.util.Set; 68 import java.util.concurrent.ConcurrentHashMap; 69 import java.util.concurrent.CopyOnWriteArrayList; 70 import java.util.concurrent.atomic.AtomicBoolean; 71 72 /** 73 * Takes updates from the CallList and notifies the InCallActivity (UI) of the changes. Responsible 74 * for starting the activity for a new call and finishing the activity when all calls are 75 * disconnected. Creates and manages the in-call state and provides a listener pattern for the 76 * presenters that want to listen in on the in-call state changes. TODO: This class has become more 77 * of a state machine at this point. Consider renaming. 78 */ 79 public class InCallPresenter implements CallList.Listener { 80 81 private static final String EXTRA_FIRST_TIME_SHOWN = 82 "com.android.incallui.intent.extra.FIRST_TIME_SHOWN"; 83 84 private static final long BLOCK_QUERY_TIMEOUT_MS = 1000; 85 86 private static final Bundle EMPTY_EXTRAS = new Bundle(); 87 88 private static InCallPresenter sInCallPresenter; 89 90 /** 91 * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is load factor before 92 * resizing, 1 means we only expect a single thread to access the map so make only a single shard 93 */ 94 private final Set<InCallStateListener> mListeners = 95 Collections.newSetFromMap(new ConcurrentHashMap<InCallStateListener, Boolean>(8, 0.9f, 1)); 96 97 private final List<IncomingCallListener> mIncomingCallListeners = new CopyOnWriteArrayList<>(); 98 private final Set<InCallDetailsListener> mDetailsListeners = 99 Collections.newSetFromMap(new ConcurrentHashMap<InCallDetailsListener, Boolean>(8, 0.9f, 1)); 100 private final Set<CanAddCallListener> mCanAddCallListeners = 101 Collections.newSetFromMap(new ConcurrentHashMap<CanAddCallListener, Boolean>(8, 0.9f, 1)); 102 private final Set<InCallUiListener> mInCallUiListeners = 103 Collections.newSetFromMap(new ConcurrentHashMap<InCallUiListener, Boolean>(8, 0.9f, 1)); 104 private final Set<InCallOrientationListener> mOrientationListeners = 105 Collections.newSetFromMap( 106 new ConcurrentHashMap<InCallOrientationListener, Boolean>(8, 0.9f, 1)); 107 private final Set<InCallEventListener> mInCallEventListeners = 108 Collections.newSetFromMap(new ConcurrentHashMap<InCallEventListener, Boolean>(8, 0.9f, 1)); 109 110 private StatusBarNotifier mStatusBarNotifier; 111 private ExternalCallNotifier mExternalCallNotifier; 112 private ContactInfoCache mContactInfoCache; 113 private Context mContext; 114 private final OnCheckBlockedListener mOnCheckBlockedListener = 115 new OnCheckBlockedListener() { 116 @Override 117 public void onCheckComplete(final Integer id) { 118 if (id != null && id != FilteredNumberAsyncQueryHandler.INVALID_ID) { 119 // Silence the ringer now to prevent ringing and vibration before the call is 120 // terminated when Telecom attempts to add it. 121 TelecomUtil.silenceRinger(mContext); 122 } 123 } 124 }; 125 private CallList mCallList; 126 private ExternalCallList mExternalCallList; 127 private InCallActivity mInCallActivity; 128 private ManageConferenceActivity mManageConferenceActivity; 129 private final android.telecom.Call.Callback mCallCallback = 130 new android.telecom.Call.Callback() { 131 @Override 132 public void onPostDialWait( 133 android.telecom.Call telecomCall, String remainingPostDialSequence) { 134 final DialerCall call = mCallList.getDialerCallFromTelecomCall(telecomCall); 135 if (call == null) { 136 Log.w(this, "DialerCall not found in call list: " + telecomCall); 137 return; 138 } 139 onPostDialCharWait(call.getId(), remainingPostDialSequence); 140 } 141 142 @Override 143 public void onDetailsChanged( 144 android.telecom.Call telecomCall, android.telecom.Call.Details details) { 145 final DialerCall call = mCallList.getDialerCallFromTelecomCall(telecomCall); 146 if (call == null) { 147 Log.w(this, "DialerCall not found in call list: " + telecomCall); 148 return; 149 } 150 151 if (details.hasProperty(Details.PROPERTY_IS_EXTERNAL_CALL) 152 && !mExternalCallList.isCallTracked(telecomCall)) { 153 154 // A regular call became an external call so swap call lists. 155 Log.i(this, "Call became external: " + telecomCall); 156 mCallList.onInternalCallMadeExternal(mContext, telecomCall); 157 mExternalCallList.onCallAdded(telecomCall); 158 return; 159 } 160 161 for (InCallDetailsListener listener : mDetailsListeners) { 162 listener.onDetailsChanged(call, details); 163 } 164 } 165 166 @Override 167 public void onConferenceableCallsChanged( 168 android.telecom.Call telecomCall, List<android.telecom.Call> conferenceableCalls) { 169 Log.i(this, "onConferenceableCallsChanged: " + telecomCall); 170 onDetailsChanged(telecomCall, telecomCall.getDetails()); 171 } 172 }; 173 private InCallState mInCallState = InCallState.NO_CALLS; 174 private ProximitySensor mProximitySensor; 175 private final PseudoScreenState mPseudoScreenState = new PseudoScreenState(); 176 private boolean mServiceConnected; 177 private InCallCameraManager mInCallCameraManager; 178 private FilteredNumberAsyncQueryHandler mFilteredQueryHandler; 179 private CallList.Listener mSpamCallListListener; 180 /** Whether or not we are currently bound and waiting for Telecom to send us a new call. */ 181 private boolean mBoundAndWaitingForOutgoingCall; 182 /** Determines if the InCall UI is in fullscreen mode or not. */ 183 private boolean mIsFullScreen = false; 184 185 private PhoneStateListener mPhoneStateListener = 186 new PhoneStateListener() { 187 @Override 188 public void onCallStateChanged(int state, String incomingNumber) { 189 if (state == TelephonyManager.CALL_STATE_RINGING) { 190 if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) { 191 return; 192 } 193 // Check if the number is blocked, to silence the ringer. 194 String countryIso = GeoUtil.getCurrentCountryIso(mContext); 195 mFilteredQueryHandler.isBlockedNumber( 196 mOnCheckBlockedListener, incomingNumber, countryIso); 197 } 198 } 199 }; 200 /** 201 * Is true when the activity has been previously started. Some code needs to know not just if the 202 * activity is currently up, but if it had been previously shown in foreground for this in-call 203 * session (e.g., StatusBarNotifier). This gets reset when the session ends in the tear-down 204 * method. 205 */ 206 private boolean mIsActivityPreviouslyStarted = false; 207 208 /** Whether or not InCallService is bound to Telecom. */ 209 private boolean mServiceBound = false; 210 211 /** 212 * When configuration changes Android kills the current activity and starts a new one. The flag is 213 * used to check if full clean up is necessary (activity is stopped and new activity won't be 214 * started), or if a new activity will be started right after the current one is destroyed, and 215 * therefore no need in release all resources. 216 */ 217 private boolean mIsChangingConfigurations = false; 218 219 private boolean mAwaitingCallListUpdate = false; 220 221 private ExternalCallList.ExternalCallListener mExternalCallListener = 222 new ExternalCallList.ExternalCallListener() { 223 224 @Override 225 public void onExternalCallPulled(android.telecom.Call call) { 226 // Note: keep this code in sync with InCallPresenter#onCallAdded 227 LatencyReport latencyReport = new LatencyReport(call); 228 latencyReport.onCallBlockingDone(); 229 // Note: External calls do not require spam checking. 230 mCallList.onCallAdded(mContext, call, latencyReport); 231 call.registerCallback(mCallCallback); 232 } 233 234 @Override 235 public void onExternalCallAdded(android.telecom.Call call) { 236 // No-op 237 } 238 239 @Override 240 public void onExternalCallRemoved(android.telecom.Call call) { 241 // No-op 242 } 243 244 @Override 245 public void onExternalCallUpdated(android.telecom.Call call) { 246 // No-op 247 } 248 }; 249 250 private ThemeColorManager mThemeColorManager; 251 private VideoSurfaceTexture mLocalVideoSurfaceTexture; 252 private VideoSurfaceTexture mRemoteVideoSurfaceTexture; 253 254 /** Inaccessible constructor. Must use getRunningInstance() to get this singleton. */ 255 @VisibleForTesting InCallPresenter()256 InCallPresenter() {} 257 getInstance()258 public static synchronized InCallPresenter getInstance() { 259 if (sInCallPresenter == null) { 260 sInCallPresenter = new InCallPresenter(); 261 } 262 return sInCallPresenter; 263 } 264 265 @VisibleForTesting setInstanceForTesting(InCallPresenter inCallPresenter)266 public static synchronized void setInstanceForTesting(InCallPresenter inCallPresenter) { 267 sInCallPresenter = inCallPresenter; 268 } 269 270 /** 271 * Determines whether or not a call has no valid phone accounts that can be used to make the call 272 * with. Emergency calls do not require a phone account. 273 * 274 * @param call to check accounts for. 275 * @return {@code true} if the call has no call capable phone accounts set, {@code false} if the 276 * call contains a phone account that could be used to initiate it with, or is an emergency 277 * call. 278 */ isCallWithNoValidAccounts(DialerCall call)279 public static boolean isCallWithNoValidAccounts(DialerCall call) { 280 if (call != null && !call.isEmergencyCall()) { 281 Bundle extras = call.getIntentExtras(); 282 283 if (extras == null) { 284 extras = EMPTY_EXTRAS; 285 } 286 287 final List<PhoneAccountHandle> phoneAccountHandles = 288 extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); 289 290 if ((call.getAccountHandle() == null 291 && (phoneAccountHandles == null || phoneAccountHandles.isEmpty()))) { 292 Log.i(InCallPresenter.getInstance(), "No valid accounts for call " + call); 293 return true; 294 } 295 } 296 return false; 297 } 298 getInCallState()299 public InCallState getInCallState() { 300 return mInCallState; 301 } 302 getCallList()303 public CallList getCallList() { 304 return mCallList; 305 } 306 setUp( @onNull Context context, CallList callList, ExternalCallList externalCallList, StatusBarNotifier statusBarNotifier, ExternalCallNotifier externalCallNotifier, ContactInfoCache contactInfoCache, ProximitySensor proximitySensor, FilteredNumberAsyncQueryHandler filteredNumberQueryHandler)307 public void setUp( 308 @NonNull Context context, 309 CallList callList, 310 ExternalCallList externalCallList, 311 StatusBarNotifier statusBarNotifier, 312 ExternalCallNotifier externalCallNotifier, 313 ContactInfoCache contactInfoCache, 314 ProximitySensor proximitySensor, 315 FilteredNumberAsyncQueryHandler filteredNumberQueryHandler) { 316 if (mServiceConnected) { 317 Log.i(this, "New service connection replacing existing one."); 318 if (context != mContext || callList != mCallList) { 319 throw new IllegalStateException(); 320 } 321 return; 322 } 323 324 Objects.requireNonNull(context); 325 mContext = context; 326 327 mContactInfoCache = contactInfoCache; 328 329 mStatusBarNotifier = statusBarNotifier; 330 mExternalCallNotifier = externalCallNotifier; 331 addListener(mStatusBarNotifier); 332 EnrichedCallComponent.get(mContext) 333 .getEnrichedCallManager() 334 .registerStateChangedListener(mStatusBarNotifier); 335 336 mProximitySensor = proximitySensor; 337 addListener(mProximitySensor); 338 339 mThemeColorManager = 340 new ThemeColorManager(new InCallUIMaterialColorMapUtils(mContext.getResources())); 341 342 mCallList = callList; 343 mExternalCallList = externalCallList; 344 externalCallList.addExternalCallListener(mExternalCallNotifier); 345 externalCallList.addExternalCallListener(mExternalCallListener); 346 347 // This only gets called by the service so this is okay. 348 mServiceConnected = true; 349 350 // The final thing we do in this set up is add ourselves as a listener to CallList. This 351 // will kick off an update and the whole process can start. 352 mCallList.addListener(this); 353 354 // Create spam call list listener and add it to the list of listeners 355 mSpamCallListListener = new SpamCallListListener(context); 356 mCallList.addListener(mSpamCallListListener); 357 358 VideoPauseController.getInstance().setUp(this); 359 360 mFilteredQueryHandler = filteredNumberQueryHandler; 361 mContext 362 .getSystemService(TelephonyManager.class) 363 .listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); 364 365 Log.d(this, "Finished InCallPresenter.setUp"); 366 } 367 368 /** 369 * Called when the telephony service has disconnected from us. This will happen when there are no 370 * more active calls. However, we may still want to continue showing the UI for certain cases like 371 * showing "Call Ended". What we really want is to wait for the activity and the service to both 372 * disconnect before we tear things down. This method sets a serviceConnected boolean and calls a 373 * secondary method that performs the aforementioned logic. 374 */ tearDown()375 public void tearDown() { 376 Log.d(this, "tearDown"); 377 mCallList.clearOnDisconnect(); 378 379 mServiceConnected = false; 380 381 mContext 382 .getSystemService(TelephonyManager.class) 383 .listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE); 384 385 attemptCleanup(); 386 VideoPauseController.getInstance().tearDown(); 387 } 388 attemptFinishActivity()389 private void attemptFinishActivity() { 390 final boolean doFinish = (mInCallActivity != null && isActivityStarted()); 391 Log.i(this, "Hide in call UI: " + doFinish); 392 if (doFinish) { 393 mInCallActivity.setExcludeFromRecents(true); 394 mInCallActivity.finish(); 395 } 396 } 397 398 /** 399 * Called when the UI ends. Attempts to tear down everything if necessary. See {@link #tearDown()} 400 * for more insight on the tear-down process. 401 */ unsetActivity(InCallActivity inCallActivity)402 public void unsetActivity(InCallActivity inCallActivity) { 403 if (inCallActivity == null) { 404 throw new IllegalArgumentException("unregisterActivity cannot be called with null"); 405 } 406 if (mInCallActivity == null) { 407 Log.i(this, "No InCallActivity currently set, no need to unset."); 408 return; 409 } 410 if (mInCallActivity != inCallActivity) { 411 Log.w( 412 this, 413 "Second instance of InCallActivity is trying to unregister when another" 414 + " instance is active. Ignoring."); 415 return; 416 } 417 updateActivity(null); 418 } 419 420 /** 421 * Updates the current instance of {@link InCallActivity} with the provided one. If a {@code null} 422 * activity is provided, it means that the activity was finished and we should attempt to cleanup. 423 */ updateActivity(InCallActivity inCallActivity)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 setManageConferenceActivity( @ullable ManageConferenceActivity manageConferenceActivity)493 public void setManageConferenceActivity( 494 @Nullable ManageConferenceActivity manageConferenceActivity) { 495 mManageConferenceActivity = manageConferenceActivity; 496 } 497 onBringToForeground(boolean showDialpad)498 public void onBringToForeground(boolean showDialpad) { 499 Log.i(this, "Bringing UI to foreground."); 500 bringToForeground(showDialpad); 501 } 502 onCallAdded(final android.telecom.Call call)503 public void onCallAdded(final android.telecom.Call call) { 504 LatencyReport latencyReport = new LatencyReport(call); 505 if (shouldAttemptBlocking(call)) { 506 maybeBlockCall(call, latencyReport); 507 } else { 508 if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { 509 mExternalCallList.onCallAdded(call); 510 } else { 511 latencyReport.onCallBlockingDone(); 512 mCallList.onCallAdded(mContext, call, latencyReport); 513 } 514 } 515 516 // Since a call has been added we are no longer waiting for Telecom to send us a call. 517 setBoundAndWaitingForOutgoingCall(false, null); 518 call.registerCallback(mCallCallback); 519 } 520 shouldAttemptBlocking(android.telecom.Call call)521 private boolean shouldAttemptBlocking(android.telecom.Call call) { 522 if (call.getState() != android.telecom.Call.STATE_RINGING) { 523 return false; 524 } 525 if (!UserManagerCompat.isUserUnlocked(mContext)) { 526 LogUtil.i( 527 "InCallPresenter.shouldAttemptBlocking", 528 "not attempting to block incoming call because user is locked"); 529 return false; 530 } 531 if (TelecomCallUtil.isEmergencyCall(call)) { 532 Log.i(this, "Not attempting to block incoming emergency call"); 533 return false; 534 } 535 if (FilteredNumbersUtil.hasRecentEmergencyCall(mContext)) { 536 Log.i(this, "Not attempting to block incoming call due to recent emergency call"); 537 return false; 538 } 539 if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { 540 return false; 541 } 542 if (FilteredNumberCompat.useNewFiltering(mContext)) { 543 LogUtil.i( 544 "InCallPresenter.shouldAttemptBlocking", 545 "not attempting to block incoming call because framework blocking is in use"); 546 return false; 547 } 548 return true; 549 } 550 551 /** 552 * Checks whether a call should be blocked, and blocks it if so. Otherwise, it adds the call to 553 * the CallList so it can proceed as normal. There is a timeout, so if the function for checking 554 * whether a function is blocked does not return in a reasonable time, we proceed with adding the 555 * call anyways. 556 */ maybeBlockCall(final android.telecom.Call call, final LatencyReport latencyReport)557 private void maybeBlockCall(final android.telecom.Call call, final LatencyReport latencyReport) { 558 final String countryIso = GeoUtil.getCurrentCountryIso(mContext); 559 final String number = TelecomCallUtil.getNumber(call); 560 final long timeAdded = System.currentTimeMillis(); 561 562 // Though AtomicBoolean's can be scary, don't fear, as in this case it is only used on the 563 // main UI thread. It is needed so we can change its value within different scopes, since 564 // that cannot be done with a final boolean. 565 final AtomicBoolean hasTimedOut = new AtomicBoolean(false); 566 567 final Handler handler = new Handler(); 568 569 // Proceed if the query is slow; the call may still be blocked after the query returns. 570 final Runnable runnable = 571 new Runnable() { 572 @Override 573 public void run() { 574 hasTimedOut.set(true); 575 latencyReport.onCallBlockingDone(); 576 mCallList.onCallAdded(mContext, call, latencyReport); 577 } 578 }; 579 handler.postDelayed(runnable, BLOCK_QUERY_TIMEOUT_MS); 580 581 OnCheckBlockedListener onCheckBlockedListener = 582 new OnCheckBlockedListener() { 583 @Override 584 public void onCheckComplete(final Integer id) { 585 if (isReadyForTearDown()) { 586 Log.i(this, "InCallPresenter is torn down, not adding call"); 587 return; 588 } 589 if (!hasTimedOut.get()) { 590 handler.removeCallbacks(runnable); 591 } 592 if (id == null) { 593 if (!hasTimedOut.get()) { 594 latencyReport.onCallBlockingDone(); 595 mCallList.onCallAdded(mContext, call, latencyReport); 596 } 597 } else if (id == FilteredNumberAsyncQueryHandler.INVALID_ID) { 598 Log.d(this, "checkForBlockedCall: invalid number, skipping block checking"); 599 if (!hasTimedOut.get()) { 600 handler.removeCallbacks(runnable); 601 602 latencyReport.onCallBlockingDone(); 603 mCallList.onCallAdded(mContext, call, latencyReport); 604 } 605 } else { 606 Log.i(this, "Rejecting incoming call from blocked number"); 607 call.reject(false, null); 608 Logger.get(mContext).logInteraction(InteractionEvent.Type.CALL_BLOCKED); 609 610 /* 611 * If mContext is null, then the InCallPresenter was torn down before the 612 * block check had a chance to complete. The context is no longer valid, so 613 * don't attempt to remove the call log entry. 614 */ 615 if (mContext == null) { 616 return; 617 } 618 // Register observer to update the call log. 619 // BlockedNumberContentObserver will unregister after successful log or timeout. 620 BlockedNumberContentObserver contentObserver = 621 new BlockedNumberContentObserver(mContext, new Handler(), number, timeAdded); 622 contentObserver.register(); 623 } 624 } 625 }; 626 627 mFilteredQueryHandler.isBlockedNumber(onCheckBlockedListener, number, countryIso); 628 } 629 onCallRemoved(android.telecom.Call call)630 public void onCallRemoved(android.telecom.Call call) { 631 if (call.getDetails().hasProperty(CallCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { 632 mExternalCallList.onCallRemoved(call); 633 } else { 634 mCallList.onCallRemoved(mContext, call); 635 call.unregisterCallback(mCallCallback); 636 } 637 } 638 onCanAddCallChanged(boolean canAddCall)639 public void onCanAddCallChanged(boolean canAddCall) { 640 for (CanAddCallListener listener : mCanAddCallListeners) { 641 listener.onCanAddCallChanged(canAddCall); 642 } 643 } 644 645 @Override onWiFiToLteHandover(DialerCall call)646 public void onWiFiToLteHandover(DialerCall call) { 647 if (mInCallActivity != null) { 648 mInCallActivity.onWiFiToLteHandover(call); 649 } 650 } 651 652 @Override onHandoverToWifiFailed(DialerCall call)653 public void onHandoverToWifiFailed(DialerCall call) { 654 if (mInCallActivity != null) { 655 mInCallActivity.onHandoverToWifiFailed(call); 656 } 657 } 658 659 @Override onInternationalCallOnWifi(@onNull DialerCall call)660 public void onInternationalCallOnWifi(@NonNull DialerCall call) { 661 LogUtil.enterBlock("InCallPresenter.onInternationalCallOnWifi"); 662 if (mInCallActivity != null) { 663 mInCallActivity.onInternationalCallOnWifi(call); 664 } 665 } 666 667 /** 668 * Called when there is a change to the call list. Sets the In-Call state for the entire in-call 669 * app based on the information it gets from CallList. Dispatches the in-call state to all 670 * listeners. Can trigger the creation or destruction of the UI based on the states that is 671 * calculates. 672 */ 673 @Override onCallListChange(CallList callList)674 public void onCallListChange(CallList callList) { 675 if (mInCallActivity != null && mInCallActivity.isInCallScreenAnimating()) { 676 mAwaitingCallListUpdate = true; 677 return; 678 } 679 if (callList == null) { 680 return; 681 } 682 683 mAwaitingCallListUpdate = false; 684 685 InCallState newState = getPotentialStateFromCallList(callList); 686 InCallState oldState = mInCallState; 687 Log.d(this, "onCallListChange oldState= " + oldState + " newState=" + newState); 688 689 // If the user placed a call and was asked to choose the account, but then pressed "Home", the 690 // incall activity for that call will still exist (even if it's not visible). In the case of 691 // an incoming call in that situation, just disconnect that "waiting for account" call and 692 // dismiss the dialog. The same activity will be reused to handle the new incoming call. See 693 // b/33247755 for more details. 694 DialerCall waitingForAccountCall; 695 if (newState == InCallState.INCOMING 696 && (waitingForAccountCall = callList.getWaitingForAccountCall()) != null) { 697 waitingForAccountCall.disconnect(); 698 mInCallActivity.dismissPendingDialogs(); 699 } 700 701 newState = startOrFinishUi(newState); 702 Log.d(this, "onCallListChange newState changed to " + newState); 703 704 // Set the new state before announcing it to the world 705 Log.i(this, "Phone switching state: " + oldState + " -> " + newState); 706 mInCallState = newState; 707 708 // notify listeners of new state 709 for (InCallStateListener listener : mListeners) { 710 Log.d(this, "Notify " + listener + " of state " + mInCallState.toString()); 711 listener.onStateChange(oldState, mInCallState, callList); 712 } 713 714 if (isActivityStarted()) { 715 final boolean hasCall = 716 callList.getActiveOrBackgroundCall() != null || callList.getOutgoingCall() != null; 717 mInCallActivity.dismissKeyguard(hasCall); 718 } 719 } 720 721 /** Called when there is a new incoming call. */ 722 @Override onIncomingCall(DialerCall call)723 public void onIncomingCall(DialerCall call) { 724 InCallState newState = startOrFinishUi(InCallState.INCOMING); 725 InCallState oldState = mInCallState; 726 727 Log.i(this, "Phone switching state: " + oldState + " -> " + newState); 728 mInCallState = newState; 729 730 for (IncomingCallListener listener : mIncomingCallListeners) { 731 listener.onIncomingCall(oldState, mInCallState, call); 732 } 733 734 if (mInCallActivity != null) { 735 // Re-evaluate which fragment is being shown. 736 mInCallActivity.onPrimaryCallStateChanged(); 737 } 738 } 739 740 @Override onUpgradeToVideo(DialerCall call)741 public void onUpgradeToVideo(DialerCall call) { 742 if (VideoUtils.hasReceivedVideoUpgradeRequest(call.getVideoTech().getSessionModificationState()) 743 && mInCallState == InCallPresenter.InCallState.INCOMING) { 744 LogUtil.i( 745 "InCallPresenter.onUpgradeToVideo", 746 "rejecting upgrade request due to existing incoming call"); 747 call.getVideoTech().declineVideoRequest(); 748 } 749 750 if (mInCallActivity != null) { 751 // Re-evaluate which fragment is being shown. 752 mInCallActivity.onPrimaryCallStateChanged(); 753 } 754 } 755 756 @Override onSessionModificationStateChange(DialerCall call)757 public void onSessionModificationStateChange(DialerCall call) { 758 int newState = call.getVideoTech().getSessionModificationState(); 759 LogUtil.i("InCallPresenter.onSessionModificationStateChange", "state: %d", newState); 760 if (mProximitySensor == null) { 761 LogUtil.i("InCallPresenter.onSessionModificationStateChange", "proximitySensor is null"); 762 return; 763 } 764 mProximitySensor.setIsAttemptingVideoCall( 765 call.hasSentVideoUpgradeRequest() || call.hasReceivedVideoUpgradeRequest()); 766 if (mInCallActivity != null) { 767 // Re-evaluate which fragment is being shown. 768 mInCallActivity.onPrimaryCallStateChanged(); 769 } 770 } 771 772 /** 773 * Called when a call becomes disconnected. Called everytime an existing call changes from being 774 * connected (incoming/outgoing/active) to disconnected. 775 */ 776 @Override onDisconnect(DialerCall call)777 public void onDisconnect(DialerCall call) { 778 maybeShowErrorDialogOnDisconnect(call); 779 780 // We need to do the run the same code as onCallListChange. 781 onCallListChange(mCallList); 782 783 if (isActivityStarted()) { 784 mInCallActivity.dismissKeyguard(false); 785 } 786 787 if (call.isEmergencyCall()) { 788 FilteredNumbersUtil.recordLastEmergencyCallTime(mContext); 789 } 790 791 if (!mCallList.hasLiveCall() 792 && !call.getLogState().isIncoming 793 && !isSecretCode(call.getNumber()) 794 && !CallerInfoUtils.isVoiceMailNumber(mContext, call)) { 795 PostCall.onCallDisconnected(mContext, call.getNumber(), call.getConnectTimeMillis()); 796 } 797 } 798 isSecretCode(@ullable String number)799 private boolean isSecretCode(@Nullable String number) { 800 return number != null 801 && (number.length() <= 8 || number.startsWith("*#*#") || number.endsWith("#*#*")); 802 } 803 804 /** Given the call list, return the state in which the in-call screen should be. */ getPotentialStateFromCallList(CallList callList)805 public InCallState getPotentialStateFromCallList(CallList callList) { 806 807 InCallState newState = InCallState.NO_CALLS; 808 809 if (callList == null) { 810 return newState; 811 } 812 if (callList.getIncomingCall() != null) { 813 newState = InCallState.INCOMING; 814 } else if (callList.getWaitingForAccountCall() != null) { 815 newState = InCallState.WAITING_FOR_ACCOUNT; 816 } else if (callList.getPendingOutgoingCall() != null) { 817 newState = InCallState.PENDING_OUTGOING; 818 } else if (callList.getOutgoingCall() != null) { 819 newState = InCallState.OUTGOING; 820 } else if (callList.getActiveCall() != null 821 || callList.getBackgroundCall() != null 822 || callList.getDisconnectedCall() != null 823 || callList.getDisconnectingCall() != null) { 824 newState = InCallState.INCALL; 825 } 826 827 if (newState == InCallState.NO_CALLS) { 828 if (mBoundAndWaitingForOutgoingCall) { 829 return InCallState.OUTGOING; 830 } 831 } 832 833 return newState; 834 } 835 isBoundAndWaitingForOutgoingCall()836 public boolean isBoundAndWaitingForOutgoingCall() { 837 return mBoundAndWaitingForOutgoingCall; 838 } 839 setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle)840 public void setBoundAndWaitingForOutgoingCall(boolean isBound, PhoneAccountHandle handle) { 841 Log.i(this, "setBoundAndWaitingForOutgoingCall: " + isBound); 842 mBoundAndWaitingForOutgoingCall = isBound; 843 mThemeColorManager.setPendingPhoneAccountHandle(handle); 844 if (isBound && mInCallState == InCallState.NO_CALLS) { 845 mInCallState = InCallState.OUTGOING; 846 } 847 } 848 onShrinkAnimationComplete()849 public void onShrinkAnimationComplete() { 850 if (mAwaitingCallListUpdate) { 851 onCallListChange(mCallList); 852 } 853 } 854 addIncomingCallListener(IncomingCallListener listener)855 public void addIncomingCallListener(IncomingCallListener listener) { 856 Objects.requireNonNull(listener); 857 mIncomingCallListeners.add(listener); 858 } 859 removeIncomingCallListener(IncomingCallListener listener)860 public void removeIncomingCallListener(IncomingCallListener listener) { 861 if (listener != null) { 862 mIncomingCallListeners.remove(listener); 863 } 864 } 865 addListener(InCallStateListener listener)866 public void addListener(InCallStateListener listener) { 867 Objects.requireNonNull(listener); 868 mListeners.add(listener); 869 } 870 removeListener(InCallStateListener listener)871 public void removeListener(InCallStateListener listener) { 872 if (listener != null) { 873 mListeners.remove(listener); 874 } 875 } 876 addDetailsListener(InCallDetailsListener listener)877 public void addDetailsListener(InCallDetailsListener listener) { 878 Objects.requireNonNull(listener); 879 mDetailsListeners.add(listener); 880 } 881 removeDetailsListener(InCallDetailsListener listener)882 public void removeDetailsListener(InCallDetailsListener listener) { 883 if (listener != null) { 884 mDetailsListeners.remove(listener); 885 } 886 } 887 addCanAddCallListener(CanAddCallListener listener)888 public void addCanAddCallListener(CanAddCallListener listener) { 889 Objects.requireNonNull(listener); 890 mCanAddCallListeners.add(listener); 891 } 892 removeCanAddCallListener(CanAddCallListener listener)893 public void removeCanAddCallListener(CanAddCallListener listener) { 894 if (listener != null) { 895 mCanAddCallListeners.remove(listener); 896 } 897 } 898 addOrientationListener(InCallOrientationListener listener)899 public void addOrientationListener(InCallOrientationListener listener) { 900 Objects.requireNonNull(listener); 901 mOrientationListeners.add(listener); 902 } 903 removeOrientationListener(InCallOrientationListener listener)904 public void removeOrientationListener(InCallOrientationListener listener) { 905 if (listener != null) { 906 mOrientationListeners.remove(listener); 907 } 908 } 909 addInCallEventListener(InCallEventListener listener)910 public void addInCallEventListener(InCallEventListener listener) { 911 Objects.requireNonNull(listener); 912 mInCallEventListeners.add(listener); 913 } 914 removeInCallEventListener(InCallEventListener listener)915 public void removeInCallEventListener(InCallEventListener listener) { 916 if (listener != null) { 917 mInCallEventListeners.remove(listener); 918 } 919 } 920 getProximitySensor()921 public ProximitySensor getProximitySensor() { 922 return mProximitySensor; 923 } 924 getPseudoScreenState()925 public PseudoScreenState getPseudoScreenState() { 926 return mPseudoScreenState; 927 } 928 929 /** Returns true if the incall app is the foreground application. */ isShowingInCallUi()930 public boolean isShowingInCallUi() { 931 if (!isActivityStarted()) { 932 return false; 933 } 934 if (mManageConferenceActivity != null && mManageConferenceActivity.isVisible()) { 935 return true; 936 } 937 return mInCallActivity.isVisible(); 938 } 939 940 /** 941 * Returns true if the activity has been created and is running. Returns true as long as activity 942 * is not destroyed or finishing. This ensures that we return true even if the activity is paused 943 * (not in foreground). 944 */ isActivityStarted()945 public boolean isActivityStarted() { 946 return (mInCallActivity != null 947 && !mInCallActivity.isDestroyed() 948 && !mInCallActivity.isFinishing()); 949 } 950 951 /** 952 * Determines if the In-Call app is currently changing configuration. 953 * 954 * @return {@code true} if the In-Call app is changing configuration. 955 */ isChangingConfigurations()956 public boolean isChangingConfigurations() { 957 return mIsChangingConfigurations; 958 } 959 960 /** 961 * Tracks whether the In-Call app is currently in the process of changing configuration (i.e. 962 * screen orientation). 963 */ 964 /*package*/ updateIsChangingConfigurations()965 void updateIsChangingConfigurations() { 966 mIsChangingConfigurations = false; 967 if (mInCallActivity != null) { 968 mIsChangingConfigurations = mInCallActivity.isChangingConfigurations(); 969 } 970 Log.v(this, "updateIsChangingConfigurations = " + mIsChangingConfigurations); 971 } 972 973 /** Called when the activity goes in/out of the foreground. */ onUiShowing(boolean showing)974 public void onUiShowing(boolean showing) { 975 // We need to update the notification bar when we leave the UI because that 976 // could trigger it to show again. 977 if (mStatusBarNotifier != null) { 978 mStatusBarNotifier.updateNotification(mCallList); 979 } 980 981 if (mProximitySensor != null) { 982 mProximitySensor.onInCallShowing(showing); 983 } 984 985 Intent broadcastIntent = Bindings.get(mContext).getUiReadyBroadcastIntent(mContext); 986 if (broadcastIntent != null) { 987 broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted); 988 989 if (showing) { 990 Log.d(this, "Sending sticky broadcast: ", broadcastIntent); 991 mContext.sendStickyBroadcast(broadcastIntent); 992 } else { 993 Log.d(this, "Removing sticky broadcast: ", broadcastIntent); 994 mContext.removeStickyBroadcast(broadcastIntent); 995 } 996 } 997 998 if (showing) { 999 mIsActivityPreviouslyStarted = true; 1000 } else { 1001 updateIsChangingConfigurations(); 1002 } 1003 1004 for (InCallUiListener listener : mInCallUiListeners) { 1005 listener.onUiShowing(showing); 1006 } 1007 1008 if (mInCallActivity != null) { 1009 // Re-evaluate which fragment is being shown. 1010 mInCallActivity.onPrimaryCallStateChanged(); 1011 } 1012 } 1013 addInCallUiListener(InCallUiListener listener)1014 public void addInCallUiListener(InCallUiListener listener) { 1015 mInCallUiListeners.add(listener); 1016 } 1017 removeInCallUiListener(InCallUiListener listener)1018 public boolean removeInCallUiListener(InCallUiListener listener) { 1019 return mInCallUiListeners.remove(listener); 1020 } 1021 1022 /*package*/ onActivityStarted()1023 void onActivityStarted() { 1024 Log.d(this, "onActivityStarted"); 1025 notifyVideoPauseController(true); 1026 if (mStatusBarNotifier != null) { 1027 // TODO - b/36649622: Investigate this redundant call 1028 mStatusBarNotifier.updateNotification(mCallList); 1029 } 1030 } 1031 1032 /*package*/ onActivityStopped()1033 void onActivityStopped() { 1034 Log.d(this, "onActivityStopped"); 1035 notifyVideoPauseController(false); 1036 } 1037 notifyVideoPauseController(boolean showing)1038 private void notifyVideoPauseController(boolean showing) { 1039 Log.d( 1040 this, "notifyVideoPauseController: mIsChangingConfigurations=" + mIsChangingConfigurations); 1041 if (!mIsChangingConfigurations) { 1042 VideoPauseController.getInstance().onUiShowing(showing); 1043 } 1044 } 1045 1046 /** Brings the app into the foreground if possible. */ bringToForeground(boolean showDialpad)1047 public void bringToForeground(boolean showDialpad) { 1048 // Before we bring the incall UI to the foreground, we check to see if: 1049 // 1. It is not currently in the foreground 1050 // 2. We are in a state where we want to show the incall ui (i.e. there are calls to 1051 // be displayed) 1052 // If the activity hadn't actually been started previously, yet there are still calls 1053 // present (e.g. a call was accepted by a bluetooth or wired headset), we want to 1054 // bring it up the UI regardless. 1055 if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) { 1056 showInCall(showDialpad, false /* newOutgoingCall */); 1057 } 1058 } 1059 onPostDialCharWait(String callId, String chars)1060 public void onPostDialCharWait(String callId, String chars) { 1061 if (isActivityStarted()) { 1062 mInCallActivity.showPostCharWaitDialog(callId, chars); 1063 } 1064 } 1065 1066 /** 1067 * Handles the green CALL key while in-call. 1068 * 1069 * @return true if we consumed the event. 1070 */ handleCallKey()1071 public boolean handleCallKey() { 1072 LogUtil.v("InCallPresenter.handleCallKey", null); 1073 1074 // The green CALL button means either "Answer", "Unhold", or 1075 // "Swap calls", or can be a no-op, depending on the current state 1076 // of the Phone. 1077 1078 /** INCOMING CALL */ 1079 final CallList calls = mCallList; 1080 final DialerCall incomingCall = calls.getIncomingCall(); 1081 LogUtil.v("InCallPresenter.handleCallKey", "incomingCall: " + incomingCall); 1082 1083 // (1) Attempt to answer a call 1084 if (incomingCall != null) { 1085 incomingCall.answer(VideoProfile.STATE_AUDIO_ONLY); 1086 return true; 1087 } 1088 1089 /** STATE_ACTIVE CALL */ 1090 final DialerCall activeCall = calls.getActiveCall(); 1091 if (activeCall != null) { 1092 // TODO: This logic is repeated from CallButtonPresenter.java. We should 1093 // consolidate this logic. 1094 final boolean canMerge = 1095 activeCall.can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE); 1096 final boolean canSwap = 1097 activeCall.can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE); 1098 1099 Log.v( 1100 this, "activeCall: " + activeCall + ", canMerge: " + canMerge + ", canSwap: " + canSwap); 1101 1102 // (2) Attempt actions on conference calls 1103 if (canMerge) { 1104 TelecomAdapter.getInstance().merge(activeCall.getId()); 1105 return true; 1106 } else if (canSwap) { 1107 TelecomAdapter.getInstance().swap(activeCall.getId()); 1108 return true; 1109 } 1110 } 1111 1112 /** BACKGROUND CALL */ 1113 final DialerCall heldCall = calls.getBackgroundCall(); 1114 if (heldCall != null) { 1115 // We have a hold call so presumeable it will always support HOLD...but 1116 // there is no harm in double checking. 1117 final boolean canHold = heldCall.can(android.telecom.Call.Details.CAPABILITY_HOLD); 1118 1119 Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold); 1120 1121 // (4) unhold call 1122 if (heldCall.getState() == DialerCall.State.ONHOLD && canHold) { 1123 heldCall.unhold(); 1124 return true; 1125 } 1126 } 1127 1128 // Always consume hard keys 1129 return true; 1130 } 1131 1132 /** 1133 * A dialog could have prevented in-call screen from being previously finished. This function 1134 * checks to see if there should be any UI left and if not attempts to tear down the UI. 1135 */ onDismissDialog()1136 public void onDismissDialog() { 1137 Log.i(this, "Dialog dismissed"); 1138 if (mInCallState == InCallState.NO_CALLS) { 1139 attemptFinishActivity(); 1140 attemptCleanup(); 1141 } 1142 } 1143 1144 /** Clears the previous fullscreen state. */ clearFullscreen()1145 public void clearFullscreen() { 1146 mIsFullScreen = false; 1147 } 1148 1149 /** 1150 * Changes the fullscreen mode of the in-call UI. 1151 * 1152 * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false} 1153 * otherwise. 1154 */ setFullScreen(boolean isFullScreen)1155 public void setFullScreen(boolean isFullScreen) { 1156 setFullScreen(isFullScreen, false /* force */); 1157 } 1158 1159 /** 1160 * Changes the fullscreen mode of the in-call UI. 1161 * 1162 * @param isFullScreen {@code true} if in-call should be in fullscreen mode, {@code false} 1163 * otherwise. 1164 * @param force {@code true} if fullscreen mode should be set regardless of its current state. 1165 */ setFullScreen(boolean isFullScreen, boolean force)1166 public void setFullScreen(boolean isFullScreen, boolean force) { 1167 Log.i(this, "setFullScreen = " + isFullScreen); 1168 1169 // As a safeguard, ensure we cannot enter fullscreen if the dialpad is shown. 1170 if (isDialpadVisible()) { 1171 isFullScreen = false; 1172 Log.v(this, "setFullScreen overridden as dialpad is shown = " + isFullScreen); 1173 } 1174 1175 if (mIsFullScreen == isFullScreen && !force) { 1176 Log.v(this, "setFullScreen ignored as already in that state."); 1177 return; 1178 } 1179 mIsFullScreen = isFullScreen; 1180 notifyFullscreenModeChange(mIsFullScreen); 1181 } 1182 1183 /** 1184 * @return {@code true} if the in-call ui is currently in fullscreen mode, {@code false} 1185 * otherwise. 1186 */ isFullscreen()1187 public boolean isFullscreen() { 1188 return mIsFullScreen; 1189 } 1190 1191 /** 1192 * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status. 1193 * 1194 * @param isFullscreenMode {@code True} if entering full screen mode. 1195 */ notifyFullscreenModeChange(boolean isFullscreenMode)1196 public void notifyFullscreenModeChange(boolean isFullscreenMode) { 1197 for (InCallEventListener listener : mInCallEventListeners) { 1198 listener.onFullscreenModeChanged(isFullscreenMode); 1199 } 1200 } 1201 1202 /** 1203 * For some disconnected causes, we show a dialog. This calls into the activity to show the dialog 1204 * if appropriate for the call. 1205 */ maybeShowErrorDialogOnDisconnect(DialerCall call)1206 private void maybeShowErrorDialogOnDisconnect(DialerCall call) { 1207 // For newly disconnected calls, we may want to show a dialog on specific error conditions 1208 if (isActivityStarted() && call.getState() == DialerCall.State.DISCONNECTED) { 1209 if (call.getAccountHandle() == null && !call.isConferenceCall()) { 1210 setDisconnectCauseForMissingAccounts(call); 1211 } 1212 mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause()); 1213 } 1214 } 1215 1216 /** 1217 * When the state of in-call changes, this is the first method to get called. It determines if the 1218 * UI needs to be started or finished depending on the new state and does it. 1219 */ startOrFinishUi(InCallState newState)1220 private InCallState startOrFinishUi(InCallState newState) { 1221 Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState); 1222 1223 // TODO: Consider a proper state machine implementation 1224 1225 // If the state isn't changing we have already done any starting/stopping of activities in 1226 // a previous pass...so lets cut out early 1227 if (newState == mInCallState) { 1228 return newState; 1229 } 1230 1231 // A new Incoming call means that the user needs to be notified of the the call (since 1232 // it wasn't them who initiated it). We do this through full screen notifications and 1233 // happens indirectly through {@link StatusBarNotifier}. 1234 // 1235 // The process for incoming calls is as follows: 1236 // 1237 // 1) CallList - Announces existence of new INCOMING call 1238 // 2) InCallPresenter - Gets announcement and calculates that the new InCallState 1239 // - should be set to INCOMING. 1240 // 3) InCallPresenter - This method is called to see if we need to start or finish 1241 // the app given the new state. 1242 // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls 1243 // StatusBarNotifier explicitly to issue a FullScreen Notification 1244 // that will either start the InCallActivity or show the user a 1245 // top-level notification dialog if the user is in an immersive app. 1246 // That notification can also start the InCallActivity. 1247 // 5) InCallActivity - Main activity starts up and at the end of its onCreate will 1248 // call InCallPresenter::setActivity() to let the presenter 1249 // know that start-up is complete. 1250 // 1251 // [ AND NOW YOU'RE IN THE CALL. voila! ] 1252 // 1253 // Our app is started using a fullScreen notification. We need to do this whenever 1254 // we get an incoming call. Depending on the current context of the device, either a 1255 // incoming call HUN or the actual InCallActivity will be shown. 1256 final boolean startIncomingCallSequence = (InCallState.INCOMING == newState); 1257 1258 // A dialog to show on top of the InCallUI to select a PhoneAccount 1259 final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState); 1260 1261 // A new outgoing call indicates that the user just now dialed a number and when that 1262 // happens we need to display the screen immediately or show an account picker dialog if 1263 // no default is set. However, if the main InCallUI is already visible, we do not want to 1264 // re-initiate the start-up animation, so we do not need to do anything here. 1265 // 1266 // It is also possible to go into an intermediate state where the call has been initiated 1267 // but Telecom has not yet returned with the details of the call (handle, gateway, etc.). 1268 // This pending outgoing state can also launch the call screen. 1269 // 1270 // This is different from the incoming call sequence because we do not need to shock the 1271 // user with a top-level notification. Just show the call UI normally. 1272 boolean callCardFragmentVisible = 1273 mInCallActivity != null && mInCallActivity.getCallCardFragmentVisible(); 1274 final boolean mainUiNotVisible = !isShowingInCallUi() || !callCardFragmentVisible; 1275 boolean showCallUi = InCallState.OUTGOING == newState && mainUiNotVisible; 1276 1277 // Direct transition from PENDING_OUTGOING -> INCALL means that there was an error in the 1278 // outgoing call process, so the UI should be brought up to show an error dialog. 1279 showCallUi |= 1280 (InCallState.PENDING_OUTGOING == mInCallState 1281 && InCallState.INCALL == newState 1282 && !isShowingInCallUi()); 1283 1284 // Another exception - InCallActivity is in charge of disconnecting a call with no 1285 // valid accounts set. Bring the UI up if this is true for the current pending outgoing 1286 // call so that: 1287 // 1) The call can be disconnected correctly 1288 // 2) The UI comes up and correctly displays the error dialog. 1289 // TODO: Remove these special case conditions by making InCallPresenter a true state 1290 // machine. Telecom should also be the component responsible for disconnecting a call 1291 // with no valid accounts. 1292 showCallUi |= 1293 InCallState.PENDING_OUTGOING == newState 1294 && mainUiNotVisible 1295 && isCallWithNoValidAccounts(mCallList.getPendingOutgoingCall()); 1296 1297 // The only time that we have an instance of mInCallActivity and it isn't started is 1298 // when it is being destroyed. In that case, lets avoid bringing up another instance of 1299 // the activity. When it is finally destroyed, we double check if we should bring it back 1300 // up so we aren't going to lose anything by avoiding a second startup here. 1301 boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted(); 1302 if (activityIsFinishing) { 1303 Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState); 1304 return mInCallState; 1305 } 1306 1307 // We're about the bring up the in-call UI for outgoing and incoming call. If we still have 1308 // dialogs up, we need to clear them out before showing in-call screen. This is necessary 1309 // to fix the bug that dialog will show up when data reaches limit even after makeing new 1310 // outgoing call after user ignore it by pressing home button. 1311 if ((newState == InCallState.INCOMING || newState == InCallState.PENDING_OUTGOING) 1312 && !showCallUi 1313 && isActivityStarted()) { 1314 mInCallActivity.dismissPendingDialogs(); 1315 } 1316 1317 if (showCallUi || showAccountPicker) { 1318 Log.i(this, "Start in call UI"); 1319 showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */); 1320 } else if (startIncomingCallSequence) { 1321 Log.i(this, "Start Full Screen in call UI"); 1322 1323 mStatusBarNotifier.updateNotification(mCallList); 1324 } else if (newState == InCallState.NO_CALLS) { 1325 // The new state is the no calls state. Tear everything down. 1326 attemptFinishActivity(); 1327 attemptCleanup(); 1328 } 1329 1330 return newState; 1331 } 1332 1333 /** 1334 * Sets the DisconnectCause for a call that was disconnected because it was missing a PhoneAccount 1335 * or PhoneAccounts to select from. 1336 */ setDisconnectCauseForMissingAccounts(DialerCall call)1337 private void setDisconnectCauseForMissingAccounts(DialerCall call) { 1338 1339 Bundle extras = call.getIntentExtras(); 1340 // Initialize the extras bundle to avoid NPE 1341 if (extras == null) { 1342 extras = new Bundle(); 1343 } 1344 1345 final List<PhoneAccountHandle> phoneAccountHandles = 1346 extras.getParcelableArrayList(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS); 1347 1348 if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) { 1349 String scheme = call.getHandle().getScheme(); 1350 final String errorMsg = 1351 PhoneAccount.SCHEME_TEL.equals(scheme) 1352 ? mContext.getString(R.string.callFailed_simError) 1353 : mContext.getString(R.string.incall_error_supp_service_unknown); 1354 DisconnectCause disconnectCause = 1355 new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg); 1356 call.setDisconnectCause(disconnectCause); 1357 } 1358 } 1359 1360 /** 1361 * @return {@code true} if the InCallPresenter is ready to be torn down, {@code false} otherwise. 1362 * Calling classes should use this as an indication whether to interact with the 1363 * InCallPresenter or not. 1364 */ isReadyForTearDown()1365 public boolean isReadyForTearDown() { 1366 return mInCallActivity == null && !mServiceConnected && mInCallState == InCallState.NO_CALLS; 1367 } 1368 1369 /** 1370 * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all down. 1371 */ attemptCleanup()1372 private void attemptCleanup() { 1373 if (isReadyForTearDown()) { 1374 Log.i(this, "Cleaning up"); 1375 1376 cleanupSurfaces(); 1377 1378 mIsActivityPreviouslyStarted = false; 1379 mIsChangingConfigurations = false; 1380 1381 // blow away stale contact info so that we get fresh data on 1382 // the next set of calls 1383 if (mContactInfoCache != null) { 1384 mContactInfoCache.clearCache(); 1385 } 1386 mContactInfoCache = null; 1387 1388 if (mProximitySensor != null) { 1389 removeListener(mProximitySensor); 1390 mProximitySensor.tearDown(); 1391 } 1392 mProximitySensor = null; 1393 1394 if (mStatusBarNotifier != null) { 1395 removeListener(mStatusBarNotifier); 1396 EnrichedCallComponent.get(mContext) 1397 .getEnrichedCallManager() 1398 .unregisterStateChangedListener(mStatusBarNotifier); 1399 } 1400 1401 if (mExternalCallNotifier != null && mExternalCallList != null) { 1402 mExternalCallList.removeExternalCallListener(mExternalCallNotifier); 1403 } 1404 mStatusBarNotifier = null; 1405 1406 if (mCallList != null) { 1407 mCallList.removeListener(this); 1408 mCallList.removeListener(mSpamCallListListener); 1409 } 1410 mCallList = null; 1411 1412 mContext = null; 1413 mInCallActivity = null; 1414 mManageConferenceActivity = null; 1415 1416 mListeners.clear(); 1417 mIncomingCallListeners.clear(); 1418 mDetailsListeners.clear(); 1419 mCanAddCallListeners.clear(); 1420 mOrientationListeners.clear(); 1421 mInCallEventListeners.clear(); 1422 mInCallUiListeners.clear(); 1423 1424 Log.d(this, "Finished InCallPresenter.CleanUp"); 1425 } 1426 } 1427 showInCall(boolean showDialpad, boolean newOutgoingCall)1428 public void showInCall(boolean showDialpad, boolean newOutgoingCall) { 1429 Log.i(this, "Showing InCallActivity"); 1430 mContext.startActivity( 1431 InCallActivity.getIntent( 1432 mContext, showDialpad, newOutgoingCall, false /* forFullScreen */)); 1433 } 1434 onServiceBind()1435 public void onServiceBind() { 1436 mServiceBound = true; 1437 } 1438 onServiceUnbind()1439 public void onServiceUnbind() { 1440 InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(false, null); 1441 mServiceBound = false; 1442 } 1443 isServiceBound()1444 public boolean isServiceBound() { 1445 return mServiceBound; 1446 } 1447 maybeStartRevealAnimation(Intent intent)1448 public void maybeStartRevealAnimation(Intent intent) { 1449 if (intent == null || mInCallActivity != null) { 1450 return; 1451 } 1452 final Bundle extras = intent.getBundleExtra(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS); 1453 if (extras == null) { 1454 // Incoming call, just show the in-call UI directly. 1455 return; 1456 } 1457 1458 if (extras.containsKey(android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS)) { 1459 // Account selection dialog will show up so don't show the animation. 1460 return; 1461 } 1462 1463 final PhoneAccountHandle accountHandle = 1464 intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE); 1465 final Point touchPoint = extras.getParcelable(TouchPointManager.TOUCH_POINT); 1466 1467 InCallPresenter.getInstance().setBoundAndWaitingForOutgoingCall(true, accountHandle); 1468 1469 final Intent activityIntent = 1470 InCallActivity.getIntent(mContext, false, true, false /* forFullScreen */); 1471 activityIntent.putExtra(TouchPointManager.TOUCH_POINT, touchPoint); 1472 mContext.startActivity(activityIntent); 1473 } 1474 1475 /** 1476 * Retrieves the current in-call camera manager instance, creating if necessary. 1477 * 1478 * @return The {@link InCallCameraManager}. 1479 */ getInCallCameraManager()1480 public InCallCameraManager getInCallCameraManager() { 1481 synchronized (this) { 1482 if (mInCallCameraManager == null) { 1483 mInCallCameraManager = new InCallCameraManager(mContext); 1484 } 1485 1486 return mInCallCameraManager; 1487 } 1488 } 1489 1490 /** 1491 * Notifies listeners of changes in orientation and notify calls of rotation angle change. 1492 * 1493 * @param orientation The screen orientation of the device (one of: {@link 1494 * InCallOrientationEventListener#SCREEN_ORIENTATION_0}, {@link 1495 * InCallOrientationEventListener#SCREEN_ORIENTATION_90}, {@link 1496 * InCallOrientationEventListener#SCREEN_ORIENTATION_180}, {@link 1497 * InCallOrientationEventListener#SCREEN_ORIENTATION_270}). 1498 */ onDeviceOrientationChange(@creenOrientation int orientation)1499 public void onDeviceOrientationChange(@ScreenOrientation int orientation) { 1500 Log.d(this, "onDeviceOrientationChange: orientation= " + orientation); 1501 1502 if (mCallList != null) { 1503 mCallList.notifyCallsOfDeviceRotation(orientation); 1504 } else { 1505 Log.w(this, "onDeviceOrientationChange: CallList is null."); 1506 } 1507 1508 // Notify listeners of device orientation changed. 1509 for (InCallOrientationListener listener : mOrientationListeners) { 1510 listener.onDeviceOrientationChanged(orientation); 1511 } 1512 } 1513 1514 /** 1515 * Configures the in-call UI activity so it can change orientations or not. Enables the 1516 * orientation event listener if allowOrientationChange is true, disables it if false. 1517 * 1518 * @param allowOrientationChange {@code true} if the in-call UI can change between portrait and 1519 * landscape. {@code false} if the in-call UI should be locked in portrait. 1520 */ setInCallAllowsOrientationChange(boolean allowOrientationChange)1521 public void setInCallAllowsOrientationChange(boolean allowOrientationChange) { 1522 if (mInCallActivity == null) { 1523 Log.e(this, "InCallActivity is null. Can't set requested orientation."); 1524 return; 1525 } 1526 mInCallActivity.setAllowOrientationChange(allowOrientationChange); 1527 } 1528 enableScreenTimeout(boolean enable)1529 public void enableScreenTimeout(boolean enable) { 1530 Log.v(this, "enableScreenTimeout: value=" + enable); 1531 if (mInCallActivity == null) { 1532 Log.e(this, "enableScreenTimeout: InCallActivity is null."); 1533 return; 1534 } 1535 1536 final Window window = mInCallActivity.getWindow(); 1537 if (enable) { 1538 window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1539 } else { 1540 window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 1541 } 1542 } 1543 1544 /** 1545 * Hides or shows the conference manager fragment. 1546 * 1547 * @param show {@code true} if the conference manager should be shown, {@code false} if it should 1548 * be hidden. 1549 */ showConferenceCallManager(boolean show)1550 public void showConferenceCallManager(boolean show) { 1551 if (mInCallActivity != null) { 1552 mInCallActivity.showConferenceFragment(show); 1553 } 1554 if (!show && mManageConferenceActivity != null) { 1555 mManageConferenceActivity.finish(); 1556 } 1557 } 1558 1559 /** 1560 * Determines if the dialpad is visible. 1561 * 1562 * @return {@code true} if the dialpad is visible, {@code false} otherwise. 1563 */ isDialpadVisible()1564 public boolean isDialpadVisible() { 1565 if (mInCallActivity == null) { 1566 return false; 1567 } 1568 return mInCallActivity.isDialpadVisible(); 1569 } 1570 getThemeColorManager()1571 public ThemeColorManager getThemeColorManager() { 1572 return mThemeColorManager; 1573 } 1574 1575 /** Called when the foreground call changes. */ onForegroundCallChanged(DialerCall newForegroundCall)1576 public void onForegroundCallChanged(DialerCall newForegroundCall) { 1577 mThemeColorManager.onForegroundCallChanged(mContext, newForegroundCall); 1578 if (mInCallActivity != null) { 1579 mInCallActivity.onForegroundCallChanged(newForegroundCall); 1580 } 1581 } 1582 getActivity()1583 public InCallActivity getActivity() { 1584 return mInCallActivity; 1585 } 1586 1587 /** Called when the UI begins, and starts the callstate callbacks if necessary. */ setActivity(InCallActivity inCallActivity)1588 public void setActivity(InCallActivity inCallActivity) { 1589 if (inCallActivity == null) { 1590 throw new IllegalArgumentException("registerActivity cannot be called with null"); 1591 } 1592 if (mInCallActivity != null && mInCallActivity != inCallActivity) { 1593 Log.w(this, "Setting a second activity before destroying the first."); 1594 } 1595 updateActivity(inCallActivity); 1596 } 1597 getExternalCallNotifier()1598 ExternalCallNotifier getExternalCallNotifier() { 1599 return mExternalCallNotifier; 1600 } 1601 getLocalVideoSurfaceTexture()1602 VideoSurfaceTexture getLocalVideoSurfaceTexture() { 1603 if (mLocalVideoSurfaceTexture == null) { 1604 mLocalVideoSurfaceTexture = VideoSurfaceBindings.createLocalVideoSurfaceTexture(); 1605 } 1606 return mLocalVideoSurfaceTexture; 1607 } 1608 getRemoteVideoSurfaceTexture()1609 VideoSurfaceTexture getRemoteVideoSurfaceTexture() { 1610 if (mRemoteVideoSurfaceTexture == null) { 1611 mRemoteVideoSurfaceTexture = VideoSurfaceBindings.createRemoteVideoSurfaceTexture(); 1612 } 1613 return mRemoteVideoSurfaceTexture; 1614 } 1615 cleanupSurfaces()1616 void cleanupSurfaces() { 1617 if (mRemoteVideoSurfaceTexture != null) { 1618 mRemoteVideoSurfaceTexture.setDoneWithSurface(); 1619 mRemoteVideoSurfaceTexture = null; 1620 } 1621 if (mLocalVideoSurfaceTexture != null) { 1622 mLocalVideoSurfaceTexture.setDoneWithSurface(); 1623 mLocalVideoSurfaceTexture = null; 1624 } 1625 } 1626 1627 /** All the main states of InCallActivity. */ 1628 public enum InCallState { 1629 // InCall Screen is off and there are no calls 1630 NO_CALLS, 1631 1632 // Incoming-call screen is up 1633 INCOMING, 1634 1635 // In-call experience is showing 1636 INCALL, 1637 1638 // Waiting for user input before placing outgoing call 1639 WAITING_FOR_ACCOUNT, 1640 1641 // UI is starting up but no call has been initiated yet. 1642 // The UI is waiting for Telecom to respond. 1643 PENDING_OUTGOING, 1644 1645 // User is dialing out 1646 OUTGOING; 1647 isIncoming()1648 public boolean isIncoming() { 1649 return (this == INCOMING); 1650 } 1651 isConnectingOrConnected()1652 public boolean isConnectingOrConnected() { 1653 return (this == INCOMING || this == OUTGOING || this == INCALL); 1654 } 1655 } 1656 1657 /** Interface implemented by classes that need to know about the InCall State. */ 1658 public interface InCallStateListener { 1659 1660 // TODO: Enhance state to contain the call objects instead of passing CallList onStateChange(InCallState oldState, InCallState newState, CallList callList)1661 void onStateChange(InCallState oldState, InCallState newState, CallList callList); 1662 } 1663 1664 public interface IncomingCallListener { 1665 onIncomingCall(InCallState oldState, InCallState newState, DialerCall call)1666 void onIncomingCall(InCallState oldState, InCallState newState, DialerCall call); 1667 } 1668 1669 public interface CanAddCallListener { 1670 onCanAddCallChanged(boolean canAddCall)1671 void onCanAddCallChanged(boolean canAddCall); 1672 } 1673 1674 public interface InCallDetailsListener { 1675 onDetailsChanged(DialerCall call, android.telecom.Call.Details details)1676 void onDetailsChanged(DialerCall call, android.telecom.Call.Details details); 1677 } 1678 1679 public interface InCallOrientationListener { 1680 onDeviceOrientationChanged(@creenOrientation int orientation)1681 void onDeviceOrientationChanged(@ScreenOrientation int orientation); 1682 } 1683 1684 /** 1685 * Interface implemented by classes that need to know about events which occur within the In-Call 1686 * UI. Used as a means of communicating between fragments that make up the UI. 1687 */ 1688 public interface InCallEventListener { 1689 onFullscreenModeChanged(boolean isFullscreenMode)1690 void onFullscreenModeChanged(boolean isFullscreenMode); 1691 } 1692 1693 public interface InCallUiListener { 1694 onUiShowing(boolean showing)1695 void onUiShowing(boolean showing); 1696 } 1697 } 1698