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 
17 package com.android.cellbroadcastservice;
18 
19 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE;
20 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI;
21 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY;
22 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE;
23 import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI;
24 
25 import android.annotation.NonNull;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.telephony.CbGeoUtils.Circle;
29 import android.telephony.CbGeoUtils.Geometry;
30 import android.telephony.CbGeoUtils.LatLng;
31 import android.telephony.CbGeoUtils.Polygon;
32 import android.telephony.SmsCbLocation;
33 import android.telephony.SmsCbMessage;
34 import android.telephony.SmsMessage;
35 import android.telephony.SubscriptionManager;
36 import android.util.Log;
37 import android.util.Pair;
38 
39 import com.android.cellbroadcastservice.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity;
40 import com.android.cellbroadcastservice.SmsCbHeader.DataCodingScheme;
41 import com.android.internal.annotations.VisibleForTesting;
42 
43 import java.io.UnsupportedEncodingException;
44 import java.util.ArrayList;
45 import java.util.List;
46 import java.util.stream.Collectors;
47 
48 /**
49  * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is
50  * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases.
51  */
52 public class GsmSmsCbMessage {
53     private static final String TAG = GsmSmsCbMessage.class.getSimpleName();
54 
55     private static final char CARRIAGE_RETURN = 0x0d;
56 
57     private static final int PDU_BODY_PAGE_LENGTH = 82;
58 
59     /** Utility class with only static methods. */
GsmSmsCbMessage()60     private GsmSmsCbMessage() { }
61 
62     /**
63      * Get built-in ETWS primary messages by category. ETWS primary message does not contain text,
64      * so we have to show the pre-built messages to the user.
65      *
66      * @param context Device context
67      * @param category ETWS message category defined in SmsCbConstants
68      * @return ETWS text message in string. Return an empty string if no match.
69      */
70     @VisibleForTesting
getEtwsPrimaryMessage(Context context, int category)71     public static String getEtwsPrimaryMessage(Context context, int category) {
72         final Resources r = context.getResources();
73         switch (category) {
74             case ETWS_WARNING_TYPE_EARTHQUAKE:
75                 return r.getString(R.string.etws_primary_default_message_earthquake);
76             case ETWS_WARNING_TYPE_TSUNAMI:
77                 return r.getString(R.string.etws_primary_default_message_tsunami);
78             case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
79                 return r.getString(R.string.etws_primary_default_message_earthquake_and_tsunami);
80             case ETWS_WARNING_TYPE_TEST_MESSAGE:
81                 return r.getString(R.string.etws_primary_default_message_test);
82             case ETWS_WARNING_TYPE_OTHER_EMERGENCY:
83                 return r.getString(R.string.etws_primary_default_message_others);
84             default:
85                 return "";
86         }
87     }
88 
89     /**
90      * Create a new SmsCbMessage object from a header object plus one or more received PDUs.
91      *
92      * @param pdus PDU bytes
93      */
createSmsCbMessage(Context context, SmsCbHeader header, SmsCbLocation location, byte[][] pdus, int slotIndex)94     public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header,
95             SmsCbLocation location, byte[][] pdus, int slotIndex)
96             throws IllegalArgumentException {
97         SubscriptionManager sm = (SubscriptionManager) context.getSystemService(
98                 Context.TELEPHONY_SUBSCRIPTION_SERVICE);
99         int subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
100         int[] subIds = sm.getSubscriptionIds(slotIndex);
101         if (subIds != null && subIds.length > 0) {
102             subId = subIds[0];
103         }
104 
105         long receivedTimeMillis = System.currentTimeMillis();
106         if (header.isEtwsPrimaryNotification()) {
107             // ETSI TS 23.041 ETWS Primary Notification message
108             // ETWS primary message only contains 4 fields including serial number,
109             // message identifier, warning type, and warning security information.
110             // There is no field for the content/text so we get the text from the resources.
111             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(),
112                     header.getSerialNumber(), location, header.getServiceCategory(), null,
113                     header.getDataCodingScheme(), getEtwsPrimaryMessage(context,
114                     header.getEtwsInfo().getWarningType()), SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY,
115                     header.getEtwsInfo(), header.getCmasInfo(), 0, null, receivedTimeMillis,
116                     slotIndex, subId);
117         } else if (header.isUmtsFormat()) {
118             // UMTS format has only 1 PDU
119             byte[] pdu = pdus[0];
120             Pair<String, String> cbData = parseUmtsBody(header, pdu);
121             String language = cbData.first;
122             String body = cbData.second;
123             int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
124                     : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
125             int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
126             int wacDataOffset = SmsCbHeader.PDU_HEADER_LENGTH
127                     + 1 // number of pages
128                     + (PDU_BODY_PAGE_LENGTH + 1) * nrPages; // cb data
129 
130             // Has Warning Area Coordinates information
131             List<Geometry> geometries = null;
132             int maximumWaitingTimeSec = 255;
133             if (pdu.length > wacDataOffset) {
134                 try {
135                     Pair<Integer, List<Geometry>> wac = parseWarningAreaCoordinates(pdu,
136                             wacDataOffset);
137                     maximumWaitingTimeSec = wac.first;
138                     geometries = wac.second;
139                 } catch (Exception ex) {
140                     // Catch the exception here, the message will be considered as having no WAC
141                     // information which means the message will be broadcasted directly.
142                     Log.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString());
143                 }
144             }
145 
146             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
147                     header.getGeographicalScope(), header.getSerialNumber(), location,
148                     header.getServiceCategory(), language, header.getDataCodingScheme(), body,
149                     priority, header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec,
150                     geometries, receivedTimeMillis, slotIndex, subId);
151         } else {
152             String language = null;
153             StringBuilder sb = new StringBuilder();
154             for (byte[] pdu : pdus) {
155                 Pair<String, String> p = parseGsmBody(header, pdu);
156                 language = p.first;
157                 sb.append(p.second);
158             }
159             int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
160                     : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
161 
162             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
163                     header.getGeographicalScope(), header.getSerialNumber(), location,
164                     header.getServiceCategory(), language, header.getDataCodingScheme(),
165                     sb.toString(), priority, header.getEtwsInfo(), header.getCmasInfo(), 0, null,
166                     receivedTimeMillis, slotIndex, subId);
167         }
168     }
169 
170     /**
171      * Parse WEA Handset Action Message(WHAM) a.k.a geo-fencing trigger message.
172      *
173      * WEA Handset Action Message(WHAM) is a cell broadcast service message broadcast by the network
174      * to direct devices to perform a geo-fencing check on selected alerts.
175      *
176      * WEA Handset Action Message(WHAM) requirements from ATIS-0700041 section 4
177      * 1. The Warning Message contents of a WHAM shall be in Cell Broadcast(CB) data format as
178      * defined in TS 23.041.
179      * 2. The Warning Message Contents of WHAM shall be limited to one CB page(max 20 referenced
180      * WEA messages).
181      * 3. The broadcast area for a WHAM shall be the union of the broadcast areas of the referenced
182      * WEA message.
183      * @param pdu cell broadcast pdu, including the header
184      * @return {@link GeoFencingTriggerMessage} instance
185      */
createGeoFencingTriggerMessage(byte[] pdu)186     public static GeoFencingTriggerMessage createGeoFencingTriggerMessage(byte[] pdu) {
187         try {
188             // Header length + 1(number of page). ATIS-0700041 define the number of page of
189             // geo-fencing trigger message is 1.
190             int whamOffset = SmsCbHeader.PDU_HEADER_LENGTH + 1;
191 
192             BitStreamReader bitReader = new BitStreamReader(pdu, whamOffset);
193             int type = bitReader.read(4);
194             int length = bitReader.read(7);
195             // Skip the remained 5 bits
196             bitReader.skip();
197 
198             int messageIdentifierCount = (length - 2) * 8 / 32;
199             List<CellBroadcastIdentity> cbIdentifiers = new ArrayList<>();
200             for (int i = 0; i < messageIdentifierCount; i++) {
201                 // Both messageIdentifier and serialNumber are 16 bits integers.
202                 // ATIS-0700041 Section 5.1.6
203                 int messageIdentifier = bitReader.read(16);
204                 int serialNumber = bitReader.read(16);
205                 cbIdentifiers.add(new CellBroadcastIdentity(messageIdentifier, serialNumber));
206             }
207             return new GeoFencingTriggerMessage(type, cbIdentifiers);
208         } catch (Exception ex) {
209             Log.e(TAG, "create geo-fencing trigger failed, ex = " + ex.toString());
210             return null;
211         }
212     }
213 
214     /**
215      * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV.
216      *
217      * @param pdu Warning Area Coordinates TLV.
218      * @param wacOffset the offset of Warning Area Coordinates TLV.
219      * @return a pair with the first element is maximum wait time and the second is the broadcast
220      * area. The default value of the maximum wait time is 255 which means use the device default
221      * value.
222      */
parseWarningAreaCoordinates( byte[] pdu, int wacOffset)223     private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates(
224             byte[] pdu, int wacOffset) {
225         // little-endian
226         int wacDataLength = ((pdu[wacOffset + 1] & 0xff) << 8) | (pdu[wacOffset] & 0xff);
227         int offset = wacOffset + 2;
228 
229         if (offset + wacDataLength > pdu.length) {
230             throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at"
231                     + "least " + offset + wacDataLength + ", actual is " + pdu.length);
232         }
233 
234         BitStreamReader bitReader = new BitStreamReader(pdu, offset);
235 
236         int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET;
237 
238         List<Geometry> geo = new ArrayList<>();
239         int remainedBytes = wacDataLength;
240         while (remainedBytes > 0) {
241             int type = bitReader.read(4);
242             int length = bitReader.read(10);
243             remainedBytes -= length;
244             // Skip the 2 remained bits
245             bitReader.skip();
246 
247             switch (type) {
248                 case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME:
249                     maximumWaitTimeSec = bitReader.read(8);
250                     break;
251                 case CbGeoUtils.GEOMETRY_TYPE_POLYGON:
252                     List<LatLng> latLngs = new ArrayList<>();
253                     // Each coordinate is represented by 44 bits integer.
254                     // ATIS-0700041 5.2.4 Coordinate coding
255                     int n = (length - 2) * 8 / 44;
256                     for (int i = 0; i < n; i++) {
257                         latLngs.add(getLatLng(bitReader));
258                     }
259                     // Skip the padding bits
260                     bitReader.skip();
261                     geo.add(new Polygon(latLngs));
262                     break;
263                 case CbGeoUtils.GEOMETRY_TYPE_CIRCLE:
264                     LatLng center = getLatLng(bitReader);
265                     // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the
266                     // distance unit during geo-fencing.
267                     // ATIS-0700041 5.2.5 radius coding
268                     double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0;
269                     geo.add(new Circle(center, radius));
270                     break;
271                 default:
272                     throw new IllegalArgumentException("Unsupported geoType = " + type);
273             }
274         }
275         return new Pair(maximumWaitTimeSec, geo);
276     }
277 
278     /**
279      * The coordinate is (latitude, longitude), represented by a 44 bits integer.
280      * The coding is defined in ATIS-0700041 5.2.4
281      * @param bitReader
282      * @return coordinate (latitude, longitude)
283      */
getLatLng(BitStreamReader bitReader)284     private static LatLng getLatLng(BitStreamReader bitReader) {
285         // wacLatitude = floor(((latitude + 90) / 180) * 2^22)
286         // wacLongitude = floor(((longitude + 180) / 360) * 2^22)
287         int wacLat = bitReader.read(22);
288         int wacLng = bitReader.read(22);
289 
290         // latitude = wacLatitude * 180 / 2^22 - 90
291         // longitude = wacLongitude * 360 / 2^22 -180
292         return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180));
293     }
294 
295     /**
296      * Parse and unpack the UMTS body text according to the encoding in the data coding scheme.
297      *
298      * @param header the message header to use
299      * @param pdu the PDU to decode
300      * @return a pair of string containing the language and body of the message in order
301      */
parseUmtsBody(SmsCbHeader header, byte[] pdu)302     private static Pair<String, String> parseUmtsBody(SmsCbHeader header,
303             byte[] pdu) {
304         // Payload may contain multiple pages
305         int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
306         String language = header.getDataCodingSchemeStructedData().language;
307 
308         if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
309                 * nrPages) {
310             throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
311                     + nrPages + " pages");
312         }
313 
314         StringBuilder sb = new StringBuilder();
315 
316         for (int i = 0; i < nrPages; i++) {
317             // Each page is 82 bytes followed by a length octet indicating
318             // the number of useful octets within those 82
319             int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i;
320             int length = pdu[offset + PDU_BODY_PAGE_LENGTH];
321 
322             if (length > PDU_BODY_PAGE_LENGTH) {
323                 throw new IllegalArgumentException("Page length " + length
324                         + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH);
325             }
326 
327             Pair<String, String> p = unpackBody(pdu, offset, length,
328                     header.getDataCodingSchemeStructedData());
329             language = p.first;
330             sb.append(p.second);
331         }
332         return new Pair(language, sb.toString());
333 
334     }
335 
336     /**
337      * Parse and unpack the GSM body text according to the encoding in the data coding scheme.
338      * @param header the message header to use
339      * @param pdu the PDU to decode
340      * @return a pair of string containing the language and body of the message in order
341      */
parseGsmBody(SmsCbHeader header, byte[] pdu)342     private static Pair<String, String> parseGsmBody(SmsCbHeader header,
343             byte[] pdu) {
344         // Payload is one single page
345         int offset = SmsCbHeader.PDU_HEADER_LENGTH;
346         int length = pdu.length - offset;
347         return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData());
348     }
349 
350     /**
351      * Unpack body text from the pdu using the given encoding, position and length within the pdu.
352      *
353      * @param pdu The pdu
354      * @param offset Position of the first byte to unpack
355      * @param length Number of bytes to unpack
356      * @param dcs data coding scheme
357      * @return a Pair of Strings containing the language and body of the message
358      */
unpackBody(byte[] pdu, int offset, int length, DataCodingScheme dcs)359     private static Pair<String, String> unpackBody(byte[] pdu, int offset,
360             int length, DataCodingScheme dcs) {
361         String body = null;
362 
363         String language = dcs.language;
364         switch (dcs.encoding) {
365             case SmsMessage.ENCODING_7BIT:
366                 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
367 
368                 if (dcs.hasLanguageIndicator && body != null && body.length() > 2) {
369                     // Language is two GSM characters followed by a CR.
370                     // The actual body text is offset by 3 characters.
371                     language = body.substring(0, 2);
372                     body = body.substring(3);
373                 }
374                 break;
375 
376             case SmsMessage.ENCODING_16BIT:
377                 if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) {
378                     // Language is two GSM characters.
379                     // The actual body text is offset by 2 bytes.
380                     language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
381                     offset += 2;
382                     length -= 2;
383                 }
384 
385                 try {
386                     body = new String(pdu, offset, (length & 0xfffe), "utf-16");
387                 } catch (UnsupportedEncodingException e) {
388                     // Apparently it wasn't valid UTF-16.
389                     throw new IllegalArgumentException("Error decoding UTF-16 message", e);
390                 }
391                 break;
392 
393             default:
394                 break;
395         }
396 
397         if (body != null) {
398             // Remove trailing carriage return
399             for (int i = body.length() - 1; i >= 0; i--) {
400                 if (body.charAt(i) != CARRIAGE_RETURN) {
401                     body = body.substring(0, i + 1);
402                     break;
403                 }
404             }
405         } else {
406             body = "";
407         }
408 
409         return new Pair<String, String>(language, body);
410     }
411 
412     /** A class use to facilitate the processing of bits stream data. */
413     private static final class BitStreamReader {
414         /** The bits stream represent by a bytes array. */
415         private final byte[] mData;
416 
417         /** The offset of the current byte. */
418         private int mCurrentOffset;
419 
420         /**
421          * The remained bits of the current byte which have not been read. The most significant
422          * will be read first, so the remained bits are always the least significant bits.
423          */
424         private int mRemainedBit;
425 
426         /**
427          * Constructor
428          * @param data bit stream data represent by byte array.
429          * @param offset the offset of the first byte.
430          */
BitStreamReader(byte[] data, int offset)431         BitStreamReader(byte[] data, int offset) {
432             mData = data;
433             mCurrentOffset = offset;
434             mRemainedBit = 8;
435         }
436 
437         /**
438          * Read the first {@code count} bits.
439          * @param count the number of bits need to read
440          * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no
441          * greater than 32.
442          */
read(int count)443         public int read(int count) throws IndexOutOfBoundsException {
444             int val = 0;
445             while (count > 0) {
446                 if (count >= mRemainedBit) {
447                     val <<= mRemainedBit;
448                     val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1);
449                     count -= mRemainedBit;
450                     mRemainedBit = 8;
451                     ++mCurrentOffset;
452                 } else {
453                     val <<= count;
454                     val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1))
455                             >> (mRemainedBit - count);
456                     mRemainedBit -= count;
457                     count = 0;
458                 }
459             }
460             return val;
461         }
462 
463         /**
464          * Skip the current bytes if the remained bits is less than 8. This is useful when
465          * processing the padding or reserved bits.
466          */
skip()467         public void skip() {
468             if (mRemainedBit < 8) {
469                 mRemainedBit = 8;
470                 ++mCurrentOffset;
471             }
472         }
473     }
474 
475     /**
476      * Part of a GSM SMS cell broadcast message which may trigger geo-fencing logic.
477      * @hide
478      */
479     public static final class GeoFencingTriggerMessage {
480         /**
481          * Indicate the list of active alerts share their warning area coordinates which means the
482          * broadcast area is the union of the broadcast areas of the active alerts in this list.
483          */
484         public static final int TYPE_ACTIVE_ALERT_SHARE_WAC = 2;
485 
486         public final int type;
487         public final List<CellBroadcastIdentity> cbIdentifiers;
488 
GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers)489         GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers) {
490             this.type = type;
491             this.cbIdentifiers = cbIdentifiers;
492         }
493 
494         /**
495          * Whether the trigger message indicates that the broadcast areas are shared between all
496          * active alerts.
497          * @return true if broadcast areas are to be shared
498          */
shouldShareBroadcastArea()499         boolean shouldShareBroadcastArea() {
500             return type == TYPE_ACTIVE_ALERT_SHARE_WAC;
501         }
502 
503         /**
504          * The GSM cell broadcast identity
505          */
506         @VisibleForTesting
507         public static final class CellBroadcastIdentity {
508             public final int messageIdentifier;
509             public final int serialNumber;
CellBroadcastIdentity(int messageIdentifier, int serialNumber)510             CellBroadcastIdentity(int messageIdentifier, int serialNumber) {
511                 this.messageIdentifier = messageIdentifier;
512                 this.serialNumber = serialNumber;
513             }
514         }
515 
516         @Override
toString()517         public String toString() {
518             String identifiers = cbIdentifiers.stream()
519                     .map(cbIdentifier ->String.format("(msgId = %d, serial = %d)",
520                             cbIdentifier.messageIdentifier, cbIdentifier.serialNumber))
521                     .collect(Collectors.joining(","));
522             return "triggerType=" + type + " identifiers=" + identifiers;
523         }
524     }
525 }
526