Support WAC decoding in UMTS format

This add a decoder for WAC information. WAC is appended to the UMTS
cell broadcast message. The encoding is defined in ATIS-0700041.

Bug: 123096618
Test: build & manual test
Merged-In: I73dc22c27c2cfd47000ae65448fbc857ed9aa42f
Change-Id: I73dc22c27c2cfd47000ae65448fbc857ed9aa42f
(cherry picked from commit 14df7e3845369e7254e6aae13160c7e1caa1d28a)
diff --git a/telephony/java/android/provider/Telephony.java b/telephony/java/android/provider/Telephony.java
index 38c81d3..f46a957 100644
--- a/telephony/java/android/provider/Telephony.java
+++ b/telephony/java/android/provider/Telephony.java
@@ -4049,8 +4049,8 @@
         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";
 
@@ -4120,6 +4120,33 @@
                 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 c973b67..73dd822 100644
--- a/telephony/java/com/android/internal/telephony/CbGeoUtils.java
+++ b/telephony/java/com/android/internal/telephony/CbGeoUtils.java
@@ -53,6 +53,11 @@
 
     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 @@
                     + 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 046bf8c..b9edb9f 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 @@
     /** 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 @@
         mPriority = priority;
         mEtwsWarningInfo = etwsWarningInfo;
         mCmasWarningInfo = cmasWarningInfo;
+        mReceivedTimeMillis = receivedTimeMillis;
+        mGeometries = geometries;
     }
 
     /** Create a new SmsCbMessage object from a Parcel. */
@@ -184,6 +214,9 @@
                 mEtwsWarningInfo = null;
                 mCmasWarningInfo = null;
         }
+        mReceivedTimeMillis = in.readLong();
+        String geoStr = in.readString();
+        mGeometries = geoStr != null ? CbGeoUtils.parseGeometriesFromString(geoStr) : null;
     }
 
     /**
@@ -214,6 +247,9 @@
             // 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 @@
     }
 
     /**
+     * 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 @@
                 + 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 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 8015b07..dca4e6b 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_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 @@
      * @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 @@
                     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 @@
             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 @@
                 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 @@
 
         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 541ca8d..5ad2b9d 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 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 0dbc186..acdc838 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 @@
 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 @@
  * 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 @@
 
     private final int mFormat;
 
+    private DataCodingScheme mDataCodingSchemeStructedData;
+
     /** ETWS warning notification info. */
     private final SmsCbEtwsInfo mEtwsInfo;
 
@@ -162,6 +200,10 @@
             mNrOfPages = 1;
         }
 
+        if (mDataCodingScheme != -1) {
+            mDataCodingSchemeStructedData = new DataCodingScheme(mDataCodingScheme);
+        }
+
         if (isEtwsMessage()) {
             boolean emergencyUserAlert = isEtwsEmergencyUserAlert();
             boolean activatePopup = isEtwsPopupAlert();
@@ -199,6 +241,10 @@
         return mDataCodingScheme;
     }
 
+    DataCodingScheme getDataCodingSchemeStructedData() {
+        return mDataCodingSchemeStructedData;
+    }
+
     int getPageIndex() {
         return mPageIndex;
     }
@@ -448,4 +494,93 @@
                 + ", 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