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