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