Home | History | Annotate | Download | only in gsm
      1 /*
      2  * Copyright (C) 2012 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 static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE;
     20 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI;
     21 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY;
     22 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE;
     23 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI;
     24 
     25 import android.content.Context;
     26 import android.content.res.Resources;
     27 import android.telephony.SmsCbLocation;
     28 import android.telephony.SmsCbMessage;
     29 import android.util.Pair;
     30 
     31 import com.android.internal.R;
     32 import com.android.internal.telephony.GsmAlphabet;
     33 import com.android.internal.telephony.SmsConstants;
     34 
     35 import java.io.UnsupportedEncodingException;
     36 
     37 /**
     38  * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is
     39  * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases.
     40  */
     41 public class GsmSmsCbMessage {
     42 
     43     /**
     44      * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5.
     45      */
     46     private static final String[] LANGUAGE_CODES_GROUP_0 = {
     47             "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu",
     48             "pl", null
     49     };
     50 
     51     /**
     52      * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5.
     53      */
     54     private static final String[] LANGUAGE_CODES_GROUP_2 = {
     55             "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null,
     56             null, null
     57     };
     58 
     59     private static final char CARRIAGE_RETURN = 0x0d;
     60 
     61     private static final int PDU_BODY_PAGE_LENGTH = 82;
     62 
     63     /** Utility class with only static methods. */
     64     private GsmSmsCbMessage() { }
     65 
     66     /**
     67      * Get built-in ETWS primary messages by category. ETWS primary message does not contain text,
     68      * so we have to show the pre-built messages to the user.
     69      *
     70      * @param context Device context
     71      * @param category ETWS message category defined in SmsCbConstants
     72      * @return ETWS text message in string. Return an empty string if no match.
     73      */
     74     private static String getEtwsPrimaryMessage(Context context, int category) {
     75         final Resources r = context.getResources();
     76         switch (category) {
     77             case ETWS_WARNING_TYPE_EARTHQUAKE:
     78                 return r.getString(R.string.etws_primary_default_message_earthquake);
     79             case ETWS_WARNING_TYPE_TSUNAMI:
     80                 return r.getString(R.string.etws_primary_default_message_tsunami);
     81             case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
     82                 return r.getString(R.string.etws_primary_default_message_earthquake_and_tsunami);
     83             case ETWS_WARNING_TYPE_TEST_MESSAGE:
     84                 return r.getString(R.string.etws_primary_default_message_test);
     85             case ETWS_WARNING_TYPE_OTHER_EMERGENCY:
     86                 return r.getString(R.string.etws_primary_default_message_others);
     87             default:
     88                 return "";
     89         }
     90     }
     91 
     92     /**
     93      * Create a new SmsCbMessage object from a header object plus one or more received PDUs.
     94      *
     95      * @param pdus PDU bytes
     96      */
     97     public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header,
     98                                                   SmsCbLocation location, byte[][] pdus)
     99             throws IllegalArgumentException {
    100         if (header.isEtwsPrimaryNotification()) {
    101             // ETSI TS 23.041 ETWS Primary Notification message
    102             // ETWS primary message only contains 4 fields including serial number,
    103             // message identifier, warning type, and warning security information.
    104             // There is no field for the content/text so we get the text from the resources.
    105             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(),
    106                     header.getSerialNumber(), location, header.getServiceCategory(), null,
    107                     getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
    108                     SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
    109                     header.getCmasInfo());
    110         } else {
    111             String language = null;
    112             StringBuilder sb = new StringBuilder();
    113             for (byte[] pdu : pdus) {
    114                 Pair<String, String> p = parseBody(header, pdu);
    115                 language = p.first;
    116                 sb.append(p.second);
    117             }
    118             int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
    119                     : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
    120 
    121             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
    122                     header.getGeographicalScope(), header.getSerialNumber(), location,
    123                     header.getServiceCategory(), language, sb.toString(), priority,
    124                     header.getEtwsInfo(), header.getCmasInfo());
    125         }
    126     }
    127 
    128     /**
    129      * Parse and unpack the body text according to the encoding in the DCS.
    130      * After completing successfully this method will have assigned the body
    131      * text into mBody, and optionally the language code into mLanguage
    132      *
    133      * @param header the message header to use
    134      * @param pdu the PDU to decode
    135      * @return a Pair of Strings containing the language and body of the message
    136      */
    137     private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) {
    138         int encoding;
    139         String language = null;
    140         boolean hasLanguageIndicator = false;
    141         int dataCodingScheme = header.getDataCodingScheme();
    142 
    143         // Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
    144         // section 5.
    145         switch ((dataCodingScheme & 0xf0) >> 4) {
    146             case 0x00:
    147                 encoding = SmsConstants.ENCODING_7BIT;
    148                 language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f];
    149                 break;
    150 
    151             case 0x01:
    152                 hasLanguageIndicator = true;
    153                 if ((dataCodingScheme & 0x0f) == 0x01) {
    154                     encoding = SmsConstants.ENCODING_16BIT;
    155                 } else {
    156                     encoding = SmsConstants.ENCODING_7BIT;
    157                 }
    158                 break;
    159 
    160             case 0x02:
    161                 encoding = SmsConstants.ENCODING_7BIT;
    162                 language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f];
    163                 break;
    164 
    165             case 0x03:
    166                 encoding = SmsConstants.ENCODING_7BIT;
    167                 break;
    168 
    169             case 0x04:
    170             case 0x05:
    171                 switch ((dataCodingScheme & 0x0c) >> 2) {
    172                     case 0x01:
    173                         encoding = SmsConstants.ENCODING_8BIT;
    174                         break;
    175 
    176                     case 0x02:
    177                         encoding = SmsConstants.ENCODING_16BIT;
    178                         break;
    179 
    180                     case 0x00:
    181                     default:
    182                         encoding = SmsConstants.ENCODING_7BIT;
    183                         break;
    184                 }
    185                 break;
    186 
    187             case 0x06:
    188             case 0x07:
    189                 // Compression not supported
    190             case 0x09:
    191                 // UDH structure not supported
    192             case 0x0e:
    193                 // Defined by the WAP forum not supported
    194                 throw new IllegalArgumentException("Unsupported GSM dataCodingScheme "
    195                         + dataCodingScheme);
    196 
    197             case 0x0f:
    198                 if (((dataCodingScheme & 0x04) >> 2) == 0x01) {
    199                     encoding = SmsConstants.ENCODING_8BIT;
    200                 } else {
    201                     encoding = SmsConstants.ENCODING_7BIT;
    202                 }
    203                 break;
    204 
    205             default:
    206                 // Reserved values are to be treated as 7-bit
    207                 encoding = SmsConstants.ENCODING_7BIT;
    208                 break;
    209         }
    210 
    211         if (header.isUmtsFormat()) {
    212             // Payload may contain multiple pages
    213             int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
    214 
    215             if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
    216                     * nrPages) {
    217                 throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
    218                         + nrPages + " pages");
    219             }
    220 
    221             StringBuilder sb = new StringBuilder();
    222 
    223             for (int i = 0; i < nrPages; i++) {
    224                 // Each page is 82 bytes followed by a length octet indicating
    225                 // the number of useful octets within those 82
    226                 int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i;
    227                 int length = pdu[offset + PDU_BODY_PAGE_LENGTH];
    228 
    229                 if (length > PDU_BODY_PAGE_LENGTH) {
    230                     throw new IllegalArgumentException("Page length " + length
    231                             + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH);
    232                 }
    233 
    234                 Pair<String, String> p = unpackBody(pdu, encoding, offset, length,
    235                         hasLanguageIndicator, language);
    236                 language = p.first;
    237                 sb.append(p.second);
    238             }
    239             return new Pair<String, String>(language, sb.toString());
    240         } else {
    241             // Payload is one single page
    242             int offset = SmsCbHeader.PDU_HEADER_LENGTH;
    243             int length = pdu.length - offset;
    244 
    245             return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language);
    246         }
    247     }
    248 
    249     /**
    250      * Unpack body text from the pdu using the given encoding, position and
    251      * length within the pdu
    252      *
    253      * @param pdu The pdu
    254      * @param encoding The encoding, as derived from the DCS
    255      * @param offset Position of the first byte to unpack
    256      * @param length Number of bytes to unpack
    257      * @param hasLanguageIndicator true if the body text is preceded by a
    258      *            language indicator. If so, this method will as a side-effect
    259      *            assign the extracted language code into mLanguage
    260      * @param language the language to return if hasLanguageIndicator is false
    261      * @return a Pair of Strings containing the language and body of the message
    262      */
    263     private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length,
    264             boolean hasLanguageIndicator, String language) {
    265         String body = null;
    266 
    267         switch (encoding) {
    268             case SmsConstants.ENCODING_7BIT:
    269                 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
    270 
    271                 if (hasLanguageIndicator && body != null && body.length() > 2) {
    272                     // Language is two GSM characters followed by a CR.
    273                     // The actual body text is offset by 3 characters.
    274                     language = body.substring(0, 2);
    275                     body = body.substring(3);
    276                 }
    277                 break;
    278 
    279             case SmsConstants.ENCODING_16BIT:
    280                 if (hasLanguageIndicator && pdu.length >= offset + 2) {
    281                     // Language is two GSM characters.
    282                     // The actual body text is offset by 2 bytes.
    283                     language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
    284                     offset += 2;
    285                     length -= 2;
    286                 }
    287 
    288                 try {
    289                     body = new String(pdu, offset, (length & 0xfffe), "utf-16");
    290                 } catch (UnsupportedEncodingException e) {
    291                     // Apparently it wasn't valid UTF-16.
    292                     throw new IllegalArgumentException("Error decoding UTF-16 message", e);
    293                 }
    294                 break;
    295 
    296             default:
    297                 break;
    298         }
    299 
    300         if (body != null) {
    301             // Remove trailing carriage return
    302             for (int i = body.length() - 1; i >= 0; i--) {
    303                 if (body.charAt(i) != CARRIAGE_RETURN) {
    304                     body = body.substring(0, i + 1);
    305                     break;
    306                 }
    307             }
    308         } else {
    309             body = "";
    310         }
    311 
    312         return new Pair<String, String>(language, body);
    313     }
    314 }
    315