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