• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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