1/*
2 * Copyright (C) 2006 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.gsm;
18import android.content.Context;
19import android.os.AsyncResult;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.Message;
23import android.os.PowerManager;
24import android.os.Registrant;
25import android.os.SystemClock;
26import android.util.Log;
27import android.telephony.PhoneNumberUtils;
28import android.telephony.ServiceState;
29
30import com.android.internal.telephony.*;
31
32/**
33 * {@hide}
34 */
35public class GsmConnection extends Connection {
36    static final String LOG_TAG = "GSM";
37
38    //***** Instance Variables
39
40    GsmCallTracker owner;
41    GsmCall parent;
42
43    String address;     // MAY BE NULL!!!
44    String dialString;          // outgoing calls only
45    String postDialString;      // outgoing calls only
46    boolean isIncoming;
47    boolean disconnected;
48
49    int index;          // index in GsmCallTracker.connections[], -1 if unassigned
50                        // The GSM index is 1 + this
51
52    /*
53     * These time/timespan values are based on System.currentTimeMillis(),
54     * i.e., "wall clock" time.
55     */
56    long createTime;
57    long connectTime;
58    long disconnectTime;
59
60    /*
61     * These time/timespan values are based on SystemClock.elapsedRealTime(),
62     * i.e., time since boot.  They are appropriate for comparison and
63     * calculating deltas.
64     */
65    long connectTimeReal;
66    long duration;
67    long holdingStartTime;  // The time when the Connection last transitioned
68                            // into HOLDING
69
70    int nextPostDialChar;       // index into postDialString
71
72    DisconnectCause cause = DisconnectCause.NOT_DISCONNECTED;
73    PostDialState postDialState = PostDialState.NOT_STARTED;
74    int numberPresentation = Connection.PRESENTATION_ALLOWED;
75    UUSInfo uusInfo;
76
77    Handler h;
78
79    private PowerManager.WakeLock mPartialWakeLock;
80
81    //***** Event Constants
82    static final int EVENT_DTMF_DONE = 1;
83    static final int EVENT_PAUSE_DONE = 2;
84    static final int EVENT_NEXT_POST_DIAL = 3;
85    static final int EVENT_WAKE_LOCK_TIMEOUT = 4;
86
87    //***** Constants
88    static final int PAUSE_DELAY_FIRST_MILLIS = 100;
89    static final int PAUSE_DELAY_MILLIS = 3 * 1000;
90    static final int WAKE_LOCK_TIMEOUT_MILLIS = 60*1000;
91
92    //***** Inner Classes
93
94    class MyHandler extends Handler {
95        MyHandler(Looper l) {super(l);}
96
97        public void
98        handleMessage(Message msg) {
99
100            switch (msg.what) {
101                case EVENT_NEXT_POST_DIAL:
102                case EVENT_DTMF_DONE:
103                case EVENT_PAUSE_DONE:
104                    processNextPostDialChar();
105                    break;
106                case EVENT_WAKE_LOCK_TIMEOUT:
107                    releaseWakeLock();
108                    break;
109            }
110        }
111    }
112
113    //***** Constructors
114
115    /** This is probably an MT call that we first saw in a CLCC response */
116    /*package*/
117    GsmConnection (Context context, DriverCall dc, GsmCallTracker ct, int index) {
118        createWakeLock(context);
119        acquireWakeLock();
120
121        owner = ct;
122        h = new MyHandler(owner.getLooper());
123
124        address = dc.number;
125
126        isIncoming = dc.isMT;
127        createTime = System.currentTimeMillis();
128        numberPresentation = dc.numberPresentation;
129        uusInfo = dc.uusInfo;
130
131        this.index = index;
132
133        parent = parentFromDCState (dc.state);
134        parent.attach(this, dc);
135    }
136
137    /** This is an MO call, created when dialing */
138    /*package*/
139    GsmConnection (Context context, String dialString, GsmCallTracker ct, GsmCall parent) {
140        createWakeLock(context);
141        acquireWakeLock();
142
143        owner = ct;
144        h = new MyHandler(owner.getLooper());
145
146        this.dialString = dialString;
147
148        this.address = PhoneNumberUtils.extractNetworkPortionAlt(dialString);
149        this.postDialString = PhoneNumberUtils.extractPostDialPortion(dialString);
150
151        index = -1;
152
153        isIncoming = false;
154        createTime = System.currentTimeMillis();
155
156        this.parent = parent;
157        parent.attachFake(this, GsmCall.State.DIALING);
158    }
159
160    public void dispose() {
161    }
162
163    static boolean
164    equalsHandlesNulls (Object a, Object b) {
165        return (a == null) ? (b == null) : a.equals (b);
166    }
167
168    /*package*/ boolean
169    compareTo(DriverCall c) {
170        // On mobile originated (MO) calls, the phone number may have changed
171        // due to a SIM Toolkit call control modification.
172        //
173        // We assume we know when MO calls are created (since we created them)
174        // and therefore don't need to compare the phone number anyway.
175        if (! (isIncoming || c.isMT)) return true;
176
177        // ... but we can compare phone numbers on MT calls, and we have
178        // no control over when they begin, so we might as well
179
180        String cAddress = PhoneNumberUtils.stringFromStringAndTOA(c.number, c.TOA);
181        return isIncoming == c.isMT && equalsHandlesNulls(address, cAddress);
182    }
183
184    public String getAddress() {
185        return address;
186    }
187
188    public GsmCall getCall() {
189        return parent;
190    }
191
192    public long getCreateTime() {
193        return createTime;
194    }
195
196    public long getConnectTime() {
197        return connectTime;
198    }
199
200    public long getDisconnectTime() {
201        return disconnectTime;
202    }
203
204    public long getDurationMillis() {
205        if (connectTimeReal == 0) {
206            return 0;
207        } else if (duration == 0) {
208            return SystemClock.elapsedRealtime() - connectTimeReal;
209        } else {
210            return duration;
211        }
212    }
213
214    public long getHoldDurationMillis() {
215        if (getState() != GsmCall.State.HOLDING) {
216            // If not holding, return 0
217            return 0;
218        } else {
219            return SystemClock.elapsedRealtime() - holdingStartTime;
220        }
221    }
222
223    public DisconnectCause getDisconnectCause() {
224        return cause;
225    }
226
227    public boolean isIncoming() {
228        return isIncoming;
229    }
230
231    public GsmCall.State getState() {
232        if (disconnected) {
233            return GsmCall.State.DISCONNECTED;
234        } else {
235            return super.getState();
236        }
237    }
238
239    public void hangup() throws CallStateException {
240        if (!disconnected) {
241            owner.hangup(this);
242        } else {
243            throw new CallStateException ("disconnected");
244        }
245    }
246
247    public void separate() throws CallStateException {
248        if (!disconnected) {
249            owner.separate(this);
250        } else {
251            throw new CallStateException ("disconnected");
252        }
253    }
254
255    public PostDialState getPostDialState() {
256        return postDialState;
257    }
258
259    public void proceedAfterWaitChar() {
260        if (postDialState != PostDialState.WAIT) {
261            Log.w(LOG_TAG, "GsmConnection.proceedAfterWaitChar(): Expected "
262                + "getPostDialState() to be WAIT but was " + postDialState);
263            return;
264        }
265
266        setPostDialState(PostDialState.STARTED);
267
268        processNextPostDialChar();
269    }
270
271    public void proceedAfterWildChar(String str) {
272        if (postDialState != PostDialState.WILD) {
273            Log.w(LOG_TAG, "GsmConnection.proceedAfterWaitChar(): Expected "
274                + "getPostDialState() to be WILD but was " + postDialState);
275            return;
276        }
277
278        setPostDialState(PostDialState.STARTED);
279
280        if (false) {
281            boolean playedTone = false;
282            int len = (str != null ? str.length() : 0);
283
284            for (int i=0; i<len; i++) {
285                char c = str.charAt(i);
286                Message msg = null;
287
288                if (i == len-1) {
289                    msg = h.obtainMessage(EVENT_DTMF_DONE);
290                }
291
292                if (PhoneNumberUtils.is12Key(c)) {
293                    owner.cm.sendDtmf(c, msg);
294                    playedTone = true;
295                }
296            }
297
298            if (!playedTone) {
299                processNextPostDialChar();
300            }
301        } else {
302            // make a new postDialString, with the wild char replacement string
303            // at the beginning, followed by the remaining postDialString.
304
305            StringBuilder buf = new StringBuilder(str);
306            buf.append(postDialString.substring(nextPostDialChar));
307            postDialString = buf.toString();
308            nextPostDialChar = 0;
309            if (Phone.DEBUG_PHONE) {
310                log("proceedAfterWildChar: new postDialString is " +
311                        postDialString);
312            }
313
314            processNextPostDialChar();
315        }
316    }
317
318    public void cancelPostDial() {
319        setPostDialState(PostDialState.CANCELLED);
320    }
321
322    /**
323     * Called when this Connection is being hung up locally (eg, user pressed "end")
324     * Note that at this point, the hangup request has been dispatched to the radio
325     * but no response has yet been received so update() has not yet been called
326     */
327    void
328    onHangupLocal() {
329        cause = DisconnectCause.LOCAL;
330    }
331
332    DisconnectCause
333    disconnectCauseFromCode(int causeCode) {
334        /**
335         * See 22.001 Annex F.4 for mapping of cause codes
336         * to local tones
337         */
338
339        switch (causeCode) {
340            case CallFailCause.USER_BUSY:
341                return DisconnectCause.BUSY;
342
343            case CallFailCause.NO_CIRCUIT_AVAIL:
344            case CallFailCause.TEMPORARY_FAILURE:
345            case CallFailCause.SWITCHING_CONGESTION:
346            case CallFailCause.CHANNEL_NOT_AVAIL:
347            case CallFailCause.QOS_NOT_AVAIL:
348            case CallFailCause.BEARER_NOT_AVAIL:
349                return DisconnectCause.CONGESTION;
350
351            case CallFailCause.ACM_LIMIT_EXCEEDED:
352                return DisconnectCause.LIMIT_EXCEEDED;
353
354            case CallFailCause.CALL_BARRED:
355                return DisconnectCause.CALL_BARRED;
356
357            case CallFailCause.FDN_BLOCKED:
358                return DisconnectCause.FDN_BLOCKED;
359
360            case CallFailCause.UNOBTAINABLE_NUMBER:
361                return DisconnectCause.UNOBTAINABLE_NUMBER;
362
363            case CallFailCause.ERROR_UNSPECIFIED:
364            case CallFailCause.NORMAL_CLEARING:
365            default:
366                GSMPhone phone = owner.phone;
367                int serviceState = phone.getServiceState().getState();
368                if (serviceState == ServiceState.STATE_POWER_OFF) {
369                    return DisconnectCause.POWER_OFF;
370                } else if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
371                        || serviceState == ServiceState.STATE_EMERGENCY_ONLY ) {
372                    return DisconnectCause.OUT_OF_SERVICE;
373                } else if (phone.getIccCard().getState() != IccCard.State.READY) {
374                    return DisconnectCause.ICC_ERROR;
375                } else if (causeCode == CallFailCause.ERROR_UNSPECIFIED) {
376                    if (phone.mSST.mRestrictedState.isCsRestricted()) {
377                        return DisconnectCause.CS_RESTRICTED;
378                    } else if (phone.mSST.mRestrictedState.isCsEmergencyRestricted()) {
379                        return DisconnectCause.CS_RESTRICTED_EMERGENCY;
380                    } else if (phone.mSST.mRestrictedState.isCsNormalRestricted()) {
381                        return DisconnectCause.CS_RESTRICTED_NORMAL;
382                    } else {
383                        return DisconnectCause.ERROR_UNSPECIFIED;
384                    }
385                } else if (causeCode == CallFailCause.NORMAL_CLEARING) {
386                    return DisconnectCause.NORMAL;
387                } else {
388                    // If nothing else matches, report unknown call drop reason
389                    // to app, not NORMAL call end.
390                    return DisconnectCause.ERROR_UNSPECIFIED;
391                }
392        }
393    }
394
395    /*package*/ void
396    onRemoteDisconnect(int causeCode) {
397        onDisconnect(disconnectCauseFromCode(causeCode));
398    }
399
400    /** Called when the radio indicates the connection has been disconnected */
401    /*package*/ void
402    onDisconnect(DisconnectCause cause) {
403        this.cause = cause;
404
405        if (!disconnected) {
406            index = -1;
407
408            disconnectTime = System.currentTimeMillis();
409            duration = SystemClock.elapsedRealtime() - connectTimeReal;
410            disconnected = true;
411
412            if (false) Log.d(LOG_TAG,
413                    "[GSMConn] onDisconnect: cause=" + cause);
414
415            owner.phone.notifyDisconnect(this);
416
417            if (parent != null) {
418                parent.connectionDisconnected(this);
419            }
420        }
421        releaseWakeLock();
422    }
423
424    // Returns true if state has changed, false if nothing changed
425    /*package*/ boolean
426    update (DriverCall dc) {
427        GsmCall newParent;
428        boolean changed = false;
429        boolean wasConnectingInOrOut = isConnectingInOrOut();
430        boolean wasHolding = (getState() == GsmCall.State.HOLDING);
431
432        newParent = parentFromDCState(dc.state);
433
434        if (!equalsHandlesNulls(address, dc.number)) {
435            if (Phone.DEBUG_PHONE) log("update: phone # changed!");
436            address = dc.number;
437            changed = true;
438        }
439
440        if (newParent != parent) {
441            if (parent != null) {
442                parent.detach(this);
443            }
444            newParent.attach(this, dc);
445            parent = newParent;
446            changed = true;
447        } else {
448            boolean parentStateChange;
449            parentStateChange = parent.update (this, dc);
450            changed = changed || parentStateChange;
451        }
452
453        /** Some state-transition events */
454
455        if (Phone.DEBUG_PHONE) log(
456                "update: parent=" + parent +
457                ", hasNewParent=" + (newParent != parent) +
458                ", wasConnectingInOrOut=" + wasConnectingInOrOut +
459                ", wasHolding=" + wasHolding +
460                ", isConnectingInOrOut=" + isConnectingInOrOut() +
461                ", changed=" + changed);
462
463
464        if (wasConnectingInOrOut && !isConnectingInOrOut()) {
465            onConnectedInOrOut();
466        }
467
468        if (changed && !wasHolding && (getState() == GsmCall.State.HOLDING)) {
469            // We've transitioned into HOLDING
470            onStartedHolding();
471        }
472
473        return changed;
474    }
475
476    /**
477     * Called when this Connection is in the foregroundCall
478     * when a dial is initiated.
479     * We know we're ACTIVE, and we know we're going to end up
480     * HOLDING in the backgroundCall
481     */
482    void
483    fakeHoldBeforeDial() {
484        if (parent != null) {
485            parent.detach(this);
486        }
487
488        parent = owner.backgroundCall;
489        parent.attachFake(this, GsmCall.State.HOLDING);
490
491        onStartedHolding();
492    }
493
494    /*package*/ int
495    getGSMIndex() throws CallStateException {
496        if (index >= 0) {
497            return index + 1;
498        } else {
499            throw new CallStateException ("GSM index not yet assigned");
500        }
501    }
502
503    /**
504     * An incoming or outgoing call has connected
505     */
506    void
507    onConnectedInOrOut() {
508        connectTime = System.currentTimeMillis();
509        connectTimeReal = SystemClock.elapsedRealtime();
510        duration = 0;
511
512        // bug #678474: incoming call interpreted as missed call, even though
513        // it sounds like the user has picked up the call.
514        if (Phone.DEBUG_PHONE) {
515            log("onConnectedInOrOut: connectTime=" + connectTime);
516        }
517
518        if (!isIncoming) {
519            // outgoing calls only
520            processNextPostDialChar();
521        }
522        releaseWakeLock();
523    }
524
525    private void
526    onStartedHolding() {
527        holdingStartTime = SystemClock.elapsedRealtime();
528    }
529    /**
530     * Performs the appropriate action for a post-dial char, but does not
531     * notify application. returns false if the character is invalid and
532     * should be ignored
533     */
534    private boolean
535    processPostDialChar(char c) {
536        if (PhoneNumberUtils.is12Key(c)) {
537            owner.cm.sendDtmf(c, h.obtainMessage(EVENT_DTMF_DONE));
538        } else if (c == PhoneNumberUtils.PAUSE) {
539            // From TS 22.101:
540
541            // "The first occurrence of the "DTMF Control Digits Separator"
542            //  shall be used by the ME to distinguish between the addressing
543            //  digits (i.e. the phone number) and the DTMF digits...."
544
545            if (nextPostDialChar == 1) {
546                // The first occurrence.
547                // We don't need to pause here, but wait for just a bit anyway
548                h.sendMessageDelayed(h.obtainMessage(EVENT_PAUSE_DONE),
549                                            PAUSE_DELAY_FIRST_MILLIS);
550            } else {
551                // It continues...
552                // "Upon subsequent occurrences of the separator, the UE shall
553                //  pause again for 3 seconds (\u00B1 20 %) before sending any
554                //  further DTMF digits."
555                h.sendMessageDelayed(h.obtainMessage(EVENT_PAUSE_DONE),
556                                            PAUSE_DELAY_MILLIS);
557            }
558        } else if (c == PhoneNumberUtils.WAIT) {
559            setPostDialState(PostDialState.WAIT);
560        } else if (c == PhoneNumberUtils.WILD) {
561            setPostDialState(PostDialState.WILD);
562        } else {
563            return false;
564        }
565
566        return true;
567    }
568
569    public String
570    getRemainingPostDialString() {
571        if (postDialState == PostDialState.CANCELLED
572            || postDialState == PostDialState.COMPLETE
573            || postDialString == null
574            || postDialString.length() <= nextPostDialChar
575        ) {
576            return "";
577        }
578
579        return postDialString.substring(nextPostDialChar);
580    }
581
582    @Override
583    protected void finalize()
584    {
585        /**
586         * It is understood that This finializer is not guaranteed
587         * to be called and the release lock call is here just in
588         * case there is some path that doesn't call onDisconnect
589         * and or onConnectedInOrOut.
590         */
591        if (mPartialWakeLock.isHeld()) {
592            Log.e(LOG_TAG, "[GSMConn] UNEXPECTED; mPartialWakeLock is held when finalizing.");
593        }
594        releaseWakeLock();
595    }
596
597    private void
598    processNextPostDialChar() {
599        char c = 0;
600        Registrant postDialHandler;
601
602        if (postDialState == PostDialState.CANCELLED) {
603            //Log.v("GSM", "##### processNextPostDialChar: postDialState == CANCELLED, bail");
604            return;
605        }
606
607        if (postDialString == null ||
608                postDialString.length() <= nextPostDialChar) {
609            setPostDialState(PostDialState.COMPLETE);
610
611            // notifyMessage.arg1 is 0 on complete
612            c = 0;
613        } else {
614            boolean isValid;
615
616            setPostDialState(PostDialState.STARTED);
617
618            c = postDialString.charAt(nextPostDialChar++);
619
620            isValid = processPostDialChar(c);
621
622            if (!isValid) {
623                // Will call processNextPostDialChar
624                h.obtainMessage(EVENT_NEXT_POST_DIAL).sendToTarget();
625                // Don't notify application
626                Log.e("GSM", "processNextPostDialChar: c=" + c + " isn't valid!");
627                return;
628            }
629        }
630
631        postDialHandler = owner.phone.mPostDialHandler;
632
633        Message notifyMessage;
634
635        if (postDialHandler != null
636                && (notifyMessage = postDialHandler.messageForRegistrant()) != null) {
637            // The AsyncResult.result is the Connection object
638            PostDialState state = postDialState;
639            AsyncResult ar = AsyncResult.forMessage(notifyMessage);
640            ar.result = this;
641            ar.userObj = state;
642
643            // arg1 is the character that was/is being processed
644            notifyMessage.arg1 = c;
645
646            //Log.v("GSM", "##### processNextPostDialChar: send msg to postDialHandler, arg1=" + c);
647            notifyMessage.sendToTarget();
648        }
649    }
650
651
652    /** "connecting" means "has never been ACTIVE" for both incoming
653     *  and outgoing calls
654     */
655    private boolean
656    isConnectingInOrOut() {
657        return parent == null || parent == owner.ringingCall
658            || parent.state == GsmCall.State.DIALING
659            || parent.state == GsmCall.State.ALERTING;
660    }
661
662    private GsmCall
663    parentFromDCState (DriverCall.State state) {
664        switch (state) {
665            case ACTIVE:
666            case DIALING:
667            case ALERTING:
668                return owner.foregroundCall;
669            //break;
670
671            case HOLDING:
672                return owner.backgroundCall;
673            //break;
674
675            case INCOMING:
676            case WAITING:
677                return owner.ringingCall;
678            //break;
679
680            default:
681                throw new RuntimeException("illegal call state: " + state);
682        }
683    }
684
685    /**
686     * Set post dial state and acquire wake lock while switching to "started"
687     * state, the wake lock will be released if state switches out of "started"
688     * state or after WAKE_LOCK_TIMEOUT_MILLIS.
689     * @param s new PostDialState
690     */
691    private void setPostDialState(PostDialState s) {
692        if (postDialState != PostDialState.STARTED
693                && s == PostDialState.STARTED) {
694            acquireWakeLock();
695            Message msg = h.obtainMessage(EVENT_WAKE_LOCK_TIMEOUT);
696            h.sendMessageDelayed(msg, WAKE_LOCK_TIMEOUT_MILLIS);
697        } else if (postDialState == PostDialState.STARTED
698                && s != PostDialState.STARTED) {
699            h.removeMessages(EVENT_WAKE_LOCK_TIMEOUT);
700            releaseWakeLock();
701        }
702        postDialState = s;
703    }
704
705    private void
706    createWakeLock(Context context) {
707        PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
708        mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
709    }
710
711    private void
712    acquireWakeLock() {
713        log("acquireWakeLock");
714        mPartialWakeLock.acquire();
715    }
716
717    private void
718    releaseWakeLock() {
719        synchronized(mPartialWakeLock) {
720            if (mPartialWakeLock.isHeld()) {
721                log("releaseWakeLock");
722                mPartialWakeLock.release();
723            }
724        }
725    }
726
727    private void log(String msg) {
728        Log.d(LOG_TAG, "[GSMConn] " + msg);
729    }
730
731    @Override
732    public int getNumberPresentation() {
733        return numberPresentation;
734    }
735
736    @Override
737    public UUSInfo getUUSInfo() {
738        return uusInfo;
739    }
740}
741