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