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