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