diff options
| author | 2019-08-27 14:15:40 -0700 | |
|---|---|---|
| committer | 2019-08-27 14:15:40 -0700 | |
| commit | 2a0cd6d35fff178dba49c4694414fb20de0f9ebe (patch) | |
| tree | 019e03bc5dc73caac9ec17ed1620cea534c8db04 | |
| parent | aef181528b8485b37aa02260f65ad964e01d2d19 (diff) | |
| parent | 49d538731b051d8f5c339b1c4688c967509d6045 (diff) | |
Merge "Support WAC decoding in UMTS format" am: 9bc9b80912 am: 6cf420a80e
am: 49d538731b
Change-Id: If03eba667943b68bed66eb77acdcc53dd22cf3ed
6 files changed, 711 insertions, 161 deletions
diff --git a/telephony/java/android/provider/Telephony.java b/telephony/java/android/provider/Telephony.java index c9883d0b0063..48373585231f 100644 --- a/telephony/java/android/provider/Telephony.java +++ b/telephony/java/android/provider/Telephony.java @@ -4093,8 +4093,8 @@ public final class Telephony { public static final String DEFAULT_SORT_ORDER = DELIVERY_TIME + " DESC"; /** - * The Epoch Unix timestamp when the device received the message. - * <P>Type: INTEGER</P> + * The timestamp in millisecond of when the device received the message. + * <P>Type: BIGINT</P> */ public static final String RECEIVED_TIME = "received_time"; @@ -4164,6 +4164,33 @@ public final class Telephony { CMAS_URGENCY, CMAS_CERTAINTY }; + + /** + * Query columns for instantiating {@link android.telephony.SmsCbMessage} objects. + */ + public static final String[] QUERY_COLUMNS_FWK = { + _ID, + GEOGRAPHICAL_SCOPE, + PLMN, + LAC, + CID, + SERIAL_NUMBER, + SERVICE_CATEGORY, + LANGUAGE_CODE, + MESSAGE_BODY, + MESSAGE_FORMAT, + MESSAGE_PRIORITY, + ETWS_WARNING_TYPE, + CMAS_MESSAGE_CLASS, + CMAS_CATEGORY, + CMAS_RESPONSE_TYPE, + CMAS_SEVERITY, + CMAS_URGENCY, + CMAS_CERTAINTY, + RECEIVED_TIME, + MESSAGE_BROADCASTED, + GEOMETRIES + }; } /** diff --git a/telephony/java/com/android/internal/telephony/CbGeoUtils.java b/telephony/java/com/android/internal/telephony/CbGeoUtils.java index c973b672ae40..73dd822903f5 100644 --- a/telephony/java/com/android/internal/telephony/CbGeoUtils.java +++ b/telephony/java/com/android/internal/telephony/CbGeoUtils.java @@ -53,6 +53,11 @@ public class CbGeoUtils { private static final String TAG = "CbGeoUtils"; + /** The TLV tags of WAC, defined in ATIS-0700041 5.2.3 WAC tag coding. */ + public static final int GEO_FENCING_MAXIMUM_WAIT_TIME = 0x01; + public static final int GEOMETRY_TYPE_POLYGON = 0x02; + public static final int GEOMETRY_TYPE_CIRCLE = 0x03; + /** The identifier of geometry in the encoded string. */ private static final String CIRCLE_SYMBOL = "circle"; private static final String POLYGON_SYMBOL = "polygon"; @@ -92,6 +97,11 @@ public class CbGeoUtils { + dlng * dlng * Math.cos(Math.toRadians(lat)) * Math.cos(Math.toRadians(p.lat)); return 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x)) * EARTH_RADIUS_METER; } + + @Override + public String toString() { + return "(" + lat + "," + lng + ")"; + } } /** diff --git a/telephony/java/com/android/internal/telephony/SmsCbMessage.java b/telephony/java/com/android/internal/telephony/SmsCbMessage.java index 046bf8c700eb..b9edb9fb1b5c 100644 --- a/telephony/java/com/android/internal/telephony/SmsCbMessage.java +++ b/telephony/java/com/android/internal/telephony/SmsCbMessage.java @@ -16,8 +16,17 @@ package android.telephony; +import android.annotation.Nullable; +import android.content.ContentValues; +import android.database.Cursor; import android.os.Parcel; import android.os.Parcelable; +import android.provider.Telephony.CellBroadcasts; + +import com.android.internal.telephony.CbGeoUtils; +import com.android.internal.telephony.CbGeoUtils.Geometry; + +import java.util.List; /** * Parcelable object containing a received cell broadcast message. There are four different types @@ -138,12 +147,31 @@ public class SmsCbMessage implements Parcelable { /** CMAS warning notification information (CMAS warnings only). */ private final SmsCbCmasInfo mCmasWarningInfo; + /** UNIX timestamp of when the message was received. */ + private final long mReceivedTimeMillis; + + /** CMAS warning area coordinates. */ + private final List<Geometry> mGeometries; + /** * Create a new SmsCbMessage with the specified data. */ public SmsCbMessage(int messageFormat, int geographicalScope, int serialNumber, SmsCbLocation location, int serviceCategory, String language, String body, int priority, SmsCbEtwsInfo etwsWarningInfo, SmsCbCmasInfo cmasWarningInfo) { + + this(messageFormat, geographicalScope, serialNumber, location, serviceCategory, language, + body, priority, etwsWarningInfo, cmasWarningInfo, null /* geometries */, + System.currentTimeMillis()); + } + + /** + * Create a new {@link SmsCbMessage} with the warning area coordinates information. + */ + public SmsCbMessage(int messageFormat, int geographicalScope, int serialNumber, + SmsCbLocation location, int serviceCategory, String language, String body, + int priority, SmsCbEtwsInfo etwsWarningInfo, SmsCbCmasInfo cmasWarningInfo, + List<Geometry> geometries, long receivedTimeMillis) { mMessageFormat = messageFormat; mGeographicalScope = geographicalScope; mSerialNumber = serialNumber; @@ -154,6 +182,8 @@ public class SmsCbMessage implements Parcelable { mPriority = priority; mEtwsWarningInfo = etwsWarningInfo; mCmasWarningInfo = cmasWarningInfo; + mReceivedTimeMillis = receivedTimeMillis; + mGeometries = geometries; } /** Create a new SmsCbMessage object from a Parcel. */ @@ -184,6 +214,9 @@ public class SmsCbMessage implements Parcelable { mEtwsWarningInfo = null; mCmasWarningInfo = null; } + mReceivedTimeMillis = in.readLong(); + String geoStr = in.readString(); + mGeometries = geoStr != null ? CbGeoUtils.parseGeometriesFromString(geoStr) : null; } /** @@ -214,6 +247,9 @@ public class SmsCbMessage implements Parcelable { // no ETWS or CMAS warning information dest.writeInt('0'); } + dest.writeLong(mReceivedTimeMillis); + dest.writeString( + mGeometries != null ? CbGeoUtils.encodeGeometriesToString(mGeometries) : null); } public static final Parcelable.Creator<SmsCbMessage> CREATOR @@ -293,6 +329,24 @@ public class SmsCbMessage implements Parcelable { } /** + * Get the warning area coordinates information represent by polygons and circles. + * @return a list of geometries, {@link Nullable} means there is no coordinate information + * associated to this message. + */ + @Nullable + public List<Geometry> getGeometries() { + return mGeometries; + } + + /** + * Get the time when this message was received. + * @return the time in millisecond + */ + public long getReceivedTime() { + return mReceivedTimeMillis; + } + + /** * Get the message format ({@link #MESSAGE_FORMAT_3GPP} or {@link #MESSAGE_FORMAT_3GPP2}). * @return an integer representing 3GPP or 3GPP2 message format */ @@ -368,7 +422,10 @@ public class SmsCbMessage implements Parcelable { + mServiceCategory + ", language=" + mLanguage + ", body=" + mBody + ", priority=" + mPriority + (mEtwsWarningInfo != null ? (", " + mEtwsWarningInfo.toString()) : "") - + (mCmasWarningInfo != null ? (", " + mCmasWarningInfo.toString()) : "") + '}'; + + (mCmasWarningInfo != null ? (", " + mCmasWarningInfo.toString()) : "") + + ", geo=" + (mGeometries != null + ? CbGeoUtils.encodeGeometriesToString(mGeometries) : "null") + + '}'; } /** @@ -379,4 +436,171 @@ public class SmsCbMessage implements Parcelable { public int describeContents() { return 0; } + + /** + * @return the {@link ContentValues} instance that includes the cell broadcast data. + */ + public ContentValues getContentValues() { + ContentValues cv = new ContentValues(16); + cv.put(CellBroadcasts.GEOGRAPHICAL_SCOPE, mGeographicalScope); + if (mLocation.getPlmn() != null) { + cv.put(CellBroadcasts.PLMN, mLocation.getPlmn()); + } + if (mLocation.getLac() != -1) { + cv.put(CellBroadcasts.LAC, mLocation.getLac()); + } + if (mLocation.getCid() != -1) { + cv.put(CellBroadcasts.CID, mLocation.getCid()); + } + cv.put(CellBroadcasts.SERIAL_NUMBER, getSerialNumber()); + cv.put(CellBroadcasts.SERVICE_CATEGORY, getServiceCategory()); + cv.put(CellBroadcasts.LANGUAGE_CODE, getLanguageCode()); + cv.put(CellBroadcasts.MESSAGE_BODY, getMessageBody()); + cv.put(CellBroadcasts.MESSAGE_FORMAT, getMessageFormat()); + cv.put(CellBroadcasts.MESSAGE_PRIORITY, getMessagePriority()); + + SmsCbEtwsInfo etwsInfo = getEtwsWarningInfo(); + if (etwsInfo != null) { + cv.put(CellBroadcasts.ETWS_WARNING_TYPE, etwsInfo.getWarningType()); + } + + SmsCbCmasInfo cmasInfo = getCmasWarningInfo(); + if (cmasInfo != null) { + cv.put(CellBroadcasts.CMAS_MESSAGE_CLASS, cmasInfo.getMessageClass()); + cv.put(CellBroadcasts.CMAS_CATEGORY, cmasInfo.getCategory()); + cv.put(CellBroadcasts.CMAS_RESPONSE_TYPE, cmasInfo.getResponseType()); + cv.put(CellBroadcasts.CMAS_SEVERITY, cmasInfo.getSeverity()); + cv.put(CellBroadcasts.CMAS_URGENCY, cmasInfo.getUrgency()); + cv.put(CellBroadcasts.CMAS_CERTAINTY, cmasInfo.getCertainty()); + } + + cv.put(CellBroadcasts.RECEIVED_TIME, mReceivedTimeMillis); + + if (mGeometries != null) { + cv.put(CellBroadcasts.GEOMETRIES, CbGeoUtils.encodeGeometriesToString(mGeometries)); + } else { + cv.put(CellBroadcasts.GEOMETRIES, (String) null); + } + + return cv; + } + + /** + * Create a {@link SmsCbMessage} instance from a row in the cell broadcast database. + * @param cursor an open SQLite cursor pointing to the row to read + * @return a {@link SmsCbMessage} instance. + * @throws IllegalArgumentException if one of the required columns is missing + */ + public static SmsCbMessage createFromCursor(Cursor cursor) { + int geoScope = cursor.getInt( + cursor.getColumnIndexOrThrow(CellBroadcasts.GEOGRAPHICAL_SCOPE)); + int serialNum = cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.SERIAL_NUMBER)); + int category = cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.SERVICE_CATEGORY)); + String language = cursor.getString( + cursor.getColumnIndexOrThrow(CellBroadcasts.LANGUAGE_CODE)); + String body = cursor.getString(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_BODY)); + int format = cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_FORMAT)); + int priority = cursor.getInt(cursor.getColumnIndexOrThrow(CellBroadcasts.MESSAGE_PRIORITY)); + + String plmn; + int plmnColumn = cursor.getColumnIndex(CellBroadcasts.PLMN); + if (plmnColumn != -1 && !cursor.isNull(plmnColumn)) { + plmn = cursor.getString(plmnColumn); + } else { + plmn = null; + } + + int lac; + int lacColumn = cursor.getColumnIndex(CellBroadcasts.LAC); + if (lacColumn != -1 && !cursor.isNull(lacColumn)) { + lac = cursor.getInt(lacColumn); + } else { + lac = -1; + } + + int cid; + int cidColumn = cursor.getColumnIndex(CellBroadcasts.CID); + if (cidColumn != -1 && !cursor.isNull(cidColumn)) { + cid = cursor.getInt(cidColumn); + } else { + cid = -1; + } + + SmsCbLocation location = new SmsCbLocation(plmn, lac, cid); + + SmsCbEtwsInfo etwsInfo; + int etwsWarningTypeColumn = cursor.getColumnIndex(CellBroadcasts.ETWS_WARNING_TYPE); + if (etwsWarningTypeColumn != -1 && !cursor.isNull(etwsWarningTypeColumn)) { + int warningType = cursor.getInt(etwsWarningTypeColumn); + etwsInfo = new SmsCbEtwsInfo(warningType, false, false, false, null); + } else { + etwsInfo = null; + } + + SmsCbCmasInfo cmasInfo = null; + int cmasMessageClassColumn = cursor.getColumnIndex(CellBroadcasts.CMAS_MESSAGE_CLASS); + if (cmasMessageClassColumn != -1 && !cursor.isNull(cmasMessageClassColumn)) { + int messageClass = cursor.getInt(cmasMessageClassColumn); + + int cmasCategory; + int cmasCategoryColumn = cursor.getColumnIndex(CellBroadcasts.CMAS_CATEGORY); + if (cmasCategoryColumn != -1 && !cursor.isNull(cmasCategoryColumn)) { + cmasCategory = cursor.getInt(cmasCategoryColumn); + } else { + cmasCategory = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN; + } + + int responseType; + int cmasResponseTypeColumn = cursor.getColumnIndex(CellBroadcasts.CMAS_RESPONSE_TYPE); + if (cmasResponseTypeColumn != -1 && !cursor.isNull(cmasResponseTypeColumn)) { + responseType = cursor.getInt(cmasResponseTypeColumn); + } else { + responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN; + } + + int severity; + int cmasSeverityColumn = cursor.getColumnIndex(CellBroadcasts.CMAS_SEVERITY); + if (cmasSeverityColumn != -1 && !cursor.isNull(cmasSeverityColumn)) { + severity = cursor.getInt(cmasSeverityColumn); + } else { + severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN; + } + + int urgency; + int cmasUrgencyColumn = cursor.getColumnIndex(CellBroadcasts.CMAS_URGENCY); + if (cmasUrgencyColumn != -1 && !cursor.isNull(cmasUrgencyColumn)) { + urgency = cursor.getInt(cmasUrgencyColumn); + } else { + urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN; + } + + int certainty; + int cmasCertaintyColumn = cursor.getColumnIndex(CellBroadcasts.CMAS_CERTAINTY); + if (cmasCertaintyColumn != -1 && !cursor.isNull(cmasCertaintyColumn)) { + certainty = cursor.getInt(cmasCertaintyColumn); + } else { + certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN; + } + + cmasInfo = new SmsCbCmasInfo(messageClass, cmasCategory, responseType, severity, + urgency, certainty); + } + + String geoStr = cursor.getString(cursor.getColumnIndex(CellBroadcasts.GEOMETRIES)); + List<Geometry> geometries = + geoStr != null ? CbGeoUtils.parseGeometriesFromString(geoStr) : null; + + long receivedTimeSec = cursor.getLong( + cursor.getColumnIndexOrThrow(CellBroadcasts.RECEIVED_TIME)); + + return new SmsCbMessage(format, geoScope, serialNum, location, category, + language, body, priority, etwsInfo, cmasInfo, geometries, receivedTimeSec); + } + + /** + * @return {@code True} if this message needs geo-fencing check. + */ + public boolean needGeoFencingCheck() { + return mGeometries != null; + } } diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java index 8015b07fa024..dca4e6b13b90 100644 --- a/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java +++ b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java @@ -22,58 +22,36 @@ import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_OTHER_EMERGENCY; import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE; import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI; +import android.annotation.NonNull; import android.content.Context; import android.content.res.Resources; import android.telephony.SmsCbLocation; import android.telephony.SmsCbMessage; import android.util.Pair; +import android.util.Slog; import com.android.internal.R; +import com.android.internal.telephony.CbGeoUtils; +import com.android.internal.telephony.CbGeoUtils.Circle; +import com.android.internal.telephony.CbGeoUtils.Geometry; +import com.android.internal.telephony.CbGeoUtils.LatLng; +import com.android.internal.telephony.CbGeoUtils.Polygon; import com.android.internal.telephony.GsmAlphabet; import com.android.internal.telephony.SmsConstants; +import com.android.internal.telephony.gsm.GsmSmsCbMessage.GeoFencingTriggerMessage.CellBroadcastIdentity; +import com.android.internal.telephony.gsm.SmsCbHeader.DataCodingScheme; import java.io.UnsupportedEncodingException; -import java.util.Locale; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; /** * Parses a GSM or UMTS format SMS-CB message into an {@link SmsCbMessage} object. The class is * public because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases. */ public class GsmSmsCbMessage { - - /** - * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. - */ - private static final String[] LANGUAGE_CODES_GROUP_0 = { - Locale.GERMAN.getLanguage(), // German - Locale.ENGLISH.getLanguage(), // English - Locale.ITALIAN.getLanguage(), // Italian - Locale.FRENCH.getLanguage(), // French - new Locale("es").getLanguage(), // Spanish - new Locale("nl").getLanguage(), // Dutch - new Locale("sv").getLanguage(), // Swedish - new Locale("da").getLanguage(), // Danish - new Locale("pt").getLanguage(), // Portuguese - new Locale("fi").getLanguage(), // Finnish - new Locale("nb").getLanguage(), // Norwegian - new Locale("el").getLanguage(), // Greek - new Locale("tr").getLanguage(), // Turkish - new Locale("hu").getLanguage(), // Hungarian - new Locale("pl").getLanguage(), // Polish - null - }; - - /** - * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. - */ - private static final String[] LANGUAGE_CODES_GROUP_2 = { - new Locale("cs").getLanguage(), // Czech - new Locale("he").getLanguage(), // Hebrew - new Locale("ar").getLanguage(), // Arabic - new Locale("ru").getLanguage(), // Russian - new Locale("is").getLanguage(), // Icelandic - null, null, null, null, null, null, null, null, null, null, null - }; + private static final String TAG = GsmSmsCbMessage.class.getSimpleName(); private static final char CARRIAGE_RETURN = 0x0d; @@ -114,8 +92,9 @@ public class GsmSmsCbMessage { * @param pdus PDU bytes */ public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header, - SmsCbLocation location, byte[][] pdus) + SmsCbLocation location, byte[][] pdus) throws IllegalArgumentException { + long receivedTimeMillis = System.currentTimeMillis(); if (header.isEtwsPrimaryNotification()) { // ETSI TS 23.041 ETWS Primary Notification message // ETWS primary message only contains 4 fields including serial number, @@ -125,12 +104,41 @@ public class GsmSmsCbMessage { header.getSerialNumber(), location, header.getServiceCategory(), null, getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()), SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(), - header.getCmasInfo()); + header.getCmasInfo(), null /* geometries */, receivedTimeMillis); + } else if (header.isUmtsFormat()) { + // UMTS format has only 1 PDU + byte[] pdu = pdus[0]; + Pair<String, String> cbData = parseUmtsBody(header, pdu); + String language = cbData.first; + String body = cbData.second; + int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY + : SmsCbMessage.MESSAGE_PRIORITY_NORMAL; + int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; + int wacDataOffset = SmsCbHeader.PDU_HEADER_LENGTH + + 1 // number of pages + + (PDU_BODY_PAGE_LENGTH + 1) * nrPages; // cb data + + // Has Warning Area Coordinates information + List<Geometry> geometries = null; + if (pdu.length > wacDataOffset) { + try { + geometries = parseWarningAreaCoordinates(pdu, wacDataOffset); + } catch (Exception ex) { + // Catch the exception here, the message will be considered as having no WAC + // information which means the message will be broadcasted directly. + Slog.e(TAG, "Can't parse warning area coordinates, ex = " + ex.toString()); + } + } + + return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, + header.getGeographicalScope(), header.getSerialNumber(), location, + header.getServiceCategory(), language, body, priority, + header.getEtwsInfo(), header.getCmasInfo(), geometries, receivedTimeMillis); } else { String language = null; StringBuilder sb = new StringBuilder(); for (byte[] pdu : pdus) { - Pair<String, String> p = parseBody(header, pdu); + Pair<String, String> p = parseGsmBody(header, pdu); language = p.first; sb.append(p.second); } @@ -140,154 +148,197 @@ public class GsmSmsCbMessage { return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, header.getGeographicalScope(), header.getSerialNumber(), location, header.getServiceCategory(), language, sb.toString(), priority, - header.getEtwsInfo(), header.getCmasInfo()); + header.getEtwsInfo(), header.getCmasInfo(), null /* geometries */, + receivedTimeMillis); } } /** - * Parse and unpack the body text according to the encoding in the DCS. - * After completing successfully this method will have assigned the body - * text into mBody, and optionally the language code into mLanguage + * Parse WEA Handset Action Message(WHAM) a.k.a geo-fencing trigger message. * - * @param header the message header to use - * @param pdu the PDU to decode - * @return a Pair of Strings containing the language and body of the message + * WEA Handset Action Message(WHAM) is a cell broadcast service message broadcast by the network + * to direct devices to perform a geo-fencing check on selected alerts. + * + * WEA Handset Action Message(WHAM) requirements from ATIS-0700041 section 4 + * 1. The Warning Message contents of a WHAM shall be in Cell Broadcast(CB) data format as + * defined in TS 23.041. + * 2. The Warning Message Contents of WHAM shall be limited to one CB page(max 20 referenced + * WEA messages). + * 3. The broadcast area for a WHAM shall be the union of the broadcast areas of the referenced + * WEA message. + * @param pdu cell broadcast pdu, including the header + * @return {@link GeoFencingTriggerMessage} instance */ - private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) { - int encoding; - String language = null; - boolean hasLanguageIndicator = false; - int dataCodingScheme = header.getDataCodingScheme(); - - // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, - // section 5. - switch ((dataCodingScheme & 0xf0) >> 4) { - case 0x00: - encoding = SmsConstants.ENCODING_7BIT; - language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f]; - break; - - case 0x01: - hasLanguageIndicator = true; - if ((dataCodingScheme & 0x0f) == 0x01) { - encoding = SmsConstants.ENCODING_16BIT; - } else { - encoding = SmsConstants.ENCODING_7BIT; - } - break; - - case 0x02: - encoding = SmsConstants.ENCODING_7BIT; - language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f]; - break; - - case 0x03: - encoding = SmsConstants.ENCODING_7BIT; - break; + public static GeoFencingTriggerMessage createGeoFencingTriggerMessage(byte[] pdu) { + try { + // Header length + 1(number of page). ATIS-0700041 define the number of page of + // geo-fencing trigger message is 1. + int whamOffset = SmsCbHeader.PDU_HEADER_LENGTH + 1; + + BitStreamReader bitReader = new BitStreamReader(pdu, whamOffset); + int type = bitReader.read(4); + int length = bitReader.read(7); + // Skip the remained 5 bits + bitReader.skip(); + + int messageIdentifierCount = (length - 2) * 8 / 32; + List<CellBroadcastIdentity> cbIdentifiers = new ArrayList<>(); + for (int i = 0; i < messageIdentifierCount; i++) { + // Both messageIdentifier and serialNumber are 16 bits integers. + // ATIS-0700041 Section 5.1.6 + int messageIdentifier = bitReader.read(16); + int serialNumber = bitReader.read(16); + cbIdentifiers.add(new CellBroadcastIdentity(messageIdentifier, serialNumber)); + } + return new GeoFencingTriggerMessage(type, cbIdentifiers); + } catch (Exception ex) { + Slog.e(TAG, "create geo-fencing trigger failed, ex = " + ex.toString()); + return null; + } + } - case 0x04: - case 0x05: - switch ((dataCodingScheme & 0x0c) >> 2) { - case 0x01: - encoding = SmsConstants.ENCODING_8BIT; - break; - - case 0x02: - encoding = SmsConstants.ENCODING_16BIT; - break; - - case 0x00: - default: - encoding = SmsConstants.ENCODING_7BIT; - break; - } - break; + private static List<Geometry> parseWarningAreaCoordinates(byte[] pdu, int wacOffset) { + // little-endian + int wacDataLength = (pdu[wacOffset + 1] << 8) | pdu[wacOffset]; + int offset = wacOffset + 2; - case 0x06: - case 0x07: - // Compression not supported - case 0x09: - // UDH structure not supported - case 0x0e: - // Defined by the WAP forum not supported - throw new IllegalArgumentException("Unsupported GSM dataCodingScheme " - + dataCodingScheme); - - case 0x0f: - if (((dataCodingScheme & 0x04) >> 2) == 0x01) { - encoding = SmsConstants.ENCODING_8BIT; - } else { - encoding = SmsConstants.ENCODING_7BIT; - } - break; - - default: - // Reserved values are to be treated as 7-bit - encoding = SmsConstants.ENCODING_7BIT; - break; + if (offset + wacDataLength > pdu.length) { + throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at" + + "least " + offset + wacDataLength + ", actual is " + pdu.length); } - if (header.isUmtsFormat()) { - // Payload may contain multiple pages - int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; - - if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) - * nrPages) { - throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " - + nrPages + " pages"); + BitStreamReader bitReader = new BitStreamReader(pdu, offset); + + List<Geometry> geo = new ArrayList<>(); + int remainedBytes = wacDataLength; + while (remainedBytes > 0) { + int type = bitReader.read(4); + int length = bitReader.read(10); + remainedBytes -= length; + // Skip the 2 remained bits + bitReader.skip(); + + switch (type) { + case CbGeoUtils.GEO_FENCING_MAXIMUM_WAIT_TIME: + // TODO: handle the maximum wait time in cell broadcast provider. + int maximumWaitTimeSec = bitReader.read(8); + break; + case CbGeoUtils.GEOMETRY_TYPE_POLYGON: + List<LatLng> latLngs = new ArrayList<>(); + // Each coordinate is represented by 44 bits integer. + // ATIS-0700041 5.2.4 Coordinate coding + int n = (length - 2) * 8 / 44; + for (int i = 0; i < n; i++) { + latLngs.add(getLatLng(bitReader)); + } + // Skip the padding bits + bitReader.skip(); + geo.add(new Polygon(latLngs)); + break; + case CbGeoUtils.GEOMETRY_TYPE_CIRCLE: + LatLng center = getLatLng(bitReader); + // radius = (wacRadius / 2^6). The unit of wacRadius is km, we use meter as the + // distance unit during geo-fencing. + // ATIS-0700041 5.2.5 radius coding + double radius = (bitReader.read(20) * 1.0 / (1 << 6)) * 1000.0; + geo.add(new Circle(center, radius)); + break; + default: + throw new IllegalArgumentException("Unsupported geoType = " + type); } + } + return geo; + } - StringBuilder sb = new StringBuilder(); + /** + * The coordinate is (latitude, longitude), represented by a 44 bits integer. + * The coding is defined in ATIS-0700041 5.2.4 + * @param bitReader + * @return coordinate (latitude, longitude) + */ + private static LatLng getLatLng(BitStreamReader bitReader) { + // wacLatitude = floor(((latitude + 90) / 180) * 2^22) + // wacLongitude = floor(((longitude + 180) / 360) * 2^22) + int wacLat = bitReader.read(22); + int wacLng = bitReader.read(22); + + // latitude = wacLatitude * 180 / 2^22 - 90 + // longitude = wacLongitude * 360 / 2^22 -180 + return new LatLng((wacLat * 180.0 / (1 << 22)) - 90, (wacLng * 360.0 / (1 << 22) - 180)); + } - for (int i = 0; i < nrPages; i++) { - // Each page is 82 bytes followed by a length octet indicating - // the number of useful octets within those 82 - int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; - int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; + /** + * Parse and unpack the UMTS body text according to the encoding in the data coding scheme. + * + * @param header the message header to use + * @param pdu the PDU to decode + * @return a pair of string containing the language and body of the message in order + */ + private static Pair<String, String> parseUmtsBody(SmsCbHeader header, byte[] pdu) { + // Payload may contain multiple pages + int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH]; + String language = header.getDataCodingSchemeStructedData().language; + + if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) + * nrPages) { + throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " + + nrPages + " pages"); + } - if (length > PDU_BODY_PAGE_LENGTH) { - throw new IllegalArgumentException("Page length " + length - + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); - } + StringBuilder sb = new StringBuilder(); - Pair<String, String> p = unpackBody(pdu, encoding, offset, length, - hasLanguageIndicator, language); - language = p.first; - sb.append(p.second); + for (int i = 0; i < nrPages; i++) { + // Each page is 82 bytes followed by a length octet indicating + // the number of useful octets within those 82 + int offset = SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1) * i; + int length = pdu[offset + PDU_BODY_PAGE_LENGTH]; + + if (length > PDU_BODY_PAGE_LENGTH) { + throw new IllegalArgumentException("Page length " + length + + " exceeds maximum value " + PDU_BODY_PAGE_LENGTH); } - return new Pair<String, String>(language, sb.toString()); - } else { - // Payload is one single page - int offset = SmsCbHeader.PDU_HEADER_LENGTH; - int length = pdu.length - offset; - return unpackBody(pdu, encoding, offset, length, hasLanguageIndicator, language); + Pair<String, String> p = unpackBody(pdu, offset, length, + header.getDataCodingSchemeStructedData()); + language = p.first; + sb.append(p.second); } + return new Pair(language, sb.toString()); + + } + + /** + * Parse and unpack the GSM body text according to the encoding in the data coding scheme. + * @param header the message header to use + * @param pdu the PDU to decode + * @return a pair of string containing the language and body of the message in order + */ + private static Pair<String, String> parseGsmBody(SmsCbHeader header, byte[] pdu) { + // Payload is one single page + int offset = SmsCbHeader.PDU_HEADER_LENGTH; + int length = pdu.length - offset; + return unpackBody(pdu, offset, length, header.getDataCodingSchemeStructedData()); } /** - * Unpack body text from the pdu using the given encoding, position and - * length within the pdu + * Unpack body text from the pdu using the given encoding, position and length within the pdu. * * @param pdu The pdu - * @param encoding The encoding, as derived from the DCS * @param offset Position of the first byte to unpack * @param length Number of bytes to unpack - * @param hasLanguageIndicator true if the body text is preceded by a - * language indicator. If so, this method will as a side-effect - * assign the extracted language code into mLanguage - * @param language the language to return if hasLanguageIndicator is false + * @param dcs data coding scheme * @return a Pair of Strings containing the language and body of the message */ - private static Pair<String, String> unpackBody(byte[] pdu, int encoding, int offset, int length, - boolean hasLanguageIndicator, String language) { + private static Pair<String, String> unpackBody(byte[] pdu, int offset, int length, + DataCodingScheme dcs) { String body = null; - switch (encoding) { + String language = dcs.language; + switch (dcs.encoding) { case SmsConstants.ENCODING_7BIT: body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); - if (hasLanguageIndicator && body != null && body.length() > 2) { + if (dcs.hasLanguageIndicator && body != null && body.length() > 2) { // Language is two GSM characters followed by a CR. // The actual body text is offset by 3 characters. language = body.substring(0, 2); @@ -296,7 +347,7 @@ public class GsmSmsCbMessage { break; case SmsConstants.ENCODING_16BIT: - if (hasLanguageIndicator && pdu.length >= offset + 2) { + if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) { // Language is two GSM characters. // The actual body text is offset by 2 bytes. language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); @@ -330,4 +381,105 @@ public class GsmSmsCbMessage { return new Pair<String, String>(language, body); } + + /** A class use to facilitate the processing of bits stream data. */ + private static final class BitStreamReader { + /** The bits stream represent by a bytes array. */ + private final byte[] mData; + + /** The offset of the current byte. */ + private int mCurrentOffset; + + /** + * The remained bits of the current byte which have not been read. The most significant + * will be read first, so the remained bits are always the least significant bits. + */ + private int mRemainedBit; + + /** + * Constructor + * @param data bit stream data represent by byte array. + * @param offset the offset of the first byte. + */ + BitStreamReader(byte[] data, int offset) { + mData = data; + mCurrentOffset = offset; + mRemainedBit = 8; + } + + /** + * Read the first {@code count} bits. + * @param count the number of bits need to read + * @return {@code bits} represent by an 32-bits integer, therefore {@code count} must be no + * greater than 32. + */ + public int read(int count) throws IndexOutOfBoundsException { + int val = 0; + while (count > 0) { + if (count >= mRemainedBit) { + val <<= mRemainedBit; + val |= mData[mCurrentOffset] & ((1 << mRemainedBit) - 1); + count -= mRemainedBit; + mRemainedBit = 8; + ++mCurrentOffset; + } else { + val <<= count; + val |= (mData[mCurrentOffset] & ((1 << mRemainedBit) - 1)) + >> (mRemainedBit - count); + mRemainedBit -= count; + count = 0; + } + } + return val; + } + + /** + * Skip the current bytes if the remained bits is less than 8. This is useful when + * processing the padding or reserved bits. + */ + public void skip() { + if (mRemainedBit < 8) { + mRemainedBit = 8; + ++mCurrentOffset; + } + } + } + + static final class GeoFencingTriggerMessage { + /** + * Indicate the list of active alerts share their warning area coordinates which means the + * broadcast area is the union of the broadcast areas of the active alerts in this list. + */ + public static final int TYPE_ACTIVE_ALERT_SHARE_WAC = 2; + + public final int type; + public final List<CellBroadcastIdentity> cbIdentifiers; + + GeoFencingTriggerMessage(int type, @NonNull List<CellBroadcastIdentity> cbIdentifiers) { + this.type = type; + this.cbIdentifiers = cbIdentifiers; + } + + boolean shouldShareBroadcastArea() { + return type == TYPE_ACTIVE_ALERT_SHARE_WAC; + } + + static final class CellBroadcastIdentity { + public final int messageIdentifier; + public final int serialNumber; + CellBroadcastIdentity(int messageIdentifier, int serialNumber) { + this.messageIdentifier = messageIdentifier; + this.serialNumber = serialNumber; + } + } + + @Override + public String toString() { + String identifiers = cbIdentifiers.stream() + .map(cbIdentifier ->String.format("(msgId = %d, serial = %d)", + cbIdentifier.messageIdentifier, cbIdentifier.serialNumber)) + .collect(Collectors.joining(",")); + return "triggerType=" + type + " identifiers=" + identifiers; + } + } } diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java b/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java index 541ca8d1e5c0..5ad2b9d8f682 100644 --- a/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java +++ b/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java @@ -215,9 +215,11 @@ public class SmsCbConstants { public static final int MESSAGE_ID_CMAS_ALERT_STATE_LOCAL_TEST_LANGUAGE = 0x112F; // 4399 - /** End of CMAS Message Identifier range (including future extensions). */ - public static final int MESSAGE_ID_CMAS_LAST_IDENTIFIER - = 0x112F; // 4399 + /** CMAS Message Identifier for CMAS geo fencing trigger message. */ + public static final int MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER = 0x1130; // 4440 + + /** End of CMAS Message Identifier range. */ + public static final int MESSAGE_ID_CMAS_LAST_IDENTIFIER = MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER; /** End of PWS Message Identifier range (includes ETWS, CMAS, and future extensions). */ public static final int MESSAGE_ID_PWS_LAST_IDENTIFIER diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java index 0dbc186ef2cf..acdc83867d2f 100644 --- a/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java +++ b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java @@ -19,7 +19,10 @@ package com.android.internal.telephony.gsm; import android.telephony.SmsCbCmasInfo; import android.telephony.SmsCbEtwsInfo; +import com.android.internal.telephony.SmsConstants; + import java.util.Arrays; +import java.util.Locale; /** * Parses a 3GPP TS 23.041 cell broadcast message header. This class is public for use by @@ -32,6 +35,39 @@ import java.util.Arrays; * The raw PDU is no longer sent to SMS CB applications. */ public class SmsCbHeader { + /** + * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5. + */ + private static final String[] LANGUAGE_CODES_GROUP_0 = { + Locale.GERMAN.getLanguage(), // German + Locale.ENGLISH.getLanguage(), // English + Locale.ITALIAN.getLanguage(), // Italian + Locale.FRENCH.getLanguage(), // French + new Locale("es").getLanguage(), // Spanish + new Locale("nl").getLanguage(), // Dutch + new Locale("sv").getLanguage(), // Swedish + new Locale("da").getLanguage(), // Danish + new Locale("pt").getLanguage(), // Portuguese + new Locale("fi").getLanguage(), // Finnish + new Locale("nb").getLanguage(), // Norwegian + new Locale("el").getLanguage(), // Greek + new Locale("tr").getLanguage(), // Turkish + new Locale("hu").getLanguage(), // Hungarian + new Locale("pl").getLanguage(), // Polish + null + }; + + /** + * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5. + */ + private static final String[] LANGUAGE_CODES_GROUP_2 = { + new Locale("cs").getLanguage(), // Czech + new Locale("he").getLanguage(), // Hebrew + new Locale("ar").getLanguage(), // Arabic + new Locale("ru").getLanguage(), // Russian + new Locale("is").getLanguage(), // Icelandic + null, null, null, null, null, null, null, null, null, null, null + }; /** * Length of SMS-CB header @@ -84,6 +120,8 @@ public class SmsCbHeader { private final int mFormat; + private DataCodingScheme mDataCodingSchemeStructedData; + /** ETWS warning notification info. */ private final SmsCbEtwsInfo mEtwsInfo; @@ -162,6 +200,10 @@ public class SmsCbHeader { mNrOfPages = 1; } + if (mDataCodingScheme != -1) { + mDataCodingSchemeStructedData = new DataCodingScheme(mDataCodingScheme); + } + if (isEtwsMessage()) { boolean emergencyUserAlert = isEtwsEmergencyUserAlert(); boolean activatePopup = isEtwsPopupAlert(); @@ -199,6 +241,10 @@ public class SmsCbHeader { return mDataCodingScheme; } + DataCodingScheme getDataCodingSchemeStructedData() { + return mDataCodingSchemeStructedData; + } + int getPageIndex() { return mPageIndex; } @@ -448,4 +494,93 @@ public class SmsCbHeader { + ", DCS=0x" + Integer.toHexString(mDataCodingScheme) + ", page " + mPageIndex + " of " + mNrOfPages + '}'; } + + /** + * CBS Data Coding Scheme. + * Reference: 3GPP TS 23.038 version 15.0.0 section #5, CBS Data Coding Scheme + */ + public static final class DataCodingScheme { + public final int encoding; + public final String language; + public final boolean hasLanguageIndicator; + + public DataCodingScheme(int dataCodingScheme) { + int encoding = 0; + String language = null; + boolean hasLanguageIndicator = false; + + // Extract encoding and language from DCS, as defined in 3gpp TS 23.038, + // section 5. + switch ((dataCodingScheme & 0xf0) >> 4) { + case 0x00: + encoding = SmsConstants.ENCODING_7BIT; + language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f]; + break; + + case 0x01: + hasLanguageIndicator = true; + if ((dataCodingScheme & 0x0f) == 0x01) { + encoding = SmsConstants.ENCODING_16BIT; + } else { + encoding = SmsConstants.ENCODING_7BIT; + } + break; + + case 0x02: + encoding = SmsConstants.ENCODING_7BIT; + language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f]; + break; + + case 0x03: + encoding = SmsConstants.ENCODING_7BIT; + break; + + case 0x04: + case 0x05: + switch ((dataCodingScheme & 0x0c) >> 2) { + case 0x01: + encoding = SmsConstants.ENCODING_8BIT; + break; + + case 0x02: + encoding = SmsConstants.ENCODING_16BIT; + break; + + case 0x00: + default: + encoding = SmsConstants.ENCODING_7BIT; + break; + } + break; + + case 0x06: + case 0x07: + // Compression not supported + case 0x09: + // UDH structure not supported + case 0x0e: + // Defined by the WAP forum not supported + throw new IllegalArgumentException("Unsupported GSM dataCodingScheme " + + dataCodingScheme); + + case 0x0f: + if (((dataCodingScheme & 0x04) >> 2) == 0x01) { + encoding = SmsConstants.ENCODING_8BIT; + } else { + encoding = SmsConstants.ENCODING_7BIT; + } + break; + + default: + // Reserved values are to be treated as 7-bit + encoding = SmsConstants.ENCODING_7BIT; + break; + } + + + this.encoding = encoding; + this.language = language; + this.hasLanguageIndicator = hasLanguageIndicator; + } + } }
\ No newline at end of file |