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