1/*
2 * Copyright (C) 2010 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 android.telephony;
18
19import android.text.format.Time;
20import android.util.Log;
21
22import com.android.internal.telephony.GsmAlphabet;
23import com.android.internal.telephony.IccUtils;
24import com.android.internal.telephony.gsm.SmsCbHeader;
25
26import java.io.UnsupportedEncodingException;
27
28/**
29 * Describes an SMS-CB message.
30 *
31 * {@hide}
32 */
33public class SmsCbMessage {
34
35    /**
36     * Cell wide immediate geographical scope
37     */
38    public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0;
39
40    /**
41     * PLMN wide geographical scope
42     */
43    public static final int GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1;
44
45    /**
46     * Location / service area wide geographical scope
47     */
48    public static final int GEOGRAPHICAL_SCOPE_LA_WIDE = 2;
49
50    /**
51     * Cell wide geographical scope
52     */
53    public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE = 3;
54
55    /**
56     * Create an instance of this class from a received PDU
57     *
58     * @param pdu PDU bytes
59     * @return An instance of this class, or null if invalid pdu
60     */
61    public static SmsCbMessage createFromPdu(byte[] pdu) {
62        try {
63            return new SmsCbMessage(pdu);
64        } catch (IllegalArgumentException e) {
65            Log.w(LOG_TAG, "Failed parsing SMS-CB pdu", e);
66            return null;
67        }
68    }
69
70    private static final String LOG_TAG = "SMSCB";
71
72    /**
73     * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5.
74     */
75    private static final String[] LANGUAGE_CODES_GROUP_0 = {
76            "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu",
77            "pl", null
78    };
79
80    /**
81     * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5.
82     */
83    private static final String[] LANGUAGE_CODES_GROUP_2 = {
84            "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null,
85            null, null
86    };
87
88    private static final char CARRIAGE_RETURN = 0x0d;
89
90    private static final int PDU_BODY_PAGE_LENGTH = 82;
91
92    private SmsCbHeader mHeader;
93
94    private String mLanguage;
95
96    private String mBody;
97
98    /** Timestamp of ETWS primary notification with security. */
99    private long mPrimaryNotificationTimestamp;
100
101    /** 43 byte digital signature of ETWS primary notification with security. */
102    private byte[] mPrimaryNotificationDigitalSignature;
103
104    private SmsCbMessage(byte[] pdu) throws IllegalArgumentException {
105        mHeader = new SmsCbHeader(pdu);
106        if (mHeader.format == SmsCbHeader.FORMAT_ETWS_PRIMARY) {
107            mBody = "ETWS";
108            // ETWS primary notification with security is 56 octets in length
109            if (pdu.length >= SmsCbHeader.PDU_LENGTH_ETWS) {
110                mPrimaryNotificationTimestamp = getTimestampMillis(pdu);
111                mPrimaryNotificationDigitalSignature = new byte[43];
112                // digital signature starts after 6 byte header and 7 byte timestamp
113                System.arraycopy(pdu, 13, mPrimaryNotificationDigitalSignature, 0, 43);
114            }
115        } else {
116            parseBody(pdu);
117        }
118    }
119
120    /**
121     * Return the geographical scope of this message, one of
122     * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE},
123     * {@link #GEOGRAPHICAL_SCOPE_PLMN_WIDE},
124     * {@link #GEOGRAPHICAL_SCOPE_LA_WIDE},
125     * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE}
126     *
127     * @return Geographical scope
128     */
129    public int getGeographicalScope() {
130        return mHeader.geographicalScope;
131    }
132
133    /**
134     * Get the ISO-639-1 language code for this message, or null if unspecified
135     *
136     * @return Language code
137     */
138    public String getLanguageCode() {
139        return mLanguage;
140    }
141
142    /**
143     * Get the body of this message, or null if no body available
144     *
145     * @return Body, or null
146     */
147    public String getMessageBody() {
148        return mBody;
149    }
150
151    /**
152     * Get the message identifier of this message (0-65535)
153     *
154     * @return Message identifier
155     */
156    public int getMessageIdentifier() {
157        return mHeader.messageIdentifier;
158    }
159
160    /**
161     * Get the message code of this message (0-1023)
162     *
163     * @return Message code
164     */
165    public int getMessageCode() {
166        return mHeader.messageCode;
167    }
168
169    /**
170     * Get the update number of this message (0-15)
171     *
172     * @return Update number
173     */
174    public int getUpdateNumber() {
175        return mHeader.updateNumber;
176    }
177
178    /**
179     * Get the format of this message.
180     * @return {@link SmsCbHeader#FORMAT_GSM}, {@link SmsCbHeader#FORMAT_UMTS}, or
181     *         {@link SmsCbHeader#FORMAT_ETWS_PRIMARY}
182     */
183    public int getMessageFormat() {
184        return mHeader.format;
185    }
186
187    /**
188     * For ETWS primary notifications, return the emergency user alert flag.
189     * @return true to notify terminal to activate emergency user alert; false otherwise
190     */
191    public boolean getEtwsEmergencyUserAlert() {
192        return mHeader.etwsEmergencyUserAlert;
193    }
194
195    /**
196     * For ETWS primary notifications, return the popup flag.
197     * @return true to notify terminal to activate display popup; false otherwise
198     */
199    public boolean getEtwsPopup() {
200        return mHeader.etwsPopup;
201    }
202
203    /**
204     * For ETWS primary notifications, return the warning type.
205     * @return a value such as {@link SmsCbConstants#ETWS_WARNING_TYPE_EARTHQUAKE}
206     */
207    public int getEtwsWarningType() {
208        return mHeader.etwsWarningType;
209    }
210
211    /**
212     * For ETWS primary notifications, return the Warning-Security-Information timestamp.
213     * @return a timestamp in System.currentTimeMillis() format.
214     */
215    public long getEtwsSecurityTimestamp() {
216        return mPrimaryNotificationTimestamp;
217    }
218
219    /**
220     * For ETWS primary notifications, return the 43 byte digital signature.
221     * @return a byte array containing a copy of the digital signature
222     */
223    public byte[] getEtwsSecuritySignature() {
224        return mPrimaryNotificationDigitalSignature.clone();
225    }
226
227    /**
228     * Parse and unpack the body text according to the encoding in the DCS.
229     * After completing successfully this method will have assigned the body
230     * text into mBody, and optionally the language code into mLanguage
231     *
232     * @param pdu The pdu
233     */
234    private void parseBody(byte[] pdu) {
235        int encoding;
236        boolean hasLanguageIndicator = false;
237
238        // Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
239        // section 5.
240        switch ((mHeader.dataCodingScheme & 0xf0) >> 4) {
241            case 0x00:
242                encoding = SmsMessage.ENCODING_7BIT;
243                mLanguage = LANGUAGE_CODES_GROUP_0[mHeader.dataCodingScheme & 0x0f];
244                break;
245
246            case 0x01:
247                hasLanguageIndicator = true;
248                if ((mHeader.dataCodingScheme & 0x0f) == 0x01) {
249                    encoding = SmsMessage.ENCODING_16BIT;
250                } else {
251                    encoding = SmsMessage.ENCODING_7BIT;
252                }
253                break;
254
255            case 0x02:
256                encoding = SmsMessage.ENCODING_7BIT;
257                mLanguage = LANGUAGE_CODES_GROUP_2[mHeader.dataCodingScheme & 0x0f];
258                break;
259
260            case 0x03:
261                encoding = SmsMessage.ENCODING_7BIT;
262                break;
263
264            case 0x04:
265            case 0x05:
266                switch ((mHeader.dataCodingScheme & 0x0c) >> 2) {
267                    case 0x01:
268                        encoding = SmsMessage.ENCODING_8BIT;
269                        break;
270
271                    case 0x02:
272                        encoding = SmsMessage.ENCODING_16BIT;
273                        break;
274
275                    case 0x00:
276                    default:
277                        encoding = SmsMessage.ENCODING_7BIT;
278                        break;
279                }
280                break;
281
282            case 0x06:
283            case 0x07:
284                // Compression not supported
285            case 0x09:
286                // UDH structure not supported
287            case 0x0e:
288                // Defined by the WAP forum not supported
289                encoding = SmsMessage.ENCODING_UNKNOWN;
290                break;
291
292            case 0x0f:
293                if (((mHeader.dataCodingScheme & 0x04) >> 2) == 0x01) {
294                    encoding = SmsMessage.ENCODING_8BIT;
295                } else {
296                    encoding = SmsMessage.ENCODING_7BIT;
297                }
298                break;
299
300            default:
301                // Reserved values are to be treated as 7-bit
302                encoding = SmsMessage.ENCODING_7BIT;
303                break;
304        }
305
306        if (mHeader.format == SmsCbHeader.FORMAT_UMTS) {
307            // Payload may contain multiple pages
308            int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
309
310            if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
311                    * nrPages) {
312                throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
313                        + nrPages + " pages");
314            }
315
316            StringBuilder sb = new StringBuilder();
317
318            for (int i = 0; i < nrPages; i++) {
319                // Each page is 82 bytes followed by a length octet indicating
320                // the number of useful octets within those 82
321                int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i;
322                int length = pdu[offset + PDU_BODY_PAGE_LENGTH];
323
324                if (length > PDU_BODY_PAGE_LENGTH) {
325                    throw new IllegalArgumentException("Page length " + length
326                            + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH);
327                }
328
329                sb.append(unpackBody(pdu, encoding, offset, length, hasLanguageIndicator));
330            }
331            mBody = sb.toString();
332        } else {
333            // Payload is one single page
334            int offset = SmsCbHeader.PDU_HEADER_LENGTH;
335            int length = pdu.length - offset;
336
337            mBody = unpackBody(pdu, encoding, offset, length, hasLanguageIndicator);
338        }
339    }
340
341    /**
342     * Unpack body text from the pdu using the given encoding, position and
343     * length within the pdu
344     *
345     * @param pdu The pdu
346     * @param encoding The encoding, as derived from the DCS
347     * @param offset Position of the first byte to unpack
348     * @param length Number of bytes to unpack
349     * @param hasLanguageIndicator true if the body text is preceded by a
350     *            language indicator. If so, this method will as a side-effect
351     *            assign the extracted language code into mLanguage
352     * @return Body text
353     */
354    private String unpackBody(byte[] pdu, int encoding, int offset, int length,
355            boolean hasLanguageIndicator) {
356        String body = null;
357
358        switch (encoding) {
359            case SmsMessage.ENCODING_7BIT:
360                body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
361
362                if (hasLanguageIndicator && body != null && body.length() > 2) {
363                    // Language is two GSM characters followed by a CR.
364                    // The actual body text is offset by 3 characters.
365                    mLanguage = body.substring(0, 2);
366                    body = body.substring(3);
367                }
368                break;
369
370            case SmsMessage.ENCODING_16BIT:
371                if (hasLanguageIndicator && pdu.length >= offset + 2) {
372                    // Language is two GSM characters.
373                    // The actual body text is offset by 2 bytes.
374                    mLanguage = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
375                    offset += 2;
376                    length -= 2;
377                }
378
379                try {
380                    body = new String(pdu, offset, (length & 0xfffe), "utf-16");
381                } catch (UnsupportedEncodingException e) {
382                    // Eeeek
383                }
384                break;
385
386            default:
387                break;
388        }
389
390        if (body != null) {
391            // Remove trailing carriage return
392            for (int i = body.length() - 1; i >= 0; i--) {
393                if (body.charAt(i) != CARRIAGE_RETURN) {
394                    body = body.substring(0, i + 1);
395                    break;
396                }
397            }
398        } else {
399            body = "";
400        }
401
402        return body;
403    }
404
405    /**
406     * Parses an ETWS primary notification timestamp and returns a currentTimeMillis()-style
407     * timestamp. Copied from com.android.internal.telephony.gsm.SmsMessage.
408     * @param pdu the ETWS primary notification PDU to decode
409     * @return the UTC timestamp from the Warning-Security-Information parameter
410     */
411    private long getTimestampMillis(byte[] pdu) {
412        // Timestamp starts after CB header, in pdu[6]
413        int year = IccUtils.gsmBcdByteToInt(pdu[6]);
414        int month = IccUtils.gsmBcdByteToInt(pdu[7]);
415        int day = IccUtils.gsmBcdByteToInt(pdu[8]);
416        int hour = IccUtils.gsmBcdByteToInt(pdu[9]);
417        int minute = IccUtils.gsmBcdByteToInt(pdu[10]);
418        int second = IccUtils.gsmBcdByteToInt(pdu[11]);
419
420        // For the timezone, the most significant bit of the
421        // least significant nibble is the sign byte
422        // (meaning the max range of this field is 79 quarter-hours,
423        // which is more than enough)
424
425        byte tzByte = pdu[12];
426
427        // Mask out sign bit.
428        int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08)));
429
430        timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset;
431
432        Time time = new Time(Time.TIMEZONE_UTC);
433
434        // It's 2006.  Should I really support years < 2000?
435        time.year = year >= 90 ? year + 1900 : year + 2000;
436        time.month = month - 1;
437        time.monthDay = day;
438        time.hour = hour;
439        time.minute = minute;
440        time.second = second;
441
442        // Timezone offset is in quarter hours.
443        return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000);
444    }
445
446    /**
447     * Append text to the message body. This is used to concatenate multi-page GSM broadcasts.
448     * @param body the text to append to this message
449     */
450    public void appendToBody(String body) {
451        mBody = mBody + body;
452    }
453
454    @Override
455    public String toString() {
456        return "SmsCbMessage{" + mHeader.toString() + ", language=" + mLanguage +
457                ", body=\"" + mBody + "\"}";
458    }
459}
460