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