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