Merge "Support WAC decoding in UMTS format"

am: 9bc9b80912

Change-Id: I7efc88e5878e68b571d55fd386ad022b9a662bc3
This commit is contained in:
Pengquan Meng
2019-08-27 13:50:18 -07:00
committed by android-build-merger
6 changed files with 714 additions and 164 deletions

View File

@@ -4093,8 +4093,8 @@ public final class Telephony {
public static final String DEFAULT_SORT_ORDER = DELIVERY_TIME + " DESC"; public static final String DEFAULT_SORT_ORDER = DELIVERY_TIME + " DESC";
/** /**
* The Epoch Unix timestamp when the device received the message. * The timestamp in millisecond of when the device received the message.
* <P>Type: INTEGER</P> * <P>Type: BIGINT</P>
*/ */
public static final String RECEIVED_TIME = "received_time"; public static final String RECEIVED_TIME = "received_time";
@@ -4164,6 +4164,33 @@ public final class Telephony {
CMAS_URGENCY, CMAS_URGENCY,
CMAS_CERTAINTY 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"; 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. */ /** The identifier of geometry in the encoded string. */
private static final String CIRCLE_SYMBOL = "circle"; private static final String CIRCLE_SYMBOL = "circle";
private static final String POLYGON_SYMBOL = "polygon"; 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)); + 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; 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; package android.telephony;
import android.annotation.Nullable;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Parcel; import android.os.Parcel;
import android.os.Parcelable; 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 * 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). */ /** CMAS warning notification information (CMAS warnings only). */
private final SmsCbCmasInfo mCmasWarningInfo; 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. * Create a new SmsCbMessage with the specified data.
*/ */
public SmsCbMessage(int messageFormat, int geographicalScope, int serialNumber, public SmsCbMessage(int messageFormat, int geographicalScope, int serialNumber,
SmsCbLocation location, int serviceCategory, String language, String body, SmsCbLocation location, int serviceCategory, String language, String body,
int priority, SmsCbEtwsInfo etwsWarningInfo, SmsCbCmasInfo cmasWarningInfo) { 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; mMessageFormat = messageFormat;
mGeographicalScope = geographicalScope; mGeographicalScope = geographicalScope;
mSerialNumber = serialNumber; mSerialNumber = serialNumber;
@@ -154,6 +182,8 @@ public class SmsCbMessage implements Parcelable {
mPriority = priority; mPriority = priority;
mEtwsWarningInfo = etwsWarningInfo; mEtwsWarningInfo = etwsWarningInfo;
mCmasWarningInfo = cmasWarningInfo; mCmasWarningInfo = cmasWarningInfo;
mReceivedTimeMillis = receivedTimeMillis;
mGeometries = geometries;
} }
/** Create a new SmsCbMessage object from a Parcel. */ /** Create a new SmsCbMessage object from a Parcel. */
@@ -184,6 +214,9 @@ public class SmsCbMessage implements Parcelable {
mEtwsWarningInfo = null; mEtwsWarningInfo = null;
mCmasWarningInfo = 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 // no ETWS or CMAS warning information
dest.writeInt('0'); dest.writeInt('0');
} }
dest.writeLong(mReceivedTimeMillis);
dest.writeString(
mGeometries != null ? CbGeoUtils.encodeGeometriesToString(mGeometries) : null);
} }
public static final Parcelable.Creator<SmsCbMessage> CREATOR public static final Parcelable.Creator<SmsCbMessage> CREATOR
@@ -292,6 +328,24 @@ public class SmsCbMessage implements Parcelable {
return mBody; 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}). * Get the message format ({@link #MESSAGE_FORMAT_3GPP} or {@link #MESSAGE_FORMAT_3GPP2}).
* @return an integer representing 3GPP or 3GPP2 message format * @return an integer representing 3GPP or 3GPP2 message format
@@ -368,7 +422,10 @@ public class SmsCbMessage implements Parcelable {
+ mServiceCategory + ", language=" + mLanguage + ", body=" + mBody + mServiceCategory + ", language=" + mLanguage + ", body=" + mBody
+ ", priority=" + mPriority + ", priority=" + mPriority
+ (mEtwsWarningInfo != null ? (", " + mEtwsWarningInfo.toString()) : "") + (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() { public int describeContents() {
return 0; 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_TEST_MESSAGE;
import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI; import static android.telephony.SmsCbEtwsInfo.ETWS_WARNING_TYPE_TSUNAMI;
import android.annotation.NonNull;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.telephony.SmsCbLocation; import android.telephony.SmsCbLocation;
import android.telephony.SmsCbMessage; import android.telephony.SmsCbMessage;
import android.util.Pair; import android.util.Pair;
import android.util.Slog;
import com.android.internal.R; 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.GsmAlphabet;
import com.android.internal.telephony.SmsConstants; 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.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 * 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 because {@link #createSmsCbMessage(SmsCbLocation, byte[][])} is used by some test cases.
*/ */
public class GsmSmsCbMessage { public class GsmSmsCbMessage {
private static final String TAG = GsmSmsCbMessage.class.getSimpleName();
/**
* 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 char CARRIAGE_RETURN = 0x0d; private static final char CARRIAGE_RETURN = 0x0d;
@@ -114,8 +92,9 @@ public class GsmSmsCbMessage {
* @param pdus PDU bytes * @param pdus PDU bytes
*/ */
public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header, public static SmsCbMessage createSmsCbMessage(Context context, SmsCbHeader header,
SmsCbLocation location, byte[][] pdus) SmsCbLocation location, byte[][] pdus)
throws IllegalArgumentException { throws IllegalArgumentException {
long receivedTimeMillis = System.currentTimeMillis();
if (header.isEtwsPrimaryNotification()) { if (header.isEtwsPrimaryNotification()) {
// ETSI TS 23.041 ETWS Primary Notification message // ETSI TS 23.041 ETWS Primary Notification message
// ETWS primary message only contains 4 fields including serial number, // ETWS primary message only contains 4 fields including serial number,
@@ -125,12 +104,41 @@ public class GsmSmsCbMessage {
header.getSerialNumber(), location, header.getServiceCategory(), null, header.getSerialNumber(), location, header.getServiceCategory(), null,
getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()), getEtwsPrimaryMessage(context, header.getEtwsInfo().getWarningType()),
SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, header.getEtwsInfo(), 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 { } else {
String language = null; String language = null;
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
for (byte[] pdu : pdus) { for (byte[] pdu : pdus) {
Pair<String, String> p = parseBody(header, pdu); Pair<String, String> p = parseGsmBody(header, pdu);
language = p.first; language = p.first;
sb.append(p.second); sb.append(p.second);
} }
@@ -140,154 +148,197 @@ public class GsmSmsCbMessage {
return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP, return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
header.getGeographicalScope(), header.getSerialNumber(), location, header.getGeographicalScope(), header.getSerialNumber(), location,
header.getServiceCategory(), language, sb.toString(), priority, 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. * Parse WEA Handset Action Message(WHAM) a.k.a geo-fencing trigger message.
* After completing successfully this method will have assigned the body *
* text into mBody, and optionally the language code into mLanguage * 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 header the message header to use
* @param pdu the PDU to decode * @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) { private static Pair<String, String> parseUmtsBody(SmsCbHeader header, byte[] pdu) {
int encoding; // Payload may contain multiple pages
String language = null; int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
boolean hasLanguageIndicator = false; String language = header.getDataCodingSchemeStructedData().language;
int dataCodingScheme = header.getDataCodingScheme();
// Extract encoding and language from DCS, as defined in 3gpp TS 23.038, if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
// section 5. * nrPages) {
switch ((dataCodingScheme & 0xf0) >> 4) { throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
case 0x00: + nrPages + " pages");
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 (header.isUmtsFormat()) { StringBuilder sb = new StringBuilder();
// 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) for (int i = 0; i < nrPages; i++) {
* nrPages) { // Each page is 82 bytes followed by a length octet indicating
throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match " // the number of useful octets within those 82
+ nrPages + " pages"); 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(); Pair<String, String> p = unpackBody(pdu, offset, length,
header.getDataCodingSchemeStructedData());
for (int i = 0; i < nrPages; i++) { language = p.first;
// Each page is 82 bytes followed by a length octet indicating sb.append(p.second);
// 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);
} }
return new Pair(language, sb.toString());
} }
/** /**
* Unpack body text from the pdu using the given encoding, position and * Parse and unpack the GSM body text according to the encoding in the data coding scheme.
* length within the pdu * @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 pdu The pdu
* @param encoding The encoding, as derived from the DCS
* @param offset Position of the first byte to unpack * @param offset Position of the first byte to unpack
* @param length Number of bytes to unpack * @param length Number of bytes to unpack
* @param hasLanguageIndicator true if the body text is preceded by a * @param dcs data coding scheme
* 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
* @return a Pair of Strings containing the language and body of the message * @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, private static Pair<String, String> unpackBody(byte[] pdu, int offset, int length,
boolean hasLanguageIndicator, String language) { DataCodingScheme dcs) {
String body = null; String body = null;
switch (encoding) { String language = dcs.language;
switch (dcs.encoding) {
case SmsConstants.ENCODING_7BIT: case SmsConstants.ENCODING_7BIT:
body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7); 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. // Language is two GSM characters followed by a CR.
// The actual body text is offset by 3 characters. // The actual body text is offset by 3 characters.
language = body.substring(0, 2); language = body.substring(0, 2);
@@ -296,7 +347,7 @@ public class GsmSmsCbMessage {
break; break;
case SmsConstants.ENCODING_16BIT: case SmsConstants.ENCODING_16BIT:
if (hasLanguageIndicator && pdu.length >= offset + 2) { if (dcs.hasLanguageIndicator && pdu.length >= offset + 2) {
// Language is two GSM characters. // Language is two GSM characters.
// The actual body text is offset by 2 bytes. // The actual body text is offset by 2 bytes.
language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2); language = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
@@ -330,4 +381,105 @@ public class GsmSmsCbMessage {
return new Pair<String, String>(language, body); 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 public static final int MESSAGE_ID_CMAS_ALERT_STATE_LOCAL_TEST_LANGUAGE
= 0x112F; // 4399 = 0x112F; // 4399
/** End of CMAS Message Identifier range (including future extensions). */ /** CMAS Message Identifier for CMAS geo fencing trigger message. */
public static final int MESSAGE_ID_CMAS_LAST_IDENTIFIER public static final int MESSAGE_ID_CMAS_GEO_FENCING_TRIGGER = 0x1130; // 4440
= 0x112F; // 4399
/** 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). */ /** End of PWS Message Identifier range (includes ETWS, CMAS, and future extensions). */
public static final int MESSAGE_ID_PWS_LAST_IDENTIFIER 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.SmsCbCmasInfo;
import android.telephony.SmsCbEtwsInfo; import android.telephony.SmsCbEtwsInfo;
import com.android.internal.telephony.SmsConstants;
import java.util.Arrays; 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 * 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. * The raw PDU is no longer sent to SMS CB applications.
*/ */
public class SmsCbHeader { 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 * Length of SMS-CB header
@@ -84,6 +120,8 @@ public class SmsCbHeader {
private final int mFormat; private final int mFormat;
private DataCodingScheme mDataCodingSchemeStructedData;
/** ETWS warning notification info. */ /** ETWS warning notification info. */
private final SmsCbEtwsInfo mEtwsInfo; private final SmsCbEtwsInfo mEtwsInfo;
@@ -162,6 +200,10 @@ public class SmsCbHeader {
mNrOfPages = 1; mNrOfPages = 1;
} }
if (mDataCodingScheme != -1) {
mDataCodingSchemeStructedData = new DataCodingScheme(mDataCodingScheme);
}
if (isEtwsMessage()) { if (isEtwsMessage()) {
boolean emergencyUserAlert = isEtwsEmergencyUserAlert(); boolean emergencyUserAlert = isEtwsEmergencyUserAlert();
boolean activatePopup = isEtwsPopupAlert(); boolean activatePopup = isEtwsPopupAlert();
@@ -199,6 +241,10 @@ public class SmsCbHeader {
return mDataCodingScheme; return mDataCodingScheme;
} }
DataCodingScheme getDataCodingSchemeStructedData() {
return mDataCodingSchemeStructedData;
}
int getPageIndex() { int getPageIndex() {
return mPageIndex; return mPageIndex;
} }
@@ -448,4 +494,93 @@ public class SmsCbHeader {
+ ", DCS=0x" + Integer.toHexString(mDataCodingScheme) + ", DCS=0x" + Integer.toHexString(mDataCodingScheme)
+ ", page " + mPageIndex + " of " + mNrOfPages + '}'; + ", 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;
}
}
} }