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