GsmMmiCode.java revision e0e2ebb5a50e580cbe6957dcafb3495a2d0a27f2
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;
18
19import android.content.Context;
20import com.android.internal.telephony.*;
21
22import android.os.*;
23import android.telephony.PhoneNumberUtils;
24import android.text.SpannableStringBuilder;
25import android.text.TextUtils;
26import android.util.Log;
27
28import static com.android.internal.telephony.CommandsInterface.*;
29
30import java.util.regex.Pattern;
31import java.util.regex.Matcher;
32
33/**
34 * The motto for this file is:
35 *
36 * "NOTE:    By using the # as a separator, most cases are expected to be unambiguous."
37 *   -- TS 22.030 6.5.2
38 *
39 * {@hide}
40 *
41 */
42public final class GsmMmiCode extends Handler implements MmiCode {
43    static final String LOG_TAG = "GSM";
44
45    //***** Constants
46
47    // Max Size of the Short Code (aka Short String from TS 22.030 6.5.2)
48    static final int MAX_LENGTH_SHORT_CODE = 2;
49
50    // TS 22.030 6.5.2 Every Short String USSD command will end with #-key
51    // (known as #-String)
52    static final char END_OF_USSD_COMMAND = '#';
53
54    // From TS 22.030 6.5.2
55    static final String ACTION_ACTIVATE = "*";
56    static final String ACTION_DEACTIVATE = "#";
57    static final String ACTION_INTERROGATE = "*#";
58    static final String ACTION_REGISTER = "**";
59    static final String ACTION_ERASURE = "##";
60
61    // Supp Service codes from TS 22.030 Annex B
62
63    //Called line presentation
64    static final String SC_CLIP    = "30";
65    static final String SC_CLIR    = "31";
66
67    // Call Forwarding
68    static final String SC_CFU     = "21";
69    static final String SC_CFB     = "67";
70    static final String SC_CFNRy   = "61";
71    static final String SC_CFNR    = "62";
72
73    static final String SC_CF_All = "002";
74    static final String SC_CF_All_Conditional = "004";
75
76    // Call Waiting
77    static final String SC_WAIT     = "43";
78
79    // Call Barring
80    static final String SC_BAOC         = "33";
81    static final String SC_BAOIC        = "331";
82    static final String SC_BAOICxH      = "332";
83    static final String SC_BAIC         = "35";
84    static final String SC_BAICr        = "351";
85
86    static final String SC_BA_ALL       = "330";
87    static final String SC_BA_MO        = "333";
88    static final String SC_BA_MT        = "353";
89
90    // Supp Service Password registration
91    static final String SC_PWD          = "03";
92
93    // PIN/PIN2/PUK/PUK2
94    static final String SC_PIN          = "04";
95    static final String SC_PIN2         = "042";
96    static final String SC_PUK          = "05";
97    static final String SC_PUK2         = "052";
98
99    //***** Event Constants
100
101    static final int EVENT_SET_COMPLETE         = 1;
102    static final int EVENT_GET_CLIR_COMPLETE    = 2;
103    static final int EVENT_QUERY_CF_COMPLETE    = 3;
104    static final int EVENT_USSD_COMPLETE        = 4;
105    static final int EVENT_QUERY_COMPLETE       = 5;
106    static final int EVENT_SET_CFF_COMPLETE     = 6;
107    static final int EVENT_USSD_CANCEL_COMPLETE = 7;
108
109    //***** Instance Variables
110
111    GSMPhone phone;
112    Context context;
113
114    String action;              // One of ACTION_*
115    String sc;                  // Service Code
116    String sia, sib, sic;       // Service Info a,b,c
117    String poundString;         // Entire MMI string up to and including #
118    String dialingNumber;
119    String pwd;                 // For password registration
120
121    /** Set to true in processCode, not at newFromDialString time */
122    private boolean isPendingUSSD;
123
124    private boolean isUssdRequest;
125
126    State state = State.PENDING;
127    CharSequence message;
128
129    //***** Class Variables
130
131
132    // See TS 22.030 6.5.2 "Structure of the MMI"
133
134    static Pattern sPatternSuppService = Pattern.compile(
135        "((\\*|#|\\*#|\\*\\*|##)(\\d{2,3})(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*)(\\*([^*#]*))?)?)?)?#)([^#]*)");
136/*       1  2                    3          4  5       6   7         8    9     10  11             12
137
138         1 = Full string up to and including #
139         2 = action (activation/interrogation/registration/erasure)
140         3 = service code
141         5 = SIA
142         7 = SIB
143         9 = SIC
144         10 = dialing number which must not include #, e.g. *SCn*SI#DN format
145*/
146
147    static final int MATCH_GROUP_POUND_STRING = 1;
148
149    static final int MATCH_GROUP_ACTION = 2;
150                        //(activation/interrogation/registration/erasure)
151
152    static final int MATCH_GROUP_SERVICE_CODE = 3;
153    static final int MATCH_GROUP_SIA = 5;
154    static final int MATCH_GROUP_SIB = 7;
155    static final int MATCH_GROUP_SIC = 9;
156    static final int MATCH_GROUP_PWD_CONFIRM = 11;
157    static final int MATCH_GROUP_DIALING_NUMBER = 12;
158    static private String[] sTwoDigitNumberPattern;
159
160    //***** Public Class methods
161
162    /**
163     * Some dial strings in GSM are defined to do non-call setup
164     * things, such as modify or query supplementary service settings (eg, call
165     * forwarding). These are generally referred to as "MMI codes".
166     * We look to see if the dial string contains a valid MMI code (potentially
167     * with a dial string at the end as well) and return info here.
168     *
169     * If the dial string contains no MMI code, we return an instance with
170     * only "dialingNumber" set
171     *
172     * Please see flow chart in TS 22.030 6.5.3.2
173     */
174
175    static GsmMmiCode
176    newFromDialString(String dialString, GSMPhone phone) {
177        Matcher m;
178        GsmMmiCode ret = null;
179
180        m = sPatternSuppService.matcher(dialString);
181
182        // Is this formatted like a standard supplementary service code?
183        if (m.matches()) {
184            ret = new GsmMmiCode(phone);
185            ret.poundString = makeEmptyNull(m.group(MATCH_GROUP_POUND_STRING));
186            ret.action = makeEmptyNull(m.group(MATCH_GROUP_ACTION));
187            ret.sc = makeEmptyNull(m.group(MATCH_GROUP_SERVICE_CODE));
188            ret.sia = makeEmptyNull(m.group(MATCH_GROUP_SIA));
189            ret.sib = makeEmptyNull(m.group(MATCH_GROUP_SIB));
190            ret.sic = makeEmptyNull(m.group(MATCH_GROUP_SIC));
191            ret.pwd = makeEmptyNull(m.group(MATCH_GROUP_PWD_CONFIRM));
192            ret.dialingNumber = makeEmptyNull(m.group(MATCH_GROUP_DIALING_NUMBER));
193
194        } else if (dialString.endsWith("#")) {
195            // TS 22.030 sec 6.5.3.2
196            // "Entry of any characters defined in the 3GPP TS 23.038 [8] Default Alphabet
197            // (up to the maximum defined in 3GPP TS 24.080 [10]), followed by #SEND".
198
199            ret = new GsmMmiCode(phone);
200            ret.poundString = dialString;
201        } else if (isTwoDigitShortCode(phone.getContext(), dialString)) {
202            //Is a country-specific exception to short codes as defined in TS 22.030, 6.5.3.2
203            ret = null;
204        } else if (isShortCode(dialString, phone)) {
205            // this may be a short code, as defined in TS 22.030, 6.5.3.2
206            ret = new GsmMmiCode(phone);
207            ret.dialingNumber = dialString;
208        }
209
210        return ret;
211    }
212
213    static GsmMmiCode
214    newNetworkInitiatedUssd (String ussdMessage,
215                                boolean isUssdRequest, GSMPhone phone) {
216        GsmMmiCode ret;
217
218        ret = new GsmMmiCode(phone);
219
220        ret.message = ussdMessage;
221        ret.isUssdRequest = isUssdRequest;
222
223        // If it's a request, set to PENDING so that it's cancelable.
224        if (isUssdRequest) {
225            ret.isPendingUSSD = true;
226            ret.state = State.PENDING;
227        } else {
228            ret.state = State.COMPLETE;
229        }
230
231        return ret;
232    }
233
234    static GsmMmiCode newFromUssdUserInput(String ussdMessge, GSMPhone phone) {
235        GsmMmiCode ret = new GsmMmiCode(phone);
236
237        ret.message = ussdMessge;
238        ret.state = State.PENDING;
239        ret.isPendingUSSD = true;
240
241        return ret;
242    }
243
244    //***** Private Class methods
245
246    /** make empty strings be null.
247     *  Regexp returns empty strings for empty groups
248     */
249    private static String
250    makeEmptyNull (String s) {
251        if (s != null && s.length() == 0) return null;
252
253        return s;
254    }
255
256    /** returns true of the string is empty or null */
257    private static boolean
258    isEmptyOrNull(CharSequence s) {
259        return s == null || (s.length() == 0);
260    }
261
262
263    private static int
264    scToCallForwardReason(String sc) {
265        if (sc == null) {
266            throw new RuntimeException ("invalid call forward sc");
267        }
268
269        if (sc.equals(SC_CF_All)) {
270           return CommandsInterface.CF_REASON_ALL;
271        } else if (sc.equals(SC_CFU)) {
272            return CommandsInterface.CF_REASON_UNCONDITIONAL;
273        } else if (sc.equals(SC_CFB)) {
274            return CommandsInterface.CF_REASON_BUSY;
275        } else if (sc.equals(SC_CFNR)) {
276            return CommandsInterface.CF_REASON_NOT_REACHABLE;
277        } else if (sc.equals(SC_CFNRy)) {
278            return CommandsInterface.CF_REASON_NO_REPLY;
279        } else if (sc.equals(SC_CF_All_Conditional)) {
280           return CommandsInterface.CF_REASON_ALL_CONDITIONAL;
281        } else {
282            throw new RuntimeException ("invalid call forward sc");
283        }
284    }
285
286    private static int
287    siToServiceClass(String si) {
288        if (si == null || si.length() == 0) {
289                return  SERVICE_CLASS_NONE;
290        } else {
291            // NumberFormatException should cause MMI fail
292            int serviceCode = Integer.parseInt(si, 10);
293
294            switch (serviceCode) {
295                case 10: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX  + SERVICE_CLASS_VOICE;
296                case 11: return SERVICE_CLASS_VOICE;
297                case 12: return SERVICE_CLASS_SMS + SERVICE_CLASS_FAX;
298                case 13: return SERVICE_CLASS_FAX;
299
300                case 16: return SERVICE_CLASS_SMS;
301
302                case 19: return SERVICE_CLASS_FAX + SERVICE_CLASS_VOICE;
303/*
304    Note for code 20:
305     From TS 22.030 Annex C:
306                "All GPRS bearer services" are not included in "All tele and bearer services"
307                    and "All bearer services"."
308....so SERVICE_CLASS_DATA, which (according to 27.007) includes GPRS
309*/
310                case 20: return SERVICE_CLASS_DATA_ASYNC + SERVICE_CLASS_DATA_SYNC;
311
312                case 21: return SERVICE_CLASS_PAD + SERVICE_CLASS_DATA_ASYNC;
313                case 22: return SERVICE_CLASS_PACKET + SERVICE_CLASS_DATA_SYNC;
314                case 24: return SERVICE_CLASS_DATA_SYNC;
315                case 25: return SERVICE_CLASS_DATA_ASYNC;
316                case 26: return SERVICE_CLASS_DATA_SYNC + SERVICE_CLASS_VOICE;
317                case 99: return SERVICE_CLASS_PACKET;
318
319                default:
320                    throw new RuntimeException("unsupported MMI service code " + si);
321            }
322        }
323    }
324
325    private static int
326    siToTime (String si) {
327        if (si == null || si.length() == 0) {
328            return 0;
329        } else {
330            // NumberFormatException should cause MMI fail
331            return Integer.parseInt(si, 10);
332        }
333    }
334
335    static boolean
336    isServiceCodeCallForwarding(String sc) {
337        return sc != null &&
338                (sc.equals(SC_CFU)
339                || sc.equals(SC_CFB) || sc.equals(SC_CFNRy)
340                || sc.equals(SC_CFNR) || sc.equals(SC_CF_All)
341                || sc.equals(SC_CF_All_Conditional));
342    }
343
344    static boolean
345    isServiceCodeCallBarring(String sc) {
346        return sc != null &&
347                (sc.equals(SC_BAOC)
348                || sc.equals(SC_BAOIC)
349                || sc.equals(SC_BAOICxH)
350                || sc.equals(SC_BAIC)
351                || sc.equals(SC_BAICr)
352                || sc.equals(SC_BA_ALL)
353                || sc.equals(SC_BA_MO)
354                || sc.equals(SC_BA_MT));
355    }
356
357    static String
358    scToBarringFacility(String sc) {
359        if (sc == null) {
360            throw new RuntimeException ("invalid call barring sc");
361        }
362
363        if (sc.equals(SC_BAOC)) {
364            return CommandsInterface.CB_FACILITY_BAOC;
365        } else if (sc.equals(SC_BAOIC)) {
366            return CommandsInterface.CB_FACILITY_BAOIC;
367        } else if (sc.equals(SC_BAOICxH)) {
368            return CommandsInterface.CB_FACILITY_BAOICxH;
369        } else if (sc.equals(SC_BAIC)) {
370            return CommandsInterface.CB_FACILITY_BAIC;
371        } else if (sc.equals(SC_BAICr)) {
372            return CommandsInterface.CB_FACILITY_BAICr;
373        } else if (sc.equals(SC_BA_ALL)) {
374            return CommandsInterface.CB_FACILITY_BA_ALL;
375        } else if (sc.equals(SC_BA_MO)) {
376            return CommandsInterface.CB_FACILITY_BA_MO;
377        } else if (sc.equals(SC_BA_MT)) {
378            return CommandsInterface.CB_FACILITY_BA_MT;
379        } else {
380            throw new RuntimeException ("invalid call barring sc");
381        }
382    }
383
384    //***** Constructor
385
386    GsmMmiCode (GSMPhone phone) {
387        // The telephony unit-test cases may create GsmMmiCode's
388        // in secondary threads
389        super(phone.getHandler().getLooper());
390        this.phone = phone;
391        this.context = phone.getContext();
392    }
393
394    //***** MmiCode implementation
395
396    public State
397    getState() {
398        return state;
399    }
400
401    public CharSequence
402    getMessage() {
403        return message;
404    }
405
406    // inherited javadoc suffices
407    public void
408    cancel() {
409        // Complete or failed cannot be cancelled
410        if (state == State.COMPLETE || state == State.FAILED) {
411            return;
412        }
413
414        state = State.CANCELLED;
415
416        if (isPendingUSSD) {
417            /*
418             * There can only be one pending USSD session, so tell the radio to
419             * cancel it.
420             */
421            phone.mCM.cancelPendingUssd(obtainMessage(EVENT_USSD_CANCEL_COMPLETE, this));
422
423            /*
424             * Don't call phone.onMMIDone here; wait for CANCEL_COMPLETE notice
425             * from RIL.
426             */
427        } else {
428            // TODO in cases other than USSD, it would be nice to cancel
429            // the pending radio operation. This requires RIL cancellation
430            // support, which does not presently exist.
431
432            phone.onMMIDone (this);
433        }
434
435    }
436
437    public boolean isCancelable() {
438        /* Can only cancel pending USSD sessions. */
439        return isPendingUSSD;
440    }
441
442    //***** Instance Methods
443
444    /** Does this dial string contain a structured or unstructured MMI code? */
445    boolean
446    isMMI() {
447        return poundString != null;
448    }
449
450    /* Is this a 1 or 2 digit "short code" as defined in TS 22.030 sec 6.5.3.2? */
451    boolean
452    isShortCode() {
453        return poundString == null
454                    && dialingNumber != null && dialingNumber.length() <= 2;
455
456    }
457
458    static private boolean
459    isTwoDigitShortCode(Context context, String dialString) {
460        Log.d(LOG_TAG, "isTwoDigitShortCode");
461
462        if (dialString == null || dialString.length() != 2) return false;
463
464        if (sTwoDigitNumberPattern == null) {
465            sTwoDigitNumberPattern = context.getResources().getStringArray(
466                    com.android.internal.R.array.config_twoDigitNumberPattern);
467        }
468
469        for (String dialnumber : sTwoDigitNumberPattern) {
470            Log.d(LOG_TAG, "Two Digit Number Pattern " + dialnumber);
471            if (dialString.equals(dialnumber)) {
472                Log.d(LOG_TAG, "Two Digit Number Pattern -true");
473                return true;
474            }
475        }
476        Log.d(LOG_TAG, "Two Digit Number Pattern -false");
477        return false;
478    }
479
480    /**
481     * Helper function for newFromDialString. Returns true if dialString appears
482     * to be a short code AND conditions are correct for it to be treated as
483     * such.
484     */
485    static private boolean isShortCode(String dialString, GSMPhone phone) {
486        // Refer to TS 22.030 Figure 3.5.3.2:
487        if (dialString == null) {
488            return false;
489        }
490
491        // Illegal dial string characters will give a ZERO length.
492        // At this point we do not want to crash as any application with
493        // call privileges may send a non dial string.
494        // It return false as when the dialString is equal to NULL.
495        if (dialString.length() == 0) {
496            return false;
497        }
498
499        if (PhoneNumberUtils.isLocalEmergencyNumber(dialString, phone.getContext())) {
500            return false;
501        } else {
502            return isShortCodeUSSD(dialString, phone);
503        }
504    }
505
506    /**
507     * Helper function for isShortCode. Returns true if dialString appears to be
508     * a short code and it is a USSD structure
509     *
510     * According to the 3PGG TS 22.030 specification Figure 3.5.3.2: A 1 or 2
511     * digit "short code" is treated as USSD if it is entered while on a call or
512     * does not satisfy the condition (exactly 2 digits && starts with '1'), there
513     * are however exceptions to this rule (see below)
514     *
515     * Exception (1) to Call initiation is: If the user of the device is already in a call
516     * and enters a Short String without any #-key at the end and the length of the Short String is
517     * equal or less then the MAX_LENGTH_SHORT_CODE [constant that is equal to 2]
518     *
519     * The phone shall initiate a USSD/SS commands.
520     *
521     * Exception (2) to Call initiation is: If the user of the device enters one
522     * Digit followed by the #-key. This rule defines this String as the
523     * #-String which is a USSD/SS command.
524     *
525     * The phone shall initiate a USSD/SS command.
526     */
527    static private boolean isShortCodeUSSD(String dialString, GSMPhone phone) {
528        if (dialString != null) {
529            if (phone.isInCall()) {
530                // The maximum length of a Short Code (aka Short String) is 2
531                if (dialString.length() <= MAX_LENGTH_SHORT_CODE) {
532                    return true;
533                }
534            }
535
536            // The maximum length of a Short Code (aka Short String) is 2
537            if (dialString.length() <= MAX_LENGTH_SHORT_CODE) {
538                if (dialString.charAt(dialString.length() - 1) == END_OF_USSD_COMMAND) {
539                    return true;
540                }
541            }
542        }
543        return false;
544    }
545
546    /**
547     * @return true if the Service Code is PIN/PIN2/PUK/PUK2-related
548     */
549    boolean isPinCommand() {
550        return sc != null && (sc.equals(SC_PIN) || sc.equals(SC_PIN2)
551                              || sc.equals(SC_PUK) || sc.equals(SC_PUK2));
552     }
553
554    /**
555     * See TS 22.030 Annex B.
556     * In temporary mode, to suppress CLIR for a single call, enter:
557     *      " * 31 # [called number] SEND "
558     *  In temporary mode, to invoke CLIR for a single call enter:
559     *       " # 31 # [called number] SEND "
560     */
561    boolean
562    isTemporaryModeCLIR() {
563        return sc != null && sc.equals(SC_CLIR) && dialingNumber != null
564                && (isActivate() || isDeactivate());
565    }
566
567    /**
568     * returns CommandsInterface.CLIR_*
569     * See also isTemporaryModeCLIR()
570     */
571    int
572    getCLIRMode() {
573        if (sc != null && sc.equals(SC_CLIR)) {
574            if (isActivate()) {
575                return CommandsInterface.CLIR_SUPPRESSION;
576            } else if (isDeactivate()) {
577                return CommandsInterface.CLIR_INVOCATION;
578            }
579        }
580
581        return CommandsInterface.CLIR_DEFAULT;
582    }
583
584    boolean isActivate() {
585        return action != null && action.equals(ACTION_ACTIVATE);
586    }
587
588    boolean isDeactivate() {
589        return action != null && action.equals(ACTION_DEACTIVATE);
590    }
591
592    boolean isInterrogate() {
593        return action != null && action.equals(ACTION_INTERROGATE);
594    }
595
596    boolean isRegister() {
597        return action != null && action.equals(ACTION_REGISTER);
598    }
599
600    boolean isErasure() {
601        return action != null && action.equals(ACTION_ERASURE);
602    }
603
604    /**
605     * Returns true if this is a USSD code that's been submitted to the
606     * network...eg, after processCode() is called
607     */
608    public boolean isPendingUSSD() {
609        return isPendingUSSD;
610    }
611
612    public boolean isUssdRequest() {
613        return isUssdRequest;
614    }
615
616    /** Process a MMI code or short code...anything that isn't a dialing number */
617    void
618    processCode () {
619        try {
620            if (isShortCode()) {
621                Log.d(LOG_TAG, "isShortCode");
622                // These just get treated as USSD.
623                sendUssd(dialingNumber);
624            } else if (dialingNumber != null) {
625                // We should have no dialing numbers here
626                throw new RuntimeException ("Invalid or Unsupported MMI Code");
627            } else if (sc != null && sc.equals(SC_CLIP)) {
628                Log.d(LOG_TAG, "is CLIP");
629                if (isInterrogate()) {
630                    phone.mCM.queryCLIP(
631                            obtainMessage(EVENT_QUERY_COMPLETE, this));
632                } else {
633                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
634                }
635            } else if (sc != null && sc.equals(SC_CLIR)) {
636                Log.d(LOG_TAG, "is CLIR");
637                if (isActivate()) {
638                    phone.mCM.setCLIR(CommandsInterface.CLIR_INVOCATION,
639                        obtainMessage(EVENT_SET_COMPLETE, this));
640                } else if (isDeactivate()) {
641                    phone.mCM.setCLIR(CommandsInterface.CLIR_SUPPRESSION,
642                        obtainMessage(EVENT_SET_COMPLETE, this));
643                } else if (isInterrogate()) {
644                    phone.mCM.getCLIR(
645                        obtainMessage(EVENT_GET_CLIR_COMPLETE, this));
646                } else {
647                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
648                }
649            } else if (isServiceCodeCallForwarding(sc)) {
650                Log.d(LOG_TAG, "is CF");
651
652                String dialingNumber = sia;
653                int serviceClass = siToServiceClass(sib);
654                int reason = scToCallForwardReason(sc);
655                int time = siToTime(sic);
656
657                if (isInterrogate()) {
658                    phone.mCM.queryCallForwardStatus(
659                            reason, serviceClass,  dialingNumber,
660                                obtainMessage(EVENT_QUERY_CF_COMPLETE, this));
661                } else {
662                    int cfAction;
663
664                    if (isActivate()) {
665                        cfAction = CommandsInterface.CF_ACTION_ENABLE;
666                    } else if (isDeactivate()) {
667                        cfAction = CommandsInterface.CF_ACTION_DISABLE;
668                    } else if (isRegister()) {
669                        cfAction = CommandsInterface.CF_ACTION_REGISTRATION;
670                    } else if (isErasure()) {
671                        cfAction = CommandsInterface.CF_ACTION_ERASURE;
672                    } else {
673                        throw new RuntimeException ("invalid action");
674                    }
675
676                    int isSettingUnconditionalVoice =
677                        (((reason == CommandsInterface.CF_REASON_UNCONDITIONAL) ||
678                                (reason == CommandsInterface.CF_REASON_ALL)) &&
679                                (((serviceClass & CommandsInterface.SERVICE_CLASS_VOICE) != 0) ||
680                                 (serviceClass == CommandsInterface.SERVICE_CLASS_NONE))) ? 1 : 0;
681
682                    int isEnableDesired =
683                        ((cfAction == CommandsInterface.CF_ACTION_ENABLE) ||
684                                (cfAction == CommandsInterface.CF_ACTION_REGISTRATION)) ? 1 : 0;
685
686                    Log.d(LOG_TAG, "is CF setCallForward");
687                    phone.mCM.setCallForward(cfAction, reason, serviceClass,
688                            dialingNumber, time, obtainMessage(
689                                    EVENT_SET_CFF_COMPLETE,
690                                    isSettingUnconditionalVoice,
691                                    isEnableDesired, this));
692                }
693            } else if (isServiceCodeCallBarring(sc)) {
694                // sia = password
695                // sib = basic service group
696
697                String password = sia;
698                int serviceClass = siToServiceClass(sib);
699                String facility = scToBarringFacility(sc);
700
701                if (isInterrogate()) {
702                    phone.mCM.queryFacilityLock(facility, password,
703                            serviceClass, obtainMessage(EVENT_QUERY_COMPLETE, this));
704                } else if (isActivate() || isDeactivate()) {
705                    phone.mCM.setFacilityLock(facility, isActivate(), password,
706                            serviceClass, obtainMessage(EVENT_SET_COMPLETE, this));
707                } else {
708                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
709                }
710
711            } else if (sc != null && sc.equals(SC_PWD)) {
712                // sia = fac
713                // sib = old pwd
714                // sic = new pwd
715                // pwd = new pwd
716                String facility;
717                String oldPwd = sib;
718                String newPwd = sic;
719                if (isActivate() || isRegister()) {
720                    // Even though ACTIVATE is acceptable, this is really termed a REGISTER
721                    action = ACTION_REGISTER;
722
723                    if (sia == null) {
724                        // If sc was not specified, treat it as BA_ALL.
725                        facility = CommandsInterface.CB_FACILITY_BA_ALL;
726                    } else {
727                        facility = scToBarringFacility(sia);
728                    }
729                    if (newPwd.equals(pwd)) {
730                        phone.mCM.changeBarringPassword(facility, oldPwd,
731                                newPwd, obtainMessage(EVENT_SET_COMPLETE, this));
732                    } else {
733                        // password mismatch; return error
734                        handlePasswordError(com.android.internal.R.string.passwordIncorrect);
735                    }
736                } else {
737                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
738                }
739
740            } else if (sc != null && sc.equals(SC_WAIT)) {
741                // sia = basic service group
742                int serviceClass = siToServiceClass(sia);
743
744                if (isActivate() || isDeactivate()) {
745                    phone.mCM.setCallWaiting(isActivate(), serviceClass,
746                            obtainMessage(EVENT_SET_COMPLETE, this));
747                } else if (isInterrogate()) {
748                    phone.mCM.queryCallWaiting(serviceClass,
749                            obtainMessage(EVENT_QUERY_COMPLETE, this));
750                } else {
751                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
752                }
753            } else if (isPinCommand()) {
754                // sia = old PIN or PUK
755                // sib = new PIN
756                // sic = new PIN
757                String oldPinOrPuk = sia;
758                String newPin = sib;
759                int pinLen = newPin.length();
760                if (isRegister()) {
761                    if (!newPin.equals(sic)) {
762                        // password mismatch; return error
763                        handlePasswordError(com.android.internal.R.string.mismatchPin);
764                    } else if (pinLen < 4 || pinLen > 8 ) {
765                        // invalid length
766                        handlePasswordError(com.android.internal.R.string.invalidPin);
767                    } else if (sc.equals(SC_PIN) && phone.getIccCard().getState() ==
768                               IccCardConstants.State.PUK_REQUIRED ) {
769                        // Sim is puk-locked
770                        handlePasswordError(com.android.internal.R.string.needPuk);
771                    } else {
772                        // pre-checks OK
773                        if (sc.equals(SC_PIN)) {
774                            phone.mCM.changeIccPin(oldPinOrPuk, newPin,
775                                    obtainMessage(EVENT_SET_COMPLETE, this));
776                        } else if (sc.equals(SC_PIN2)) {
777                            phone.mCM.changeIccPin2(oldPinOrPuk, newPin,
778                                    obtainMessage(EVENT_SET_COMPLETE, this));
779                        } else if (sc.equals(SC_PUK)) {
780                            phone.mCM.supplyIccPuk(oldPinOrPuk, newPin,
781                                    obtainMessage(EVENT_SET_COMPLETE, this));
782                        } else if (sc.equals(SC_PUK2)) {
783                            phone.mCM.supplyIccPuk2(oldPinOrPuk, newPin,
784                                    obtainMessage(EVENT_SET_COMPLETE, this));
785                        }
786                    }
787                } else {
788                    throw new RuntimeException ("Invalid or Unsupported MMI Code");
789                }
790            } else if (poundString != null) {
791                sendUssd(poundString);
792            } else {
793                throw new RuntimeException ("Invalid or Unsupported MMI Code");
794            }
795        } catch (RuntimeException exc) {
796            state = State.FAILED;
797            message = context.getText(com.android.internal.R.string.mmiError);
798            phone.onMMIDone(this);
799        }
800    }
801
802    private void handlePasswordError(int res) {
803        state = State.FAILED;
804        StringBuilder sb = new StringBuilder(getScString());
805        sb.append("\n");
806        sb.append(context.getText(res));
807        message = sb;
808        phone.onMMIDone(this);
809    }
810
811    /**
812     * Called from GSMPhone
813     *
814     * An unsolicited USSD NOTIFY or REQUEST has come in matching
815     * up with this pending USSD request
816     *
817     * Note: If REQUEST, this exchange is complete, but the session remains
818     *       active (ie, the network expects user input).
819     */
820    void
821    onUssdFinished(String ussdMessage, boolean isUssdRequest) {
822        if (state == State.PENDING) {
823            if (ussdMessage == null) {
824                message = context.getText(com.android.internal.R.string.mmiComplete);
825            } else {
826                message = ussdMessage;
827            }
828            this.isUssdRequest = isUssdRequest;
829            // If it's a request, leave it PENDING so that it's cancelable.
830            if (!isUssdRequest) {
831                state = State.COMPLETE;
832            }
833
834            phone.onMMIDone(this);
835        }
836    }
837
838    /**
839     * Called from GSMPhone
840     *
841     * The radio has reset, and this is still pending
842     */
843
844    void
845    onUssdFinishedError() {
846        if (state == State.PENDING) {
847            state = State.FAILED;
848            message = context.getText(com.android.internal.R.string.mmiError);
849
850            phone.onMMIDone(this);
851        }
852    }
853
854    void sendUssd(String ussdMessage) {
855        // Treat this as a USSD string
856        isPendingUSSD = true;
857
858        // Note that unlike most everything else, the USSD complete
859        // response does not complete this MMI code...we wait for
860        // an unsolicited USSD "Notify" or "Request".
861        // The matching up of this is done in GSMPhone.
862
863        phone.mCM.sendUSSD(ussdMessage,
864            obtainMessage(EVENT_USSD_COMPLETE, this));
865    }
866
867    /** Called from GSMPhone.handleMessage; not a Handler subclass */
868    public void
869    handleMessage (Message msg) {
870        AsyncResult ar;
871
872        switch (msg.what) {
873            case EVENT_SET_COMPLETE:
874                ar = (AsyncResult) (msg.obj);
875
876                onSetComplete(ar);
877                break;
878
879            case EVENT_SET_CFF_COMPLETE:
880                ar = (AsyncResult) (msg.obj);
881
882                /*
883                * msg.arg1 = 1 means to set unconditional voice call forwarding
884                * msg.arg2 = 1 means to enable voice call forwarding
885                */
886                if ((ar.exception == null) && (msg.arg1 == 1)) {
887                    boolean cffEnabled = (msg.arg2 == 1);
888                    IccRecords r = phone.mIccRecords.get();
889                    if (r != null) {
890                        r.setVoiceCallForwardingFlag(1, cffEnabled);
891                    }
892                }
893
894                onSetComplete(ar);
895                break;
896
897            case EVENT_GET_CLIR_COMPLETE:
898                ar = (AsyncResult) (msg.obj);
899                onGetClirComplete(ar);
900            break;
901
902            case EVENT_QUERY_CF_COMPLETE:
903                ar = (AsyncResult) (msg.obj);
904                onQueryCfComplete(ar);
905            break;
906
907            case EVENT_QUERY_COMPLETE:
908                ar = (AsyncResult) (msg.obj);
909                onQueryComplete(ar);
910            break;
911
912            case EVENT_USSD_COMPLETE:
913                ar = (AsyncResult) (msg.obj);
914
915                if (ar.exception != null) {
916                    state = State.FAILED;
917                    message = getErrorMessage(ar);
918
919                    phone.onMMIDone(this);
920                }
921
922                // Note that unlike most everything else, the USSD complete
923                // response does not complete this MMI code...we wait for
924                // an unsolicited USSD "Notify" or "Request".
925                // The matching up of this is done in GSMPhone.
926
927            break;
928
929            case EVENT_USSD_CANCEL_COMPLETE:
930                phone.onMMIDone(this);
931            break;
932        }
933    }
934    //***** Private instance methods
935
936    private CharSequence getErrorMessage(AsyncResult ar) {
937
938        if (ar.exception instanceof CommandException) {
939            CommandException.Error err = ((CommandException)(ar.exception)).getCommandError();
940            if (err == CommandException.Error.FDN_CHECK_FAILURE) {
941                Log.i(LOG_TAG, "FDN_CHECK_FAILURE");
942                return context.getText(com.android.internal.R.string.mmiFdnError);
943            }
944        }
945
946        return context.getText(com.android.internal.R.string.mmiError);
947    }
948
949    private CharSequence getScString() {
950        if (sc != null) {
951            if (isServiceCodeCallBarring(sc)) {
952                return context.getText(com.android.internal.R.string.BaMmi);
953            } else if (isServiceCodeCallForwarding(sc)) {
954                return context.getText(com.android.internal.R.string.CfMmi);
955            } else if (sc.equals(SC_CLIP)) {
956                return context.getText(com.android.internal.R.string.ClipMmi);
957            } else if (sc.equals(SC_CLIR)) {
958                return context.getText(com.android.internal.R.string.ClirMmi);
959            } else if (sc.equals(SC_PWD)) {
960                return context.getText(com.android.internal.R.string.PwdMmi);
961            } else if (sc.equals(SC_WAIT)) {
962                return context.getText(com.android.internal.R.string.CwMmi);
963            } else if (isPinCommand()) {
964                return context.getText(com.android.internal.R.string.PinMmi);
965            }
966        }
967
968        return "";
969    }
970
971    private void
972    onSetComplete(AsyncResult ar){
973        StringBuilder sb = new StringBuilder(getScString());
974        sb.append("\n");
975
976        if (ar.exception != null) {
977            state = State.FAILED;
978            if (ar.exception instanceof CommandException) {
979                CommandException.Error err = ((CommandException)(ar.exception)).getCommandError();
980                if (err == CommandException.Error.PASSWORD_INCORRECT) {
981                    if (isPinCommand()) {
982                        // look specifically for the PUK commands and adjust
983                        // the message accordingly.
984                        if (sc.equals(SC_PUK) || sc.equals(SC_PUK2)) {
985                            sb.append(context.getText(
986                                    com.android.internal.R.string.badPuk));
987                        } else {
988                            sb.append(context.getText(
989                                    com.android.internal.R.string.badPin));
990                        }
991                    } else {
992                        sb.append(context.getText(
993                                com.android.internal.R.string.passwordIncorrect));
994                    }
995                } else if (err == CommandException.Error.SIM_PUK2) {
996                    sb.append(context.getText(
997                            com.android.internal.R.string.badPin));
998                    sb.append("\n");
999                    sb.append(context.getText(
1000                            com.android.internal.R.string.needPuk2));
1001                } else if (err == CommandException.Error.FDN_CHECK_FAILURE) {
1002                    Log.i(LOG_TAG, "FDN_CHECK_FAILURE");
1003                    sb.append(context.getText(com.android.internal.R.string.mmiFdnError));
1004                } else {
1005                    sb.append(context.getText(
1006                            com.android.internal.R.string.mmiError));
1007                }
1008            } else {
1009                sb.append(context.getText(
1010                        com.android.internal.R.string.mmiError));
1011            }
1012        } else if (isActivate()) {
1013            state = State.COMPLETE;
1014            sb.append(context.getText(
1015                    com.android.internal.R.string.serviceEnabled));
1016            // Record CLIR setting
1017            if (sc.equals(SC_CLIR)) {
1018                phone.saveClirSetting(CommandsInterface.CLIR_INVOCATION);
1019            }
1020        } else if (isDeactivate()) {
1021            state = State.COMPLETE;
1022            sb.append(context.getText(
1023                    com.android.internal.R.string.serviceDisabled));
1024            // Record CLIR setting
1025            if (sc.equals(SC_CLIR)) {
1026                phone.saveClirSetting(CommandsInterface.CLIR_SUPPRESSION);
1027            }
1028        } else if (isRegister()) {
1029            state = State.COMPLETE;
1030            sb.append(context.getText(
1031                    com.android.internal.R.string.serviceRegistered));
1032        } else if (isErasure()) {
1033            state = State.COMPLETE;
1034            sb.append(context.getText(
1035                    com.android.internal.R.string.serviceErased));
1036        } else {
1037            state = State.FAILED;
1038            sb.append(context.getText(
1039                    com.android.internal.R.string.mmiError));
1040        }
1041
1042        message = sb;
1043        phone.onMMIDone(this);
1044    }
1045
1046    private void
1047    onGetClirComplete(AsyncResult ar) {
1048        StringBuilder sb = new StringBuilder(getScString());
1049        sb.append("\n");
1050
1051        if (ar.exception != null) {
1052            state = State.FAILED;
1053            sb.append(getErrorMessage(ar));
1054        } else {
1055            int clirArgs[];
1056
1057            clirArgs = (int[])ar.result;
1058
1059            // the 'm' parameter from TS 27.007 7.7
1060            switch (clirArgs[1]) {
1061                case 0: // CLIR not provisioned
1062                    sb.append(context.getText(
1063                                com.android.internal.R.string.serviceNotProvisioned));
1064                    state = State.COMPLETE;
1065                break;
1066
1067                case 1: // CLIR provisioned in permanent mode
1068                    sb.append(context.getText(
1069                                com.android.internal.R.string.CLIRPermanent));
1070                    state = State.COMPLETE;
1071                break;
1072
1073                case 2: // unknown (e.g. no network, etc.)
1074                    sb.append(context.getText(
1075                                com.android.internal.R.string.mmiError));
1076                    state = State.FAILED;
1077                break;
1078
1079                case 3: // CLIR temporary mode presentation restricted
1080
1081                    // the 'n' parameter from TS 27.007 7.7
1082                    switch (clirArgs[0]) {
1083                        default:
1084                        case 0: // Default
1085                            sb.append(context.getText(
1086                                    com.android.internal.R.string.CLIRDefaultOnNextCallOn));
1087                        break;
1088                        case 1: // CLIR invocation
1089                            sb.append(context.getText(
1090                                    com.android.internal.R.string.CLIRDefaultOnNextCallOn));
1091                        break;
1092                        case 2: // CLIR suppression
1093                            sb.append(context.getText(
1094                                    com.android.internal.R.string.CLIRDefaultOnNextCallOff));
1095                        break;
1096                    }
1097                    state = State.COMPLETE;
1098                break;
1099
1100                case 4: // CLIR temporary mode presentation allowed
1101                    // the 'n' parameter from TS 27.007 7.7
1102                    switch (clirArgs[0]) {
1103                        default:
1104                        case 0: // Default
1105                            sb.append(context.getText(
1106                                    com.android.internal.R.string.CLIRDefaultOffNextCallOff));
1107                        break;
1108                        case 1: // CLIR invocation
1109                            sb.append(context.getText(
1110                                    com.android.internal.R.string.CLIRDefaultOffNextCallOn));
1111                        break;
1112                        case 2: // CLIR suppression
1113                            sb.append(context.getText(
1114                                    com.android.internal.R.string.CLIRDefaultOffNextCallOff));
1115                        break;
1116                    }
1117
1118                    state = State.COMPLETE;
1119                break;
1120            }
1121        }
1122
1123        message = sb;
1124        phone.onMMIDone(this);
1125    }
1126
1127    /**
1128     * @param serviceClass 1 bit of the service class bit vectory
1129     * @return String to be used for call forward query MMI response text.
1130     *        Returns null if unrecognized
1131     */
1132
1133    private CharSequence
1134    serviceClassToCFString (int serviceClass) {
1135        switch (serviceClass) {
1136            case SERVICE_CLASS_VOICE:
1137                return context.getText(com.android.internal.R.string.serviceClassVoice);
1138            case SERVICE_CLASS_DATA:
1139                return context.getText(com.android.internal.R.string.serviceClassData);
1140            case SERVICE_CLASS_FAX:
1141                return context.getText(com.android.internal.R.string.serviceClassFAX);
1142            case SERVICE_CLASS_SMS:
1143                return context.getText(com.android.internal.R.string.serviceClassSMS);
1144            case SERVICE_CLASS_DATA_SYNC:
1145                return context.getText(com.android.internal.R.string.serviceClassDataSync);
1146            case SERVICE_CLASS_DATA_ASYNC:
1147                return context.getText(com.android.internal.R.string.serviceClassDataAsync);
1148            case SERVICE_CLASS_PACKET:
1149                return context.getText(com.android.internal.R.string.serviceClassPacket);
1150            case SERVICE_CLASS_PAD:
1151                return context.getText(com.android.internal.R.string.serviceClassPAD);
1152            default:
1153                return null;
1154        }
1155    }
1156
1157
1158    /** one CallForwardInfo + serviceClassMask -> one line of text */
1159    private CharSequence
1160    makeCFQueryResultMessage(CallForwardInfo info, int serviceClassMask) {
1161        CharSequence template;
1162        String sources[] = {"{0}", "{1}", "{2}"};
1163        CharSequence destinations[] = new CharSequence[3];
1164        boolean needTimeTemplate;
1165
1166        // CF_REASON_NO_REPLY also has a time value associated with
1167        // it. All others don't.
1168
1169        needTimeTemplate =
1170            (info.reason == CommandsInterface.CF_REASON_NO_REPLY);
1171
1172        if (info.status == 1) {
1173            if (needTimeTemplate) {
1174                template = context.getText(
1175                        com.android.internal.R.string.cfTemplateForwardedTime);
1176            } else {
1177                template = context.getText(
1178                        com.android.internal.R.string.cfTemplateForwarded);
1179            }
1180        } else if (info.status == 0 && isEmptyOrNull(info.number)) {
1181            template = context.getText(
1182                        com.android.internal.R.string.cfTemplateNotForwarded);
1183        } else { /* (info.status == 0) && !isEmptyOrNull(info.number) */
1184            // A call forward record that is not active but contains
1185            // a phone number is considered "registered"
1186
1187            if (needTimeTemplate) {
1188                template = context.getText(
1189                        com.android.internal.R.string.cfTemplateRegisteredTime);
1190            } else {
1191                template = context.getText(
1192                        com.android.internal.R.string.cfTemplateRegistered);
1193            }
1194        }
1195
1196        // In the template (from strings.xmls)
1197        //         {0} is one of "bearerServiceCode*"
1198        //        {1} is dialing number
1199        //      {2} is time in seconds
1200
1201        destinations[0] = serviceClassToCFString(info.serviceClass & serviceClassMask);
1202        destinations[1] = PhoneNumberUtils.stringFromStringAndTOA(info.number, info.toa);
1203        destinations[2] = Integer.toString(info.timeSeconds);
1204
1205        if (info.reason == CommandsInterface.CF_REASON_UNCONDITIONAL &&
1206                (info.serviceClass & serviceClassMask)
1207                        == CommandsInterface.SERVICE_CLASS_VOICE) {
1208            boolean cffEnabled = (info.status == 1);
1209            IccRecords r = phone.mIccRecords.get();
1210            if (r != null) {
1211                r.setVoiceCallForwardingFlag(1, cffEnabled);
1212            }
1213        }
1214
1215        return TextUtils.replace(template, sources, destinations);
1216    }
1217
1218
1219    private void
1220    onQueryCfComplete(AsyncResult ar) {
1221        StringBuilder sb = new StringBuilder(getScString());
1222        sb.append("\n");
1223
1224        if (ar.exception != null) {
1225            state = State.FAILED;
1226            sb.append(getErrorMessage(ar));
1227        } else {
1228            CallForwardInfo infos[];
1229
1230            infos = (CallForwardInfo[]) ar.result;
1231
1232            if (infos.length == 0) {
1233                // Assume the default is not active
1234                sb.append(context.getText(com.android.internal.R.string.serviceDisabled));
1235
1236                // Set unconditional CFF in SIM to false
1237                IccRecords r = phone.mIccRecords.get();
1238                if (r != null) {
1239                    r.setVoiceCallForwardingFlag(1, false);
1240                }
1241            } else {
1242
1243                SpannableStringBuilder tb = new SpannableStringBuilder();
1244
1245                // Each bit in the service class gets its own result line
1246                // The service classes may be split up over multiple
1247                // CallForwardInfos. So, for each service class, find out
1248                // which CallForwardInfo represents it and then build
1249                // the response text based on that
1250
1251                for (int serviceClassMask = 1
1252                            ; serviceClassMask <= SERVICE_CLASS_MAX
1253                            ; serviceClassMask <<= 1
1254                ) {
1255                    for (int i = 0, s = infos.length; i < s ; i++) {
1256                        if ((serviceClassMask & infos[i].serviceClass) != 0) {
1257                            tb.append(makeCFQueryResultMessage(infos[i],
1258                                            serviceClassMask));
1259                            tb.append("\n");
1260                        }
1261                    }
1262                }
1263                sb.append(tb);
1264            }
1265
1266            state = State.COMPLETE;
1267        }
1268
1269        message = sb;
1270        phone.onMMIDone(this);
1271
1272    }
1273
1274    private void
1275    onQueryComplete(AsyncResult ar) {
1276        StringBuilder sb = new StringBuilder(getScString());
1277        sb.append("\n");
1278
1279        if (ar.exception != null) {
1280            state = State.FAILED;
1281            sb.append(getErrorMessage(ar));
1282        } else {
1283            int[] ints = (int[])ar.result;
1284
1285            if (ints.length != 0) {
1286                if (ints[0] == 0) {
1287                    sb.append(context.getText(com.android.internal.R.string.serviceDisabled));
1288                } else if (sc.equals(SC_WAIT)) {
1289                    // Call Waiting includes additional data in the response.
1290                    sb.append(createQueryCallWaitingResultMessage(ints[1]));
1291                } else if (isServiceCodeCallBarring(sc)) {
1292                    // ints[0] for Call Barring is a bit vector of services
1293                    sb.append(createQueryCallBarringResultMessage(ints[0]));
1294                } else if (ints[0] == 1) {
1295                    // for all other services, treat it as a boolean
1296                    sb.append(context.getText(com.android.internal.R.string.serviceEnabled));
1297                } else {
1298                    sb.append(context.getText(com.android.internal.R.string.mmiError));
1299                }
1300            } else {
1301                sb.append(context.getText(com.android.internal.R.string.mmiError));
1302            }
1303            state = State.COMPLETE;
1304        }
1305
1306        message = sb;
1307        phone.onMMIDone(this);
1308    }
1309
1310    private CharSequence
1311    createQueryCallWaitingResultMessage(int serviceClass) {
1312        StringBuilder sb =
1313                new StringBuilder(context.getText(com.android.internal.R.string.serviceEnabledFor));
1314
1315        for (int classMask = 1
1316                    ; classMask <= SERVICE_CLASS_MAX
1317                    ; classMask <<= 1
1318        ) {
1319            if ((classMask & serviceClass) != 0) {
1320                sb.append("\n");
1321                sb.append(serviceClassToCFString(classMask & serviceClass));
1322            }
1323        }
1324        return sb;
1325    }
1326    private CharSequence
1327    createQueryCallBarringResultMessage(int serviceClass)
1328    {
1329        StringBuilder sb = new StringBuilder(context.getText(com.android.internal.R.string.serviceEnabledFor));
1330
1331        for (int classMask = 1
1332                    ; classMask <= SERVICE_CLASS_MAX
1333                    ; classMask <<= 1
1334        ) {
1335            if ((classMask & serviceClass) != 0) {
1336                sb.append("\n");
1337                sb.append(serviceClassToCFString(classMask & serviceClass));
1338            }
1339        }
1340        return sb;
1341    }
1342
1343    /***
1344     * TODO: It would be nice to have a method here that can take in a dialstring and
1345     * figure out if there is an MMI code embedded within it.  This code would replace
1346     * some of the string parsing functionality in the Phone App's
1347     * SpecialCharSequenceMgr class.
1348     */
1349
1350    @Override
1351    public String toString() {
1352        StringBuilder sb = new StringBuilder("GsmMmiCode {");
1353
1354        sb.append("State=" + getState());
1355        if (action != null) sb.append(" action=" + action);
1356        if (sc != null) sb.append(" sc=" + sc);
1357        if (sia != null) sb.append(" sia=" + sia);
1358        if (sib != null) sb.append(" sib=" + sib);
1359        if (sic != null) sb.append(" sic=" + sic);
1360        if (poundString != null) sb.append(" poundString=" + poundString);
1361        if (dialingNumber != null) sb.append(" dialingNumber=" + dialingNumber);
1362        if (pwd != null) sb.append(" pwd=" + pwd);
1363        sb.append("}");
1364        return sb.toString();
1365    }
1366}
1367