• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package android.telephony;
18 
19 import android.text.format.Time;
20 import android.util.Log;
21 
22 import com.android.internal.telephony.GsmAlphabet;
23 import com.android.internal.telephony.IccUtils;
24 import com.android.internal.telephony.gsm.SmsCbHeader;
25 
26 import java.io.UnsupportedEncodingException;
27 
28 /**
29  * Describes an SMS-CB message.
30  *
31  * {@hide}
32  */
33 public 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      */
createFromPdu(byte[] pdu)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 
SmsCbMessage(byte[] pdu)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      */
getGeographicalScope()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      */
getLanguageCode()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      */
getMessageBody()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      */
getMessageIdentifier()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      */
getMessageCode()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      */
getUpdateNumber()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      */
getMessageFormat()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      */
getEtwsEmergencyUserAlert()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      */
getEtwsPopup()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      */
getEtwsWarningType()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      */
getEtwsSecurityTimestamp()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      */
getEtwsSecuritySignature()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      */
parseBody(byte[] pdu)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      */
unpackBody(byte[] pdu, int encoding, int offset, int length, boolean hasLanguageIndicator)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      */
getTimestampMillis(byte[] pdu)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      */
appendToBody(String body)450     public void appendToBody(String body) {
451         mBody = mBody + body;
452     }
453 
454     @Override
toString()455     public String toString() {
456         return "SmsCbMessage{" + mHeader.toString() + ", language=" + mLanguage +
457                 ", body=\"" + mBody + "\"}";
458     }
459 }
460