Merge "Support WAC decoding in UMTS format"

This commit is contained in:
Pengquan Meng
2019-08-27 20:16:54 +00:00
committed by Gerrit Code Review
6 changed files with 714 additions and 164 deletions

View File

@@ -4021,8 +4021,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";
@@ -4092,6 +4092,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
};
}
/**

View File

@@ -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 + ")";
}
}
/**

View File

@@ -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
@@ -292,6 +328,24 @@ public class SmsCbMessage implements Parcelable {
return mBody;
}
/**
* 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;
}
}

View File

@@ -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.
*
* 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
*/
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;
}
}
private static List<Geometry> parseWarningAreaCoordinates(byte[] pdu, int wacOffset) {
// little-endian
int wacDataLength = (pdu[wacOffset + 1] << 8) | pdu[wacOffset];
int offset = wacOffset + 2;
if (offset + wacDataLength > pdu.length) {
throw new IllegalArgumentException("Invalid wac data, expected the length of pdu at"
+ "least " + offset + wacDataLength + ", actual is " + pdu.length);
}
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;
}
/**
* 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));
}
/**
* 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 Strings containing the language and body of the message
* @return a pair of string containing the language and body of the message in order
*/
private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) {
int encoding;
String language = null;
boolean hasLanguageIndicator = false;
int dataCodingScheme = header.getDataCodingScheme();
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;
// 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;
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 (header.isUmtsFormat()) {
// Payload may contain multiple pages
int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
StringBuilder sb = new StringBuilder();
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");
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);
}
StringBuilder sb = new StringBuilder();
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);
}
Pair<String, String> p = unpackBody(pdu, encoding, offset, length,
hasLanguageIndicator, language);
language = p.first;
sb.append(p.second);
}
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());
}
/**
* Unpack body text from the pdu using the given encoding, position and
* length within the pdu
* 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.
*
* @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;
}
}
}

View File

@@ -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

View File

@@ -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;
}
}
}