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    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