ImsPhoneConnection.java revision 04b0216a5f64b70ec4b06f129946fff4ef359878
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
17package com.android.internal.telephony.imsphone;
18
19import android.content.Context;
20import android.net.Uri;
21import android.os.AsyncResult;
22import android.os.Handler;
23import android.os.Looper;
24import android.os.Message;
25import android.os.PowerManager;
26import android.os.Registrant;
27import android.os.SystemClock;
28import android.telecom.Log;
29import android.telephony.DisconnectCause;
30import android.telephony.PhoneNumberUtils;
31import android.telephony.Rlog;
32
33import com.android.ims.ImsException;
34import com.android.ims.ImsStreamMediaProfile;
35import com.android.internal.telephony.CallStateException;
36import com.android.internal.telephony.Connection;
37import com.android.internal.telephony.Phone;
38import com.android.internal.telephony.PhoneConstants;
39import com.android.internal.telephony.UUSInfo;
40
41import com.android.ims.ImsCall;
42import com.android.ims.ImsCallProfile;
43
44/**
45 * {@hide}
46 */
47public class ImsPhoneConnection extends Connection {
48    private static final String LOG_TAG = "ImsPhoneConnection";
49    private static final boolean DBG = true;
50
51    //***** Instance Variables
52
53    private ImsPhoneCallTracker mOwner;
54    private ImsPhoneCall mParent;
55    private ImsCall mImsCall;
56
57    private String mPostDialString;      // outgoing calls only
58    private boolean mDisconnected;
59
60    /*
61    int mIndex;          // index in ImsPhoneCallTracker.connections[], -1 if unassigned
62                        // The GSM index is 1 + this
63    */
64
65    /*
66     * These time/timespan values are based on System.currentTimeMillis(),
67     * i.e., "wall clock" time.
68     */
69    private long mDisconnectTime;
70
71    private int mNextPostDialChar;       // index into postDialString
72
73    private int mCause = DisconnectCause.NOT_DISCONNECTED;
74    private PostDialState mPostDialState = PostDialState.NOT_STARTED;
75    private UUSInfo mUusInfo;
76    private Handler mHandler;
77
78    private PowerManager.WakeLock mPartialWakeLock;
79
80    // The cached connect time of the connection when it turns into a conference.
81    private long mConferenceConnectTime = 0;
82
83    //***** Event Constants
84    private static final int EVENT_DTMF_DONE = 1;
85    private static final int EVENT_PAUSE_DONE = 2;
86    private static final int EVENT_NEXT_POST_DIAL = 3;
87    private static final int EVENT_WAKE_LOCK_TIMEOUT = 4;
88
89    //***** Constants
90    private static final int PAUSE_DELAY_MILLIS = 3 * 1000;
91    private static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000;
92
93    //***** Inner Classes
94
95    class MyHandler extends Handler {
96        MyHandler(Looper l) {super(l);}
97
98        @Override
99        public void
100        handleMessage(Message msg) {
101
102            switch (msg.what) {
103                case EVENT_NEXT_POST_DIAL:
104                case EVENT_DTMF_DONE:
105                case EVENT_PAUSE_DONE:
106                    processNextPostDialChar();
107                    break;
108                case EVENT_WAKE_LOCK_TIMEOUT:
109                    releaseWakeLock();
110                    break;
111            }
112        }
113    }
114
115    //***** Constructors
116
117    /** This is probably an MT call */
118    /*package*/
119    ImsPhoneConnection(Context context, ImsCall imsCall, ImsPhoneCallTracker ct, ImsPhoneCall parent) {
120        createWakeLock(context);
121        acquireWakeLock();
122
123        mOwner = ct;
124        mHandler = new MyHandler(mOwner.getLooper());
125        mImsCall = imsCall;
126
127        if ((imsCall != null) && (imsCall.getCallProfile() != null)) {
128            mAddress = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_OI);
129            mCnapName = imsCall.getCallProfile().getCallExtra(ImsCallProfile.EXTRA_CNA);
130            mNumberPresentation = ImsCallProfile.OIRToPresentation(
131                    imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_OIR));
132            mCnapNamePresentation = ImsCallProfile.OIRToPresentation(
133                    imsCall.getCallProfile().getCallExtraInt(ImsCallProfile.EXTRA_CNAP));
134
135            updateMediaCapabilities(imsCall);
136        } else {
137            mNumberPresentation = PhoneConstants.PRESENTATION_UNKNOWN;
138            mCnapNamePresentation = PhoneConstants.PRESENTATION_UNKNOWN;
139        }
140
141        mIsIncoming = true;
142        mCreateTime = System.currentTimeMillis();
143        mUusInfo = null;
144
145        //mIndex = index;
146
147        updateWifiState();
148
149        mParent = parent;
150        mParent.attach(this, ImsPhoneCall.State.INCOMING);
151    }
152
153    /** This is an MO call, created when dialing */
154    /*package*/
155    ImsPhoneConnection(Context context, String dialString, ImsPhoneCallTracker ct, ImsPhoneCall parent) {
156        createWakeLock(context);
157        acquireWakeLock();
158
159        mOwner = ct;
160        mHandler = new MyHandler(mOwner.getLooper());
161
162        mDialString = dialString;
163
164        mAddress = PhoneNumberUtils.extractNetworkPortionAlt(dialString);
165        mPostDialString = PhoneNumberUtils.extractPostDialPortion(dialString);
166
167        //mIndex = -1;
168
169        mIsIncoming = false;
170        mCnapName = null;
171        mCnapNamePresentation = PhoneConstants.PRESENTATION_ALLOWED;
172        mNumberPresentation = PhoneConstants.PRESENTATION_ALLOWED;
173        mCreateTime = System.currentTimeMillis();
174
175        mParent = parent;
176        parent.attachFake(this, ImsPhoneCall.State.DIALING);
177    }
178
179    public void dispose() {
180    }
181
182    static boolean
183    equalsHandlesNulls (Object a, Object b) {
184        return (a == null) ? (b == null) : a.equals (b);
185    }
186
187    @Override
188    public String getOrigDialString(){
189        return mDialString;
190    }
191
192    @Override
193    public ImsPhoneCall getCall() {
194        return mParent;
195    }
196
197    @Override
198    public long getDisconnectTime() {
199        return mDisconnectTime;
200    }
201
202    @Override
203    public long getHoldingStartTime() {
204        return mHoldingStartTime;
205    }
206
207    @Override
208    public long getHoldDurationMillis() {
209        if (getState() != ImsPhoneCall.State.HOLDING) {
210            // If not holding, return 0
211            return 0;
212        } else {
213            return SystemClock.elapsedRealtime() - mHoldingStartTime;
214        }
215    }
216
217    @Override
218    public int getDisconnectCause() {
219        return mCause;
220    }
221
222    public void setDisconnectCause(int cause) {
223        mCause = cause;
224    }
225
226    public ImsPhoneCallTracker getOwner () {
227        return mOwner;
228    }
229
230    @Override
231    public ImsPhoneCall.State getState() {
232        if (mDisconnected) {
233            return ImsPhoneCall.State.DISCONNECTED;
234        } else {
235            return super.getState();
236        }
237    }
238
239    @Override
240    public void hangup() throws CallStateException {
241        if (!mDisconnected) {
242            mOwner.hangup(this);
243        } else {
244            throw new CallStateException ("disconnected");
245        }
246    }
247
248    @Override
249    public void separate() throws CallStateException {
250        throw new CallStateException ("not supported");
251    }
252
253    @Override
254    public PostDialState getPostDialState() {
255        return mPostDialState;
256    }
257
258    @Override
259    public void proceedAfterWaitChar() {
260        if (mPostDialState != PostDialState.WAIT) {
261            Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected "
262                    + "getPostDialState() to be WAIT but was " + mPostDialState);
263            return;
264        }
265
266        setPostDialState(PostDialState.STARTED);
267
268        processNextPostDialChar();
269    }
270
271    @Override
272    public void proceedAfterWildChar(String str) {
273        if (mPostDialState != PostDialState.WILD) {
274            Rlog.w(LOG_TAG, "ImsPhoneConnection.proceedAfterWaitChar(): Expected "
275                    + "getPostDialState() to be WILD but was " + mPostDialState);
276            return;
277        }
278
279        setPostDialState(PostDialState.STARTED);
280
281        // make a new postDialString, with the wild char replacement string
282        // at the beginning, followed by the remaining postDialString.
283
284        StringBuilder buf = new StringBuilder(str);
285        buf.append(mPostDialString.substring(mNextPostDialChar));
286        mPostDialString = buf.toString();
287        mNextPostDialChar = 0;
288        if (Phone.DEBUG_PHONE) {
289            Rlog.d(LOG_TAG, "proceedAfterWildChar: new postDialString is " +
290                    mPostDialString);
291        }
292
293        processNextPostDialChar();
294    }
295
296    @Override
297    public void cancelPostDial() {
298        setPostDialState(PostDialState.CANCELLED);
299    }
300
301    /**
302     * Called when this Connection is being hung up locally (eg, user pressed "end")
303     */
304    void
305    onHangupLocal() {
306        mCause = DisconnectCause.LOCAL;
307    }
308
309    /** Called when the connection has been disconnected */
310    public boolean
311    onDisconnect(int cause) {
312        Rlog.d(LOG_TAG, "onDisconnect: cause=" + cause);
313        if (mCause != DisconnectCause.LOCAL) mCause = cause;
314        return onDisconnect();
315    }
316
317    /*package*/ boolean
318    onDisconnect() {
319        boolean changed = false;
320
321        if (!mDisconnected) {
322            //mIndex = -1;
323
324            mDisconnectTime = System.currentTimeMillis();
325            mDuration = SystemClock.elapsedRealtime() - mConnectTimeReal;
326            mDisconnected = true;
327
328            mOwner.mPhone.notifyDisconnect(this);
329
330            if (mParent != null) {
331                changed = mParent.connectionDisconnected(this);
332            } else {
333                Rlog.d(LOG_TAG, "onDisconnect: no parent");
334            }
335            if (mImsCall != null) mImsCall.close();
336            mImsCall = null;
337        }
338        releaseWakeLock();
339        return changed;
340    }
341
342    /**
343     * An incoming or outgoing call has connected
344     */
345    void
346    onConnectedInOrOut() {
347        mConnectTime = System.currentTimeMillis();
348        mConnectTimeReal = SystemClock.elapsedRealtime();
349        mDuration = 0;
350
351        if (Phone.DEBUG_PHONE) {
352            Rlog.d(LOG_TAG, "onConnectedInOrOut: connectTime=" + mConnectTime);
353        }
354
355        if (!mIsIncoming) {
356            // outgoing calls only
357            processNextPostDialChar();
358        }
359        releaseWakeLock();
360    }
361
362    /*package*/ void
363    onStartedHolding() {
364        mHoldingStartTime = SystemClock.elapsedRealtime();
365    }
366    /**
367     * Performs the appropriate action for a post-dial char, but does not
368     * notify application. returns false if the character is invalid and
369     * should be ignored
370     */
371    private boolean
372    processPostDialChar(char c) {
373        if (PhoneNumberUtils.is12Key(c)) {
374            mOwner.sendDtmf(c, mHandler.obtainMessage(EVENT_DTMF_DONE));
375        } else if (c == PhoneNumberUtils.PAUSE) {
376            // From TS 22.101:
377            // It continues...
378            // Upon the called party answering the UE shall send the DTMF digits
379            // automatically to the network after a delay of 3 seconds( 20 ).
380            // The digits shall be sent according to the procedures and timing
381            // specified in 3GPP TS 24.008 [13]. The first occurrence of the
382            // "DTMF Control Digits Separator" shall be used by the ME to
383            // distinguish between the addressing digits (i.e. the phone number)
384            // and the DTMF digits. Upon subsequent occurrences of the
385            // separator,
386            // the UE shall pause again for 3 seconds ( 20 ) before sending
387            // any further DTMF digits.
388            mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_PAUSE_DONE),
389                    PAUSE_DELAY_MILLIS);
390        } else if (c == PhoneNumberUtils.WAIT) {
391            setPostDialState(PostDialState.WAIT);
392        } else if (c == PhoneNumberUtils.WILD) {
393            setPostDialState(PostDialState.WILD);
394        } else {
395            return false;
396        }
397
398        return true;
399    }
400
401    @Override
402    public String
403    getRemainingPostDialString() {
404        if (mPostDialState == PostDialState.CANCELLED
405            || mPostDialState == PostDialState.COMPLETE
406            || mPostDialString == null
407            || mPostDialString.length() <= mNextPostDialChar
408        ) {
409            return "";
410        }
411
412        return mPostDialString.substring(mNextPostDialChar);
413    }
414
415    @Override
416    protected void finalize()
417    {
418        releaseWakeLock();
419    }
420
421    private void
422    processNextPostDialChar() {
423        char c = 0;
424        Registrant postDialHandler;
425
426        if (mPostDialState == PostDialState.CANCELLED) {
427            //Rlog.d(LOG_TAG, "##### processNextPostDialChar: postDialState == CANCELLED, bail");
428            return;
429        }
430
431        if (mPostDialString == null || mPostDialString.length() <= mNextPostDialChar) {
432            setPostDialState(PostDialState.COMPLETE);
433
434            // notifyMessage.arg1 is 0 on complete
435            c = 0;
436        } else {
437            boolean isValid;
438
439            setPostDialState(PostDialState.STARTED);
440
441            c = mPostDialString.charAt(mNextPostDialChar++);
442
443            isValid = processPostDialChar(c);
444
445            if (!isValid) {
446                // Will call processNextPostDialChar
447                mHandler.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget();
448                // Don't notify application
449                Rlog.e(LOG_TAG, "processNextPostDialChar: c=" + c + " isn't valid!");
450                return;
451            }
452        }
453
454        notifyPostDialListenersNextChar(c);
455
456        // TODO: remove the following code since the handler no longer executes anything.
457        postDialHandler = mOwner.mPhone.mPostDialHandler;
458
459        Message notifyMessage;
460
461        if (postDialHandler != null
462                && (notifyMessage = postDialHandler.messageForRegistrant()) != null) {
463            // The AsyncResult.result is the Connection object
464            PostDialState state = mPostDialState;
465            AsyncResult ar = AsyncResult.forMessage(notifyMessage);
466            ar.result = this;
467            ar.userObj = state;
468
469            // arg1 is the character that was/is being processed
470            notifyMessage.arg1 = c;
471
472            //Rlog.v(LOG_TAG, "##### processNextPostDialChar: send msg to postDialHandler, arg1=" + c);
473            notifyMessage.sendToTarget();
474        }
475    }
476
477    /**
478     * Set post dial state and acquire wake lock while switching to "started"
479     * state, the wake lock will be released if state switches out of "started"
480     * state or after WAKE_LOCK_TIMEOUT_MILLIS.
481     * @param s new PostDialState
482     */
483    private void setPostDialState(PostDialState s) {
484        if (mPostDialState != PostDialState.STARTED
485                && s == PostDialState.STARTED) {
486            acquireWakeLock();
487            Message msg = mHandler.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT);
488            mHandler.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS);
489        } else if (mPostDialState == PostDialState.STARTED
490                && s != PostDialState.STARTED) {
491            mHandler.removeMessages(EVENT_WAKE_LOCK_TIMEOUT);
492            releaseWakeLock();
493        }
494        mPostDialState = s;
495        notifyPostDialListeners();
496    }
497
498    private void
499    createWakeLock(Context context) {
500        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
501        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
502    }
503
504    private void
505    acquireWakeLock() {
506        Rlog.d(LOG_TAG, "acquireWakeLock");
507        mPartialWakeLock.acquire();
508    }
509
510    void
511    releaseWakeLock() {
512        synchronized(mPartialWakeLock) {
513            if (mPartialWakeLock.isHeld()) {
514                Rlog.d(LOG_TAG, "releaseWakeLock");
515                mPartialWakeLock.release();
516            }
517        }
518    }
519
520    @Override
521    public int getNumberPresentation() {
522        return mNumberPresentation;
523    }
524
525    @Override
526    public UUSInfo getUUSInfo() {
527        return mUusInfo;
528    }
529
530    @Override
531    public Connection getOrigConnection() {
532        return null;
533    }
534
535    @Override
536    public boolean isMultiparty() {
537        return mImsCall != null && mImsCall.isMultiparty();
538    }
539
540    /*package*/ ImsCall getImsCall() {
541        return mImsCall;
542    }
543
544    /*package*/ void setImsCall(ImsCall imsCall) {
545        mImsCall = imsCall;
546    }
547
548    /*package*/ void changeParent(ImsPhoneCall parent) {
549        mParent = parent;
550    }
551
552    /**
553     * @return {@code true} if the {@link ImsPhoneConnection} or its media capabilities have been
554     *     changed, and {@code false} otherwise.
555     */
556    /*package*/ boolean update(ImsCall imsCall, ImsPhoneCall.State state) {
557        if (state == ImsPhoneCall.State.ACTIVE) {
558            if (mParent.getState().isRinging() || mParent.getState().isDialing()) {
559                onConnectedInOrOut();
560            }
561
562            if (mParent.getState().isRinging() || mParent == mOwner.mBackgroundCall) {
563                //mForegroundCall should be IDLE
564                //when accepting WAITING call
565                //before accept WAITING call,
566                //the ACTIVE call should be held ahead
567                mParent.detach(this);
568                mParent = mOwner.mForegroundCall;
569                mParent.attach(this);
570            }
571        } else if (state == ImsPhoneCall.State.HOLDING) {
572            onStartedHolding();
573        }
574
575        boolean updateParent = mParent.update(this, imsCall, state);
576        boolean updateMediaCapabilities = updateMediaCapabilities(imsCall);
577        boolean updateWifiState = updateWifiState();
578        return updateParent || updateMediaCapabilities || updateWifiState;
579    }
580
581    @Override
582    public int getPreciseDisconnectCause() {
583        return 0;
584    }
585
586    /**
587     * Notifies this Connection of a request to disconnect a participant of the conference managed
588     * by the connection.
589     *
590     * @param endpoint the {@link android.net.Uri} of the participant to disconnect.
591     */
592    @Override
593    public void onDisconnectConferenceParticipant(Uri endpoint) {
594        ImsCall imsCall = getImsCall();
595        if (imsCall == null) {
596            return;
597        }
598        try {
599            imsCall.removeParticipants(new String[]{endpoint.toString()});
600        } catch (ImsException e) {
601            // No session in place -- no change
602            Rlog.e(LOG_TAG, "onDisconnectConferenceParticipant: no session in place. "+
603                    "Failed to disconnect endpoint = " + endpoint);
604        }
605    }
606
607    /**
608     * Sets the conference connect time.  Used when an {@code ImsConference} is created to out of
609     * this phone connection.
610     *
611     * @param conferenceConnectTime The conference connect time.
612     */
613    public void setConferenceConnectTime(long conferenceConnectTime) {
614        mConferenceConnectTime = conferenceConnectTime;
615    }
616
617    /**
618     * @return The conference connect time.
619     */
620    public long getConferenceConnectTime() {
621        return mConferenceConnectTime;
622    }
623
624    /**
625     * Check for a change in the video capabilities and audio quality for the {@link ImsCall}, and
626     * update the {@link ImsPhoneConnection} with this information.
627     *
628     * @param imsCall The call to check for changes in media capabilities.
629     * @return Whether the media capabilities have been changed.
630     */
631    private boolean updateMediaCapabilities(ImsCall imsCall) {
632        if (imsCall == null) {
633            return false;
634        }
635
636        boolean changed = false;
637
638        try {
639            // The actual call profile (negotiated between local and peer).
640            ImsCallProfile negotiatedCallProfile = imsCall.getCallProfile();
641            // The capabilities of the local device.
642            ImsCallProfile localCallProfile = imsCall.getLocalCallProfile();
643            // The capabilities of the peer device.
644            ImsCallProfile remoteCallProfile = imsCall.getRemoteCallProfile();
645
646            if (negotiatedCallProfile != null) {
647                int callType = negotiatedCallProfile.mCallType;
648
649                int newVideoState = ImsCallProfile.getVideoStateFromCallType(callType);
650                if (getVideoState() != newVideoState) {
651                    setVideoState(newVideoState);
652                    changed = true;
653                }
654            }
655
656            if (localCallProfile != null) {
657                int callType = localCallProfile.mCallType;
658
659                boolean newLocalVideoCapable = callType == ImsCallProfile.CALL_TYPE_VT;
660                if (isLocalVideoCapable() != newLocalVideoCapable) {
661                    setLocalVideoCapable(newLocalVideoCapable);
662                    changed = true;
663                }
664            }
665
666            int newAudioQuality =
667                    getAudioQualityFromCallProfile(localCallProfile, remoteCallProfile);
668            if (getAudioQuality() != newAudioQuality) {
669                setAudioQuality(newAudioQuality);
670                changed = true;
671            }
672        } catch (ImsException e) {
673            // No session in place -- no change
674        }
675
676        return changed;
677    }
678
679    /**
680     * Check for a change in the wifi state of the ImsPhoneCallTracker and update the
681     * {@link ImsPhoneConnection} with this information.
682     *
683     * @return Whether the ImsPhoneCallTracker's usage of wifi has been changed.
684     */
685    public boolean updateWifiState() {
686        Rlog.d(LOG_TAG, "updateWifiState: " + mOwner.isVowifiEnabled());
687        if (isWifi() != mOwner.isVowifiEnabled()) {
688            setWifi(mOwner.isVowifiEnabled());
689            return true;
690        }
691        return false;
692    }
693
694    /**
695     * Determines the {@link ImsPhoneConnection} audio quality based on the local and remote
696     * {@link ImsCallProfile}. If indicate a HQ audio call if the local stream profile
697     * indicates AMR_WB or EVRC_WB and there is no remote restrict cause.
698     *
699     * @param localCallProfile The local call profile.
700     * @param remoteCallProfile The remote call profile.
701     * @return The audio quality.
702     */
703    private int getAudioQualityFromCallProfile(
704            ImsCallProfile localCallProfile, ImsCallProfile remoteCallProfile) {
705        if (localCallProfile == null || remoteCallProfile == null
706                || localCallProfile.mMediaProfile == null) {
707            return AUDIO_QUALITY_STANDARD;
708        }
709
710        boolean isHighDef = (localCallProfile.mMediaProfile.mAudioQuality
711                        == ImsStreamMediaProfile.AUDIO_QUALITY_AMR_WB
712                || localCallProfile.mMediaProfile.mAudioQuality
713                        == ImsStreamMediaProfile.AUDIO_QUALITY_EVRC_WB)
714                && remoteCallProfile.mRestrictCause == ImsCallProfile.CALL_RESTRICT_CAUSE_NONE;
715        return isHighDef ? AUDIO_QUALITY_HIGH_DEFINITION : AUDIO_QUALITY_STANDARD;
716    }
717
718    /**
719     * Provides a string representation of the {@link ImsPhoneConnection}.  Primarily intended for
720     * use in log statements.
721     *
722     * @return String representation of call.
723     */
724    @Override
725    public String toString() {
726        StringBuilder sb = new StringBuilder();
727        sb.append("[ImsPhoneConnection objId: ");
728        sb.append(System.identityHashCode(this));
729        sb.append(" address:");
730        sb.append(Log.pii(getAddress()));
731        sb.append(" ImsCall:");
732        if (mImsCall == null) {
733            sb.append("null");
734        } else {
735            sb.append(mImsCall);
736        }
737        sb.append("]");
738        return sb.toString();
739    }
740}
741
742