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