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 static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE;
20import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI;
21import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY;
22import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE;
23import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI;
24
25import android.content.Context;
26import android.content.res.Resources;
27import android.telephony.SmsCbLocation;
28import android.telephony.SmsCbMessage;
29import android.util.Pair;
30
31import com.android.internal.R;
32import com.android.internal.telephony.GsmAlphabet;
33import com.android.internal.telephony.SmsConstants;
34
35import 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 */
41public 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