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.cellbroadcastreceiver.tests;
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.content.Context;
26 import android.content.res.Resources;
27 import android.telephony.CbGeoUtils.Circle;
28 import android.telephony.CbGeoUtils.Geometry;
29 import android.telephony.CbGeoUtils.LatLng;
30 import android.telephony.CbGeoUtils.Polygon;
31 import android.telephony.Rlog;
32 import android.telephony.SmsCbLocation;
33 import android.telephony.SmsCbMessage;
34 import android.telephony.SubscriptionManager;
35 import android.util.Pair;
36 
37 import com.android.cellbroadcastservice.CbGeoUtils;
38 import com.android.cellbroadcastservice.GsmAlphabet;
39 import com.android.cellbroadcastservice.SmsCbHeader;
40 import com.android.cellbroadcastservice.SmsCbHeader.DataCodingScheme;
41 import com.android.internal.telephony.SmsConstants;
42 import com.android.modules.utils.build.SdkLevel;
43 
44 import java.io.UnsupportedEncodingException;
45 import java.util.ArrayList;
46 import java.util.List;
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      */
getEtwsPrimaryMessage(Context context, int category)70     private static String getEtwsPrimaryMessage(Context context, int category) {
71         final Resources r = context.getResources();
72         switch (category) {
73             case ETWS_WARNING_TYPE_EARTHQUAKE:
74                 return "CBR test app string for earthquake";
75             case ETWS_WARNING_TYPE_TSUNAMI:
76                 return "CBR test app string for tsunami";
77             case ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI:
78                 return "CBR test app string for earthquake and tsunami";
79             case ETWS_WARNING_TYPE_TEST_MESSAGE:
80                 return "CBR test app string for test";
81             case ETWS_WARNING_TYPE_OTHER_EMERGENCY:
82                 return "CBR test app string for emergency";
83             default:
84                 return "";
85         }
86     }
87 
88     /**
89      * Create a new SmsCbMessage object from a header object plus one or more received PDUs.
90      *
91      * @param pdus PDU bytes
92      * @slotIndex slotIndex for which received sms cb message
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         int subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID;
98         if (SdkLevel.isAtLeastU()) {
99             subId = SubscriptionManager.getSubscriptionId(slotIndex);
100         } else {
101             SubscriptionManager sm = context.getSystemService(SubscriptionManager.class);
102             int[] subIds = sm.getSubscriptionIds(slotIndex);
103             if (subIds != null && subIds.length > 0) {
104                 subId = subIds[0];
105             }
106         }
107 
108         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
109             subId = SubscriptionManager.DEFAULT_SUBSCRIPTION_ID;
110         }
111 
112         long receivedTimeMillis = System.currentTimeMillis();
113         if (header.isEtwsPrimaryNotification()) {
114             // ETSI TS 23.041 ETWS Primary Notification message
115             // ETWS primary message only contains 4 fields including serial number,
116             // message identifier, warning type, and warning security information.
117             // There is no field for the content/text so we get the text from the resources.
118             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(),
119                     header.getSerialNumber(), location, header.getServiceCategory(), null, 0,
120                     getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
121                     SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(),
122                     header.getCmasInfo(), 0, null /* geometries */, receivedTimeMillis, slotIndex,
123                     subId);
124         } else if (header.isUmtsFormat()) {
125             // UMTS format has only 1 PDU
126             byte[] pdu = pdus[0];
127             Pair<String, String> cbData = parseUmtsBody(header, pdu);
128             String language = cbData.first;
129             String body = cbData.second;
130             int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
131                     : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
132             int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
133             int wacDataOffset = SmsCbHeader.PDU_HEADER_LENGTH
134                     + 1 // number of pages
135                     + (PDU_BODY_PAGE_LENGTH + 1) * nrPages; // cb data
136 
137             // Has Warning Area Coordinates information
138             List<Geometry> geometries = null;
139             int maximumWaitingTimeSec = 255;
140             if (pdu.length > wacDataOffset) {
141                 try {
142                     Pair<Integer, List<Geometry>> wac = parseWarningAreaCoordinates(pdu,
143                             wacDataOffset);
144                     maximumWaitingTimeSec = wac.first;
145                     geometries = wac.second;
146                 } catch (Exception ex) {
147                     // Catch the exception here, the message will be considered as having no WAC
148                     // information which means the message will be broadcasted directly.
149                     Rlog.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString());
150                 }
151             }
152 
153             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
154                     header.getGeographicalScope(), header.getSerialNumber(), location,
155                     header.getServiceCategory(), language, header.getDataCodingScheme(), body,
156                     priority, header.getEtwsInfo(), header.getCmasInfo(), maximumWaitingTimeSec,
157                     geometries, receivedTimeMillis, slotIndex, subId);
158         } else {
159             String language = null;
160             StringBuilder sb = new StringBuilder();
161             for (byte[] pdu : pdus) {
162                 Pair<String, String> p = parseGsmBody(header, pdu);
163                 language = p.first;
164                 sb.append(p.second);
165             }
166             int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
167                     : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
168 
169             return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
170                     header.getGeographicalScope(), header.getSerialNumber(), location,
171                     header.getServiceCategory(), language, header.getDataCodingScheme(),
172                     sb.toString(), priority, header.getEtwsInfo(), header.getCmasInfo(), 0,
173                     null /* geometries */, receivedTimeMillis, slotIndex, subId);
174         }
175     }
176 
177     /**
178      * Parse the broadcast area and maximum wait time from the Warning Area Coordinates TLV.
179      *
180      * @param pdu Warning Area Coordinates TLV.
181      * @param wacOffset the offset of Warning Area Coordinates TLV.
182      * @return a pair with the first element is maximum wait time and the second is the broadcast
183      * area. The default value of the maximum wait time is 255 which means use the device default
184      * value.
185      */
parseWarningAreaCoordinates( byte[] pdu, int wacOffset)186     private static Pair<Integer, List<Geometry>> parseWarningAreaCoordinates(
187             byte[] pdu, int wacOffset) {
188         // little-endian
189         int wacDataLength = ((pdu[wacOffset + 1] & 0xff) << 8) | (pdu[wacOffset] & 0xff);
190         int offset = wacOffset + 2;
191 
192         if (offset + wacDataLength > pdu.length) {
193             throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at"
194                     + "least " + offset + wacDataLength + ", actual is " + pdu.length);
195         }
196 
197         BitStreamReader bitReader = new BitStreamReader(pdu, offset);
198 
199         int maximumWaitTimeSec = SmsCbMessage.MAXIMUM_WAIT_TIME_NOT_SET;
200 
201         List<Geometry> geo = new ArrayList<>();
202         int remainedBytes = wacDataLength;
203         while (remainedBytes > 0) {
204             int type = bitReader.read(4);
205             int length = bitReader.read(10);
206             remainedBytes -= length;
207             // Skip the 2 remained bits
208             bitReader.skip();
209 
210             switch (type) {
211                 case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME:
212                     maximumWaitTimeSec = bitReader.read(8);
213                     break;
214                 case CbGeoUtils.GEOMETRY_TYPE_POLYGON:
215                     List<LatLng> latLngs = new ArrayList<>();
216                     // Each coordinate is represented by 44 bits integer.
217                     // ATIS-0700041 5.2.4 Coordinate coding
218                     int n = (length - 2) * 8 / 44;
219                     for (int i = 0; i < n; i++) {
220                         latLngs.add(getLatLng(bitReader));
221                     }
222                     // Skip the padding bits
223                     bitReader.skip();
224                     geo.add(new Polygon(latLngs));
225                     break;
226                 case CbGeoUtils.GEOMETRY_TYPE_CIRCLE:
227                     LatLng center = getLatLng(bitReader);
228                     // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the
229                     // distance unit during geo-fencing.
230                     // ATIS-0700041 5.2.5 radius coding
231                     double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0;
232                     geo.add(new Circle(center, radius));
233                     break;
234                 default:
235                     throw new IllegalArgumentException("Unsupported geoType = " + type);
236             }
237         }
238         return new Pair(maximumWaitTimeSec, geo);
239     }
240 
241     /**
242      * The coordinate is (latitude, longitude), represented by a 44 bits integer.
243      * The coding is defined in ATIS-0700041 5.2.4
244      * @param bitReader
245      * @return coordinate (latitude, longitude)
246      */
getLatLng(BitStreamReader bitReader)247     private static LatLng getLatLng(BitStreamReader bitReader) {
248         // wacLatitude = floor(((latitude + 90) / 180) * 2^22)
249         // wacLongitude = floor(((longitude + 180) / 360) * 2^22)
250         int wacLat = bitReader.read(22);
251         int wacLng = bitReader.read(22);
252 
253         // latitude = wacLatitude * 180 / 2^22 - 90
254         // longitude = wacLongitude * 360 / 2^22 -180
255         return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180));
256     }
257 
258     /**
259      * Parse and unpack the UMTS body text according to the encoding in the data coding scheme.
260      *
261      * @param header the message header to use
262      * @param pdu the PDU to decode
263      * @return a pair of string containing the language and body of the message in order
264      */
parseUmtsBody(SmsCbHeader header, byte[] pdu)265     private static Pair<String, String> parseUmtsBody(SmsCbHeader header, byte[] pdu) {
266         // Payload may contain multiple pages
267         int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
268         String language = header.getDataCodingSchemeStructedData().language;
269 
270         if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
271                 * nrPages) {
272             throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
273                     + nrPages + " pages");
274         }
275 
276         StringBuilder sb = new StringBuilder();
277 
278         for (int i = 0; i < nrPages; i++) {
279             // Each page is 82 bytes followed by a length octet indicating
280             // the number of useful octets within those 82
281             int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i;
282             int length = pdu[offset + PDU_BODY_PAGE_LENGTH];
283 
284             if (length > PDU_BODY_PAGE_LENGTH) {
285                 throw new IllegalArgumentException("Page length " + length
286                         + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH);
287             }
288 
289             Pair<String, String> p = unpackBody(pdu, offset, length,
290                     header.getDataCodingSchemeStructedData());
291             language = p.first;
292             sb.append(p.second);
293         }
294         return new Pair(language, sb.toString());
295 
296     }
297 
298     /**
299      * Parse and unpack the GSM body text according to the encoding in the data coding scheme.
300      * @param header the message header to use
301      * @param pdu the PDU to decode
302      * @return a pair of string containing the language and body of the message in order
303      */
parseGsmBody(SmsCbHeader header, byte[] pdu)304     private static Pair<String, String> parseGsmBody(SmsCbHeader header, byte[] pdu) {
305         // Payload is one single page
306         int offset = SmsCbHeader.PDU_HEADER_LENGTH;
307         int length = pdu.length - offset;
308         return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData());
309     }
310 
311     /**
312      * Unpack body text from the pdu using the given encoding, position and length within the pdu.
313      *
314      * @param pdu The pdu
315      * @param offset Position of the first byte to unpack
316      * @param length Number of bytes to unpack
317      * @param dcs data coding scheme
318      * @return a Pair of Strings containing the language and body of the message
319      */
unpackBody(byte[] pdu, int offset, int length, DataCodingScheme dcs)320     private static Pair<String, String> unpackBody(byte[] pdu, int offset, int length,
321             DataCodingScheme dcs) {
322         String body = null;
323 
324         String language = dcs.language;
325         switch (dcs.encoding) {
326             case SmsConstants.ENCODING_7BIT:
327                 body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
328 
329                 if (dcs.hasLanguageIndicator && body != null && body.length() > 2) {
330                     // Language is two GSM characters followed by a CR.
331                     // The actual body text is offset by 3 characters.
332                     language = body.substring(0, 2);
333                     body = body.substring(3);
334                 }
335                 break;
336 
337             case SmsConstants.ENCODING_16BIT:
338                 if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) {
339                     // Language is two GSM characters.
340                     // The actual body text is offset by 2 bytes.
341                     language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
342                     offset += 2;
343                     length -= 2;
344                 }
345 
346                 try {
347                     body = new String(pdu, offset, (length & 0xfffe), "utf-16");
348                 } catch (UnsupportedEncodingException e) {
349                     // Apparently it wasn't valid UTF-16.
350                     throw new IllegalArgumentException("Error decoding UTF-16 message", e);
351                 }
352                 break;
353 
354             default:
355                 break;
356         }
357 
358         if (body != null) {
359             // Remove trailing carriage return
360             for (int i = body.length() - 1; i >= 0; i--) {
361                 if (body.charAt(i) != CARRIAGE_RETURN) {
362                     body = body.substring(0, i + 1);
363                     break;
364                 }
365             }
366         } else {
367             body = "";
368         }
369 
370         return new Pair<String, String>(language, body);
371     }
372 
373     /** A class use to facilitate the processing of bits stream data. */
374     private static final class BitStreamReader {
375         /** The bits stream represent by a bytes array. */
376         private final byte[] mData;
377 
378         /** The offset of the current byte. */
379         private int mCurrentOffset;
380 
381         /**
382          * The remained bits of the current byte which have not been read. The most significant
383          * will be read first, so the remained bits are always the least significant bits.
384          */
385         private int mRemainedBit;
386 
387         /**
388          * Constructor
389          * @param data bit stream data represent by byte array.
390          * @param offset the offset of the first byte.
391          */
BitStreamReader(byte[] data, int offset)392         BitStreamReader(byte[] data, int offset) {
393             mData = data;
394             mCurrentOffset = offset;
395             mRemainedBit = 8;
396         }
397 
398         /**
399          * Read the first {@code count} bits.
400          * @param count the number of bits need to read
401          * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no
402          * greater than 32.
403          */
read(int count)404         public int read(int count) throws IndexOutOfBoundsException {
405             int val = 0;
406             while (count > 0) {
407                 if (count >= mRemainedBit) {
408                     val <<= mRemainedBit;
409                     val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1);
410                     count -= mRemainedBit;
411                     mRemainedBit = 8;
412                     ++mCurrentOffset;
413                 } else {
414                     val <<= count;
415                     val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1))
416                             >> (mRemainedBit - count);
417                     mRemainedBit -= count;
418                     count = 0;
419                 }
420             }
421             return val;
422         }
423 
424         /**
425          * Skip the current bytes if the remained bits is less than 8. This is useful when
426          * processing the padding or reserved bits.
427          */
skip()428         public void skip() {
429             if (mRemainedBit < 8) {
430                 mRemainedBit = 8;
431                 ++mCurrentOffset;
432             }
433         }
434     }
435 }
436