1/*
2 * Copyright (C) 2014 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
17package com.android.server.telecom;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.drawable.Drawable;
22import android.net.Uri;
23import android.os.Bundle;
24import android.os.Handler;
25import android.os.Trace;
26import android.provider.ContactsContract.Contacts;
27import android.telecom.CallState;
28import android.telecom.DisconnectCause;
29import android.telecom.Connection;
30import android.telecom.GatewayInfo;
31import android.telecom.ParcelableConnection;
32import android.telecom.PhoneAccount;
33import android.telecom.PhoneAccountHandle;
34import android.telecom.Response;
35import android.telecom.StatusHints;
36import android.telecom.TelecomManager;
37import android.telecom.VideoProfile;
38import android.telephony.PhoneNumberUtils;
39import android.text.TextUtils;
40
41import com.android.internal.telecom.IVideoProvider;
42import com.android.internal.telephony.CallerInfo;
43import com.android.internal.telephony.CallerInfoAsyncQuery;
44import com.android.internal.telephony.CallerInfoAsyncQuery.OnQueryCompleteListener;
45import com.android.internal.telephony.SmsApplication;
46import com.android.server.telecom.ContactsAsyncHelper.OnImageLoadCompleteListener;
47import com.android.internal.util.Preconditions;
48
49import java.util.ArrayList;
50import java.util.Collections;
51import java.util.LinkedList;
52import java.util.List;
53import java.util.Locale;
54import java.util.Objects;
55import java.util.Set;
56import java.util.concurrent.ConcurrentHashMap;
57
58/**
59 *  Encapsulates all aspects of a given phone call throughout its lifecycle, starting
60 *  from the time the call intent was received by Telecom (vs. the time the call was
61 *  connected etc).
62 */
63final class Call implements CreateConnectionResponse {
64    /**
65     * Listener for events on the call.
66     */
67    interface Listener {
68        void onSuccessfulOutgoingCall(Call call, int callState);
69        void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause);
70        void onSuccessfulIncomingCall(Call call);
71        void onFailedIncomingCall(Call call);
72        void onSuccessfulUnknownCall(Call call, int callState);
73        void onFailedUnknownCall(Call call);
74        void onRingbackRequested(Call call, boolean ringbackRequested);
75        void onPostDialWait(Call call, String remaining);
76        void onPostDialChar(Call call, char nextChar);
77        void onConnectionCapabilitiesChanged(Call call);
78        void onParentChanged(Call call);
79        void onChildrenChanged(Call call);
80        void onCannedSmsResponsesLoaded(Call call);
81        void onVideoCallProviderChanged(Call call);
82        void onCallerInfoChanged(Call call);
83        void onIsVoipAudioModeChanged(Call call);
84        void onStatusHintsChanged(Call call);
85        void onHandleChanged(Call call);
86        void onCallerDisplayNameChanged(Call call);
87        void onVideoStateChanged(Call call);
88        void onTargetPhoneAccountChanged(Call call);
89        void onConnectionManagerPhoneAccountChanged(Call call);
90        void onPhoneAccountChanged(Call call);
91        void onConferenceableCallsChanged(Call call);
92        boolean onCanceledViaNewOutgoingCallBroadcast(Call call);
93    }
94
95    abstract static class ListenerBase implements Listener {
96        @Override
97        public void onSuccessfulOutgoingCall(Call call, int callState) {}
98        @Override
99        public void onFailedOutgoingCall(Call call, DisconnectCause disconnectCause) {}
100        @Override
101        public void onSuccessfulIncomingCall(Call call) {}
102        @Override
103        public void onFailedIncomingCall(Call call) {}
104        @Override
105        public void onSuccessfulUnknownCall(Call call, int callState) {}
106        @Override
107        public void onFailedUnknownCall(Call call) {}
108        @Override
109        public void onRingbackRequested(Call call, boolean ringbackRequested) {}
110        @Override
111        public void onPostDialWait(Call call, String remaining) {}
112        @Override
113        public void onPostDialChar(Call call, char nextChar) {}
114        @Override
115        public void onConnectionCapabilitiesChanged(Call call) {}
116        @Override
117        public void onParentChanged(Call call) {}
118        @Override
119        public void onChildrenChanged(Call call) {}
120        @Override
121        public void onCannedSmsResponsesLoaded(Call call) {}
122        @Override
123        public void onVideoCallProviderChanged(Call call) {}
124        @Override
125        public void onCallerInfoChanged(Call call) {}
126        @Override
127        public void onIsVoipAudioModeChanged(Call call) {}
128        @Override
129        public void onStatusHintsChanged(Call call) {}
130        @Override
131        public void onHandleChanged(Call call) {}
132        @Override
133        public void onCallerDisplayNameChanged(Call call) {}
134        @Override
135        public void onVideoStateChanged(Call call) {}
136        @Override
137        public void onTargetPhoneAccountChanged(Call call) {}
138        @Override
139        public void onConnectionManagerPhoneAccountChanged(Call call) {}
140        @Override
141        public void onPhoneAccountChanged(Call call) {}
142        @Override
143        public void onConferenceableCallsChanged(Call call) {}
144        @Override
145        public boolean onCanceledViaNewOutgoingCallBroadcast(Call call) {
146            return false;
147        }
148    }
149
150    private static final OnQueryCompleteListener sCallerInfoQueryListener =
151            new OnQueryCompleteListener() {
152                /** ${inheritDoc} */
153                @Override
154                public void onQueryComplete(int token, Object cookie, CallerInfo callerInfo) {
155                    if (cookie != null) {
156                        ((Call) cookie).setCallerInfo(callerInfo, token);
157                    }
158                }
159            };
160
161    private static final OnImageLoadCompleteListener sPhotoLoadListener =
162            new OnImageLoadCompleteListener() {
163                /** ${inheritDoc} */
164                @Override
165                public void onImageLoadComplete(
166                        int token, Drawable photo, Bitmap photoIcon, Object cookie) {
167                    if (cookie != null) {
168                        ((Call) cookie).setPhoto(photo, photoIcon, token);
169                    }
170                }
171            };
172
173    private final Runnable mDirectToVoicemailRunnable = new Runnable() {
174        @Override
175        public void run() {
176            processDirectToVoicemail();
177        }
178    };
179
180    /** True if this is an incoming call. */
181    private final boolean mIsIncoming;
182
183    /** True if this is a currently unknown call that was not previously tracked by CallsManager,
184     *  and did not originate via the regular incoming/outgoing call code paths.
185     */
186    private boolean mIsUnknown;
187
188    /**
189     * The time this call was created. Beyond logging and such, may also be used for bookkeeping
190     * and specifically for marking certain call attempts as failed attempts.
191     */
192    private long mCreationTimeMillis = System.currentTimeMillis();
193
194    /** The time this call was made active. */
195    private long mConnectTimeMillis = 0;
196
197    /** The time this call was disconnected. */
198    private long mDisconnectTimeMillis = 0;
199
200    /** The gateway information associated with this call. This stores the original call handle
201     * that the user is attempting to connect to via the gateway, the actual handle to dial in
202     * order to connect the call via the gateway, as well as the package name of the gateway
203     * service. */
204    private GatewayInfo mGatewayInfo;
205
206    private PhoneAccountHandle mConnectionManagerPhoneAccountHandle;
207
208    private PhoneAccountHandle mTargetPhoneAccountHandle;
209
210    private final Handler mHandler = new Handler();
211
212    private final List<Call> mConferenceableCalls = new ArrayList<>();
213
214    /** The state of the call. */
215    private int mState;
216
217    /** The handle with which to establish this call. */
218    private Uri mHandle;
219
220    /**
221     * The presentation requirements for the handle. See {@link TelecomManager} for valid values.
222     */
223    private int mHandlePresentation;
224
225    /** The caller display name (CNAP) set by the connection service. */
226    private String mCallerDisplayName;
227
228    /**
229     * The presentation requirements for the handle. See {@link TelecomManager} for valid values.
230     */
231    private int mCallerDisplayNamePresentation;
232
233    /**
234     * The connection service which is attempted or already connecting this call.
235     */
236    private ConnectionServiceWrapper mConnectionService;
237
238    private boolean mIsEmergencyCall;
239
240    private boolean mSpeakerphoneOn;
241
242    /**
243     * Tracks the video states which were applicable over the duration of a call.
244     * See {@link VideoProfile} for a list of valid video states.
245     */
246    private int mVideoStateHistory;
247
248    private int mVideoState;
249
250    /**
251     * Disconnect cause for the call. Only valid if the state of the call is STATE_DISCONNECTED.
252     * See {@link android.telecom.DisconnectCause}.
253     */
254    private DisconnectCause mDisconnectCause = new DisconnectCause(DisconnectCause.UNKNOWN);
255
256    /** Info used by the connection services. */
257    private Bundle mExtras = Bundle.EMPTY;
258
259    /** Set of listeners on this call.
260     *
261     * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
262     * load factor before resizing, 1 means we only expect a single thread to
263     * access the map so make only a single shard
264     */
265    private final Set<Listener> mListeners = Collections.newSetFromMap(
266            new ConcurrentHashMap<Listener, Boolean>(8, 0.9f, 1));
267
268    private CreateConnectionProcessor mCreateConnectionProcessor;
269
270    /** Caller information retrieved from the latest contact query. */
271    private CallerInfo mCallerInfo;
272
273    /** The latest token used with a contact info query. */
274    private int mQueryToken = 0;
275
276    /** Whether this call is requesting that Telecom play the ringback tone on its behalf. */
277    private boolean mRingbackRequested = false;
278
279    /** Whether direct-to-voicemail query is pending. */
280    private boolean mDirectToVoicemailQueryPending;
281
282    private int mConnectionCapabilities;
283
284    private boolean mIsConference = false;
285
286    private Call mParentCall = null;
287
288    private List<Call> mChildCalls = new LinkedList<>();
289
290    /** Set of text message responses allowed for this call, if applicable. */
291    private List<String> mCannedSmsResponses = Collections.EMPTY_LIST;
292
293    /** Whether an attempt has been made to load the text message responses. */
294    private boolean mCannedSmsResponsesLoadingStarted = false;
295
296    private IVideoProvider mVideoProvider;
297
298    private boolean mIsVoipAudioMode;
299    private StatusHints mStatusHints;
300    private final ConnectionServiceRepository mRepository;
301    private final Context mContext;
302
303    private boolean mWasConferencePreviouslyMerged = false;
304
305    // For conferences which support merge/swap at their level, we retain a notion of an active call.
306    // This is used for BluetoothPhoneService.  In order to support hold/merge, it must have the notion
307    // of the current "active" call within the conference call. This maintains the "active" call and
308    // switches every time the user hits "swap".
309    private Call mConferenceLevelActiveCall = null;
310
311    private boolean mIsLocallyDisconnecting = false;
312
313    /**
314     * Persists the specified parameters and initializes the new instance.
315     *
316     * @param context The context.
317     * @param repository The connection service repository.
318     * @param handle The handle to dial.
319     * @param gatewayInfo Gateway information to use for the call.
320     * @param connectionManagerPhoneAccountHandle Account to use for the service managing the call.
321     *         This account must be one that was registered with the
322     *         {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
323     * @param targetPhoneAccountHandle Account information to use for the call. This account must be
324     *         one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
325     * @param isIncoming True if this is an incoming call.
326     */
327    Call(
328            Context context,
329            ConnectionServiceRepository repository,
330            Uri handle,
331            GatewayInfo gatewayInfo,
332            PhoneAccountHandle connectionManagerPhoneAccountHandle,
333            PhoneAccountHandle targetPhoneAccountHandle,
334            boolean isIncoming,
335            boolean isConference) {
336        mState = isConference ? CallState.ACTIVE : CallState.NEW;
337        mContext = context;
338        mRepository = repository;
339        setHandle(handle);
340        setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
341        mGatewayInfo = gatewayInfo;
342        setConnectionManagerPhoneAccount(connectionManagerPhoneAccountHandle);
343        setTargetPhoneAccount(targetPhoneAccountHandle);
344        mIsIncoming = isIncoming;
345        mIsConference = isConference;
346        maybeLoadCannedSmsResponses();
347    }
348
349    /**
350     * Persists the specified parameters and initializes the new instance.
351     *
352     * @param context The context.
353     * @param repository The connection service repository.
354     * @param handle The handle to dial.
355     * @param gatewayInfo Gateway information to use for the call.
356     * @param connectionManagerPhoneAccountHandle Account to use for the service managing the call.
357     *         This account must be one that was registered with the
358     *         {@link PhoneAccount#CAPABILITY_CONNECTION_MANAGER} flag.
359     * @param targetPhoneAccountHandle Account information to use for the call. This account must be
360     *         one that was registered with the {@link PhoneAccount#CAPABILITY_CALL_PROVIDER} flag.
361     * @param isIncoming True if this is an incoming call.
362     * @param connectTimeMillis The connection time of the call.
363     */
364    Call(
365            Context context,
366            ConnectionServiceRepository repository,
367            Uri handle,
368            GatewayInfo gatewayInfo,
369            PhoneAccountHandle connectionManagerPhoneAccountHandle,
370            PhoneAccountHandle targetPhoneAccountHandle,
371            boolean isIncoming,
372            boolean isConference,
373            long connectTimeMillis) {
374        this(context, repository, handle, gatewayInfo, connectionManagerPhoneAccountHandle,
375                targetPhoneAccountHandle, isIncoming, isConference);
376
377        mConnectTimeMillis = connectTimeMillis;
378    }
379
380    void addListener(Listener listener) {
381        mListeners.add(listener);
382    }
383
384    void removeListener(Listener listener) {
385        if (listener != null) {
386            mListeners.remove(listener);
387        }
388    }
389
390    /** {@inheritDoc} */
391    @Override
392    public String toString() {
393        String component = null;
394        if (mConnectionService != null && mConnectionService.getComponentName() != null) {
395            component = mConnectionService.getComponentName().flattenToShortString();
396        }
397
398        return String.format(Locale.US, "[%s, %s, %s, %s, %d, childs(%d), has_parent(%b), [%s]",
399                System.identityHashCode(this),
400                CallState.toString(mState),
401                component,
402                Log.piiHandle(mHandle),
403                getVideoState(),
404                getChildCalls().size(),
405                getParentCall() != null,
406                Connection.capabilitiesToString(getConnectionCapabilities()));
407    }
408
409    int getState() {
410        return mState;
411    }
412
413    private boolean shouldContinueProcessingAfterDisconnect() {
414        // Stop processing once the call is active.
415        if (!CreateConnectionTimeout.isCallBeingPlaced(this)) {
416            return false;
417        }
418
419        // Make sure that there are additional connection services to process.
420        if (mCreateConnectionProcessor == null
421            || !mCreateConnectionProcessor.isProcessingComplete()
422            || !mCreateConnectionProcessor.hasMorePhoneAccounts()) {
423            return false;
424        }
425
426        if (mDisconnectCause == null) {
427            return false;
428        }
429
430        // Continue processing if the current attempt failed or timed out.
431        return mDisconnectCause.getCode() == DisconnectCause.ERROR ||
432            mCreateConnectionProcessor.isCallTimedOut();
433    }
434
435    /**
436     * Sets the call state. Although there exists the notion of appropriate state transitions
437     * (see {@link CallState}), in practice those expectations break down when cellular systems
438     * misbehave and they do this very often. The result is that we do not enforce state transitions
439     * and instead keep the code resilient to unexpected state changes.
440     */
441    void setState(int newState) {
442        if (mState != newState) {
443            Log.v(this, "setState %s -> %s", mState, newState);
444
445            if (newState == CallState.DISCONNECTED && shouldContinueProcessingAfterDisconnect()) {
446                Log.w(this, "continuing processing disconnected call with another service");
447                mCreateConnectionProcessor.continueProcessingIfPossible(this, mDisconnectCause);
448                return;
449            }
450
451            mState = newState;
452            maybeLoadCannedSmsResponses();
453
454            if (mState == CallState.ACTIVE || mState == CallState.ON_HOLD) {
455                if (mConnectTimeMillis == 0) {
456                    // We check to see if mConnectTime is already set to prevent the
457                    // call from resetting active time when it goes in and out of
458                    // ACTIVE/ON_HOLD
459                    mConnectTimeMillis = System.currentTimeMillis();
460                }
461
462                // We're clearly not disconnected, so reset the disconnected time.
463                mDisconnectTimeMillis = 0;
464            } else if (mState == CallState.DISCONNECTED) {
465                mDisconnectTimeMillis = System.currentTimeMillis();
466                setLocallyDisconnecting(false);
467                fixParentAfterDisconnect();
468            }
469        }
470    }
471
472    void setRingbackRequested(boolean ringbackRequested) {
473        mRingbackRequested = ringbackRequested;
474        for (Listener l : mListeners) {
475            l.onRingbackRequested(this, mRingbackRequested);
476        }
477    }
478
479    boolean isRingbackRequested() {
480        return mRingbackRequested;
481    }
482
483    boolean isConference() {
484        return mIsConference;
485    }
486
487    Uri getHandle() {
488        return mHandle;
489    }
490
491    int getHandlePresentation() {
492        return mHandlePresentation;
493    }
494
495
496    void setHandle(Uri handle) {
497        setHandle(handle, TelecomManager.PRESENTATION_ALLOWED);
498    }
499
500    void setHandle(Uri handle, int presentation) {
501        if (!Objects.equals(handle, mHandle) || presentation != mHandlePresentation) {
502            mHandlePresentation = presentation;
503            if (mHandlePresentation == TelecomManager.PRESENTATION_RESTRICTED ||
504                    mHandlePresentation == TelecomManager.PRESENTATION_UNKNOWN) {
505                mHandle = null;
506            } else {
507                mHandle = handle;
508                if (mHandle != null && !PhoneAccount.SCHEME_VOICEMAIL.equals(mHandle.getScheme())
509                        && TextUtils.isEmpty(mHandle.getSchemeSpecificPart())) {
510                    // If the number is actually empty, set it to null, unless this is a
511                    // SCHEME_VOICEMAIL uri which always has an empty number.
512                    mHandle = null;
513                }
514            }
515
516            mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(mContext,
517                    mHandle.getSchemeSpecificPart());
518            startCallerInfoLookup();
519            for (Listener l : mListeners) {
520                l.onHandleChanged(this);
521            }
522        }
523    }
524
525    String getCallerDisplayName() {
526        return mCallerDisplayName;
527    }
528
529    int getCallerDisplayNamePresentation() {
530        return mCallerDisplayNamePresentation;
531    }
532
533    void setCallerDisplayName(String callerDisplayName, int presentation) {
534        if (!TextUtils.equals(callerDisplayName, mCallerDisplayName) ||
535                presentation != mCallerDisplayNamePresentation) {
536            mCallerDisplayName = callerDisplayName;
537            mCallerDisplayNamePresentation = presentation;
538            for (Listener l : mListeners) {
539                l.onCallerDisplayNameChanged(this);
540            }
541        }
542    }
543
544    String getName() {
545        return mCallerInfo == null ? null : mCallerInfo.name;
546    }
547
548    Bitmap getPhotoIcon() {
549        return mCallerInfo == null ? null : mCallerInfo.cachedPhotoIcon;
550    }
551
552    Drawable getPhoto() {
553        return mCallerInfo == null ? null : mCallerInfo.cachedPhoto;
554    }
555
556    /**
557     * @param disconnectCause The reason for the disconnection, represented by
558     *         {@link android.telecom.DisconnectCause}.
559     */
560    void setDisconnectCause(DisconnectCause disconnectCause) {
561        // TODO: Consider combining this method with a setDisconnected() method that is totally
562        // separate from setState.
563        mDisconnectCause = disconnectCause;
564    }
565
566    DisconnectCause getDisconnectCause() {
567        return mDisconnectCause;
568    }
569
570    boolean isEmergencyCall() {
571        return mIsEmergencyCall;
572    }
573
574    /**
575     * @return The original handle this call is associated with. In-call services should use this
576     * handle when indicating in their UI the handle that is being called.
577     */
578    public Uri getOriginalHandle() {
579        if (mGatewayInfo != null && !mGatewayInfo.isEmpty()) {
580            return mGatewayInfo.getOriginalAddress();
581        }
582        return getHandle();
583    }
584
585    GatewayInfo getGatewayInfo() {
586        return mGatewayInfo;
587    }
588
589    void setGatewayInfo(GatewayInfo gatewayInfo) {
590        mGatewayInfo = gatewayInfo;
591    }
592
593    PhoneAccountHandle getConnectionManagerPhoneAccount() {
594        return mConnectionManagerPhoneAccountHandle;
595    }
596
597    void setConnectionManagerPhoneAccount(PhoneAccountHandle accountHandle) {
598        if (!Objects.equals(mConnectionManagerPhoneAccountHandle, accountHandle)) {
599            mConnectionManagerPhoneAccountHandle = accountHandle;
600            for (Listener l : mListeners) {
601                l.onConnectionManagerPhoneAccountChanged(this);
602            }
603        }
604
605    }
606
607    PhoneAccountHandle getTargetPhoneAccount() {
608        return mTargetPhoneAccountHandle;
609    }
610
611    void setTargetPhoneAccount(PhoneAccountHandle accountHandle) {
612        if (!Objects.equals(mTargetPhoneAccountHandle, accountHandle)) {
613            mTargetPhoneAccountHandle = accountHandle;
614            for (Listener l : mListeners) {
615                l.onTargetPhoneAccountChanged(this);
616            }
617        }
618    }
619
620    boolean isIncoming() {
621        return mIsIncoming;
622    }
623
624    /**
625     * @return The "age" of this call object in milliseconds, which typically also represents the
626     *     period since this call was added to the set pending outgoing calls, see
627     *     mCreationTimeMillis.
628     */
629    long getAgeMillis() {
630        if (mState == CallState.DISCONNECTED &&
631                (mDisconnectCause.getCode() == DisconnectCause.REJECTED ||
632                 mDisconnectCause.getCode() == DisconnectCause.MISSED)) {
633            // Rejected and missed calls have no age. They're immortal!!
634            return 0;
635        } else if (mConnectTimeMillis == 0) {
636            // Age is measured in the amount of time the call was active. A zero connect time
637            // indicates that we never went active, so return 0 for the age.
638            return 0;
639        } else if (mDisconnectTimeMillis == 0) {
640            // We connected, but have not yet disconnected
641            return System.currentTimeMillis() - mConnectTimeMillis;
642        }
643
644        return mDisconnectTimeMillis - mConnectTimeMillis;
645    }
646
647    /**
648     * @return The time when this call object was created and added to the set of pending outgoing
649     *     calls.
650     */
651    long getCreationTimeMillis() {
652        return mCreationTimeMillis;
653    }
654
655    void setCreationTimeMillis(long time) {
656        mCreationTimeMillis = time;
657    }
658
659    long getConnectTimeMillis() {
660        return mConnectTimeMillis;
661    }
662
663    int getConnectionCapabilities() {
664        return mConnectionCapabilities;
665    }
666
667    void setConnectionCapabilities(int connectionCapabilities) {
668        setConnectionCapabilities(connectionCapabilities, false /* forceUpdate */);
669    }
670
671    void setConnectionCapabilities(int connectionCapabilities, boolean forceUpdate) {
672        Log.v(this, "setConnectionCapabilities: %s", Connection.capabilitiesToString(
673                connectionCapabilities));
674        if (forceUpdate || mConnectionCapabilities != connectionCapabilities) {
675           mConnectionCapabilities = connectionCapabilities;
676            for (Listener l : mListeners) {
677                l.onConnectionCapabilitiesChanged(this);
678            }
679        }
680    }
681
682    Call getParentCall() {
683        return mParentCall;
684    }
685
686    List<Call> getChildCalls() {
687        return mChildCalls;
688    }
689
690    boolean wasConferencePreviouslyMerged() {
691        return mWasConferencePreviouslyMerged;
692    }
693
694    Call getConferenceLevelActiveCall() {
695        return mConferenceLevelActiveCall;
696    }
697
698    ConnectionServiceWrapper getConnectionService() {
699        return mConnectionService;
700    }
701
702    /**
703     * Retrieves the {@link Context} for the call.
704     *
705     * @return The {@link Context}.
706     */
707    Context getContext() {
708        return mContext;
709    }
710
711    void setConnectionService(ConnectionServiceWrapper service) {
712        Preconditions.checkNotNull(service);
713
714        clearConnectionService();
715
716        service.incrementAssociatedCallCount();
717        mConnectionService = service;
718        mConnectionService.addCall(this);
719    }
720
721    /**
722     * Clears the associated connection service.
723     */
724    void clearConnectionService() {
725        if (mConnectionService != null) {
726            ConnectionServiceWrapper serviceTemp = mConnectionService;
727            mConnectionService = null;
728            serviceTemp.removeCall(this);
729
730            // Decrementing the count can cause the service to unbind, which itself can trigger the
731            // service-death code.  Since the service death code tries to clean up any associated
732            // calls, we need to make sure to remove that information (e.g., removeCall()) before
733            // we decrement. Technically, invoking removeCall() prior to decrementing is all that is
734            // necessary, but cleaning up mConnectionService prior to triggering an unbind is good
735            // to do.
736            decrementAssociatedCallCount(serviceTemp);
737        }
738    }
739
740    private void processDirectToVoicemail() {
741        if (mDirectToVoicemailQueryPending) {
742            if (mCallerInfo != null && mCallerInfo.shouldSendToVoicemail) {
743                Log.i(this, "Directing call to voicemail: %s.", this);
744                // TODO: Once we move State handling from CallsManager to Call, we
745                // will not need to set STATE_RINGING state prior to calling reject.
746                setState(CallState.RINGING);
747                reject(false, null);
748            } else {
749                // TODO: Make this class (not CallsManager) responsible for changing
750                // the call state to STATE_RINGING.
751
752                // TODO: Replace this with state transition to STATE_RINGING.
753                for (Listener l : mListeners) {
754                    l.onSuccessfulIncomingCall(this);
755                }
756            }
757
758            mDirectToVoicemailQueryPending = false;
759        }
760    }
761
762    /**
763     * Starts the create connection sequence. Upon completion, there should exist an active
764     * connection through a connection service (or the call will have failed).
765     *
766     * @param phoneAccountRegistrar The phone account registrar.
767     */
768    void startCreateConnection(PhoneAccountRegistrar phoneAccountRegistrar) {
769        Preconditions.checkState(mCreateConnectionProcessor == null);
770        mCreateConnectionProcessor = new CreateConnectionProcessor(this, mRepository, this,
771                phoneAccountRegistrar, mContext);
772        mCreateConnectionProcessor.process();
773    }
774
775    @Override
776    public void handleCreateConnectionSuccess(
777            CallIdMapper idMapper,
778            ParcelableConnection connection) {
779        Log.v(this, "handleCreateConnectionSuccessful %s", connection);
780        setTargetPhoneAccount(connection.getPhoneAccount());
781        setHandle(connection.getHandle(), connection.getHandlePresentation());
782        setCallerDisplayName(
783                connection.getCallerDisplayName(), connection.getCallerDisplayNamePresentation());
784        setConnectionCapabilities(connection.getConnectionCapabilities());
785        setVideoProvider(connection.getVideoProvider());
786        setVideoState(connection.getVideoState());
787        setRingbackRequested(connection.isRingbackRequested());
788        setIsVoipAudioMode(connection.getIsVoipAudioMode());
789        setStatusHints(connection.getStatusHints());
790
791        mConferenceableCalls.clear();
792        for (String id : connection.getConferenceableConnectionIds()) {
793            mConferenceableCalls.add(idMapper.getCall(id));
794        }
795
796        if (mIsUnknown) {
797            for (Listener l : mListeners) {
798                l.onSuccessfulUnknownCall(this, getStateFromConnectionState(connection.getState()));
799            }
800        } else if (mIsIncoming) {
801            // We do not handle incoming calls immediately when they are verified by the connection
802            // service. We allow the caller-info-query code to execute first so that we can read the
803            // direct-to-voicemail property before deciding if we want to show the incoming call to
804            // the user or if we want to reject the call.
805            mDirectToVoicemailQueryPending = true;
806
807            // Timeout the direct-to-voicemail lookup execution so that we dont wait too long before
808            // showing the user the incoming call screen.
809            mHandler.postDelayed(mDirectToVoicemailRunnable, Timeouts.getDirectToVoicemailMillis(
810                    mContext.getContentResolver()));
811        } else {
812            for (Listener l : mListeners) {
813                l.onSuccessfulOutgoingCall(this,
814                        getStateFromConnectionState(connection.getState()));
815            }
816        }
817    }
818
819    @Override
820    public void handleCreateConnectionFailure(DisconnectCause disconnectCause) {
821        clearConnectionService();
822        setDisconnectCause(disconnectCause);
823        CallsManager.getInstance().markCallAsDisconnected(this, disconnectCause);
824
825        if (mIsUnknown) {
826            for (Listener listener : mListeners) {
827                listener.onFailedUnknownCall(this);
828            }
829        } else if (mIsIncoming) {
830            for (Listener listener : mListeners) {
831                listener.onFailedIncomingCall(this);
832            }
833        } else {
834            for (Listener listener : mListeners) {
835                listener.onFailedOutgoingCall(this, disconnectCause);
836            }
837        }
838    }
839
840    /**
841     * Plays the specified DTMF tone.
842     */
843    void playDtmfTone(char digit) {
844        if (mConnectionService == null) {
845            Log.w(this, "playDtmfTone() request on a call without a connection service.");
846        } else {
847            Log.i(this, "Send playDtmfTone to connection service for call %s", this);
848            mConnectionService.playDtmfTone(this, digit);
849        }
850    }
851
852    /**
853     * Stops playing any currently playing DTMF tone.
854     */
855    void stopDtmfTone() {
856        if (mConnectionService == null) {
857            Log.w(this, "stopDtmfTone() request on a call without a connection service.");
858        } else {
859            Log.i(this, "Send stopDtmfTone to connection service for call %s", this);
860            mConnectionService.stopDtmfTone(this);
861        }
862    }
863
864    void disconnect() {
865        disconnect(false);
866    }
867
868    /**
869     * Attempts to disconnect the call through the connection service.
870     */
871    void disconnect(boolean wasViaNewOutgoingCallBroadcaster) {
872        // Track that the call is now locally disconnecting.
873        setLocallyDisconnecting(true);
874
875        if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT ||
876                mState == CallState.CONNECTING) {
877            Log.v(this, "Aborting call %s", this);
878            abort(wasViaNewOutgoingCallBroadcaster);
879        } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
880            if (mConnectionService == null) {
881                Log.e(this, new Exception(), "disconnect() request on a call without a"
882                        + " connection service.");
883            } else {
884                Log.i(this, "Send disconnect to connection service for call: %s", this);
885                // The call isn't officially disconnected until the connection service
886                // confirms that the call was actually disconnected. Only then is the
887                // association between call and connection service severed, see
888                // {@link CallsManager#markCallAsDisconnected}.
889                mConnectionService.disconnect(this);
890            }
891        }
892    }
893
894    void abort(boolean wasViaNewOutgoingCallBroadcaster) {
895        if (mCreateConnectionProcessor != null &&
896                !mCreateConnectionProcessor.isProcessingComplete()) {
897            mCreateConnectionProcessor.abort();
898        } else if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT
899                || mState == CallState.CONNECTING) {
900            if (wasViaNewOutgoingCallBroadcaster) {
901                // If the cancelation was from NEW_OUTGOING_CALL, then we do not automatically
902                // destroy the call.  Instead, we announce the cancelation and CallsManager handles
903                // it through a timer. Since apps often cancel calls through NEW_OUTGOING_CALL and
904                // then re-dial them quickly using a gateway, allowing the first call to end
905                // causes jank. This timeout allows CallsManager to transition the first call into
906                // the second call so that in-call only ever sees a single call...eliminating the
907                // jank altogether.
908                for (Listener listener : mListeners) {
909                    if (listener.onCanceledViaNewOutgoingCallBroadcast(this)) {
910                        // The first listener to handle this wins. A return value of true means that
911                        // the listener will handle the disconnection process later and so we
912                        // should not continue it here.
913                        setLocallyDisconnecting(false);
914                        return;
915                    }
916                }
917            }
918
919            handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.CANCELED));
920        } else {
921            Log.v(this, "Cannot abort a call which isn't either PRE_DIAL_WAIT or CONNECTING");
922        }
923    }
924
925    /**
926     * Answers the call if it is ringing.
927     *
928     * @param videoState The video state in which to answer the call.
929     */
930    void answer(int videoState) {
931        Preconditions.checkNotNull(mConnectionService);
932
933        // Check to verify that the call is still in the ringing state. A call can change states
934        // between the time the user hits 'answer' and Telecom receives the command.
935        if (isRinging("answer")) {
936            // At this point, we are asking the connection service to answer but we don't assume
937            // that it will work. Instead, we wait until confirmation from the connectino service
938            // that the call is in a non-STATE_RINGING state before changing the UI. See
939            // {@link ConnectionServiceAdapter#setActive} and other set* methods.
940            mConnectionService.answer(this, videoState);
941        }
942    }
943
944    /**
945     * Rejects the call if it is ringing.
946     *
947     * @param rejectWithMessage Whether to send a text message as part of the call rejection.
948     * @param textMessage An optional text message to send as part of the rejection.
949     */
950    void reject(boolean rejectWithMessage, String textMessage) {
951        Preconditions.checkNotNull(mConnectionService);
952
953        // Check to verify that the call is still in the ringing state. A call can change states
954        // between the time the user hits 'reject' and Telecomm receives the command.
955        if (isRinging("reject")) {
956            mConnectionService.reject(this);
957        }
958    }
959
960    /**
961     * Puts the call on hold if it is currently active.
962     */
963    void hold() {
964        Preconditions.checkNotNull(mConnectionService);
965
966        if (mState == CallState.ACTIVE) {
967            mConnectionService.hold(this);
968        }
969    }
970
971    /**
972     * Releases the call from hold if it is currently active.
973     */
974    void unhold() {
975        Preconditions.checkNotNull(mConnectionService);
976
977        if (mState == CallState.ON_HOLD) {
978            mConnectionService.unhold(this);
979        }
980    }
981
982    /** Checks if this is a live call or not. */
983    boolean isAlive() {
984        switch (mState) {
985            case CallState.NEW:
986            case CallState.RINGING:
987            case CallState.DISCONNECTED:
988            case CallState.ABORTED:
989                return false;
990            default:
991                return true;
992        }
993    }
994
995    boolean isActive() {
996        return mState == CallState.ACTIVE;
997    }
998
999    Bundle getExtras() {
1000        return mExtras;
1001    }
1002
1003    void setExtras(Bundle extras) {
1004        mExtras = extras;
1005    }
1006
1007    /**
1008     * @return the uri of the contact associated with this call.
1009     */
1010    Uri getContactUri() {
1011        if (mCallerInfo == null || !mCallerInfo.contactExists) {
1012            return getHandle();
1013        }
1014        return Contacts.getLookupUri(mCallerInfo.contactIdOrZero, mCallerInfo.lookupKey);
1015    }
1016
1017    Uri getRingtone() {
1018        return mCallerInfo == null ? null : mCallerInfo.contactRingtoneUri;
1019    }
1020
1021    void onPostDialWait(String remaining) {
1022        for (Listener l : mListeners) {
1023            l.onPostDialWait(this, remaining);
1024        }
1025    }
1026
1027    void onPostDialChar(char nextChar) {
1028        for (Listener l : mListeners) {
1029            l.onPostDialChar(this, nextChar);
1030        }
1031    }
1032
1033    void postDialContinue(boolean proceed) {
1034        mConnectionService.onPostDialContinue(this, proceed);
1035    }
1036
1037    void conferenceWith(Call otherCall) {
1038        if (mConnectionService == null) {
1039            Log.w(this, "conference requested on a call without a connection service.");
1040        } else {
1041            mConnectionService.conference(this, otherCall);
1042        }
1043    }
1044
1045    void splitFromConference() {
1046        if (mConnectionService == null) {
1047            Log.w(this, "splitting from conference call without a connection service");
1048        } else {
1049            mConnectionService.splitFromConference(this);
1050        }
1051    }
1052
1053    void mergeConference() {
1054        if (mConnectionService == null) {
1055            Log.w(this, "merging conference calls without a connection service.");
1056        } else if (can(Connection.CAPABILITY_MERGE_CONFERENCE)) {
1057            mConnectionService.mergeConference(this);
1058            mWasConferencePreviouslyMerged = true;
1059        }
1060    }
1061
1062    void swapConference() {
1063        if (mConnectionService == null) {
1064            Log.w(this, "swapping conference calls without a connection service.");
1065        } else if (can(Connection.CAPABILITY_SWAP_CONFERENCE)) {
1066            mConnectionService.swapConference(this);
1067            switch (mChildCalls.size()) {
1068                case 1:
1069                    mConferenceLevelActiveCall = mChildCalls.get(0);
1070                    break;
1071                case 2:
1072                    // swap
1073                    mConferenceLevelActiveCall = mChildCalls.get(0) == mConferenceLevelActiveCall ?
1074                            mChildCalls.get(1) : mChildCalls.get(0);
1075                    break;
1076                default:
1077                    // For anything else 0, or 3+, set it to null since it is impossible to tell.
1078                    mConferenceLevelActiveCall = null;
1079                    break;
1080            }
1081        }
1082    }
1083
1084    void setParentCall(Call parentCall) {
1085        if (parentCall == this) {
1086            Log.e(this, new Exception(), "setting the parent to self");
1087            return;
1088        }
1089        if (parentCall == mParentCall) {
1090            // nothing to do
1091            return;
1092        }
1093        Preconditions.checkState(parentCall == null || mParentCall == null);
1094
1095        Call oldParent = mParentCall;
1096        if (mParentCall != null) {
1097            mParentCall.removeChildCall(this);
1098        }
1099        mParentCall = parentCall;
1100        if (mParentCall != null) {
1101            mParentCall.addChildCall(this);
1102        }
1103
1104        for (Listener l : mListeners) {
1105            l.onParentChanged(this);
1106        }
1107    }
1108
1109    void setConferenceableCalls(List<Call> conferenceableCalls) {
1110        mConferenceableCalls.clear();
1111        mConferenceableCalls.addAll(conferenceableCalls);
1112
1113        for (Listener l : mListeners) {
1114            l.onConferenceableCallsChanged(this);
1115        }
1116    }
1117
1118    List<Call> getConferenceableCalls() {
1119        return mConferenceableCalls;
1120    }
1121
1122    boolean can(int capability) {
1123        return (mConnectionCapabilities & capability) == capability;
1124    }
1125
1126    private void addChildCall(Call call) {
1127        if (!mChildCalls.contains(call)) {
1128            // Set the pseudo-active call to the latest child added to the conference.
1129            // See definition of mConferenceLevelActiveCall for more detail.
1130            mConferenceLevelActiveCall = call;
1131            mChildCalls.add(call);
1132
1133            for (Listener l : mListeners) {
1134                l.onChildrenChanged(this);
1135            }
1136        }
1137    }
1138
1139    private void removeChildCall(Call call) {
1140        if (mChildCalls.remove(call)) {
1141            for (Listener l : mListeners) {
1142                l.onChildrenChanged(this);
1143            }
1144        }
1145    }
1146
1147    /**
1148     * Return whether the user can respond to this {@code Call} via an SMS message.
1149     *
1150     * @return true if the "Respond via SMS" feature should be enabled
1151     * for this incoming call.
1152     *
1153     * The general rule is that we *do* allow "Respond via SMS" except for
1154     * the few (relatively rare) cases where we know for sure it won't
1155     * work, namely:
1156     *   - a bogus or blank incoming number
1157     *   - a call from a SIP address
1158     *   - a "call presentation" that doesn't allow the number to be revealed
1159     *
1160     * In all other cases, we allow the user to respond via SMS.
1161     *
1162     * Note that this behavior isn't perfect; for example we have no way
1163     * to detect whether the incoming call is from a landline (with most
1164     * networks at least), so we still enable this feature even though
1165     * SMSes to that number will silently fail.
1166     */
1167    boolean isRespondViaSmsCapable() {
1168        if (mState != CallState.RINGING) {
1169            return false;
1170        }
1171
1172        if (getHandle() == null) {
1173            // No incoming number known or call presentation is "PRESENTATION_RESTRICTED", in
1174            // other words, the user should not be able to see the incoming phone number.
1175            return false;
1176        }
1177
1178        if (PhoneNumberUtils.isUriNumber(getHandle().toString())) {
1179            // The incoming number is actually a URI (i.e. a SIP address),
1180            // not a regular PSTN phone number, and we can't send SMSes to
1181            // SIP addresses.
1182            // (TODO: That might still be possible eventually, though. Is
1183            // there some SIP-specific equivalent to sending a text message?)
1184            return false;
1185        }
1186
1187        // Is there a valid SMS application on the phone?
1188        if (SmsApplication.getDefaultRespondViaMessageApplication(mContext,
1189                true /*updateIfNeeded*/) == null) {
1190            return false;
1191        }
1192
1193        // TODO: with some carriers (in certain countries) you *can* actually
1194        // tell whether a given number is a mobile phone or not. So in that
1195        // case we could potentially return false here if the incoming call is
1196        // from a land line.
1197
1198        // If none of the above special cases apply, it's OK to enable the
1199        // "Respond via SMS" feature.
1200        return true;
1201    }
1202
1203    List<String> getCannedSmsResponses() {
1204        return mCannedSmsResponses;
1205    }
1206
1207    /**
1208     * We need to make sure that before we move a call to the disconnected state, it no
1209     * longer has any parent/child relationships.  We want to do this to ensure that the InCall
1210     * Service always has the right data in the right order.  We also want to do it in telecom so
1211     * that the insurance policy lives in the framework side of things.
1212     */
1213    private void fixParentAfterDisconnect() {
1214        setParentCall(null);
1215    }
1216
1217    /**
1218     * @return True if the call is ringing, else logs the action name.
1219     */
1220    private boolean isRinging(String actionName) {
1221        if (mState == CallState.RINGING) {
1222            return true;
1223        }
1224
1225        Log.i(this, "Request to %s a non-ringing call %s", actionName, this);
1226        return false;
1227    }
1228
1229    @SuppressWarnings("rawtypes")
1230    private void decrementAssociatedCallCount(ServiceBinder binder) {
1231        if (binder != null) {
1232            binder.decrementAssociatedCallCount();
1233        }
1234    }
1235
1236    /**
1237     * Looks up contact information based on the current handle.
1238     */
1239    private void startCallerInfoLookup() {
1240        String number = mHandle == null ? null : mHandle.getSchemeSpecificPart();
1241
1242        mQueryToken++;  // Updated so that previous queries can no longer set the information.
1243        mCallerInfo = null;
1244        if (!TextUtils.isEmpty(number)) {
1245            Log.v(this, "Looking up information for: %s.", Log.piiHandle(number));
1246            CallerInfoAsyncQuery.startQuery(
1247                    mQueryToken,
1248                    mContext,
1249                    number,
1250                    sCallerInfoQueryListener,
1251                    this);
1252        }
1253    }
1254
1255    /**
1256     * Saves the specified caller info if the specified token matches that of the last query
1257     * that was made.
1258     *
1259     * @param callerInfo The new caller information to set.
1260     * @param token The token used with this query.
1261     */
1262    private void setCallerInfo(CallerInfo callerInfo, int token) {
1263        Trace.beginSection("setCallerInfo");
1264        Preconditions.checkNotNull(callerInfo);
1265
1266        if (mQueryToken == token) {
1267            mCallerInfo = callerInfo;
1268            Log.i(this, "CallerInfo received for %s: %s", Log.piiHandle(mHandle), callerInfo);
1269
1270            if (mCallerInfo.contactDisplayPhotoUri != null) {
1271                Log.d(this, "Searching person uri %s for call %s",
1272                        mCallerInfo.contactDisplayPhotoUri, this);
1273                ContactsAsyncHelper.startObtainPhotoAsync(
1274                        token,
1275                        mContext,
1276                        mCallerInfo.contactDisplayPhotoUri,
1277                        sPhotoLoadListener,
1278                        this);
1279                // Do not call onCallerInfoChanged yet in this case.  We call it in setPhoto().
1280            } else {
1281                for (Listener l : mListeners) {
1282                    l.onCallerInfoChanged(this);
1283                }
1284            }
1285
1286            processDirectToVoicemail();
1287        }
1288        Trace.endSection();
1289    }
1290
1291    CallerInfo getCallerInfo() {
1292        return mCallerInfo;
1293    }
1294
1295    /**
1296     * Saves the specified photo information if the specified token matches that of the last query.
1297     *
1298     * @param photo The photo as a drawable.
1299     * @param photoIcon The photo as a small icon.
1300     * @param token The token used with this query.
1301     */
1302    private void setPhoto(Drawable photo, Bitmap photoIcon, int token) {
1303        if (mQueryToken == token) {
1304            mCallerInfo.cachedPhoto = photo;
1305            mCallerInfo.cachedPhotoIcon = photoIcon;
1306
1307            for (Listener l : mListeners) {
1308                l.onCallerInfoChanged(this);
1309            }
1310        }
1311    }
1312
1313    private void maybeLoadCannedSmsResponses() {
1314        if (mIsIncoming && isRespondViaSmsCapable() && !mCannedSmsResponsesLoadingStarted) {
1315            Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
1316            mCannedSmsResponsesLoadingStarted = true;
1317            RespondViaSmsManager.getInstance().loadCannedTextMessages(
1318                    new Response<Void, List<String>>() {
1319                        @Override
1320                        public void onResult(Void request, List<String>... result) {
1321                            if (result.length > 0) {
1322                                Log.d(this, "maybeLoadCannedSmsResponses: got %s", result[0]);
1323                                mCannedSmsResponses = result[0];
1324                                for (Listener l : mListeners) {
1325                                    l.onCannedSmsResponsesLoaded(Call.this);
1326                                }
1327                            }
1328                        }
1329
1330                        @Override
1331                        public void onError(Void request, int code, String msg) {
1332                            Log.w(Call.this, "Error obtaining canned SMS responses: %d %s", code,
1333                                    msg);
1334                        }
1335                    },
1336                    mContext
1337            );
1338        } else {
1339            Log.d(this, "maybeLoadCannedSmsResponses: doing nothing");
1340        }
1341    }
1342
1343    /**
1344     * Sets speakerphone option on when call begins.
1345     */
1346    public void setStartWithSpeakerphoneOn(boolean startWithSpeakerphone) {
1347        mSpeakerphoneOn = startWithSpeakerphone;
1348    }
1349
1350    /**
1351     * Returns speakerphone option.
1352     *
1353     * @return Whether or not speakerphone should be set automatically when call begins.
1354     */
1355    public boolean getStartWithSpeakerphoneOn() {
1356        return mSpeakerphoneOn;
1357    }
1358
1359    /**
1360     * Sets a video call provider for the call.
1361     */
1362    public void setVideoProvider(IVideoProvider videoProvider) {
1363        mVideoProvider = videoProvider;
1364        for (Listener l : mListeners) {
1365            l.onVideoCallProviderChanged(Call.this);
1366        }
1367    }
1368
1369    /**
1370     * @return Return the {@link Connection.VideoProvider} binder.
1371     */
1372    public IVideoProvider getVideoProvider() {
1373        return mVideoProvider;
1374    }
1375
1376    /**
1377     * The current video state for the call.
1378     * Valid values: see {@link VideoProfile.VideoState}.
1379     */
1380    public int getVideoState() {
1381        return mVideoState;
1382    }
1383
1384    /**
1385     * Returns the video states which were applicable over the duration of a call.
1386     * See {@link VideoProfile} for a list of valid video states.
1387     *
1388     * @return The video states applicable over the duration of the call.
1389     */
1390    public int getVideoStateHistory() {
1391        return mVideoStateHistory;
1392    }
1393
1394    /**
1395     * Determines the current video state for the call.
1396     * For an outgoing call determines the desired video state for the call.
1397     * Valid values: see {@link VideoProfile.VideoState}
1398     *
1399     * @param videoState The video state for the call.
1400     */
1401    public void setVideoState(int videoState) {
1402        // Track which video states were applicable over the duration of the call.
1403        mVideoStateHistory = mVideoStateHistory | videoState;
1404
1405        mVideoState = videoState;
1406        for (Listener l : mListeners) {
1407            l.onVideoStateChanged(this);
1408        }
1409    }
1410
1411    public boolean getIsVoipAudioMode() {
1412        return mIsVoipAudioMode;
1413    }
1414
1415    public void setIsVoipAudioMode(boolean audioModeIsVoip) {
1416        mIsVoipAudioMode = audioModeIsVoip;
1417        for (Listener l : mListeners) {
1418            l.onIsVoipAudioModeChanged(this);
1419        }
1420    }
1421
1422    public StatusHints getStatusHints() {
1423        return mStatusHints;
1424    }
1425
1426    public void setStatusHints(StatusHints statusHints) {
1427        mStatusHints = statusHints;
1428        for (Listener l : mListeners) {
1429            l.onStatusHintsChanged(this);
1430        }
1431    }
1432
1433    public boolean isUnknown() {
1434        return mIsUnknown;
1435    }
1436
1437    public void setIsUnknown(boolean isUnknown) {
1438        mIsUnknown = isUnknown;
1439    }
1440
1441    /**
1442     * Determines if this call is in a disconnecting state.
1443     *
1444     * @return {@code true} if this call is locally disconnecting.
1445     */
1446    public boolean isLocallyDisconnecting() {
1447        return mIsLocallyDisconnecting;
1448    }
1449
1450    /**
1451     * Sets whether this call is in a disconnecting state.
1452     *
1453     * @param isLocallyDisconnecting {@code true} if this call is locally disconnecting.
1454     */
1455    private void setLocallyDisconnecting(boolean isLocallyDisconnecting) {
1456        mIsLocallyDisconnecting = isLocallyDisconnecting;
1457    }
1458
1459    static int getStateFromConnectionState(int state) {
1460        switch (state) {
1461            case Connection.STATE_INITIALIZING:
1462                return CallState.CONNECTING;
1463            case Connection.STATE_ACTIVE:
1464                return CallState.ACTIVE;
1465            case Connection.STATE_DIALING:
1466                return CallState.DIALING;
1467            case Connection.STATE_DISCONNECTED:
1468                return CallState.DISCONNECTED;
1469            case Connection.STATE_HOLDING:
1470                return CallState.ON_HOLD;
1471            case Connection.STATE_NEW:
1472                return CallState.NEW;
1473            case Connection.STATE_RINGING:
1474                return CallState.RINGING;
1475        }
1476        return CallState.DISCONNECTED;
1477    }
1478}
1479