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