• 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.Manifest;
20 import android.app.PendingIntent;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ActivityInfo;
24 import android.os.Bundle;
25 import android.telecom.DisconnectCause;
26 import android.telecom.PhoneAccount;
27 import android.telecom.PhoneCapabilities;
28 import android.telecom.Phone;
29 import android.telecom.PhoneAccountHandle;
30 import android.telecom.VideoProfile;
31 import android.text.TextUtils;
32 import android.view.Surface;
33 import android.view.View;
34 
35 import com.google.common.base.Preconditions;
36 
37 import com.android.incalluibind.ObjectFactory;
38 
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Locale;
42 import java.util.Set;
43 import java.util.concurrent.ConcurrentHashMap;
44 import java.util.concurrent.CopyOnWriteArrayList;
45 
46 /**
47  * Takes updates from the CallList and notifies the InCallActivity (UI)
48  * of the changes.
49  * Responsible for starting the activity for a new call and finishing the activity when all calls
50  * are disconnected.
51  * Creates and manages the in-call state and provides a listener pattern for the presenters
52  * that want to listen in on the in-call state changes.
53  * TODO: This class has become more of a state machine at this point.  Consider renaming.
54  */
55 public class InCallPresenter implements CallList.Listener, InCallPhoneListener {
56 
57     private static final String EXTRA_FIRST_TIME_SHOWN =
58             "com.android.incallui.intent.extra.FIRST_TIME_SHOWN";
59 
60     private static InCallPresenter sInCallPresenter;
61 
62     /**
63      * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
64      * load factor before resizing, 1 means we only expect a single thread to
65      * access the map so make only a single shard
66      */
67     private final Set<InCallStateListener> mListeners = Collections.newSetFromMap(
68             new ConcurrentHashMap<InCallStateListener, Boolean>(8, 0.9f, 1));
69     private final List<IncomingCallListener> mIncomingCallListeners = new CopyOnWriteArrayList<>();
70     private final Set<InCallDetailsListener> mDetailsListeners = Collections.newSetFromMap(
71             new ConcurrentHashMap<InCallDetailsListener, Boolean>(8, 0.9f, 1));
72     private final Set<InCallOrientationListener> mOrientationListeners = Collections.newSetFromMap(
73             new ConcurrentHashMap<InCallOrientationListener, Boolean>(8, 0.9f, 1));
74     private final Set<InCallEventListener> mInCallEventListeners = Collections.newSetFromMap(
75             new ConcurrentHashMap<InCallEventListener, Boolean>(8, 0.9f, 1));
76 
77     private AudioModeProvider mAudioModeProvider;
78     private StatusBarNotifier mStatusBarNotifier;
79     private ContactInfoCache mContactInfoCache;
80     private Context mContext;
81     private CallList mCallList;
82     private InCallActivity mInCallActivity;
83     private InCallState mInCallState = InCallState.NO_CALLS;
84     private ProximitySensor mProximitySensor;
85     private boolean mServiceConnected = false;
86     private boolean mAccountSelectionCancelled = false;
87     private InCallCameraManager mInCallCameraManager = null;
88 
89     private final Phone.Listener mPhoneListener = new Phone.Listener() {
90         @Override
91         public void onBringToForeground(Phone phone, boolean showDialpad) {
92             Log.i(this, "Bringing UI to foreground.");
93             bringToForeground(showDialpad);
94         }
95         @Override
96         public void onCallAdded(Phone phone, android.telecom.Call call) {
97             call.addListener(mCallListener);
98         }
99         @Override
100         public void onCallRemoved(Phone phone, android.telecom.Call call) {
101             call.removeListener(mCallListener);
102         }
103     };
104 
105     private final android.telecom.Call.Listener mCallListener =
106             new android.telecom.Call.Listener() {
107         @Override
108         public void onPostDialWait(android.telecom.Call call, String remainingPostDialSequence) {
109             onPostDialCharWait(
110                     CallList.getInstance().getCallByTelecommCall(call).getId(),
111                     remainingPostDialSequence);
112         }
113 
114         @Override
115         public void onDetailsChanged(android.telecom.Call call,
116                 android.telecom.Call.Details details) {
117             for (InCallDetailsListener listener : mDetailsListeners) {
118                 listener.onDetailsChanged(CallList.getInstance().getCallByTelecommCall(call),
119                         details);
120             }
121         }
122 
123         @Override
124         public void onConferenceableCallsChanged(
125                 android.telecom.Call call, List<android.telecom.Call> conferenceableCalls) {
126             Log.i(this, "onConferenceableCallsChanged: " + call);
127             for (InCallDetailsListener listener : mDetailsListeners) {
128                 listener.onDetailsChanged(CallList.getInstance().getCallByTelecommCall(call),
129                         call.getDetails());
130             }
131         }
132     };
133 
134     /**
135      * Is true when the activity has been previously started. Some code needs to know not just if
136      * the activity is currently up, but if it had been previously shown in foreground for this
137      * in-call session (e.g., StatusBarNotifier). This gets reset when the session ends in the
138      * tear-down method.
139      */
140     private boolean mIsActivityPreviouslyStarted = false;
141 
142     private Phone mPhone;
143 
getInstance()144     public static synchronized InCallPresenter getInstance() {
145         if (sInCallPresenter == null) {
146             sInCallPresenter = new InCallPresenter();
147         }
148         return sInCallPresenter;
149     }
150 
151     @Override
setPhone(Phone phone)152     public void setPhone(Phone phone) {
153         mPhone = phone;
154         mPhone.addListener(mPhoneListener);
155     }
156 
157     @Override
clearPhone()158     public void clearPhone() {
159         mPhone.removeListener(mPhoneListener);
160         mPhone = null;
161     }
162 
getInCallState()163     public InCallState getInCallState() {
164         return mInCallState;
165     }
166 
getCallList()167     public CallList getCallList() {
168         return mCallList;
169     }
170 
setUp(Context context, CallList callList, AudioModeProvider audioModeProvider)171     public void setUp(Context context, CallList callList, AudioModeProvider audioModeProvider) {
172         if (mServiceConnected) {
173             Log.i(this, "New service connection replacing existing one.");
174             // retain the current resources, no need to create new ones.
175             Preconditions.checkState(context == mContext);
176             Preconditions.checkState(callList == mCallList);
177             Preconditions.checkState(audioModeProvider == mAudioModeProvider);
178             return;
179         }
180 
181         Preconditions.checkNotNull(context);
182         mContext = context;
183 
184         mContactInfoCache = ContactInfoCache.getInstance(context);
185 
186         mStatusBarNotifier = new StatusBarNotifier(context, mContactInfoCache);
187         addListener(mStatusBarNotifier);
188 
189         mAudioModeProvider = audioModeProvider;
190 
191         mProximitySensor = new ProximitySensor(context, mAudioModeProvider);
192         addListener(mProximitySensor);
193 
194         mCallList = callList;
195 
196         // This only gets called by the service so this is okay.
197         mServiceConnected = true;
198 
199         // The final thing we do in this set up is add ourselves as a listener to CallList.  This
200         // will kick off an update and the whole process can start.
201         mCallList.addListener(this);
202 
203         Log.d(this, "Finished InCallPresenter.setUp");
204     }
205 
206     /**
207      * Called when the telephony service has disconnected from us.  This will happen when there are
208      * no more active calls. However, we may still want to continue showing the UI for
209      * certain cases like showing "Call Ended".
210      * What we really want is to wait for the activity and the service to both disconnect before we
211      * tear things down. This method sets a serviceConnected boolean and calls a secondary method
212      * that performs the aforementioned logic.
213      */
tearDown()214     public void tearDown() {
215         Log.d(this, "tearDown");
216         mServiceConnected = false;
217         attemptCleanup();
218     }
219 
attemptFinishActivity()220     private void attemptFinishActivity() {
221         final boolean doFinish = (mInCallActivity != null && isActivityStarted());
222         Log.i(this, "Hide in call UI: " + doFinish);
223 
224         if (doFinish) {
225             mInCallActivity.finish();
226 
227             if (mAccountSelectionCancelled) {
228                 // This finish is a result of account selection cancellation
229                 // do not include activity ending transition
230                 mInCallActivity.overridePendingTransition(0, 0);
231             }
232         }
233     }
234 
235     /**
236      * Called when the UI begins or ends. Starts the callstate callbacks if the UI just began.
237      * Attempts to tear down everything if the UI just ended. See #tearDown for more insight on
238      * the tear-down process.
239      */
setActivity(InCallActivity inCallActivity)240     public void setActivity(InCallActivity inCallActivity) {
241         boolean updateListeners = false;
242         boolean doAttemptCleanup = false;
243 
244         if (inCallActivity != null) {
245             if (mInCallActivity == null) {
246                 updateListeners = true;
247                 Log.i(this, "UI Initialized");
248             } else if (mInCallActivity != inCallActivity) {
249                 Log.wtf(this, "Setting a second activity before destroying the first.");
250             } else {
251                 // since setActivity is called onStart(), it can be called multiple times.
252                 // This is fine and ignorable, but we do not want to update the world every time
253                 // this happens (like going to/from background) so we do not set updateListeners.
254             }
255 
256             mInCallActivity = inCallActivity;
257 
258             // By the time the UI finally comes up, the call may already be disconnected.
259             // If that's the case, we may need to show an error dialog.
260             if (mCallList != null && mCallList.getDisconnectedCall() != null) {
261                 maybeShowErrorDialogOnDisconnect(mCallList.getDisconnectedCall());
262             }
263 
264             // When the UI comes up, we need to first check the in-call state.
265             // If we are showing NO_CALLS, that means that a call probably connected and
266             // then immediately disconnected before the UI was able to come up.
267             // If we dont have any calls, start tearing down the UI instead.
268             // NOTE: This code relies on {@link #mInCallActivity} being set so we run it after
269             // it has been set.
270             if (mInCallState == InCallState.NO_CALLS) {
271                 Log.i(this, "UI Intialized, but no calls left.  shut down.");
272                 attemptFinishActivity();
273                 return;
274             }
275         } else {
276             Log.i(this, "UI Destroyed)");
277             updateListeners = true;
278             mInCallActivity = null;
279 
280             // We attempt cleanup for the destroy case but only after we recalculate the state
281             // to see if we need to come back up or stay shut down. This is why we do the cleanup
282             // after the call to onCallListChange() instead of directly here.
283             doAttemptCleanup = true;
284         }
285 
286         // Messages can come from the telephony layer while the activity is coming up
287         // and while the activity is going down.  So in both cases we need to recalculate what
288         // state we should be in after they complete.
289         // Examples: (1) A new incoming call could come in and then get disconnected before
290         //               the activity is created.
291         //           (2) All calls could disconnect and then get a new incoming call before the
292         //               activity is destroyed.
293         //
294         // b/1122139 - We previously had a check for mServiceConnected here as well, but there are
295         // cases where we need to recalculate the current state even if the service in not
296         // connected.  In particular the case where startOrFinish() is called while the app is
297         // already finish()ing. In that case, we skip updating the state with the knowledge that
298         // we will check again once the activity has finished. That means we have to recalculate the
299         // state here even if the service is disconnected since we may not have finished a state
300         // transition while finish()ing.
301         if (updateListeners) {
302             onCallListChange(mCallList);
303         }
304 
305         if (doAttemptCleanup) {
306             attemptCleanup();
307         }
308     }
309 
310     /**
311      * Called when there is a change to the call list.
312      * Sets the In-Call state for the entire in-call app based on the information it gets from
313      * CallList. Dispatches the in-call state to all listeners. Can trigger the creation or
314      * destruction of the UI based on the states that is calculates.
315      */
316     @Override
onCallListChange(CallList callList)317     public void onCallListChange(CallList callList) {
318         if (callList == null) {
319             return;
320         }
321         InCallState newState = getPotentialStateFromCallList(callList);
322         InCallState oldState = mInCallState;
323         newState = startOrFinishUi(newState);
324 
325         // Set the new state before announcing it to the world
326         Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
327         mInCallState = newState;
328 
329         // notify listeners of new state
330         for (InCallStateListener listener : mListeners) {
331             Log.d(this, "Notify " + listener + " of state " + mInCallState.toString());
332             listener.onStateChange(oldState, mInCallState, callList);
333         }
334 
335         if (isActivityStarted()) {
336             final boolean hasCall = callList.getActiveOrBackgroundCall() != null ||
337                     callList.getOutgoingCall() != null;
338             mInCallActivity.dismissKeyguard(hasCall);
339         }
340     }
341 
342     /**
343      * Called when there is a new incoming call.
344      *
345      * @param call
346      */
347     @Override
onIncomingCall(Call call)348     public void onIncomingCall(Call call) {
349         InCallState newState = startOrFinishUi(InCallState.INCOMING);
350         InCallState oldState = mInCallState;
351 
352         Log.i(this, "Phone switching state: " + oldState + " -> " + newState);
353         mInCallState = newState;
354 
355         for (IncomingCallListener listener : mIncomingCallListeners) {
356             listener.onIncomingCall(oldState, mInCallState, call);
357         }
358     }
359 
360     /**
361      * Called when a call becomes disconnected. Called everytime an existing call
362      * changes from being connected (incoming/outgoing/active) to disconnected.
363      */
364     @Override
onDisconnect(Call call)365     public void onDisconnect(Call call) {
366         hideDialpadForDisconnect();
367         maybeShowErrorDialogOnDisconnect(call);
368 
369         // We need to do the run the same code as onCallListChange.
370         onCallListChange(CallList.getInstance());
371 
372         if (isActivityStarted()) {
373             mInCallActivity.dismissKeyguard(false);
374         }
375     }
376 
377     /**
378      * Given the call list, return the state in which the in-call screen should be.
379      */
getPotentialStateFromCallList(CallList callList)380     public static InCallState getPotentialStateFromCallList(CallList callList) {
381 
382         InCallState newState = InCallState.NO_CALLS;
383 
384         if (callList == null) {
385             return newState;
386         }
387         if (callList.getIncomingCall() != null) {
388             newState = InCallState.INCOMING;
389         } else if (callList.getWaitingForAccountCall() != null) {
390             newState = InCallState.WAITING_FOR_ACCOUNT;
391         } else if (callList.getPendingOutgoingCall() != null) {
392             newState = InCallState.PENDING_OUTGOING;
393         } else if (callList.getOutgoingCall() != null) {
394             newState = InCallState.OUTGOING;
395         } else if (callList.getActiveCall() != null ||
396                 callList.getBackgroundCall() != null ||
397                 callList.getDisconnectedCall() != null ||
398                 callList.getDisconnectingCall() != null) {
399             newState = InCallState.INCALL;
400         }
401 
402         return newState;
403     }
404 
addIncomingCallListener(IncomingCallListener listener)405     public void addIncomingCallListener(IncomingCallListener listener) {
406         Preconditions.checkNotNull(listener);
407         mIncomingCallListeners.add(listener);
408     }
409 
removeIncomingCallListener(IncomingCallListener listener)410     public void removeIncomingCallListener(IncomingCallListener listener) {
411         if (listener != null) {
412             mIncomingCallListeners.remove(listener);
413         }
414     }
415 
addListener(InCallStateListener listener)416     public void addListener(InCallStateListener listener) {
417         Preconditions.checkNotNull(listener);
418         mListeners.add(listener);
419     }
420 
removeListener(InCallStateListener listener)421     public void removeListener(InCallStateListener listener) {
422         if (listener != null) {
423             mListeners.remove(listener);
424         }
425     }
426 
addDetailsListener(InCallDetailsListener listener)427     public void addDetailsListener(InCallDetailsListener listener) {
428         Preconditions.checkNotNull(listener);
429         mDetailsListeners.add(listener);
430     }
431 
removeDetailsListener(InCallDetailsListener listener)432     public void removeDetailsListener(InCallDetailsListener listener) {
433         if (listener != null) {
434             mDetailsListeners.remove(listener);
435         }
436     }
437 
addOrientationListener(InCallOrientationListener listener)438     public void addOrientationListener(InCallOrientationListener listener) {
439         Preconditions.checkNotNull(listener);
440         mOrientationListeners.add(listener);
441     }
442 
removeOrientationListener(InCallOrientationListener listener)443     public void removeOrientationListener(InCallOrientationListener listener) {
444         if (listener != null) {
445             mOrientationListeners.remove(listener);
446         }
447     }
448 
addInCallEventListener(InCallEventListener listener)449     public void addInCallEventListener(InCallEventListener listener) {
450         Preconditions.checkNotNull(listener);
451         mInCallEventListeners.add(listener);
452     }
453 
removeInCallEventListener(InCallEventListener listener)454     public void removeInCallEventListener(InCallEventListener listener) {
455         if (listener != null) {
456             mInCallEventListeners.remove(listener);
457         }
458     }
459 
getProximitySensor()460     public ProximitySensor getProximitySensor() {
461         return mProximitySensor;
462     }
463 
handleAccountSelection(PhoneAccountHandle accountHandle)464     public void handleAccountSelection(PhoneAccountHandle accountHandle) {
465         Call call = mCallList.getWaitingForAccountCall();
466         if (call != null) {
467             String callId = call.getId();
468             TelecomAdapter.getInstance().phoneAccountSelected(callId, accountHandle);
469         }
470     }
471 
cancelAccountSelection()472     public void cancelAccountSelection() {
473         mAccountSelectionCancelled = true;
474         Call call = mCallList.getWaitingForAccountCall();
475         if (call != null) {
476             String callId = call.getId();
477             TelecomAdapter.getInstance().disconnectCall(callId);
478         }
479     }
480 
481     /**
482      * Hangs up any active or outgoing calls.
483      */
hangUpOngoingCall(Context context)484     public void hangUpOngoingCall(Context context) {
485         // By the time we receive this intent, we could be shut down and call list
486         // could be null.  Bail in those cases.
487         if (mCallList == null) {
488             if (mStatusBarNotifier == null) {
489                 // The In Call UI has crashed but the notification still stayed up. We should not
490                 // come to this stage.
491                 StatusBarNotifier.clearInCallNotification(context);
492             }
493             return;
494         }
495 
496         Call call = mCallList.getOutgoingCall();
497         if (call == null) {
498             call = mCallList.getActiveOrBackgroundCall();
499         }
500 
501         if (call != null) {
502             TelecomAdapter.getInstance().disconnectCall(call.getId());
503             call.setState(Call.State.DISCONNECTING);
504             mCallList.onUpdate(call);
505         }
506     }
507 
508     /**
509      * Answers any incoming call.
510      */
answerIncomingCall(Context context, int videoState)511     public void answerIncomingCall(Context context, int videoState) {
512         // By the time we receive this intent, we could be shut down and call list
513         // could be null.  Bail in those cases.
514         if (mCallList == null) {
515             StatusBarNotifier.clearInCallNotification(context);
516             return;
517         }
518 
519         Call call = mCallList.getIncomingCall();
520         if (call != null) {
521             TelecomAdapter.getInstance().answerCall(call.getId(), videoState);
522             showInCall(false, false/* newOutgoingCall */);
523         }
524     }
525 
526     /**
527      * Declines any incoming call.
528      */
declineIncomingCall(Context context)529     public void declineIncomingCall(Context context) {
530         // By the time we receive this intent, we could be shut down and call list
531         // could be null.  Bail in those cases.
532         if (mCallList == null) {
533             StatusBarNotifier.clearInCallNotification(context);
534             return;
535         }
536 
537         Call call = mCallList.getIncomingCall();
538         if (call != null) {
539             TelecomAdapter.getInstance().rejectCall(call.getId(), false, null);
540         }
541     }
542 
acceptUpgradeRequest(Context context)543     public void acceptUpgradeRequest(Context context) {
544         // Bail if we have been shut down and the call list is null.
545         if (mCallList == null) {
546             StatusBarNotifier.clearInCallNotification(context);
547             return;
548         }
549 
550         Call call = mCallList.getVideoUpgradeRequestCall();
551         if (call != null) {
552             VideoProfile videoProfile =
553                     new VideoProfile(VideoProfile.VideoState.BIDIRECTIONAL);
554             call.getVideoCall().sendSessionModifyResponse(videoProfile);
555             call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
556         }
557     }
558 
declineUpgradeRequest(Context context)559     public void declineUpgradeRequest(Context context) {
560         // Bail if we have been shut down and the call list is null.
561         if (mCallList == null) {
562             StatusBarNotifier.clearInCallNotification(context);
563             return;
564         }
565 
566         Call call = mCallList.getVideoUpgradeRequestCall();
567         if (call != null) {
568             VideoProfile videoProfile =
569                     new VideoProfile(VideoProfile.VideoState.AUDIO_ONLY);
570             call.getVideoCall().sendSessionModifyResponse(videoProfile);
571             call.setSessionModificationState(Call.SessionModificationState.NO_REQUEST);
572         }
573     }
574 
575     /**
576      * Returns true if the incall app is the foreground application.
577      */
isShowingInCallUi()578     public boolean isShowingInCallUi() {
579         return (isActivityStarted() && mInCallActivity.isForegroundActivity());
580     }
581 
582     /**
583      * Returns true if the activity has been created and is running.
584      * Returns true as long as activity is not destroyed or finishing.  This ensures that we return
585      * true even if the activity is paused (not in foreground).
586      */
isActivityStarted()587     public boolean isActivityStarted() {
588         return (mInCallActivity != null &&
589                 !mInCallActivity.isDestroyed() &&
590                 !mInCallActivity.isFinishing());
591     }
592 
isActivityPreviouslyStarted()593     public boolean isActivityPreviouslyStarted() {
594         return mIsActivityPreviouslyStarted;
595     }
596 
597     /**
598      * Called when the activity goes in/out of the foreground.
599      */
onUiShowing(boolean showing)600     public void onUiShowing(boolean showing) {
601         // We need to update the notification bar when we leave the UI because that
602         // could trigger it to show again.
603         if (mStatusBarNotifier != null) {
604             mStatusBarNotifier.updateNotification(mInCallState, mCallList);
605         }
606 
607         if (mProximitySensor != null) {
608             mProximitySensor.onInCallShowing(showing);
609         }
610 
611         Intent broadcastIntent = ObjectFactory.getUiReadyBroadcastIntent(mContext);
612         if (broadcastIntent != null) {
613             broadcastIntent.putExtra(EXTRA_FIRST_TIME_SHOWN, !mIsActivityPreviouslyStarted);
614 
615             if (showing) {
616                 Log.d(this, "Sending sticky broadcast: ", broadcastIntent);
617                 mContext.sendStickyBroadcast(broadcastIntent);
618             } else {
619                 Log.d(this, "Removing sticky broadcast: ", broadcastIntent);
620                 mContext.removeStickyBroadcast(broadcastIntent);
621             }
622         }
623 
624         if (showing) {
625             mIsActivityPreviouslyStarted = true;
626         }
627     }
628 
629     /**
630      * Brings the app into the foreground if possible.
631      */
bringToForeground(boolean showDialpad)632     public void bringToForeground(boolean showDialpad) {
633         // Before we bring the incall UI to the foreground, we check to see if:
634         // 1. It is not currently in the foreground
635         // 2. We are in a state where we want to show the incall ui (i.e. there are calls to
636         // be displayed)
637         // If the activity hadn't actually been started previously, yet there are still calls
638         // present (e.g. a call was accepted by a bluetooth or wired headset), we want to
639         // bring it up the UI regardless.
640         if (!isShowingInCallUi() && mInCallState != InCallState.NO_CALLS) {
641             showInCall(showDialpad, false /* newOutgoingCall */);
642         }
643     }
644 
onPostDialCharWait(String callId, String chars)645     public void onPostDialCharWait(String callId, String chars) {
646         if (isActivityStarted()) {
647             mInCallActivity.showPostCharWaitDialog(callId, chars);
648         }
649     }
650 
651     /**
652      * Handles the green CALL key while in-call.
653      * @return true if we consumed the event.
654      */
handleCallKey()655     public boolean handleCallKey() {
656         Log.v(this, "handleCallKey");
657 
658         // The green CALL button means either "Answer", "Unhold", or
659         // "Swap calls", or can be a no-op, depending on the current state
660         // of the Phone.
661 
662         /**
663          * INCOMING CALL
664          */
665         final CallList calls = CallList.getInstance();
666         final Call incomingCall = calls.getIncomingCall();
667         Log.v(this, "incomingCall: " + incomingCall);
668 
669         // (1) Attempt to answer a call
670         if (incomingCall != null) {
671             TelecomAdapter.getInstance().answerCall(
672                     incomingCall.getId(), VideoProfile.VideoState.AUDIO_ONLY);
673             return true;
674         }
675 
676         /**
677          * STATE_ACTIVE CALL
678          */
679         final Call activeCall = calls.getActiveCall();
680         if (activeCall != null) {
681             // TODO: This logic is repeated from CallButtonPresenter.java. We should
682             // consolidate this logic.
683             final boolean canMerge = activeCall.can(PhoneCapabilities.MERGE_CONFERENCE);
684             final boolean canSwap = activeCall.can(PhoneCapabilities.SWAP_CONFERENCE);
685 
686             Log.v(this, "activeCall: " + activeCall + ", canMerge: " + canMerge +
687                     ", canSwap: " + canSwap);
688 
689             // (2) Attempt actions on conference calls
690             if (canMerge) {
691                 TelecomAdapter.getInstance().merge(activeCall.getId());
692                 return true;
693             } else if (canSwap) {
694                 TelecomAdapter.getInstance().swap(activeCall.getId());
695                 return true;
696             }
697         }
698 
699         /**
700          * BACKGROUND CALL
701          */
702         final Call heldCall = calls.getBackgroundCall();
703         if (heldCall != null) {
704             // We have a hold call so presumeable it will always support HOLD...but
705             // there is no harm in double checking.
706             final boolean canHold = heldCall.can(PhoneCapabilities.HOLD);
707 
708             Log.v(this, "heldCall: " + heldCall + ", canHold: " + canHold);
709 
710             // (4) unhold call
711             if (heldCall.getState() == Call.State.ONHOLD && canHold) {
712                 TelecomAdapter.getInstance().unholdCall(heldCall.getId());
713                 return true;
714             }
715         }
716 
717         // Always consume hard keys
718         return true;
719     }
720 
721     /**
722      * A dialog could have prevented in-call screen from being previously finished.
723      * This function checks to see if there should be any UI left and if not attempts
724      * to tear down the UI.
725      */
onDismissDialog()726     public void onDismissDialog() {
727         Log.i(this, "Dialog dismissed");
728         if (mInCallState == InCallState.NO_CALLS) {
729             attemptFinishActivity();
730             attemptCleanup();
731         }
732     }
733 
734     /**
735      * Called by the {@link VideoCallPresenter} to inform of a change in full screen video status.
736      *
737      * @param isFullScreenVideo {@code True} if entering full screen video mode.
738      */
setFullScreenVideoState(boolean isFullScreenVideo)739     public void setFullScreenVideoState(boolean isFullScreenVideo) {
740         for (InCallEventListener listener : mInCallEventListeners) {
741             listener.onFullScreenVideoStateChanged(isFullScreenVideo);
742         }
743     }
744 
745     /**
746      * For some disconnected causes, we show a dialog.  This calls into the activity to show
747      * the dialog if appropriate for the call.
748      */
maybeShowErrorDialogOnDisconnect(Call call)749     private void maybeShowErrorDialogOnDisconnect(Call call) {
750         // For newly disconnected calls, we may want to show a dialog on specific error conditions
751         if (isActivityStarted() && call.getState() == Call.State.DISCONNECTED) {
752             if (call.getAccountHandle() == null && !call.isConferenceCall()) {
753                 setDisconnectCauseForMissingAccounts(call);
754             }
755             mInCallActivity.maybeShowErrorDialogOnDisconnect(call.getDisconnectCause());
756         }
757     }
758 
759     /**
760      * Hides the dialpad.  Called when a call is disconnected (Requires hiding dialpad).
761      */
hideDialpadForDisconnect()762     private void hideDialpadForDisconnect() {
763         if (isActivityStarted()) {
764             mInCallActivity.hideDialpadForDisconnect();
765         }
766     }
767 
768     /**
769      * When the state of in-call changes, this is the first method to get called. It determines if
770      * the UI needs to be started or finished depending on the new state and does it.
771      */
startOrFinishUi(InCallState newState)772     private InCallState startOrFinishUi(InCallState newState) {
773         Log.d(this, "startOrFinishUi: " + mInCallState + " -> " + newState);
774 
775         // TODO: Consider a proper state machine implementation
776 
777         // If the state isn't changing or if we're transitioning from pending outgoing to actual
778         // outgoing, we have already done any starting/stopping of activities in a previous pass
779         // ...so lets cut out early
780         boolean alreadyOutgoing = mInCallState == InCallState.PENDING_OUTGOING &&
781                 newState == InCallState.OUTGOING;
782         if (newState == mInCallState || alreadyOutgoing) {
783             return newState;
784         }
785 
786         // A new Incoming call means that the user needs to be notified of the the call (since
787         // it wasn't them who initiated it).  We do this through full screen notifications and
788         // happens indirectly through {@link StatusBarNotifier}.
789         //
790         // The process for incoming calls is as follows:
791         //
792         // 1) CallList          - Announces existence of new INCOMING call
793         // 2) InCallPresenter   - Gets announcement and calculates that the new InCallState
794         //                      - should be set to INCOMING.
795         // 3) InCallPresenter   - This method is called to see if we need to start or finish
796         //                        the app given the new state.
797         // 4) StatusBarNotifier - Listens to InCallState changes. InCallPresenter calls
798         //                        StatusBarNotifier explicitly to issue a FullScreen Notification
799         //                        that will either start the InCallActivity or show the user a
800         //                        top-level notification dialog if the user is in an immersive app.
801         //                        That notification can also start the InCallActivity.
802         // 5) InCallActivity    - Main activity starts up and at the end of its onCreate will
803         //                        call InCallPresenter::setActivity() to let the presenter
804         //                        know that start-up is complete.
805         //
806         //          [ AND NOW YOU'RE IN THE CALL. voila! ]
807         //
808         // Our app is started using a fullScreen notification.  We need to do this whenever
809         // we get an incoming call.
810         final boolean startStartupSequence = (InCallState.INCOMING == newState);
811 
812         // A dialog to show on top of the InCallUI to select a PhoneAccount
813         final boolean showAccountPicker = (InCallState.WAITING_FOR_ACCOUNT == newState);
814 
815         // A new outgoing call indicates that the user just now dialed a number and when that
816         // happens we need to display the screen immediately or show an account picker dialog if
817         // no default is set. However, if the main InCallUI is already visible, we do not want to
818         // re-initiate the start-up animation, so we do not need to do anything here.
819         //
820         // It is also possible to go into an intermediate state where the call has been initiated
821         // but Telecomm has not yet returned with the details of the call (handle, gateway, etc.).
822         // This pending outgoing state can also launch the call screen.
823         //
824         // This is different from the incoming call sequence because we do not need to shock the
825         // user with a top-level notification.  Just show the call UI normally.
826         final boolean mainUiNotVisible = !isShowingInCallUi() || !getCallCardFragmentVisible();
827         final boolean showCallUi = ((InCallState.PENDING_OUTGOING == newState ||
828                 InCallState.OUTGOING == newState) && mainUiNotVisible);
829 
830         // TODO: Can we be suddenly in a call without it having been in the outgoing or incoming
831         // state?  I havent seen that but if it can happen, the code below should be enabled.
832         // showCallUi |= (InCallState.INCALL && !isActivityStarted());
833 
834         // The only time that we have an instance of mInCallActivity and it isn't started is
835         // when it is being destroyed.  In that case, lets avoid bringing up another instance of
836         // the activity.  When it is finally destroyed, we double check if we should bring it back
837         // up so we aren't going to lose anything by avoiding a second startup here.
838         boolean activityIsFinishing = mInCallActivity != null && !isActivityStarted();
839         if (activityIsFinishing) {
840             Log.i(this, "Undo the state change: " + newState + " -> " + mInCallState);
841             return mInCallState;
842         }
843 
844         if (showCallUi || showAccountPicker) {
845             Log.i(this, "Start in call UI");
846             showInCall(false /* showDialpad */, !showAccountPicker /* newOutgoingCall */);
847         } else if (startStartupSequence) {
848             Log.i(this, "Start Full Screen in call UI");
849 
850             // We're about the bring up the in-call UI for an incoming call. If we still have
851             // dialogs up, we need to clear them out before showing incoming screen.
852             if (isActivityStarted()) {
853                 mInCallActivity.dismissPendingDialogs();
854             }
855             if (!startUi(newState)) {
856                 // startUI refused to start the UI. This indicates that it needed to restart the
857                 // activity.  When it finally restarts, it will call us back, so we do not actually
858                 // change the state yet (we return mInCallState instead of newState).
859                 return mInCallState;
860             }
861         } else if (newState == InCallState.NO_CALLS) {
862             // The new state is the no calls state.  Tear everything down.
863             attemptFinishActivity();
864             attemptCleanup();
865         }
866 
867         return newState;
868     }
869 
870     /**
871      * Sets the DisconnectCause for a call that was disconnected because it was missing a
872      * PhoneAccount or PhoneAccounts to select from.
873      * @param call
874      */
setDisconnectCauseForMissingAccounts(Call call)875     private void setDisconnectCauseForMissingAccounts(Call call) {
876         android.telecom.Call telecomCall = call.getTelecommCall();
877 
878         Bundle extras = telecomCall.getDetails().getExtras();
879         // Initialize the extras bundle to avoid NPE
880         if (extras == null) {
881             extras = new Bundle();
882         }
883 
884         final List<PhoneAccountHandle> phoneAccountHandles = extras.getParcelableArrayList(
885                 android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS);
886 
887         if (phoneAccountHandles == null || phoneAccountHandles.isEmpty()) {
888             String scheme = telecomCall.getDetails().getHandle().getScheme();
889             final String errorMsg = PhoneAccount.SCHEME_TEL.equals(scheme) ?
890                     mContext.getString(R.string.callFailed_simError) :
891                         mContext.getString(R.string.incall_error_supp_service_unknown);
892             DisconnectCause disconnectCause =
893                     new DisconnectCause(DisconnectCause.ERROR, null, errorMsg, errorMsg);
894             call.setDisconnectCause(disconnectCause);
895         }
896     }
897 
startUi(InCallState inCallState)898     private boolean startUi(InCallState inCallState) {
899         boolean isCallWaiting = mCallList.getActiveCall() != null &&
900                 mCallList.getIncomingCall() != null;
901 
902         // If the screen is off, we need to make sure it gets turned on for incoming calls.
903         // This normally works just fine thanks to FLAG_TURN_SCREEN_ON but that only works
904         // when the activity is first created. Therefore, to ensure the screen is turned on
905         // for the call waiting case, we finish() the current activity and start a new one.
906         // There should be no jank from this since the screen is already off and will remain so
907         // until our new activity is up.
908 
909         if (isCallWaiting) {
910             if (mProximitySensor.isScreenReallyOff() && isActivityStarted()) {
911                 mInCallActivity.finish();
912                 // When the activity actually finishes, we will start it again if there are
913                 // any active calls, so we do not need to start it explicitly here. Note, we
914                 // actually get called back on this function to restart it.
915 
916                 // We return false to indicate that we did not actually start the UI.
917                 return false;
918             } else {
919                 showInCall(false, false);
920             }
921         } else {
922             mStatusBarNotifier.updateNotification(inCallState, mCallList);
923         }
924         return true;
925     }
926 
927     /**
928      * Checks to see if both the UI is gone and the service is disconnected. If so, tear it all
929      * down.
930      */
attemptCleanup()931     private void attemptCleanup() {
932         boolean shouldCleanup = (mInCallActivity == null && !mServiceConnected &&
933                 mInCallState == InCallState.NO_CALLS);
934         Log.i(this, "attemptCleanup? " + shouldCleanup);
935 
936         if (shouldCleanup) {
937             mIsActivityPreviouslyStarted = false;
938 
939             // blow away stale contact info so that we get fresh data on
940             // the next set of calls
941             if (mContactInfoCache != null) {
942                 mContactInfoCache.clearCache();
943             }
944             mContactInfoCache = null;
945 
946             if (mProximitySensor != null) {
947                 removeListener(mProximitySensor);
948                 mProximitySensor.tearDown();
949             }
950             mProximitySensor = null;
951 
952             mAudioModeProvider = null;
953 
954             if (mStatusBarNotifier != null) {
955                 removeListener(mStatusBarNotifier);
956             }
957             mStatusBarNotifier = null;
958 
959             if (mCallList != null) {
960                 mCallList.removeListener(this);
961             }
962             mCallList = null;
963 
964             mContext = null;
965             mInCallActivity = null;
966 
967             mListeners.clear();
968             mIncomingCallListeners.clear();
969 
970             Log.d(this, "Finished InCallPresenter.CleanUp");
971         }
972     }
973 
showInCall(boolean showDialpad, boolean newOutgoingCall)974     private void showInCall(boolean showDialpad, boolean newOutgoingCall) {
975         mContext.startActivity(getInCallIntent(showDialpad, newOutgoingCall));
976     }
977 
getInCallIntent(boolean showDialpad, boolean newOutgoingCall)978     public Intent getInCallIntent(boolean showDialpad, boolean newOutgoingCall) {
979         final Intent intent = new Intent(Intent.ACTION_MAIN, null);
980         intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
981                 | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
982                 | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
983         intent.setClass(mContext, InCallActivity.class);
984         if (showDialpad) {
985             intent.putExtra(InCallActivity.SHOW_DIALPAD_EXTRA, true);
986         }
987 
988         intent.putExtra(InCallActivity.NEW_OUTGOING_CALL, newOutgoingCall);
989         return intent;
990     }
991 
992     /**
993      * Retrieves the current in-call camera manager instance, creating if necessary.
994      *
995      * @return The {@link InCallCameraManager}.
996      */
getInCallCameraManager()997     public InCallCameraManager getInCallCameraManager() {
998         synchronized(this) {
999             if (mInCallCameraManager == null) {
1000                 mInCallCameraManager = new InCallCameraManager(mContext);
1001             }
1002 
1003             return mInCallCameraManager;
1004         }
1005     }
1006 
1007     /**
1008      * Handles changes to the device rotation.
1009      *
1010      * @param rotation The device rotation.
1011      */
onDeviceRotationChange(int rotation)1012     public void onDeviceRotationChange(int rotation) {
1013         // First translate to rotation in degrees.
1014         int rotationAngle;
1015         switch (rotation) {
1016             case Surface.ROTATION_0:
1017                 rotationAngle = 0;
1018                 break;
1019             case Surface.ROTATION_90:
1020                 rotationAngle = 90;
1021                 break;
1022             case Surface.ROTATION_180:
1023                 rotationAngle = 180;
1024                 break;
1025             case Surface.ROTATION_270:
1026                 rotationAngle = 270;
1027                 break;
1028             default:
1029                 rotationAngle = 0;
1030         }
1031 
1032         mCallList.notifyCallsOfDeviceRotation(rotationAngle);
1033     }
1034 
1035     /**
1036      * Notifies listeners of changes in orientation (e.g. portrait/landscape).
1037      *
1038      * @param orientation The orientation of the device.
1039      */
onDeviceOrientationChange(int orientation)1040     public void onDeviceOrientationChange(int orientation) {
1041         for (InCallOrientationListener listener : mOrientationListeners) {
1042             listener.onDeviceOrientationChanged(orientation);
1043         }
1044     }
1045 
1046     /**
1047      * Configures the in-call UI activity so it can change orientations or not.
1048      *
1049      * @param allowOrientationChange {@code True} if the in-call UI can change between portrait
1050      *      and landscape.  {@Code False} if the in-call UI should be locked in portrait.
1051      */
setInCallAllowsOrientationChange(boolean allowOrientationChange)1052     public void setInCallAllowsOrientationChange(boolean allowOrientationChange) {
1053         if (!allowOrientationChange) {
1054             mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
1055         } else {
1056             mInCallActivity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
1057         }
1058     }
1059 
1060     /**
1061      * Returns the space available beside the call card.
1062      *
1063      * @return The space beside the call card.
1064      */
getSpaceBesideCallCard()1065     public float getSpaceBesideCallCard() {
1066         return mInCallActivity.getCallCardFragment().getSpaceBesideCallCard();
1067     }
1068 
1069     /**
1070      * Returns whether the call card fragment is currently visible.
1071      *
1072      * @return True if the call card fragment is visible.
1073      */
getCallCardFragmentVisible()1074     public boolean getCallCardFragmentVisible() {
1075         if (mInCallActivity != null) {
1076             return mInCallActivity.getCallCardFragment().isVisible();
1077         }
1078         return false;
1079     }
1080 
1081     /**
1082      * @return True if the application is currently running in a right-to-left locale.
1083      */
isRtl()1084     public static boolean isRtl() {
1085         return TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) ==
1086                 View.LAYOUT_DIRECTION_RTL;
1087     }
1088 
1089     /**
1090      * Private constructor. Must use getInstance() to get this singleton.
1091      */
InCallPresenter()1092     private InCallPresenter() {
1093     }
1094 
1095     /**
1096      * All the main states of InCallActivity.
1097      */
1098     public enum InCallState {
1099         // InCall Screen is off and there are no calls
1100         NO_CALLS,
1101 
1102         // Incoming-call screen is up
1103         INCOMING,
1104 
1105         // In-call experience is showing
1106         INCALL,
1107 
1108         // Waiting for user input before placing outgoing call
1109         WAITING_FOR_ACCOUNT,
1110 
1111         // UI is starting up but no call has been initiated yet.
1112         // The UI is waiting for Telecomm to respond.
1113         PENDING_OUTGOING,
1114 
1115         // User is dialing out
1116         OUTGOING;
1117 
isIncoming()1118         public boolean isIncoming() {
1119             return (this == INCOMING);
1120         }
1121 
isConnectingOrConnected()1122         public boolean isConnectingOrConnected() {
1123             return (this == INCOMING ||
1124                     this == OUTGOING ||
1125                     this == INCALL);
1126         }
1127     }
1128 
1129     /**
1130      * Interface implemented by classes that need to know about the InCall State.
1131      */
1132     public interface InCallStateListener {
1133         // TODO: Enhance state to contain the call objects instead of passing CallList
onStateChange(InCallState oldState, InCallState newState, CallList callList)1134         public void onStateChange(InCallState oldState, InCallState newState, CallList callList);
1135     }
1136 
1137     public interface IncomingCallListener {
onIncomingCall(InCallState oldState, InCallState newState, Call call)1138         public void onIncomingCall(InCallState oldState, InCallState newState, Call call);
1139     }
1140 
1141     public interface InCallDetailsListener {
onDetailsChanged(Call call, android.telecom.Call.Details details)1142         public void onDetailsChanged(Call call, android.telecom.Call.Details details);
1143     }
1144 
1145     public interface InCallOrientationListener {
onDeviceOrientationChanged(int orientation)1146         public void onDeviceOrientationChanged(int orientation);
1147     }
1148 
1149     /**
1150      * Interface implemented by classes that need to know about events which occur within the
1151      * In-Call UI.  Used as a means of communicating between fragments that make up the UI.
1152      */
1153     public interface InCallEventListener {
onFullScreenVideoStateChanged(boolean isFullScreenVideo)1154         public void onFullScreenVideoStateChanged(boolean isFullScreenVideo);
1155     }
1156 }
1157