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