OSDN Git Service

Add support for CMAS warning notifications over CDMA.
authorJake Hamby <jhamby@google.com>
Mon, 6 Feb 2012 22:53:43 +0000 (14:53 -0800)
committerJake Hamby <jhamby@google.com>
Wed, 29 Feb 2012 00:49:17 +0000 (16:49 -0800)
Refactor SMS Cell Broadcast support to enable receiving CMAS warning
notifications over CDMA. The CellBroadcastReceiver app must also be
updated with the corresponding change. All cell broadcasts are now
delivered as a Parcelable SmsCbMessage object in the "message" extra
of the SMS_CB_RECEIVED_ACTION or SMS_EMERGENCY_CB_RECEIVED_ACTION,
instead of as a GSM/UMTS "pdu" byte array.

Existing functionality for ETWS and CMAS alerts over GSM/UMTS continues
to be supported using the new radio-technology independent SmsCbMessage
and related objects. Test cases are added to verify that valid and
invalid broadcast data is handled appropriately.

Unit testing discovered a bug in the BitwiseOutputStream utility class
used by the added test cases. When the BitwiseOutputStream object must be
expanded (in the private possExpand() method), the mEnd field is not
updated to the new array size. This causes a new array to be allocated
on every new write, and for all data beyond the original array allocation
to be replaced with zeroes. Fixed by adding a line to possExpand() to
update mEnd. Added a test case to BitwiseStreamsTest to verify the fix.
Besides the test cases, BitwiseOutputStream is only used by BearerData in
two places, both of which allocate a sufficient initial buffer. So the
bug in BitwiseOutputStream is not critical to fix for normal operation,
but should be fixed so that the test cases using it function correctly.

Bug: 5856308
Change-Id: Ie3e6af747976ce9b8a3e71e76fec71709cf86545

20 files changed:
core/java/android/provider/Telephony.java
core/java/com/android/internal/util/BitwiseOutputStream.java
core/tests/coretests/src/com/android/internal/util/BitwiseStreamsTest.java
telephony/java/android/telephony/SmsCbCmasInfo.java [new file with mode: 0644]
telephony/java/android/telephony/SmsCbEtwsInfo.java [new file with mode: 0644]
telephony/java/android/telephony/SmsCbLocation.java [new file with mode: 0644]
telephony/java/android/telephony/SmsCbMessage.java
telephony/java/android/telephony/cdma/CdmaSmsCbProgramData.java [new file with mode: 0644]
telephony/java/com/android/internal/telephony/SMSDispatcher.java
telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
telephony/java/com/android/internal/telephony/cdma/SmsMessage.java
telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java
telephony/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java
telephony/java/com/android/internal/telephony/cdma/sms/UserData.java
telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java [new file with mode: 0644]
telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java [moved from telephony/java/android/telephony/SmsCbConstants.java with 84% similarity]
telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java
telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaSmsCbTest.java [new file with mode: 0644]
telephony/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmSmsCbTest.java [moved from telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsCbTest.java with 89% similarity]

index 0e6d07d..9612151 100755 (executable)
@@ -596,8 +596,8 @@ public final class Telephony {
              * values:</p>
              *
              * <ul>
-             *   <li><em>pdus</em> - An Object[] of byte[]s containing the PDUs
-             *   that make up the message.</li>
+             *   <li><em>message</em> - An SmsCbMessage object containing the broadcast message
+             *   data. This is not an emergency alert, so ETWS and CMAS data will be null.</li>
              * </ul>
              *
              * <p>The extra values can be extracted using
@@ -616,8 +616,8 @@ public final class Telephony {
              * values:</p>
              *
              * <ul>
-             *   <li><em>pdus</em> - An Object[] of byte[]s containing the PDUs
-             *   that make up the message.</li>
+             *   <li><em>message</em> - An SmsCbMessage object containing the broadcast message
+             *   data, including ETWS or CMAS warning notification info if present.</li>
              * </ul>
              *
              * <p>The extra values can be extracted using
@@ -631,6 +631,26 @@ public final class Telephony {
                     "android.provider.Telephony.SMS_EMERGENCY_CB_RECEIVED";
 
             /**
+             * Broadcast Action: A new CDMA SMS has been received containing Service Category
+             * Program Data (updates the list of enabled broadcast channels). The intent will
+             * have the following extra values:</p>
+             *
+             * <ul>
+             *   <li><em>operations</em> - An array of CdmaSmsCbProgramData objects containing
+             *   the service category operations (add/delete/clear) to perform.</li>
+             * </ul>
+             *
+             * <p>The extra values can be extracted using
+             * {@link #getMessagesFromIntent(Intent)}.</p>
+             *
+             * <p>If a BroadcastReceiver encounters an error while processing
+             * this intent it should set the result code appropriately.</p>
+             */
+            @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+            public static final String SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION =
+                    "android.provider.Telephony.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED";
+
+            /**
              * Broadcast Action: The SIM storage for SMS messages is full.  If
              * space is not freed, messages targeted for the SIM (class 2) may
              * not be saved.
index 70c0be8..ddecbed 100644 (file)
@@ -77,6 +77,7 @@ public class BitwiseOutputStream {
         byte[] newBuf = new byte[(mPos + bits) >>> 2];
         System.arraycopy(mBuf, 0, newBuf, 0, mEnd >>> 3);
         mBuf = newBuf;
+        mEnd = newBuf.length << 3;
     }
 
     /**
index a304b68..306f58f 100644 (file)
@@ -133,4 +133,25 @@ public class BitwiseStreamsTest extends AndroidTestCase {
         long end = android.os.SystemClock.elapsedRealtime();
         Log.d(LOG_TAG, "repeated encode-decode took " + (end - start) + " ms");
     }
+
+    @SmallTest
+    public void testExpandArray() throws Exception {
+        Random random = new Random();
+        int iterations = 10000;
+        int[] sizeArr = new int[iterations];
+        int[] valueArr = new int[iterations];
+        BitwiseOutputStream outStream = new BitwiseOutputStream(8);
+        for (int i = 0; i < iterations; i++) {
+            int x = random.nextInt();
+            int size = (x & 0x07) + 1;
+            int value = x & (-1 >>> (32 - size));
+            sizeArr[i] = size;
+            valueArr[i] = value;
+            outStream.write(size, value);
+        }
+        BitwiseInputStream inStream = new BitwiseInputStream(outStream.toByteArray());
+        for (int i = 0; i < iterations; i++) {
+            assertEquals(valueArr[i], inStream.read(sizeArr[i]));
+        }
+    }
 }
diff --git a/telephony/java/android/telephony/SmsCbCmasInfo.java b/telephony/java/android/telephony/SmsCbCmasInfo.java
new file mode 100644 (file)
index 0000000..7a89d94
--- /dev/null
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Contains CMAS warning notification Type 1 elements for a {@link SmsCbMessage}.
+ * Supported values for each element are defined in TIA-1149-0-1 (CMAS over CDMA) and
+ * 3GPP TS 23.041 (for GSM/UMTS).
+ *
+ * {@hide}
+ */
+public class SmsCbCmasInfo implements Parcelable {
+
+    // CMAS message class (in GSM/UMTS message identifier or CDMA service category).
+
+    /** Presidential-level alert (Korean Public Alert System Class 0 message). */
+    public static final int CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT = 0x00;
+
+    /** Extreme threat to life and property (Korean Public Alert System Class 1 message). */
+    public static final int CMAS_CLASS_EXTREME_THREAT = 0x01;
+
+    /** Severe threat to life and property (Korean Public Alert System Class 1 message). */
+    public static final int CMAS_CLASS_SEVERE_THREAT = 0x02;
+
+    /** Child abduction emergency (AMBER Alert). */
+    public static final int CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY = 0x03;
+
+    /** CMAS test message. */
+    public static final int CMAS_CLASS_REQUIRED_MONTHLY_TEST = 0x04;
+
+    /** CMAS exercise. */
+    public static final int CMAS_CLASS_CMAS_EXERCISE = 0x05;
+
+    /** CMAS category for operator defined use. */
+    public static final int CMAS_CLASS_OPERATOR_DEFINED_USE = 0x06;
+
+    /** CMAS category for warning types that are reserved for future extension. */
+    public static final int CMAS_CLASS_UNKNOWN = -1;
+
+    // CMAS alert category (in CDMA type 1 elements record).
+
+    /** CMAS alert category: Geophysical including landslide. */
+    public static final int CMAS_CATEGORY_GEO = 0x00;
+
+    /** CMAS alert category: Meteorological including flood. */
+    public static final int CMAS_CATEGORY_MET = 0x01;
+
+    /** CMAS alert category: General emergency and public safety. */
+    public static final int CMAS_CATEGORY_SAFETY = 0x02;
+
+    /** CMAS alert category: Law enforcement, military, homeland/local/private security. */
+    public static final int CMAS_CATEGORY_SECURITY = 0x03;
+
+    /** CMAS alert category: Rescue and recovery. */
+    public static final int CMAS_CATEGORY_RESCUE = 0x04;
+
+    /** CMAS alert category: Fire suppression and rescue. */
+    public static final int CMAS_CATEGORY_FIRE = 0x05;
+
+    /** CMAS alert category: Medical and public health. */
+    public static final int CMAS_CATEGORY_HEALTH = 0x06;
+
+    /** CMAS alert category: Pollution and other environmental. */
+    public static final int CMAS_CATEGORY_ENV = 0x07;
+
+    /** CMAS alert category: Public and private transportation. */
+    public static final int CMAS_CATEGORY_TRANSPORT = 0x08;
+
+    /** CMAS alert category: Utility, telecom, other non-transport infrastructure. */
+    public static final int CMAS_CATEGORY_INFRA = 0x09;
+
+    /** CMAS alert category: Chem, bio, radiological, nuclear, high explosive threat or attack. */
+    public static final int CMAS_CATEGORY_CBRNE = 0x0a;
+
+    /** CMAS alert category: Other events. */
+    public static final int CMAS_CATEGORY_OTHER = 0x0b;
+
+    /**
+     * CMAS alert category is unknown. The category is only available for CDMA broadcasts
+     * containing a type 1 elements record, so GSM and UMTS broadcasts always return unknown.
+     */
+    public static final int CMAS_CATEGORY_UNKNOWN = -1;
+
+    // CMAS response type (in CDMA type 1 elements record).
+
+    /** CMAS response type: Take shelter in place. */
+    public static final int CMAS_RESPONSE_TYPE_SHELTER = 0x00;
+
+    /** CMAS response type: Evacuate (Relocate). */
+    public static final int CMAS_RESPONSE_TYPE_EVACUATE = 0x01;
+
+    /** CMAS response type: Make preparations. */
+    public static final int CMAS_RESPONSE_TYPE_PREPARE = 0x02;
+
+    /** CMAS response type: Execute a pre-planned activity. */
+    public static final int CMAS_RESPONSE_TYPE_EXECUTE = 0x03;
+
+    /** CMAS response type: Attend to information sources. */
+    public static final int CMAS_RESPONSE_TYPE_MONITOR = 0x04;
+
+    /** CMAS response type: Avoid hazard. */
+    public static final int CMAS_RESPONSE_TYPE_AVOID = 0x05;
+
+    /** CMAS response type: Evaluate the information in this message (not for public warnings). */
+    public static final int CMAS_RESPONSE_TYPE_ASSESS = 0x06;
+
+    /** CMAS response type: No action recommended. */
+    public static final int CMAS_RESPONSE_TYPE_NONE = 0x07;
+
+    /**
+     * CMAS response type is unknown. The response type is only available for CDMA broadcasts
+     * containing a type 1 elements record, so GSM and UMTS broadcasts always return unknown.
+     */
+    public static final int CMAS_RESPONSE_TYPE_UNKNOWN = -1;
+
+    // 4-bit CMAS severity (in GSM/UMTS message identifier or CDMA type 1 elements record).
+
+    /** CMAS severity type: Extraordinary threat to life or property. */
+    public static final int CMAS_SEVERITY_EXTREME = 0x0;
+
+    /** CMAS severity type: Significant threat to life or property. */
+    public static final int CMAS_SEVERITY_SEVERE = 0x1;
+
+    /**
+     * CMAS alert severity is unknown. The severity is available for CDMA warning alerts
+     * containing a type 1 elements record and for all GSM and UMTS alerts except for the
+     * Presidential-level alert class (Korean Public Alert System Class 0).
+     */
+    public static final int CMAS_SEVERITY_UNKNOWN = -1;
+
+    // CMAS urgency (in GSM/UMTS message identifier or CDMA type 1 elements record).
+
+    /** CMAS urgency type: Responsive action should be taken immediately. */
+    public static final int CMAS_URGENCY_IMMEDIATE = 0x0;
+
+    /** CMAS urgency type: Responsive action should be taken within the next hour. */
+    public static final int CMAS_URGENCY_EXPECTED = 0x1;
+
+    /**
+     * CMAS alert urgency is unknown. The urgency is available for CDMA warning alerts
+     * containing a type 1 elements record and for all GSM and UMTS alerts except for the
+     * Presidential-level alert class (Korean Public Alert System Class 0).
+     */
+    public static final int CMAS_URGENCY_UNKNOWN = -1;
+
+    // CMAS certainty (in GSM/UMTS message identifier or CDMA type 1 elements record).
+
+    /** CMAS certainty type: Determined to have occurred or to be ongoing. */
+    public static final int CMAS_CERTAINTY_OBSERVED = 0x0;
+
+    /** CMAS certainty type: Likely (probability > ~50%). */
+    public static final int CMAS_CERTAINTY_LIKELY = 0x1;
+
+    /**
+     * CMAS alert certainty is unknown. The certainty is available for CDMA warning alerts
+     * containing a type 1 elements record and for all GSM and UMTS alerts except for the
+     * Presidential-level alert class (Korean Public Alert System Class 0).
+     */
+    public static final int CMAS_CERTAINTY_UNKNOWN = -1;
+
+    /** CMAS message class. */
+    private final int mMessageClass;
+
+    /** CMAS category. */
+    private final int mCategory;
+
+    /** CMAS response type. */
+    private final int mResponseType;
+
+    /** CMAS severity. */
+    private final int mSeverity;
+
+    /** CMAS urgency. */
+    private final int mUrgency;
+
+    /** CMAS certainty. */
+    private final int mCertainty;
+
+    /** Create a new SmsCbCmasInfo object with the specified values. */
+    public SmsCbCmasInfo(int messageClass, int category, int responseType, int severity,
+            int urgency, int certainty) {
+        mMessageClass = messageClass;
+        mCategory = category;
+        mResponseType = responseType;
+        mSeverity = severity;
+        mUrgency = urgency;
+        mCertainty = certainty;
+    }
+
+    /** Create a new SmsCbCmasInfo object from a Parcel. */
+    SmsCbCmasInfo(Parcel in) {
+        mMessageClass = in.readInt();
+        mCategory = in.readInt();
+        mResponseType = in.readInt();
+        mSeverity = in.readInt();
+        mUrgency = in.readInt();
+        mCertainty = in.readInt();
+    }
+
+    /**
+     * Flatten this object into a Parcel.
+     *
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written (ignored).
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mMessageClass);
+        dest.writeInt(mCategory);
+        dest.writeInt(mResponseType);
+        dest.writeInt(mSeverity);
+        dest.writeInt(mUrgency);
+        dest.writeInt(mCertainty);
+    }
+
+    /**
+     * Returns the CMAS message class, e.g. {@link #CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT}.
+     * @return one of the {@code CMAS_CLASS} values
+     */
+    public int getMessageClass() {
+        return mMessageClass;
+    }
+
+    /**
+     * Returns the CMAS category, e.g. {@link #CMAS_CATEGORY_GEO}.
+     * @return one of the {@code CMAS_CATEGORY} values
+     */
+    public int getCategory() {
+        return mCategory;
+    }
+
+    /**
+     * Returns the CMAS response type, e.g. {@link #CMAS_RESPONSE_TYPE_SHELTER}.
+     * @return one of the {@code CMAS_RESPONSE_TYPE} values
+     */
+    public int getResponseType() {
+        return mResponseType;
+    }
+
+    /**
+     * Returns the CMAS severity, e.g. {@link #CMAS_SEVERITY_EXTREME}.
+     * @return one of the {@code CMAS_SEVERITY} values
+     */
+    public int getSeverity() {
+        return mSeverity;
+    }
+
+    /**
+     * Returns the CMAS urgency, e.g. {@link #CMAS_URGENCY_IMMEDIATE}.
+     * @return one of the {@code CMAS_URGENCY} values
+     */
+    public int getUrgency() {
+        return mUrgency;
+    }
+
+    /**
+     * Returns the CMAS certainty, e.g. {@link #CMAS_CERTAINTY_OBSERVED}.
+     * @return one of the {@code CMAS_CERTAINTY} values
+     */
+    public int getCertainty() {
+        return mCertainty;
+    }
+
+    @Override
+    public String toString() {
+        return "SmsCbCmasInfo{messageClass=" + mMessageClass + ", category=" + mCategory
+                + ", responseType=" + mResponseType + ", severity=" + mSeverity
+                + ", urgency=" + mUrgency + ", certainty=" + mCertainty + '}';
+    }
+
+    /**
+     * Describe the kinds of special objects contained in the marshalled representation.
+     * @return a bitmask indicating this Parcelable contains no special objects
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Creator for unparcelling objects. */
+    public static final Parcelable.Creator<SmsCbCmasInfo>
+            CREATOR = new Parcelable.Creator<SmsCbCmasInfo>() {
+        public SmsCbCmasInfo createFromParcel(Parcel in) {
+            return new SmsCbCmasInfo(in);
+        }
+
+        public SmsCbCmasInfo[] newArray(int size) {
+            return new SmsCbCmasInfo[size];
+        }
+    };
+}
diff --git a/telephony/java/android/telephony/SmsCbEtwsInfo.java b/telephony/java/android/telephony/SmsCbEtwsInfo.java
new file mode 100644 (file)
index 0000000..0890d52
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.format.Time;
+
+import com.android.internal.telephony.IccUtils;
+
+import java.util.Arrays;
+
+/**
+ * Contains information elements for a GSM or UMTS ETWS warning notification.
+ * Supported values for each element are defined in 3GPP TS 23.041.
+ *
+ * {@hide}
+ */
+public class SmsCbEtwsInfo implements Parcelable {
+
+    /** ETWS warning type for earthquake. */
+    public static final int ETWS_WARNING_TYPE_EARTHQUAKE = 0x00;
+
+    /** ETWS warning type for tsunami. */
+    public static final int ETWS_WARNING_TYPE_TSUNAMI = 0x01;
+
+    /** ETWS warning type for earthquake and tsunami. */
+    public static final int ETWS_WARNING_TYPE_EARTHQUAKE_AND_TSUNAMI = 0x02;
+
+    /** ETWS warning type for test messages. */
+    public static final int ETWS_WARNING_TYPE_TEST_MESSAGE = 0x03;
+
+    /** ETWS warning type for other emergency types. */
+    public static final int ETWS_WARNING_TYPE_OTHER_EMERGENCY = 0x04;
+
+    /** Unknown ETWS warning type. */
+    public static final int ETWS_WARNING_TYPE_UNKNOWN = -1;
+
+    /** One of the ETWS warning type constants defined in this class. */
+    private final int mWarningType;
+
+    /** Whether or not to activate the emergency user alert tone and vibration. */
+    private final boolean mEmergencyUserAlert;
+
+    /** Whether or not to activate a popup alert. */
+    private final boolean mActivatePopup;
+
+    /**
+     * 50-byte security information (ETWS primary notification for GSM only). As of Release 10,
+     * 3GPP TS 23.041 states that the UE shall ignore the ETWS primary notification timestamp
+     * and digital signature if received. Therefore it is treated as a raw byte array and
+     * parceled with the broadcast intent if present, but the timestamp is only computed if an
+     * application asks for the individual components.
+     */
+    private final byte[] mWarningSecurityInformation;
+
+    /** Create a new SmsCbEtwsInfo object with the specified values. */
+    public SmsCbEtwsInfo(int warningType, boolean emergencyUserAlert, boolean activatePopup,
+            byte[] warningSecurityInformation) {
+        mWarningType = warningType;
+        mEmergencyUserAlert = emergencyUserAlert;
+        mActivatePopup = activatePopup;
+        mWarningSecurityInformation = warningSecurityInformation;
+    }
+
+    /** Create a new SmsCbEtwsInfo object from a Parcel. */
+    SmsCbEtwsInfo(Parcel in) {
+        mWarningType = in.readInt();
+        mEmergencyUserAlert = (in.readInt() != 0);
+        mActivatePopup = (in.readInt() != 0);
+        mWarningSecurityInformation = in.createByteArray();
+    }
+
+    /**
+     * Flatten this object into a Parcel.
+     *
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written (ignored).
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mWarningType);
+        dest.writeInt(mEmergencyUserAlert ? 1 : 0);
+        dest.writeInt(mActivatePopup ? 1 : 0);
+        dest.writeByteArray(mWarningSecurityInformation);
+    }
+
+    /**
+     * Returns the ETWS warning type.
+     * @return a warning type such as {@link #ETWS_WARNING_TYPE_EARTHQUAKE}
+     */
+    public int getWarningType() {
+        return mWarningType;
+    }
+
+    /**
+     * Returns the ETWS emergency user alert flag.
+     * @return true to notify terminal to activate emergency user alert; false otherwise
+     */
+    public boolean isEmergencyUserAlert() {
+        return mEmergencyUserAlert;
+    }
+
+    /**
+     * Returns the ETWS activate popup flag.
+     * @return true to notify terminal to activate display popup; false otherwise
+     */
+    public boolean isPopupAlert() {
+        return mActivatePopup;
+    }
+
+    /**
+     * Returns the Warning-Security-Information timestamp (GSM primary notifications only).
+     * As of Release 10, 3GPP TS 23.041 states that the UE shall ignore this value if received.
+     * @return a UTC timestamp in System.currentTimeMillis() format, or 0 if not present
+     */
+    public long getPrimaryNotificationTimestamp() {
+        if (mWarningSecurityInformation == null || mWarningSecurityInformation.length < 7) {
+            return 0;
+        }
+
+        int year = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[0]);
+        int month = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[1]);
+        int day = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[2]);
+        int hour = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[3]);
+        int minute = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[4]);
+        int second = IccUtils.gsmBcdByteToInt(mWarningSecurityInformation[5]);
+
+        // For the timezone, the most significant bit of the
+        // least significant nibble is the sign byte
+        // (meaning the max range of this field is 79 quarter-hours,
+        // which is more than enough)
+
+        byte tzByte = mWarningSecurityInformation[6];
+
+        // Mask out sign bit.
+        int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08)));
+
+        timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset;
+
+        Time time = new Time(Time.TIMEZONE_UTC);
+
+        // We only need to support years above 2000.
+        time.year = year + 2000;
+        time.month = month - 1;
+        time.monthDay = day;
+        time.hour = hour;
+        time.minute = minute;
+        time.second = second;
+
+        // Timezone offset is in quarter hours.
+        return time.toMillis(true) - (long) (timezoneOffset * 15 * 60 * 1000);
+    }
+
+    /**
+     * Returns the digital signature (GSM primary notifications only). As of Release 10,
+     * 3GPP TS 23.041 states that the UE shall ignore this value if received.
+     * @return a byte array containing a copy of the primary notification digital signature
+     */
+    public byte[] getPrimaryNotificationSignature() {
+        if (mWarningSecurityInformation == null || mWarningSecurityInformation.length < 50) {
+            return null;
+        }
+        return Arrays.copyOfRange(mWarningSecurityInformation, 7, 50);
+    }
+
+    @Override
+    public String toString() {
+        return "SmsCbEtwsInfo{warningType=" + mWarningType + ", emergencyUserAlert="
+                + mEmergencyUserAlert + ", activatePopup=" + mActivatePopup + '}';
+    }
+
+    /**
+     * Describe the kinds of special objects contained in the marshalled representation.
+     * @return a bitmask indicating this Parcelable contains no special objects
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Creator for unparcelling objects. */
+    public static final Creator<SmsCbEtwsInfo> CREATOR = new Creator<SmsCbEtwsInfo>() {
+        public SmsCbEtwsInfo createFromParcel(Parcel in) {
+            return new SmsCbEtwsInfo(in);
+        }
+
+        public SmsCbEtwsInfo[] newArray(int size) {
+            return new SmsCbEtwsInfo[size];
+        }
+    };
+}
diff --git a/telephony/java/android/telephony/SmsCbLocation.java b/telephony/java/android/telephony/SmsCbLocation.java
new file mode 100644 (file)
index 0000000..7b5bd0d
--- /dev/null
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.gsm.GsmCellLocation;
+
+/**
+ * Represents the location and geographical scope of a cell broadcast message.
+ * For GSM/UMTS, the Location Area and Cell ID are set when the broadcast
+ * geographical scope is cell wide or Location Area wide. For CDMA, the
+ * broadcast geographical scope is always PLMN wide.
+ *
+ * @hide
+ */
+public class SmsCbLocation implements Parcelable {
+
+    /** The PLMN. Note that this field may be an empty string, but isn't allowed to be null. */
+    private final String mPlmn;
+
+    private final int mLac;
+    private final int mCid;
+
+    /**
+     * Construct an empty location object. This is used for some test cases, and for
+     * cell broadcasts saved in older versions of the database without location info.
+     */
+    public SmsCbLocation() {
+        mPlmn = "";
+        mLac = -1;
+        mCid = -1;
+    }
+
+    /**
+     * Construct a location object for the PLMN. This class is immutable, so
+     * the same object can be reused for multiple broadcasts.
+     */
+    public SmsCbLocation(String plmn) {
+        mPlmn = plmn;
+        mLac = -1;
+        mCid = -1;
+    }
+
+    /**
+     * Construct a location object for the PLMN, LAC, and Cell ID. This class is immutable, so
+     * the same object can be reused for multiple broadcasts.
+     */
+    public SmsCbLocation(String plmn, int lac, int cid) {
+        mPlmn = plmn;
+        mLac = lac;
+        mCid = cid;
+    }
+
+    /**
+     * Initialize the object from a Parcel.
+     */
+    public SmsCbLocation(Parcel in) {
+        mPlmn = in.readString();
+        mLac = in.readInt();
+        mCid = in.readInt();
+    }
+
+    /**
+     * Returns the MCC/MNC of the network as a String.
+     * @return the PLMN identifier (MCC+MNC) as a String
+     */
+    public String getPlmn() {
+        return mPlmn;
+    }
+
+    /**
+     * Returns the GSM location area code, or UMTS service area code.
+     * @return location area code, -1 if unknown, 0xffff max legal value
+     */
+    public int getLac() {
+        return mLac;
+    }
+
+    /**
+     * Returns the GSM or UMTS cell ID.
+     * @return gsm cell id, -1 if unknown, 0xffff max legal value
+     */
+    public int getCid() {
+        return mCid;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = mPlmn.hashCode();
+        hash = hash * 31 + mLac;
+        hash = hash * 31 + mCid;
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (o == null || !(o instanceof SmsCbLocation)) {
+            return false;
+        }
+        SmsCbLocation other = (SmsCbLocation) o;
+        return mPlmn.equals(other.mPlmn) && mLac == other.mLac && mCid == other.mCid;
+    }
+
+    @Override
+    public String toString() {
+        return '[' + mPlmn + ',' + mLac + ',' + mCid + ']';
+    }
+
+    /**
+     * Test whether this location is within the location area of the specified object.
+     *
+     * @param area the location area to compare with this location
+     * @return true if this location is contained within the specified location area
+     */
+    public boolean isInLocationArea(SmsCbLocation area) {
+        if (mCid != -1 && mCid != area.mCid) {
+            return false;
+        }
+        if (mLac != -1 && mLac != area.mLac) {
+            return false;
+        }
+        return mPlmn.equals(area.mPlmn);
+    }
+
+    /**
+     * Test whether this location is within the location area of the CellLocation.
+     *
+     * @param plmn the PLMN to use for comparison
+     * @param lac the Location Area (GSM) or Service Area (UMTS) to compare with
+     * @param cid the Cell ID to compare with
+     * @return true if this location is contained within the specified PLMN, LAC, and Cell ID
+     */
+    public boolean isInLocationArea(String plmn, int lac, int cid) {
+        if (!mPlmn.equals(plmn)) {
+            return false;
+        }
+
+        if (mLac != -1 && mLac != lac) {
+            return false;
+        }
+
+        if (mCid != -1 && mCid != cid) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Flatten this object into a Parcel.
+     *
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written (ignored).
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mPlmn);
+        dest.writeInt(mLac);
+        dest.writeInt(mCid);
+    }
+
+    public static final Parcelable.Creator<SmsCbLocation> CREATOR
+            = new Parcelable.Creator<SmsCbLocation>() {
+        @Override
+        public SmsCbLocation createFromParcel(Parcel in) {
+            return new SmsCbLocation(in);
+        }
+
+        @Override
+        public SmsCbLocation[] newArray(int size) {
+            return new SmsCbLocation[size];
+        }
+    };
+
+    /**
+     * Describe the kinds of special objects contained in the marshalled representation.
+     * @return a bitmask indicating this Parcelable contains no special objects
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+}
index 383e0f9..046bf8c 100644 (file)
 
 package android.telephony;
 
-import android.text.format.Time;
-import android.util.Log;
-
-import com.android.internal.telephony.GsmAlphabet;
-import com.android.internal.telephony.IccUtils;
-import com.android.internal.telephony.gsm.SmsCbHeader;
-
-import java.io.UnsupportedEncodingException;
+import android.os.Parcel;
+import android.os.Parcelable;
 
 /**
- * Describes an SMS-CB message.
+ * Parcelable object containing a received cell broadcast message. There are four different types
+ * of Cell Broadcast messages:
+ *
+ * <ul>
+ * <li>opt-in informational broadcasts, e.g. news, weather, stock quotes, sports scores</li>
+ * <li>cell information messages, broadcast on channel 50, indicating the current cell name for
+ *  roaming purposes (required to display on the idle screen in Brazil)</li>
+ * <li>emergency broadcasts for the Japanese Earthquake and Tsunami Warning System (ETWS)</li>
+ * <li>emergency broadcasts for the American Commercial Mobile Alert Service (CMAS)</li>
+ * </ul>
+ *
+ * <p>There are also four different CB message formats: GSM, ETWS Primary Notification (GSM only),
+ * UMTS, and CDMA. Some fields are only applicable for some message formats. Other fields were
+ * unified under a common name, avoiding some names, such as "Message Identifier", that refer to
+ * two completely different concepts in 3GPP and CDMA.
+ *
+ * <p>The GSM/UMTS Message Identifier field is available via {@link #getServiceCategory}, the name
+ * of the equivalent field in CDMA. In both cases the service category is a 16-bit value, but 3GPP
+ * and 3GPP2 have completely different meanings for the respective values. For ETWS and CMAS, the
+ * application should
+ *
+ * <p>The CDMA Message Identifier field is available via {@link #getSerialNumber}, which is used
+ * to detect the receipt of a duplicate message to be discarded. In CDMA, the message ID is
+ * unique to the current PLMN. In GSM/UMTS, there is a 16-bit serial number containing a 2-bit
+ * Geographical Scope field which indicates whether the 10-bit message code and 4-bit update number
+ * are considered unique to the PLMN, to the current cell, or to the current Location Area (or
+ * Service Area in UMTS). The relevant values are concatenated into a single String which will be
+ * unique if the messages are not duplicates.
+ *
+ * <p>The SMS dispatcher does not detect duplicate messages. However, it does concatenate the
+ * pages of a GSM multi-page cell broadcast into a single SmsCbMessage object.
+ *
+ * <p>Interested applications with {@code RECEIVE_SMS_PERMISSION} can register to receive
+ * {@code SMS_CB_RECEIVED_ACTION} broadcast intents for incoming non-emergency broadcasts.
+ * Only system applications such as the CellBroadcastReceiver may receive notifications for
+ * emergency broadcasts (ETWS and CMAS). This is intended to prevent any potential for delays or
+ * interference with the immediate display of the alert message and playing of the alert sound and
+ * vibration pattern, which could be caused by poorly written or malicious non-system code.
  *
- * {@hide}
+ * @hide
  */
-public class SmsCbMessage {
+public class SmsCbMessage implements Parcelable {
 
-    /**
-     * Cell wide immediate geographical scope
-     */
+    protected static final String LOG_TAG = "SMSCB";
+
+    /** Cell wide geographical scope with immediate display (GSM/UMTS only). */
     public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE = 0;
 
-    /**
-     * PLMN wide geographical scope
-     */
+    /** PLMN wide geographical scope (GSM/UMTS and all CDMA broadcasts). */
     public static final int GEOGRAPHICAL_SCOPE_PLMN_WIDE = 1;
 
-    /**
-     * Location / service area wide geographical scope
-     */
+    /** Location / service area wide geographical scope (GSM/UMTS only). */
     public static final int GEOGRAPHICAL_SCOPE_LA_WIDE = 2;
 
-    /**
-     * Cell wide geographical scope
-     */
+    /** Cell wide geographical scope (GSM/UMTS only). */
     public static final int GEOGRAPHICAL_SCOPE_CELL_WIDE = 3;
 
+    /** GSM or UMTS format cell broadcast. */
+    public static final int MESSAGE_FORMAT_3GPP = 1;
+
+    /** CDMA format cell broadcast. */
+    public static final int MESSAGE_FORMAT_3GPP2 = 2;
+
+    /** Normal message priority. */
+    public static final int MESSAGE_PRIORITY_NORMAL = 0;
+
+    /** Interactive message priority. */
+    public static final int MESSAGE_PRIORITY_INTERACTIVE = 1;
+
+    /** Urgent message priority. */
+    public static final int MESSAGE_PRIORITY_URGENT = 2;
+
+    /** Emergency message priority. */
+    public static final int MESSAGE_PRIORITY_EMERGENCY = 3;
+
+    /** Format of this message (for interpretation of service category values). */
+    private final int mMessageFormat;
+
+    /** Geographical scope of broadcast. */
+    private final int mGeographicalScope;
+
     /**
-     * Create an instance of this class from a received PDU
-     *
-     * @param pdu PDU bytes
-     * @return An instance of this class, or null if invalid pdu
+     * Serial number of broadcast (message identifier for CDMA, geographical scope + message code +
+     * update number for GSM/UMTS). The serial number plus the location code uniquely identify
+     * a cell broadcast for duplicate detection.
      */
-    public static SmsCbMessage createFromPdu(byte[] pdu) {
-        try {
-            return new SmsCbMessage(pdu);
-        } catch (IllegalArgumentException e) {
-            Log.w(LOG_TAG, "Failed parsing SMS-CB pdu", e);
-            return null;
-        }
-    }
-
-    private static final String LOG_TAG = "SMSCB";
+    private final int mSerialNumber;
 
     /**
-     * Languages in the 0000xxxx DCS group as defined in 3GPP TS 23.038, section 5.
+     * Location identifier for this message. It consists of the current operator MCC/MNC as a
+     * 5 or 6-digit decimal string. In addition, for GSM/UMTS, if the Geographical Scope of the
+     * message is not binary 01, the Location Area is included for comparison. If the GS is
+     * 00 or 11, the Cell ID is also included. LAC and Cell ID are -1 if not specified.
      */
-    private static final String[] LANGUAGE_CODES_GROUP_0 = {
-            "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu",
-            "pl", null
-    };
+    private final SmsCbLocation mLocation;
 
     /**
-     * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5.
+     * 16-bit CDMA service category or GSM/UMTS message identifier. For ETWS and CMAS warnings,
+     * the information provided by the category is also available via {@link #getEtwsWarningInfo()}
+     * or {@link #getCmasWarningInfo()}.
      */
-    private static final String[] LANGUAGE_CODES_GROUP_2 = {
-            "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null,
-            null, null
-    };
+    private final int mServiceCategory;
+
+    /** Message language, as a two-character string, e.g. "en". */
+    private final String mLanguage;
 
-    private static final char CARRIAGE_RETURN = 0x0d;
+    /** Message body, as a String. */
+    private final String mBody;
 
-    private static final int PDU_BODY_PAGE_LENGTH = 82;
+    /** Message priority (including emergency priority). */
+    private final int mPriority;
 
-    private SmsCbHeader mHeader;
+    /** ETWS warning notification information (ETWS warnings only). */
+    private final SmsCbEtwsInfo mEtwsWarningInfo;
 
-    private String mLanguage;
+    /** CMAS warning notification information (CMAS warnings only). */
+    private final SmsCbCmasInfo mCmasWarningInfo;
 
-    private String mBody;
+    /**
+     * 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) {
+        mMessageFormat = messageFormat;
+        mGeographicalScope = geographicalScope;
+        mSerialNumber = serialNumber;
+        mLocation = location;
+        mServiceCategory = serviceCategory;
+        mLanguage = language;
+        mBody = body;
+        mPriority = priority;
+        mEtwsWarningInfo = etwsWarningInfo;
+        mCmasWarningInfo = cmasWarningInfo;
+    }
 
-    /** Timestamp of ETWS primary notification with security. */
-    private long mPrimaryNotificationTimestamp;
+    /** Create a new SmsCbMessage object from a Parcel. */
+    public SmsCbMessage(Parcel in) {
+        mMessageFormat = in.readInt();
+        mGeographicalScope = in.readInt();
+        mSerialNumber = in.readInt();
+        mLocation = new SmsCbLocation(in);
+        mServiceCategory = in.readInt();
+        mLanguage = in.readString();
+        mBody = in.readString();
+        mPriority = in.readInt();
+        int type = in.readInt();
+        switch (type) {
+            case 'E':
+                // unparcel ETWS warning information
+                mEtwsWarningInfo = new SmsCbEtwsInfo(in);
+                mCmasWarningInfo = null;
+                break;
 
-    /** 43 byte digital signature of ETWS primary notification with security. */
-    private byte[] mPrimaryNotificationDigitalSignature;
+            case 'C':
+                // unparcel CMAS warning information
+                mEtwsWarningInfo = null;
+                mCmasWarningInfo = new SmsCbCmasInfo(in);
+                break;
 
-    private SmsCbMessage(byte[] pdu) throws IllegalArgumentException {
-        mHeader = new SmsCbHeader(pdu);
-        if (mHeader.format == SmsCbHeader.FORMAT_ETWS_PRIMARY) {
-            mBody = "ETWS";
-            // ETWS primary notification with security is 56 octets in length
-            if (pdu.length >= SmsCbHeader.PDU_LENGTH_ETWS) {
-                mPrimaryNotificationTimestamp = getTimestampMillis(pdu);
-                mPrimaryNotificationDigitalSignature = new byte[43];
-                // digital signature starts after 6 byte header and 7 byte timestamp
-                System.arraycopy(pdu, 13, mPrimaryNotificationDigitalSignature, 0, 43);
-            }
-        } else {
-            parseBody(pdu);
+            default:
+                mEtwsWarningInfo = null;
+                mCmasWarningInfo = null;
         }
     }
 
     /**
-     * Return the geographical scope of this message, one of
-     * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE},
-     * {@link #GEOGRAPHICAL_SCOPE_PLMN_WIDE},
-     * {@link #GEOGRAPHICAL_SCOPE_LA_WIDE},
-     * {@link #GEOGRAPHICAL_SCOPE_CELL_WIDE}
+     * Flatten this object into a Parcel.
      *
-     * @return Geographical scope
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written (ignored).
      */
-    public int getGeographicalScope() {
-        return mHeader.geographicalScope;
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mMessageFormat);
+        dest.writeInt(mGeographicalScope);
+        dest.writeInt(mSerialNumber);
+        mLocation.writeToParcel(dest, flags);
+        dest.writeInt(mServiceCategory);
+        dest.writeString(mLanguage);
+        dest.writeString(mBody);
+        dest.writeInt(mPriority);
+        if (mEtwsWarningInfo != null) {
+            // parcel ETWS warning information
+            dest.writeInt('E');
+            mEtwsWarningInfo.writeToParcel(dest, flags);
+        } else if (mCmasWarningInfo != null) {
+            // parcel CMAS warning information
+            dest.writeInt('C');
+            mCmasWarningInfo.writeToParcel(dest, flags);
+        } else {
+            // no ETWS or CMAS warning information
+            dest.writeInt('0');
+        }
     }
 
+    public static final Parcelable.Creator<SmsCbMessage> CREATOR
+            = new Parcelable.Creator<SmsCbMessage>() {
+        @Override
+        public SmsCbMessage createFromParcel(Parcel in) {
+            return new SmsCbMessage(in);
+        }
+
+        @Override
+        public SmsCbMessage[] newArray(int size) {
+            return new SmsCbMessage[size];
+        }
+    };
+
     /**
-     * Get the ISO-639-1 language code for this message, or null if unspecified
+     * Return the geographical scope of this message (GSM/UMTS only).
      *
-     * @return Language code
+     * @return Geographical scope
      */
-    public String getLanguageCode() {
-        return mLanguage;
+    public int getGeographicalScope() {
+        return mGeographicalScope;
     }
 
     /**
-     * Get the body of this message, or null if no body available
+     * Return the broadcast serial number of broadcast (message identifier for CDMA, or
+     * geographical scope + message code + update number for GSM/UMTS). The serial number plus
+     * the location code uniquely identify a cell broadcast for duplicate detection.
      *
-     * @return Body, or null
+     * @return the 16-bit CDMA message identifier or GSM/UMTS serial number
      */
-    public String getMessageBody() {
-        return mBody;
+    public int getSerialNumber() {
+        return mSerialNumber;
     }
 
     /**
-     * Get the message identifier of this message (0-65535)
+     * Return the location identifier for this message, consisting of the MCC/MNC as a
+     * 5 or 6-digit decimal string. In addition, for GSM/UMTS, if the Geographical Scope of the
+     * message is not binary 01, the Location Area is included. If the GS is 00 or 11, the
+     * cell ID is also included. The {@link SmsCbLocation} object includes a method to test
+     * if the location is included within another location area or within a PLMN and CellLocation.
      *
-     * @return Message identifier
+     * @return the geographical location code for duplicate message detection
      */
-    public int getMessageIdentifier() {
-        return mHeader.messageIdentifier;
+    public SmsCbLocation getLocation() {
+        return mLocation;
     }
 
     /**
-     * Get the message code of this message (0-1023)
+     * Return the 16-bit CDMA service category or GSM/UMTS message identifier. The interpretation
+     * of the category is radio technology specific. For ETWS and CMAS warnings, the information
+     * provided by the category is available via {@link #getEtwsWarningInfo()} or
+     * {@link #getCmasWarningInfo()} in a radio technology independent format.
      *
-     * @return Message code
+     * @return the radio technology specific service category
      */
-    public int getMessageCode() {
-        return mHeader.messageCode;
+    public int getServiceCategory() {
+        return mServiceCategory;
     }
 
     /**
-     * Get the update number of this message (0-15)
+     * Get the ISO-639-1 language code for this message, or null if unspecified
      *
-     * @return Update number
+     * @return Language code
      */
-    public int getUpdateNumber() {
-        return mHeader.updateNumber;
+    public String getLanguageCode() {
+        return mLanguage;
     }
 
     /**
-     * Get the format of this message.
-     * @return {@link SmsCbHeader#FORMAT_GSM}, {@link SmsCbHeader#FORMAT_UMTS}, or
-     *         {@link SmsCbHeader#FORMAT_ETWS_PRIMARY}
+     * Get the body of this message, or null if no body available
+     *
+     * @return Body, or null
      */
-    public int getMessageFormat() {
-        return mHeader.format;
+    public String getMessageBody() {
+        return mBody;
     }
 
     /**
-     * For ETWS primary notifications, return the emergency user alert flag.
-     * @return true to notify terminal to activate emergency user alert; false otherwise
+     * Get the message format ({@link #MESSAGE_FORMAT_3GPP} or {@link #MESSAGE_FORMAT_3GPP2}).
+     * @return an integer representing 3GPP or 3GPP2 message format
      */
-    public boolean getEtwsEmergencyUserAlert() {
-        return mHeader.etwsEmergencyUserAlert;
+    public int getMessageFormat() {
+        return mMessageFormat;
     }
 
     /**
-     * For ETWS primary notifications, return the popup flag.
-     * @return true to notify terminal to activate display popup; false otherwise
+     * Get the message priority. Normal broadcasts return {@link #MESSAGE_PRIORITY_NORMAL}
+     * and emergency broadcasts return {@link #MESSAGE_PRIORITY_EMERGENCY}. CDMA also may return
+     * {@link #MESSAGE_PRIORITY_INTERACTIVE} or {@link #MESSAGE_PRIORITY_URGENT}.
+     * @return an integer representing the message priority
      */
-    public boolean getEtwsPopup() {
-        return mHeader.etwsPopup;
+    public int getMessagePriority() {
+        return mPriority;
     }
 
     /**
-     * For ETWS primary notifications, return the warning type.
-     * @return a value such as {@link SmsCbConstants#ETWS_WARNING_TYPE_EARTHQUAKE}
+     * If this is an ETWS warning notification then this method will return an object containing
+     * the ETWS warning type, the emergency user alert flag, and the popup flag. If this is an
+     * ETWS primary notification (GSM only), there will also be a 7-byte timestamp and 43-byte
+     * digital signature. As of Release 10, 3GPP TS 23.041 states that the UE shall ignore the
+     * ETWS primary notification timestamp and digital signature if received.
+     *
+     * @return an SmsCbEtwsInfo object, or null if this is not an ETWS warning notification
      */
-    public int getEtwsWarningType() {
-        return mHeader.etwsWarningType;
+    public SmsCbEtwsInfo getEtwsWarningInfo() {
+        return mEtwsWarningInfo;
     }
 
     /**
-     * For ETWS primary notifications, return the Warning-Security-Information timestamp.
-     * @return a timestamp in System.currentTimeMillis() format.
+     * If this is a CMAS warning notification then this method will return an object containing
+     * the CMAS message class, category, response type, severity, urgency and certainty.
+     * The message class is always present. Severity, urgency and certainty are present for CDMA
+     * warning notifications containing a type 1 elements record and for GSM and UMTS warnings
+     * except for the Presidential-level alert category. Category and response type are only
+     * available for CDMA notifications containing a type 1 elements record.
+     *
+     * @return an SmsCbCmasInfo object, or null if this is not a CMAS warning notification
      */
-    public long getEtwsSecurityTimestamp() {
-        return mPrimaryNotificationTimestamp;
+    public SmsCbCmasInfo getCmasWarningInfo() {
+        return mCmasWarningInfo;
     }
 
     /**
-     * For ETWS primary notifications, return the 43 byte digital signature.
-     * @return a byte array containing a copy of the digital signature
+     * Return whether this message is an emergency (PWS) message type.
+     * @return true if the message is a public warning notification; false otherwise
      */
-    public byte[] getEtwsSecuritySignature() {
-        return mPrimaryNotificationDigitalSignature.clone();
+    public boolean isEmergencyMessage() {
+        return mPriority == MESSAGE_PRIORITY_EMERGENCY;
     }
 
     /**
-     * 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
-     *
-     * @param pdu The pdu
+     * Return whether this message is an ETWS warning alert.
+     * @return true if the message is an ETWS warning notification; false otherwise
      */
-    private void parseBody(byte[] pdu) {
-        int encoding;
-        boolean hasLanguageIndicator = false;
-
-        // Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
-        // section 5.
-        switch ((mHeader.dataCodingScheme & 0xf0) >> 4) {
-            case 0x00:
-                encoding = SmsMessage.ENCODING_7BIT;
-                mLanguage = LANGUAGE_CODES_GROUP_0[mHeader.dataCodingScheme & 0x0f];
-                break;
-
-            case 0x01:
-                hasLanguageIndicator = true;
-                if ((mHeader.dataCodingScheme & 0x0f) == 0x01) {
-                    encoding = SmsMessage.ENCODING_16BIT;
-                } else {
-                    encoding = SmsMessage.ENCODING_7BIT;
-                }
-                break;
-
-            case 0x02:
-                encoding = SmsMessage.ENCODING_7BIT;
-                mLanguage = LANGUAGE_CODES_GROUP_2[mHeader.dataCodingScheme & 0x0f];
-                break;
-
-            case 0x03:
-                encoding = SmsMessage.ENCODING_7BIT;
-                break;
-
-            case 0x04:
-            case 0x05:
-                switch ((mHeader.dataCodingScheme & 0x0c) >> 2) {
-                    case 0x01:
-                        encoding = SmsMessage.ENCODING_8BIT;
-                        break;
-
-                    case 0x02:
-                        encoding = SmsMessage.ENCODING_16BIT;
-                        break;
-
-                    case 0x00:
-                    default:
-                        encoding = SmsMessage.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
-                encoding = SmsMessage.ENCODING_UNKNOWN;
-                break;
-
-            case 0x0f:
-                if (((mHeader.dataCodingScheme & 0x04) >> 2) == 0x01) {
-                    encoding = SmsMessage.ENCODING_8BIT;
-                } else {
-                    encoding = SmsMessage.ENCODING_7BIT;
-                }
-                break;
-
-            default:
-                // Reserved values are to be treated as 7-bit
-                encoding = SmsMessage.ENCODING_7BIT;
-                break;
-        }
-
-        if (mHeader.format == SmsCbHeader.FORMAT_UMTS) {
-            // Payload may contain multiple pages
-            int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
-
-            if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
-                    * nrPages) {
-                throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
-                        + nrPages + " pages");
-            }
-
-            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);
-                }
-
-                sb.append(unpackBody(pdu, encoding, offset, length, hasLanguageIndicator));
-            }
-            mBody = sb.toString();
-        } else {
-            // Payload is one single page
-            int offset = SmsCbHeader.PDU_HEADER_LENGTH;
-            int length = pdu.length - offset;
-
-            mBody = unpackBody(pdu, encoding, offset, length, hasLanguageIndicator);
-        }
+    public boolean isEtwsMessage() {
+        return mEtwsWarningInfo != null;
     }
 
     /**
-     * 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
-     * @return Body text
+     * Return whether this message is a CMAS warning alert.
+     * @return true if the message is a CMAS warning notification; false otherwise
      */
-    private String unpackBody(byte[] pdu, int encoding, int offset, int length,
-            boolean hasLanguageIndicator) {
-        String body = null;
-
-        switch (encoding) {
-            case SmsMessage.ENCODING_7BIT:
-                body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
-
-                if (hasLanguageIndicator && body != null && body.length() > 2) {
-                    // Language is two GSM characters followed by a CR.
-                    // The actual body text is offset by 3 characters.
-                    mLanguage = body.substring(0, 2);
-                    body = body.substring(3);
-                }
-                break;
-
-            case SmsMessage.ENCODING_16BIT:
-                if (hasLanguageIndicator && pdu.length >= offset + 2) {
-                    // Language is two GSM characters.
-                    // The actual body text is offset by 2 bytes.
-                    mLanguage = GsmAlphabet.gsm7BitPackedToString(pdu, offset, 2);
-                    offset += 2;
-                    length -= 2;
-                }
-
-                try {
-                    body = new String(pdu, offset, (length & 0xfffe), "utf-16");
-                } catch (UnsupportedEncodingException e) {
-                    // Eeeek
-                }
-                break;
-
-            default:
-                break;
-        }
-
-        if (body != null) {
-            // Remove trailing carriage return
-            for (int i = body.length() - 1; i >= 0; i--) {
-                if (body.charAt(i) != CARRIAGE_RETURN) {
-                    body = body.substring(0, i + 1);
-                    break;
-                }
-            }
-        } else {
-            body = "";
-        }
-
-        return body;
+    public boolean isCmasMessage() {
+        return mCmasWarningInfo != null;
     }
 
-    /**
-     * Parses an ETWS primary notification timestamp and returns a currentTimeMillis()-style
-     * timestamp. Copied from com.android.internal.telephony.gsm.SmsMessage.
-     * @param pdu the ETWS primary notification PDU to decode
-     * @return the UTC timestamp from the Warning-Security-Information parameter
-     */
-    private long getTimestampMillis(byte[] pdu) {
-        // Timestamp starts after CB header, in pdu[6]
-        int year = IccUtils.gsmBcdByteToInt(pdu[6]);
-        int month = IccUtils.gsmBcdByteToInt(pdu[7]);
-        int day = IccUtils.gsmBcdByteToInt(pdu[8]);
-        int hour = IccUtils.gsmBcdByteToInt(pdu[9]);
-        int minute = IccUtils.gsmBcdByteToInt(pdu[10]);
-        int second = IccUtils.gsmBcdByteToInt(pdu[11]);
-
-        // For the timezone, the most significant bit of the
-        // least significant nibble is the sign byte
-        // (meaning the max range of this field is 79 quarter-hours,
-        // which is more than enough)
-
-        byte tzByte = pdu[12];
-
-        // Mask out sign bit.
-        int timezoneOffset = IccUtils.gsmBcdByteToInt((byte) (tzByte & (~0x08)));
-
-        timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset : -timezoneOffset;
-
-        Time time = new Time(Time.TIMEZONE_UTC);
-
-        // It's 2006.  Should I really support years < 2000?
-        time.year = year >= 90 ? year + 1900 : year + 2000;
-        time.month = month - 1;
-        time.monthDay = day;
-        time.hour = hour;
-        time.minute = minute;
-        time.second = second;
-
-        // Timezone offset is in quarter hours.
-        return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000);
+    @Override
+    public String toString() {
+        return "SmsCbMessage{geographicalScope=" + mGeographicalScope + ", serialNumber="
+                + mSerialNumber + ", location=" + mLocation + ", serviceCategory="
+                + mServiceCategory + ", language=" + mLanguage + ", body=" + mBody
+                + ", priority=" + mPriority
+                + (mEtwsWarningInfo != null ? (", " + mEtwsWarningInfo.toString()) : "")
+                + (mCmasWarningInfo != null ? (", " + mCmasWarningInfo.toString()) : "") + '}';
     }
 
     /**
-     * Append text to the message body. This is used to concatenate multi-page GSM broadcasts.
-     * @param body the text to append to this message
+     * Describe the kinds of special objects contained in the marshalled representation.
+     * @return a bitmask indicating this Parcelable contains no special objects
      */
-    public void appendToBody(String body) {
-        mBody = mBody + body;
-    }
-
     @Override
-    public String toString() {
-        return "SmsCbMessage{" + mHeader.toString() + ", language=" + mLanguage +
-                ", body=\"" + mBody + "\"}";
+    public int describeContents() {
+        return 0;
     }
 }
diff --git a/telephony/java/android/telephony/cdma/CdmaSmsCbProgramData.java b/telephony/java/android/telephony/cdma/CdmaSmsCbProgramData.java
new file mode 100644 (file)
index 0000000..f94efd8
--- /dev/null
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telephony.cdma;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * CDMA Service Category Program Data from SCPT teleservice SMS.
+ * The CellBroadcastReceiver app receives an Intent with action
+ * {@link android.provider.Telephony.Sms.Intents#SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION}
+ * containing an array of these objects to update its list of cell broadcast service categories
+ * to display.
+ *
+ * {@hide}
+ */
+public class CdmaSmsCbProgramData implements Parcelable {
+
+    /** Delete the specified service category from the list of enabled categories. */
+    public static final int OPERATION_DELETE_CATEGORY   = 0;
+
+    /** Add the specified service category to the list of enabled categories. */
+    public static final int OPERATION_ADD_CATEGORY      = 1;
+
+    /** Clear all service categories from the list of enabled categories. */
+    public static final int OPERATION_CLEAR_CATEGORIES  = 2;
+
+    /** Alert option: no alert. */
+    public static final int ALERT_OPTION_NO_ALERT               = 0;
+
+    /** Alert option: default alert. */
+    public static final int ALERT_OPTION_DEFAULT_ALERT          = 1;
+
+    /** Alert option: vibrate alert once. */
+    public static final int ALERT_OPTION_VIBRATE_ONCE           = 2;
+
+    /** Alert option: vibrate alert - repeat. */
+    public static final int ALERT_OPTION_VIBRATE_REPEAT         = 3;
+
+    /** Alert option: visual alert once. */
+    public static final int ALERT_OPTION_VISUAL_ONCE            = 4;
+
+    /** Alert option: visual alert - repeat. */
+    public static final int ALERT_OPTION_VISUAL_REPEAT          = 5;
+
+    /** Alert option: low-priority alert once. */
+    public static final int ALERT_OPTION_LOW_PRIORITY_ONCE      = 6;
+
+    /** Alert option: low-priority alert - repeat. */
+    public static final int ALERT_OPTION_LOW_PRIORITY_REPEAT    = 7;
+
+    /** Alert option: medium-priority alert once. */
+    public static final int ALERT_OPTION_MED_PRIORITY_ONCE      = 8;
+
+    /** Alert option: medium-priority alert - repeat. */
+    public static final int ALERT_OPTION_MED_PRIORITY_REPEAT    = 9;
+
+    /** Alert option: high-priority alert once. */
+    public static final int ALERT_OPTION_HIGH_PRIORITY_ONCE     = 10;
+
+    /** Alert option: high-priority alert - repeat. */
+    public static final int ALERT_OPTION_HIGH_PRIORITY_REPEAT   = 11;
+
+    /** Service category operation (add/delete/clear). */
+    private final int mOperation;
+
+    /** Service category to modify. */
+    private final int mCategory;
+
+    /** Language used for service category name (ISO 639 two character code). */
+    private final String mLanguage;
+
+    /** Maximum number of messages to store for this service category. */
+    private final int mMaxMessages;
+
+    /** Service category alert option. */
+    private final int mAlertOption;
+
+    /** Name of service category. */
+    private final String mCategoryName;
+
+    /** Create a new CdmaSmsCbProgramData object with the specified values. */
+    public CdmaSmsCbProgramData(int operation, int category, String language, int maxMessages,
+            int alertOption, String categoryName) {
+        mOperation = operation;
+        mCategory = category;
+        mLanguage = language;
+        mMaxMessages = maxMessages;
+        mAlertOption = alertOption;
+        mCategoryName = categoryName;
+    }
+
+    /** Create a new CdmaSmsCbProgramData object from a Parcel. */
+    CdmaSmsCbProgramData(Parcel in) {
+        mOperation = in.readInt();
+        mCategory = in.readInt();
+        mLanguage = in.readString();
+        mMaxMessages = in.readInt();
+        mAlertOption = in.readInt();
+        mCategoryName = in.readString();
+    }
+
+    /**
+     * Flatten this object into a Parcel.
+     *
+     * @param dest  The Parcel in which the object should be written.
+     * @param flags Additional flags about how the object should be written (ignored).
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mOperation);
+        dest.writeInt(mCategory);
+        dest.writeString(mLanguage);
+        dest.writeInt(mMaxMessages);
+        dest.writeInt(mAlertOption);
+        dest.writeString(mCategoryName);
+    }
+
+    /**
+     * Returns the service category operation, e.g. {@link #OPERATION_ADD_CATEGORY}.
+     * @return one of the {@code OPERATION_*} values
+     */
+    public int getOperation() {
+        return mOperation;
+    }
+
+    /**
+     * Returns the CDMA service category to modify.
+     * @return a 16-bit CDMA service category value
+     */
+    public int getCategory() {
+        return mCategory;
+    }
+
+    /**
+     * Returns the ISO-639-1 language code for the service category name, or null if not present.
+     * @return a two-digit ISO-639-1 language code, e.g. "en" for English
+     */
+    public String getLanguageCode() {
+        return mLanguage;
+    }
+
+    /**
+     * Returns the maximum number of messages to store for this service category.
+     * @return the maximum number of messages to store for this service category
+     */
+    public int getMaxMessages() {
+        return mMaxMessages;
+    }
+
+    /**
+     * Returns the service category alert option, e.g. {@link #ALERT_OPTION_DEFAULT_ALERT}.
+     * @return one of the {@code ALERT_OPTION_*} values
+     */
+    public int getAlertOption() {
+        return mAlertOption;
+    }
+
+    /**
+     * Returns the service category name, in the language specified by {@link #getLanguageCode()}.
+     * @return an optional service category name
+     */
+    public String getCategoryName() {
+        return mCategoryName;
+    }
+
+    @Override
+    public String toString() {
+        return "CdmaSmsCbProgramData{operation=" + mOperation + ", category=" + mCategory
+                + ", language=" + mLanguage + ", max messages=" + mMaxMessages
+                + ", alert option=" + mAlertOption + ", category name=" + mCategoryName + '}';
+    }
+
+    /**
+     * Describe the kinds of special objects contained in the marshalled representation.
+     * @return a bitmask indicating this Parcelable contains no special objects
+     */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Creator for unparcelling objects. */
+    public static final Parcelable.Creator<CdmaSmsCbProgramData>
+            CREATOR = new Parcelable.Creator<CdmaSmsCbProgramData>() {
+        @Override
+        public CdmaSmsCbProgramData createFromParcel(Parcel in) {
+            return new CdmaSmsCbProgramData(in);
+        }
+
+        @Override
+        public CdmaSmsCbProgramData[] newArray(int size) {
+            return new CdmaSmsCbProgramData[size];
+        }
+    };
+}
index a42a267..d7aaa38 100644 (file)
@@ -39,6 +39,7 @@ import android.os.SystemProperties;
 import android.provider.Telephony;
 import android.provider.Telephony.Sms.Intents;
 import android.provider.Settings;
+import android.telephony.SmsCbMessage;
 import android.telephony.SmsMessage;
 import android.telephony.ServiceState;
 import android.util.Log;
@@ -1078,16 +1079,16 @@ public abstract class SMSDispatcher extends Handler {
         }
     };
 
-    protected void dispatchBroadcastPdus(byte[][] pdus, boolean isEmergencyMessage) {
-        if (isEmergencyMessage) {
+    protected void dispatchBroadcastMessage(SmsCbMessage message) {
+        if (message.isEmergencyMessage()) {
             Intent intent = new Intent(Intents.SMS_EMERGENCY_CB_RECEIVED_ACTION);
-            intent.putExtra("pdus", pdus);
-            Log.d(TAG, "Dispatching " + pdus.length + " emergency SMS CB pdus");
+            intent.putExtra("message", message);
+            Log.d(TAG, "Dispatching emergency SMS CB");
             dispatch(intent, RECEIVE_EMERGENCY_BROADCAST_PERMISSION);
         } else {
             Intent intent = new Intent(Intents.SMS_CB_RECEIVED_ACTION);
-            intent.putExtra("pdus", pdus);
-            Log.d(TAG, "Dispatching " + pdus.length + " SMS CB pdus");
+            intent.putExtra("message", message);
+            Log.d(TAG, "Dispatching SMS CB");
             dispatch(intent, RECEIVE_SMS_PERMISSION);
         }
     }
index ca8d9ae..4f75c84 100755 (executable)
@@ -30,6 +30,7 @@ import android.os.SystemProperties;
 import android.preference.PreferenceManager;
 import android.provider.Telephony;
 import android.provider.Telephony.Sms.Intents;
+import android.telephony.SmsCbMessage;
 import android.telephony.SmsManager;
 import android.telephony.SmsMessage.MessageClass;
 import android.util.Log;
@@ -97,6 +98,10 @@ final class CdmaSMSDispatcher extends SMSDispatcher {
         }
     }
 
+    private void handleServiceCategoryProgramData(SmsMessage sms) {
+
+    }
+
     /** {@inheritDoc} */
     @Override
     public int dispatchMessage(SmsMessageBase smsb) {
@@ -119,8 +124,19 @@ final class CdmaSMSDispatcher extends SMSDispatcher {
             return Intents.RESULT_SMS_HANDLED;
         }
 
-        // See if we have a network duplicate SMS.
         SmsMessage sms = (SmsMessage) smsb;
+
+        // Handle CMAS emergency broadcast messages.
+        if (SmsEnvelope.MESSAGE_TYPE_BROADCAST == sms.getMessageType()) {
+            Log.d(TAG, "Broadcast type message");
+            SmsCbMessage message = sms.parseBroadcastSms();
+            if (message != null) {
+                dispatchBroadcastMessage(message);
+            }
+            return Intents.RESULT_SMS_HANDLED;
+        }
+
+        // See if we have a network duplicate SMS.
         mLastDispatchedSmsFingerprint = sms.getIncomingSmsFingerprint();
         if (mLastAcknowledgedSmsFingerprint != null &&
                 Arrays.equals(mLastDispatchedSmsFingerprint, mLastAcknowledgedSmsFingerprint)) {
@@ -149,6 +165,9 @@ final class CdmaSMSDispatcher extends SMSDispatcher {
                 sms.isStatusReportMessage()) {
             handleCdmaStatusReport(sms);
             handled = true;
+        } else if (SmsEnvelope.TELESERVICE_SCPT == teleService) {
+            handleServiceCategoryProgramData(sms);
+            handled = true;
         } else if ((sms.getUserData() == null)) {
             if (false) {
                 Log.d(TAG, "Received SMS without user data");
index 1409cab..ad76192 100644 (file)
@@ -19,7 +19,10 @@ package com.android.internal.telephony.cdma;
 import android.os.Parcel;
 import android.os.SystemProperties;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
 import android.util.Log;
+
 import com.android.internal.telephony.IccUtils;
 import com.android.internal.telephony.SmsHeader;
 import com.android.internal.telephony.SmsMessageBase;
@@ -39,6 +42,8 @@ import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
 
+import static android.telephony.SmsMessage.MessageClass;
+
 /**
  * TODO(cleanup): these constants are disturbing... are they not just
  * different interpretations on one number?  And if we did not have
@@ -47,12 +52,6 @@ import java.io.IOException;
  * named CdmaSmsMessage, could it not?
  */
 
-import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES;
-import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES_WITH_HEADER;
-import static android.telephony.SmsMessage.MAX_USER_DATA_SEPTETS;
-import static android.telephony.SmsMessage.MAX_USER_DATA_SEPTETS_WITH_HEADER;
-import static android.telephony.SmsMessage.MessageClass;
-
 /**
  * TODO(cleanup): internally returning null in many places makes
  * debugging very hard (among many other reasons) and should be made
@@ -192,16 +191,17 @@ public class SmsMessage extends SmsMessageBase {
 
         // bearer data
         countInt = p.readInt(); //p_cur->uBearerDataLen
-        if (countInt >0) {
-            data = new byte[countInt];
-             //p_cur->aBearerData[digitCount] :
-            for (int index=0; index < countInt; index++) {
-                data[index] = p.readByte();
-            }
-            env.bearerData = data;
-            // BD gets further decoded when accessed in SMSDispatcher
+        if (countInt < 0) {
+            countInt = 0;
         }
 
+        data = new byte[countInt];
+        for (int index=0; index < countInt; index++) {
+            data[index] = p.readByte();
+        }
+        // BD gets further decoded when accessed in SMSDispatcher
+        env.bearerData = data;
+
         // link the the filled objects to the SMS
         env.origAddress = addr;
         env.origSubaddress = subaddr;
@@ -732,6 +732,29 @@ public class SmsMessage extends SmsMessageBase {
     }
 
     /**
+     * Parses a broadcast SMS, possibly containing a CMAS alert.
+     */
+    SmsCbMessage parseBroadcastSms() {
+        BearerData bData = BearerData.decode(mEnvelope.bearerData, mEnvelope.serviceCategory);
+        if (bData == null) {
+            Log.w(LOG_TAG, "BearerData.decode() returned null");
+            return null;
+        }
+
+        if (Log.isLoggable(LOGGABLE_TAG, Log.VERBOSE)) {
+            Log.d(LOG_TAG, "MT raw BearerData = " + HexDump.toHexString(mEnvelope.bearerData));
+        }
+
+        String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC);
+        SmsCbLocation location = new SmsCbLocation(plmn);
+
+        return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP2,
+                SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, bData.messageId, location,
+                mEnvelope.serviceCategory, bData.getLanguage(), bData.userData.payloadStr,
+                bData.priority, null, bData.cmasWarningInfo);
+    }
+
+    /**
      * {@inheritDoc}
      */
     public MessageClass getMessageClass() {
index 6743da0..e70ff18 100755 (executable)
 
 package com.android.internal.telephony.cdma.sms;
 
-import static android.telephony.SmsMessage.ENCODING_16BIT;
-import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES;
-import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES_WITH_HEADER;
-
-import android.util.Log;
-
+import android.content.res.Resources;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbMessage;
 import android.telephony.SmsMessage;
-
+import android.telephony.cdma.CdmaSmsCbProgramData;
 import android.text.format.Time;
+import android.util.Log;
 
-import com.android.internal.telephony.IccUtils;
 import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.IccUtils;
 import com.android.internal.telephony.SmsHeader;
 import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails;
-
 import com.android.internal.util.BitwiseInputStream;
 import com.android.internal.util.BitwiseOutputStream;
 
-import android.content.res.Resources;
-
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.TimeZone;
 
+import static android.telephony.SmsMessage.ENCODING_16BIT;
+import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES;
+import static android.telephony.SmsMessage.MAX_USER_DATA_BYTES_WITH_HEADER;
 
 /**
  * An object to encode and decode CDMA SMS bearer data.
@@ -68,8 +69,8 @@ public final class BearerData {
     private final static byte SUBPARAM_MESSAGE_DISPLAY_MODE             = 0x0F;
     //private final static byte SUBPARAM_MULTIPLE_ENCODING_USER_DATA      = 0x10;
     private final static byte SUBPARAM_MESSAGE_DEPOSIT_INDEX            = 0x11;
-    //private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA    = 0x12;
-    //private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_RESULTS = 0x13;
+    private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA    = 0x12;
+    private final static byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_RESULTS = 0x13;
     private final static byte SUBPARAM_MESSAGE_STATUS                   = 0x14;
     //private final static byte SUBPARAM_TP_FAILURE_CAUSE                 = 0x15;
     //private final static byte SUBPARAM_ENHANCED_VMN                     = 0x16;
@@ -339,12 +340,68 @@ public final class BearerData {
      */
     public CdmaSmsAddress callbackNumber;
 
+    /**
+     * CMAS warning notification information.
+     * @see #decodeCmasUserData(BearerData, int)
+     */
+    public SmsCbCmasInfo cmasWarningInfo;
+
+    /**
+     * The Service Category Program Data subparameter is used to enable and disable
+     * SMS broadcast service categories to display. If this subparameter is present,
+     * this field will contain a list of one or more
+     * {@link android.telephony.cdma.CdmaSmsCbProgramData} objects containing the
+     * operation(s) to perform.
+     */
+    public List<CdmaSmsCbProgramData> serviceCategoryProgramData;
+
     private static class CodingException extends Exception {
         public CodingException(String s) {
             super(s);
         }
     }
 
+    /**
+     * Returns the language indicator as a two-character ISO 639 string.
+     * @return a two character ISO 639 language code
+     */
+    public String getLanguage() {
+        return getLanguageCodeForValue(language);
+    }
+
+    /**
+     * Converts a CDMA language indicator value to an ISO 639 two character language code.
+     * @param languageValue the CDMA language value to convert
+     * @return the two character ISO 639 language code for the specified value, or null if unknown
+     */
+    private static String getLanguageCodeForValue(int languageValue) {
+        switch (languageValue) {
+            case LANGUAGE_ENGLISH:
+                return "en";
+
+            case LANGUAGE_FRENCH:
+                return "fr";
+
+            case LANGUAGE_SPANISH:
+                return "es";
+
+            case LANGUAGE_JAPANESE:
+                return "ja";
+
+            case LANGUAGE_KOREAN:
+                return "ko";
+
+            case LANGUAGE_CHINESE:
+                return "zh";
+
+            case LANGUAGE_HEBREW:
+                return "he";
+
+            default:
+                return null;
+        }
+    }
+
     @Override
     public String toString() {
         StringBuilder builder = new StringBuilder();
@@ -916,6 +973,9 @@ public final class BearerData {
     private static String decodeUtf8(byte[] data, int offset, int numFields)
         throws CodingException
     {
+        if (numFields < 0 || (numFields + offset) > data.length) {
+            throw new CodingException("UTF-8 decode failed: offset or length out of range");
+        }
         try {
             return new String(data, offset, numFields, "UTF-8");
         } catch (java.io.UnsupportedEncodingException ex) {
@@ -926,11 +986,12 @@ public final class BearerData {
     private static String decodeUtf16(byte[] data, int offset, int numFields)
         throws CodingException
     {
-        // Start reading from the next 16-bit aligned boundary after offset.
-        int padding = offset % 2;
-        numFields -= (offset + padding) / 2;
+        int byteCount = numFields * 2;
+        if (byteCount < 0 || (byteCount + offset) > data.length) {
+            throw new CodingException("UTF-16 decode failed: offset or length out of range");
+        }
         try {
-            return new String(data, offset, numFields * 2, "utf-16be");
+            return new String(data, offset, byteCount, "utf-16be");
         } catch (java.io.UnsupportedEncodingException ex) {
             throw new CodingException("UTF-16 decode failed: " + ex);
         }
@@ -988,8 +1049,11 @@ public final class BearerData {
     private static String decodeLatin(byte[] data, int offset, int numFields)
         throws CodingException
     {
+        if (numFields < 0 || (numFields + offset) > data.length) {
+            throw new CodingException("ISO-8859-1 decode failed: offset or length out of range");
+        }
         try {
-            return new String(data, offset, numFields - offset, "ISO-8859-1");
+            return new String(data, offset, numFields, "ISO-8859-1");
         } catch (java.io.UnsupportedEncodingException ex) {
             throw new CodingException("ISO-8859-1 decode failed: " + ex);
         }
@@ -1032,6 +1096,7 @@ public final class BearerData {
                 userData.payloadStr = decodeUtf8(userData.payload, offset, userData.numFields);
             }
             break;
+
         case UserData.ENCODING_IA5:
         case UserData.ENCODING_7BIT_ASCII:
             userData.payloadStr = decode7bitAscii(userData.payload, offset, userData.numFields);
@@ -1114,8 +1179,9 @@ public final class BearerData {
         BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
         int dataLen = inStream.available() / 6;  // 6-bit packed character encoding.
         int numFields = bData.userData.numFields;
-        if ((dataLen > 14) || (dataLen < numFields)) {
-            throw new CodingException("IS-91 voicemail status decoding failed");
+        // dataLen may be > 14 characters due to octet padding
+        if ((numFields > 14) || (dataLen < numFields)) {
+            throw new CodingException("IS-91 short message decoding failed");
         }
         StringBuffer strbuf = new StringBuffer(dataLen);
         for (int i = 0; i < numFields; i++) {
@@ -1539,7 +1605,7 @@ public final class BearerData {
             bData.userResponseCode = inStream.read(8);
         }
         if ((! decodeSuccess) || (paramBits > 0)) {
-            Log.d(LOG_TAG, "USER_REPONSE_CODE decode " +
+            Log.d(LOG_TAG, "USER_RESPONSE_CODE decode " +
                       (decodeSuccess ? "succeeded" : "failed") +
                       " (extra bits = " + paramBits + ")");
         }
@@ -1548,27 +1614,240 @@ public final class BearerData {
         return decodeSuccess;
     }
 
+    private static boolean decodeServiceCategoryProgramData(BearerData bData,
+            BitwiseInputStream inStream) throws BitwiseInputStream.AccessException, CodingException
+    {
+        if (inStream.available() < 13) {
+            throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only "
+                    + inStream.available() + " bits available");
+        }
+
+        int paramBits = inStream.read(8) * 8;
+        int msgEncoding = inStream.read(5);
+        paramBits -= 5;
+
+        if (inStream.available() < paramBits) {
+            throw new CodingException("SERVICE_CATEGORY_PROGRAM_DATA decode failed: only "
+                    + inStream.available() + " bits available (" + paramBits + " bits expected)");
+        }
+
+        ArrayList<CdmaSmsCbProgramData> programDataList = new ArrayList<CdmaSmsCbProgramData>();
+
+        final int CATEGORY_FIELD_MIN_SIZE = 6 * 8;
+        boolean decodeSuccess = false;
+        while (paramBits >= CATEGORY_FIELD_MIN_SIZE) {
+            int operation = inStream.read(4);
+            int category = (inStream.read(8) << 8) | inStream.read(8);
+            String language = getLanguageCodeForValue(inStream.read(8));
+            int maxMessages = inStream.read(8);
+            int alertOption = inStream.read(4);
+            int numFields = inStream.read(8);
+            paramBits -= CATEGORY_FIELD_MIN_SIZE;
+
+            int textBits = getBitsForNumFields(msgEncoding, numFields);
+            if (paramBits < textBits) {
+                throw new CodingException("category name is " + textBits + " bits in length,"
+                        + " but there are only " + paramBits + " bits available");
+            }
+
+            UserData userData = new UserData();
+            userData.msgEncoding = msgEncoding;
+            userData.msgEncodingSet = true;
+            userData.numFields = numFields;
+            userData.payload = inStream.readByteArray(textBits);
+            paramBits -= textBits;
+
+            decodeUserDataPayload(userData, false);
+            String categoryName = userData.payloadStr;
+            CdmaSmsCbProgramData programData = new CdmaSmsCbProgramData(operation, category,
+                    language, maxMessages, alertOption, categoryName);
+            programDataList.add(programData);
+
+            decodeSuccess = true;
+        }
+
+        if ((! decodeSuccess) || (paramBits > 0)) {
+            Log.d(LOG_TAG, "SERVICE_CATEGORY_PROGRAM_DATA decode " +
+                      (decodeSuccess ? "succeeded" : "failed") +
+                      " (extra bits = " + paramBits + ')');
+        }
+
+        inStream.skip(paramBits);
+        bData.serviceCategoryProgramData = programDataList;
+        return decodeSuccess;
+    }
+
+    private static int serviceCategoryToCmasMessageClass(int serviceCategory) {
+        switch (serviceCategory) {
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT:
+                return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT:
+                return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT:
+                return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY:
+                return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY;
+
+            case SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE:
+                return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN;
+        }
+    }
+
+    /**
+     * Calculates the number of bits to read for the specified number of encoded characters.
+     * @param msgEncoding the message encoding to use
+     * @param numFields the number of characters to read. For Shift-JIS and Korean encodings,
+     *  this is the number of bytes to read.
+     * @return the number of bits to read from the stream
+     * @throws CodingException if the specified encoding is not supported
+     */
+    private static int getBitsForNumFields(int msgEncoding, int numFields)
+            throws CodingException {
+        switch (msgEncoding) {
+            case UserData.ENCODING_OCTET:
+            case UserData.ENCODING_SHIFT_JIS:
+            case UserData.ENCODING_KOREAN:
+            case UserData.ENCODING_LATIN:
+            case UserData.ENCODING_LATIN_HEBREW:
+                return numFields * 8;
+
+            case UserData.ENCODING_IA5:
+            case UserData.ENCODING_7BIT_ASCII:
+            case UserData.ENCODING_GSM_7BIT_ALPHABET:
+                return numFields * 7;
+
+            case UserData.ENCODING_UNICODE_16:
+                return numFields * 16;
+
+            default:
+                throw new CodingException("unsupported message encoding (" + msgEncoding + ')');
+        }
+    }
+
+    /**
+     * CMAS message decoding.
+     * (See TIA-1149-0-1, CMAS over CDMA)
+     *
+     * @param serviceCategory is the service category from the SMS envelope
+     */
+    private static void decodeCmasUserData(BearerData bData, int serviceCategory)
+            throws BitwiseInputStream.AccessException, CodingException {
+        BitwiseInputStream inStream = new BitwiseInputStream(bData.userData.payload);
+        if (inStream.available() < 8) {
+            throw new CodingException("emergency CB with no CMAE_protocol_version");
+        }
+        int protocolVersion = inStream.read(8);
+        if (protocolVersion != 0) {
+            throw new CodingException("unsupported CMAE_protocol_version " + protocolVersion);
+        }
+
+        int messageClass = serviceCategoryToCmasMessageClass(serviceCategory);
+        int category = SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN;
+        int responseType = SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN;
+        int severity = SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN;
+        int urgency = SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN;
+        int certainty = SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN;
+
+        while (inStream.available() >= 16) {
+            int recordType = inStream.read(8);
+            int recordLen = inStream.read(8);
+            switch (recordType) {
+                case 0:     // Type 0 elements (Alert text)
+                    UserData alertUserData = new UserData();
+                    alertUserData.msgEncoding = inStream.read(5);
+                    alertUserData.msgEncodingSet = true;
+                    alertUserData.msgType = 0;
+
+                    int numFields;                          // number of chars to decode
+                    switch (alertUserData.msgEncoding) {
+                        case UserData.ENCODING_OCTET:
+                        case UserData.ENCODING_LATIN:
+                            numFields = recordLen - 1;      // subtract 1 byte for encoding
+                            break;
+
+                        case UserData.ENCODING_IA5:
+                        case UserData.ENCODING_7BIT_ASCII:
+                        case UserData.ENCODING_GSM_7BIT_ALPHABET:
+                            numFields = ((recordLen * 8) - 5) / 7;  // subtract 5 bits for encoding
+                            break;
+
+                        case UserData.ENCODING_UNICODE_16:
+                            numFields = (recordLen - 1) / 2;
+                            break;
+
+                        default:
+                            numFields = 0;      // unsupported encoding
+                    }
+
+                    alertUserData.numFields = numFields;
+                    alertUserData.payload = inStream.readByteArray(recordLen * 8 - 5);
+                    decodeUserDataPayload(alertUserData, false);
+                    bData.userData = alertUserData;
+                    break;
+
+                case 1:     // Type 1 elements
+                    category = inStream.read(8);
+                    responseType = inStream.read(8);
+                    severity = inStream.read(4);
+                    urgency = inStream.read(4);
+                    certainty = inStream.read(4);
+                    inStream.skip(recordLen * 8 - 28);
+                    break;
+
+                default:
+                    Log.w(LOG_TAG, "skipping unsupported CMAS record type " + recordType);
+                    inStream.skip(recordLen * 8);
+                    break;
+            }
+        }
+
+        bData.cmasWarningInfo = new SmsCbCmasInfo(messageClass, category, responseType, severity,
+                urgency, certainty);
+    }
+
     /**
      * Create BearerData object from serialized representation.
      * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
      *
      * @param smsData byte array of raw encoded SMS bearer data.
-     *
      * @return an instance of BearerData.
      */
     public static BearerData decode(byte[] smsData) {
+        return decode(smsData, 0);
+    }
+
+    private static boolean isCmasAlertCategory(int category) {
+        return category >= SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT
+                && category <= SmsEnvelope.SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE;
+    }
+
+    /**
+     * Create BearerData object from serialized representation.
+     * (See 3GPP2 C.R1001-F, v1.0, section 4.5 for layout details)
+     *
+     * @param smsData byte array of raw encoded SMS bearer data.
+     * @param serviceCategory the envelope service category (for CMAS alert handling)
+     * @return an instance of BearerData.
+     */
+    public static BearerData decode(byte[] smsData, int serviceCategory) {
         try {
             BitwiseInputStream inStream = new BitwiseInputStream(smsData);
             BearerData bData = new BearerData();
             int foundSubparamMask = 0;
             while (inStream.available() > 0) {
-                boolean decodeSuccess = false;
                 int subparamId = inStream.read(8);
                 int subparamIdBit = 1 << subparamId;
                 if ((foundSubparamMask & subparamIdBit) != 0) {
                     throw new CodingException("illegal duplicate subparameter (" +
                                               subparamId + ")");
                 }
+                boolean decodeSuccess;
                 switch (subparamId) {
                 case SUBPARAM_MESSAGE_IDENTIFIER:
                     decodeSuccess = decodeMessageId(bData, inStream);
@@ -1624,6 +1903,9 @@ public final class BearerData {
                 case SUBPARAM_MESSAGE_DEPOSIT_INDEX:
                     decodeSuccess = decodeDepositIndex(bData, inStream);
                     break;
+                case SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA:
+                    decodeSuccess = decodeServiceCategoryProgramData(bData, inStream);
+                    break;
                 default:
                     throw new CodingException("unsupported bearer data subparameter ("
                                               + subparamId + ")");
@@ -1634,7 +1916,10 @@ public final class BearerData {
                 throw new CodingException("missing MESSAGE_IDENTIFIER subparam");
             }
             if (bData.userData != null) {
-                if (bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) {
+                if (isCmasAlertCategory(serviceCategory) && bData.priorityIndicatorSet
+                        && bData.priority == SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY) {
+                    decodeCmasUserData(bData, serviceCategory);
+                } else if (bData.userData.msgEncoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) {
                     if ((foundSubparamMask ^
                              (1 << SUBPARAM_MESSAGE_IDENTIFIER) ^
                              (1 << SUBPARAM_USER_DATA))
index 4a60231..f73df56 100644 (file)
@@ -37,6 +37,7 @@ public final class SmsEnvelope {
     static public final int TELESERVICE_VMN               = 0x1003;
     static public final int TELESERVICE_WAP               = 0x1004;
     static public final int TELESERVICE_WEMT              = 0x1005;
+    static public final int TELESERVICE_SCPT              = 0x1006;
 
     /**
      * The following are defined as extensions to the standard teleservices
@@ -46,14 +47,17 @@ public final class SmsEnvelope {
     // number of messages waiting, it's used by some CDMA carriers for a voice mail count.
     static public final int TELESERVICE_MWI               = 0x40000;
 
-    // ServiceCategories for Cell Broadcast, see 3GPP2 C.R1001 table 9.3.1-1
-    //static public final int SERVICECATEGORY_EMERGENCY      = 0x0010;
+    // Service Categories for Cell Broadcast, see 3GPP2 C.R1001 table 9.3.1-1
+    // static final int SERVICE_CATEGORY_EMERGENCY      = 0x0001;
     //...
 
-    /**
-     *  maximum lengths for fields as defined in ril_cdma_sms.h
-     */
-    static public final int SMS_BEARER_DATA_MAX = 255;
+    // CMAS alert service category assignments, see 3GPP2 C.R1001 table 9.3.3-1
+    public static final int SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT  = 0x1000;
+    public static final int SERVICE_CATEGORY_CMAS_EXTREME_THREAT            = 0x1001;
+    public static final int SERVICE_CATEGORY_CMAS_SEVERE_THREAT             = 0x1002;
+    public static final int SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY = 0x1003;
+    public static final int SERVICE_CATEGORY_CMAS_TEST_MESSAGE              = 0x1004;
+    public static final int SERVICE_CATEGORY_CMAS_LAST_RESERVED_VALUE       = 0x10ff;
 
     /**
      * Provides the type of a SMS message like point to point, broadcast or acknowledge
index 189d97d..599c2b3 100644 (file)
@@ -32,9 +32,9 @@ public class UserData {
     public static final int ENCODING_7BIT_ASCII                 = 0x02;
     public static final int ENCODING_IA5                        = 0x03;
     public static final int ENCODING_UNICODE_16                 = 0x04;
-    //public static final int ENCODING_SHIFT_JIS                  = 0x05;
-    //public static final int ENCODING_KOREAN                     = 0x06;
-    //public static final int ENCODING_LATIN_HEBREW               = 0x07;
+    public static final int ENCODING_SHIFT_JIS                  = 0x05;
+    public static final int ENCODING_KOREAN                     = 0x06;
+    public static final int ENCODING_LATIN_HEBREW               = 0x07;
     public static final int ENCODING_LATIN                      = 0x08;
     public static final int ENCODING_GSM_7BIT_ALPHABET          = 0x09;
     public static final int ENCODING_GSM_DCS                    = 0x0A;
index 8a75f51..931c662 100644 (file)
@@ -26,6 +26,7 @@ import android.os.SystemProperties;
 import android.provider.Telephony.Sms;
 import android.provider.Telephony.Sms.Intents;
 import android.telephony.PhoneNumberUtils;
+import android.telephony.SmsCbLocation;
 import android.telephony.SmsCbMessage;
 import android.telephony.SmsManager;
 import android.telephony.gsm.GsmCellLocation;
@@ -318,24 +319,18 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
      * concatenated message
      */
     private static final class SmsCbConcatInfo {
-        private final SmsCbHeader mHeader;
-
-        private final String mPlmn;
 
-        private final int mLac;
-
-        private final int mCid;
+        private final SmsCbHeader mHeader;
+        private final SmsCbLocation mLocation;
 
-        public SmsCbConcatInfo(SmsCbHeader header, String plmn, int lac, int cid) {
+        public SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) {
             mHeader = header;
-            mPlmn = plmn;
-            mLac = lac;
-            mCid = cid;
+            mLocation = location;
         }
 
         @Override
         public int hashCode() {
-            return mHeader.messageIdentifier * 31 + mHeader.updateNumber;
+            return (mHeader.getSerialNumber() * 31) + mLocation.hashCode();
         }
 
         @Override
@@ -343,49 +338,28 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
             if (obj instanceof SmsCbConcatInfo) {
                 SmsCbConcatInfo other = (SmsCbConcatInfo)obj;
 
-                // Two pages match if all header attributes (except the page
-                // index) are identical, and both pages belong to the same
-                // location (which is also determined by the scope parameter)
-                if (mHeader.geographicalScope == other.mHeader.geographicalScope
-                        && mHeader.messageCode == other.mHeader.messageCode
-                        && mHeader.updateNumber == other.mHeader.updateNumber
-                        && mHeader.messageIdentifier == other.mHeader.messageIdentifier
-                        && mHeader.dataCodingScheme == other.mHeader.dataCodingScheme
-                        && mHeader.nrOfPages == other.mHeader.nrOfPages) {
-                    return matchesLocation(other.mPlmn, other.mLac, other.mCid);
-                }
+                // Two pages match if they have the same serial number (which includes the
+                // geographical scope and update number), and both pages belong to the same
+                // location (PLMN, plus LAC and CID if these are part of the geographical scope).
+                return mHeader.getSerialNumber() == other.mHeader.getSerialNumber()
+                        && mLocation.equals(other.mLocation);
             }
 
             return false;
         }
 
         /**
-         * Checks if this concatenation info matches the given location. The
-         * granularity of the match depends on the geographical scope.
+         * Compare the location code for this message to the current location code. The match is
+         * relative to the geographical scope of the message, which determines whether the LAC
+         * and Cell ID are saved in mLocation or set to -1 to match all values.
          *
-         * @param plmn PLMN
-         * @param lac Location area code
-         * @param cid Cell ID
-         * @return true if matching, false otherwise
+         * @param plmn the current PLMN
+         * @param lac the current Location Area (GSM) or Service Area (UMTS)
+         * @param cid the current Cell ID
+         * @return true if this message is valid for the current location; false otherwise
          */
         public boolean matchesLocation(String plmn, int lac, int cid) {
-            switch (mHeader.geographicalScope) {
-                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
-                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
-                    if (mCid != cid) {
-                        return false;
-                    }
-                    // deliberate fall-through
-                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
-                    if (mLac != lac) {
-                        return false;
-                    }
-                    // deliberate fall-through
-                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
-                    return mPlmn != null && mPlmn.equals(plmn);
-            }
-
-            return false;
+            return mLocation.isInLocationArea(plmn, lac, cid);
         }
     }
 
@@ -421,24 +395,42 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
             int lac = cellLocation.getLac();
             int cid = cellLocation.getCid();
 
+            SmsCbLocation location;
+            switch (header.getGeographicalScope()) {
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
+                    location = new SmsCbLocation(plmn, lac, -1);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
+                    location = new SmsCbLocation(plmn, lac, cid);
+                    break;
+
+                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
+                default:
+                    location = new SmsCbLocation(plmn);
+                    break;
+            }
+
             byte[][] pdus;
-            if (header.nrOfPages > 1) {
+            int pageCount = header.getNumberOfPages();
+            if (pageCount > 1) {
                 // Multi-page message
-                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, plmn, lac, cid);
+                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location);
 
                 // Try to find other pages of the same message
                 pdus = mSmsCbPageMap.get(concatInfo);
 
                 if (pdus == null) {
-                    // This it the first page of this message, make room for all
+                    // This is the first page of this message, make room for all
                     // pages and keep until complete
-                    pdus = new byte[header.nrOfPages][];
+                    pdus = new byte[pageCount][];
 
                     mSmsCbPageMap.put(concatInfo, pdus);
                 }
 
                 // Page parameter is one-based
-                pdus[header.pageIndex - 1] = receivedPdu;
+                pdus[header.getPageIndex() - 1] = receivedPdu;
 
                 for (int i = 0; i < pdus.length; i++) {
                     if (pdus[i] == null) {
@@ -455,8 +447,8 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
                 pdus[0] = receivedPdu;
             }
 
-            boolean isEmergencyMessage = SmsCbHeader.isEmergencyMessage(header.messageIdentifier);
-            dispatchBroadcastPdus(pdus, isEmergencyMessage);
+            SmsCbMessage message = GsmSmsCbMessage.createSmsCbMessage(header, location, pdus);
+            dispatchBroadcastMessage(message);
 
             // Remove messages that are out of scope to prevent the map from
             // growing indefinitely, containing incomplete messages that were
diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java
new file mode 100644 (file)
index 0000000..dc9554a
--- /dev/null
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.gsm;
+
+import android.telephony.SmsCbLocation;
+import android.telephony.SmsCbMessage;
+import android.util.Pair;
+
+import com.android.internal.telephony.GsmAlphabet;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * 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 = {
+            "de", "en", "it", "fr", "es", "nl", "sv", "da", "pt", "fi", "no", "el", "tr", "hu",
+            "pl", null
+    };
+
+    /**
+     * Languages in the 0010xxxx DCS group as defined in 3GPP TS 23.038, section 5.
+     */
+    private static final String[] LANGUAGE_CODES_GROUP_2 = {
+            "cs", "he", "ar", "ru", "is", null, null, null, null, null, null, null, null, null,
+            null, null
+    };
+
+    private static final char CARRIAGE_RETURN = 0x0d;
+
+    private static final int PDU_BODY_PAGE_LENGTH = 82;
+
+    /** Utility class with only static methods. */
+    private GsmSmsCbMessage() { }
+
+    /**
+     * Create a new SmsCbMessage object from a header object plus one or more received PDUs.
+     *
+     * @param pdus PDU bytes
+     */
+    static SmsCbMessage createSmsCbMessage(SmsCbHeader header, SmsCbLocation location,
+            byte[][] pdus) throws IllegalArgumentException {
+        if (header.isEtwsPrimaryNotification()) {
+            return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
+                    header.getGeographicalScope(), header.getSerialNumber(),
+                    location, header.getServiceCategory(),
+                    null, "ETWS", SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY,
+                    header.getEtwsInfo(), header.getCmasInfo());
+        } else {
+            String language = null;
+            StringBuilder sb = new StringBuilder();
+            for (byte[] pdu : pdus) {
+                Pair<String, String> p = parseBody(header, pdu);
+                language = p.first;
+                sb.append(p.second);
+            }
+            int priority = header.isEmergencyMessage() ? SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY
+                    : SmsCbMessage.MESSAGE_PRIORITY_NORMAL;
+
+            return new SmsCbMessage(SmsCbMessage.MESSAGE_FORMAT_3GPP,
+                    header.getGeographicalScope(), header.getSerialNumber(), location,
+                    header.getServiceCategory(), language, sb.toString(), priority,
+                    header.getEtwsInfo(), header.getCmasInfo());
+        }
+    }
+
+    /**
+     * Create a new SmsCbMessage object from one or more received PDUs. This is used by some
+     * CellBroadcastReceiver test cases, because SmsCbHeader is now package local.
+     *
+     * @param location the location (geographical scope) for the message
+     * @param pdus PDU bytes
+     */
+    public static SmsCbMessage createSmsCbMessage(SmsCbLocation location, byte[][] pdus)
+            throws IllegalArgumentException {
+        SmsCbHeader header = new SmsCbHeader(pdus[0]);
+        return createSmsCbMessage(header, location, pdus);
+    }
+
+    /**
+     * 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
+     *
+     * @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
+     */
+    private static Pair<String, String> parseBody(SmsCbHeader header, byte[] pdu) {
+        int encoding;
+        String language = null;
+        boolean hasLanguageIndicator = false;
+        int dataCodingScheme = header.getDataCodingScheme();
+
+        // Extract encoding and language from DCS, as defined in 3gpp TS 23.038,
+        // section 5.
+        switch ((dataCodingScheme & 0xf0) >> 4) {
+            case 0x00:
+                encoding = android.telephony.SmsMessage.ENCODING_7BIT;
+                language = LANGUAGE_CODES_GROUP_0[dataCodingScheme & 0x0f];
+                break;
+
+            case 0x01:
+                hasLanguageIndicator = true;
+                if ((dataCodingScheme & 0x0f) == 0x01) {
+                    encoding = android.telephony.SmsMessage.ENCODING_16BIT;
+                } else {
+                    encoding = android.telephony.SmsMessage.ENCODING_7BIT;
+                }
+                break;
+
+            case 0x02:
+                encoding = android.telephony.SmsMessage.ENCODING_7BIT;
+                language = LANGUAGE_CODES_GROUP_2[dataCodingScheme & 0x0f];
+                break;
+
+            case 0x03:
+                encoding = android.telephony.SmsMessage.ENCODING_7BIT;
+                break;
+
+            case 0x04:
+            case 0x05:
+                switch ((dataCodingScheme & 0x0c) >> 2) {
+                    case 0x01:
+                        encoding = android.telephony.SmsMessage.ENCODING_8BIT;
+                        break;
+
+                    case 0x02:
+                        encoding = android.telephony.SmsMessage.ENCODING_16BIT;
+                        break;
+
+                    case 0x00:
+                    default:
+                        encoding = android.telephony.SmsMessage.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 = android.telephony.SmsMessage.ENCODING_8BIT;
+                } else {
+                    encoding = android.telephony.SmsMessage.ENCODING_7BIT;
+                }
+                break;
+
+            default:
+                // Reserved values are to be treated as 7-bit
+                encoding = android.telephony.SmsMessage.ENCODING_7BIT;
+                break;
+        }
+
+        if (header.isUmtsFormat()) {
+            // Payload may contain multiple pages
+            int nrPages = pdu[SmsCbHeader.PDU_HEADER_LENGTH];
+
+            if (pdu.length < SmsCbHeader.PDU_HEADER_LENGTH + 1 + (PDU_BODY_PAGE_LENGTH + 1)
+                    * nrPages) {
+                throw new IllegalArgumentException("Pdu length " + pdu.length + " does not match "
+                        + nrPages + " pages");
+            }
+
+            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);
+        }
+    }
+
+    /**
+     * 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
+     * @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) {
+        String body = null;
+
+        switch (encoding) {
+            case android.telephony.SmsMessage.ENCODING_7BIT:
+                body = GsmAlphabet.gsm7BitPackedToString(pdu, offset, length * 8 / 7);
+
+                if (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);
+                    body = body.substring(3);
+                }
+                break;
+
+            case android.telephony.SmsMessage.ENCODING_16BIT:
+                if (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);
+                    offset += 2;
+                    length -= 2;
+                }
+
+                try {
+                    body = new String(pdu, offset, (length & 0xfffe), "utf-16");
+                } catch (UnsupportedEncodingException e) {
+                    // Apparently it wasn't valid UTF-16.
+                    throw new IllegalArgumentException("Error decoding UTF-16 message", e);
+                }
+                break;
+
+            default:
+                break;
+        }
+
+        if (body != null) {
+            // Remove trailing carriage return
+            for (int i = body.length() - 1; i >= 0; i--) {
+                if (body.charAt(i) != CARRIAGE_RETURN) {
+                    body = body.substring(0, i + 1);
+                    break;
+                }
+            }
+        } else {
+            body = "";
+        }
+
+        return new Pair<String, String>(language, body);
+    }
+}
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2012 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * limitations under the License.
  */
 
-package android.telephony;
+package com.android.internal.telephony.gsm;
 
 /**
- * Constants used in SMS Cell Broadcast messages.
+ * Constants used in SMS Cell Broadcast messages (see 3GPP TS 23.041). This class is used by the
+ * boot-time broadcast channel enable and database upgrade code in CellBroadcastReceiver, so it
+ * is public, but should be avoided in favor of the radio technology independent constants in
+ * {@link android.telephony.SmsCbMessage}, {@link android.telephony.SmsCbEtwsInfo}, and
+ * {@link android.telephony.SmsCbCmasInfo} classes.
  *
  * {@hide}
  */
-public interface SmsCbConstants {
+public class SmsCbConstants {
+
+    /** Private constructor for utility class. */
+    private SmsCbConstants() { }
+
     /** Start of PWS Message Identifier range (includes ETWS and CMAS). */
     public static final int MESSAGE_ID_PWS_FIRST_IDENTIFIER = 0x1100;
 
@@ -94,11 +102,11 @@ public interface SmsCbConstants {
     /** End of PWS Message Identifier range (includes ETWS, CMAS, and future extensions). */
     public static final int MESSAGE_ID_PWS_LAST_IDENTIFIER                  = 0x18FF;
 
-    /** ETWS message code flag to activate the popup display. */
-    public static final int MESSAGE_CODE_ETWS_ACTIVATE_POPUP                = 0x100;
+    /** ETWS serial number flag to activate the popup display. */
+    public static final int SERIAL_NUMBER_ETWS_ACTIVATE_POPUP                = 0x1000;
 
-    /** ETWS message code flag to activate the emergency user alert. */
-    public static final int MESSAGE_CODE_ETWS_EMERGENCY_USER_ALERT          = 0x200;
+    /** ETWS serial number flag to activate the emergency user alert. */
+    public static final int SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT          = 0x2000;
 
     /** ETWS warning type value for earthquake. */
     public static final int ETWS_WARNING_TYPE_EARTHQUAKE                    = 0x00;
index 8e6b79b..5692044 100644 (file)
 
 package com.android.internal.telephony.gsm;
 
-import android.telephony.SmsCbConstants;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbEtwsInfo;
+
+import java.util.Arrays;
+
+/**
+ * Parses a 3GPP TS 23.041 cell broadcast message header. This class is public for use by
+ * CellBroadcastReceiver test cases, but should not be used by applications.
+ *
+ * All relevant header information is now sent as a Parcelable
+ * {@link android.telephony.SmsCbMessage} object in the "message" extra of the
+ * {@link android.provider.Telephony.Sms.Intents#SMS_CB_RECEIVED_ACTION} or
+ * {@link android.provider.Telephony.Sms.Intents#SMS_EMERGENCY_CB_RECEIVED_ACTION} intent.
+ * The raw PDU is no longer sent to SMS CB applications.
+ */
+class SmsCbHeader {
 
-public class SmsCbHeader implements SmsCbConstants {
     /**
      * Length of SMS-CB header
      */
-    public static final int PDU_HEADER_LENGTH = 6;
+    static final int PDU_HEADER_LENGTH = 6;
 
     /**
      * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1
      */
-    public static final int FORMAT_GSM = 1;
+    static final int FORMAT_GSM = 1;
 
     /**
      * UMTS pdu format, as defined in 3gpp TS 23.041, section 9.4.2
      */
-    public static final int FORMAT_UMTS = 2;
+    static final int FORMAT_UMTS = 2;
 
     /**
      * GSM pdu format, as defined in 3gpp TS 23.041, section 9.4.1.3
      */
-    public static final int FORMAT_ETWS_PRIMARY = 3;
+    static final int FORMAT_ETWS_PRIMARY = 3;
 
     /**
      * Message type value as defined in 3gpp TS 25.324, section 11.1.
@@ -47,34 +61,34 @@ public class SmsCbHeader implements SmsCbConstants {
     /**
      * Length of GSM pdus
      */
-    public static final int PDU_LENGTH_GSM = 88;
+    private static final int PDU_LENGTH_GSM = 88;
 
     /**
      * Maximum length of ETWS primary message GSM pdus
      */
-    public static final int PDU_LENGTH_ETWS = 56;
-
-    public final int geographicalScope;
+    private static final int PDU_LENGTH_ETWS = 56;
 
-    public final int messageCode;
+    private final int geographicalScope;
 
-    public final int updateNumber;
+    /** The serial number combines geographical scope, message code, and update number. */
+    private final int serialNumber;
 
-    public final int messageIdentifier;
+    /** The Message Identifier in 3GPP is the same as the Service Category in CDMA. */
+    private final int messageIdentifier;
 
-    public final int dataCodingScheme;
+    private final int dataCodingScheme;
 
-    public final int pageIndex;
+    private final int pageIndex;
 
-    public final int nrOfPages;
+    private final int nrOfPages;
 
-    public final int format;
+    private final int format;
 
-    public final boolean etwsEmergencyUserAlert;
+    /** ETWS warning notification info. */
+    private final SmsCbEtwsInfo mEtwsInfo;
 
-    public final boolean etwsPopup;
-
-    public final int etwsWarningType;
+    /** CMAS warning notification info. */
+    private final SmsCbCmasInfo mCmasInfo;
 
     public SmsCbHeader(byte[] pdu) throws IllegalArgumentException {
         if (pdu == null || pdu.length < PDU_HEADER_LENGTH) {
@@ -83,22 +97,31 @@ public class SmsCbHeader implements SmsCbConstants {
 
         if (pdu.length <= PDU_LENGTH_ETWS) {
             format = FORMAT_ETWS_PRIMARY;
-            geographicalScope = -1; //not applicable
-            messageCode = -1;
-            updateNumber = -1;
+            geographicalScope = (pdu[0] & 0xc0) >> 6;
+            serialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff);
             messageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff);
             dataCodingScheme = -1;
             pageIndex = -1;
             nrOfPages = -1;
-            etwsEmergencyUserAlert = (pdu[4] & 0x1) != 0;
-            etwsPopup = (pdu[5] & 0x80) != 0;
-            etwsWarningType = (pdu[4] & 0xfe) >> 1;
+            boolean emergencyUserAlert = (pdu[4] & 0x1) != 0;
+            boolean activatePopup = (pdu[5] & 0x80) != 0;
+            int warningType = (pdu[4] & 0xfe) >> 1;
+            byte[] warningSecurityInfo;
+            // copy the Warning-Security-Information, if present
+            if (pdu.length > PDU_HEADER_LENGTH) {
+                warningSecurityInfo = Arrays.copyOfRange(pdu, 6, pdu.length);
+            } else {
+                warningSecurityInfo = null;
+            }
+            mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup,
+                    warningSecurityInfo);
+            mCmasInfo = null;
+            return;     // skip the ETWS/CMAS initialization code for regular notifications
         } else if (pdu.length <= PDU_LENGTH_GSM) {
             // GSM pdus are no more than 88 bytes
             format = FORMAT_GSM;
             geographicalScope = (pdu[0] & 0xc0) >> 6;
-            messageCode = ((pdu[0] & 0x3f) << 4) | ((pdu[1] & 0xf0) >> 4);
-            updateNumber = pdu[1] & 0x0f;
+            serialNumber = ((pdu[0] & 0xff) << 8) | (pdu[1] & 0xff);
             messageIdentifier = ((pdu[2] & 0xff) << 8) | (pdu[3] & 0xff);
             dataCodingScheme = pdu[4] & 0xff;
 
@@ -113,9 +136,6 @@ public class SmsCbHeader implements SmsCbConstants {
 
             this.pageIndex = pageIndex;
             this.nrOfPages = nrOfPages;
-            etwsEmergencyUserAlert = false;
-            etwsPopup = false;
-            etwsWarningType = -1;
         } else {
             // UMTS pdus are always at least 90 bytes since the payload includes
             // a number-of-pages octet and also one length octet per page
@@ -129,8 +149,7 @@ public class SmsCbHeader implements SmsCbConstants {
 
             messageIdentifier = ((pdu[1] & 0xff) << 8) | pdu[2] & 0xff;
             geographicalScope = (pdu[3] & 0xc0) >> 6;
-            messageCode = ((pdu[3] & 0x3f) << 4) | ((pdu[4] & 0xf0) >> 4);
-            updateNumber = pdu[4] & 0x0f;
+            serialNumber = ((pdu[3] & 0xff) << 8) | (pdu[4] & 0xff);
             dataCodingScheme = pdu[5] & 0xff;
 
             // We will always consider a UMTS message as having one single page
@@ -138,75 +157,251 @@ public class SmsCbHeader implements SmsCbConstants {
             // actual payload may contain several pages.
             pageIndex = 1;
             nrOfPages = 1;
-            etwsEmergencyUserAlert = false;
-            etwsPopup = false;
-            etwsWarningType = -1;
         }
+
+        if (isEtwsMessage()) {
+            boolean emergencyUserAlert = isEtwsEmergencyUserAlert();
+            boolean activatePopup = isEtwsPopupAlert();
+            int warningType = getEtwsWarningType();
+            mEtwsInfo = new SmsCbEtwsInfo(warningType, emergencyUserAlert, activatePopup, null);
+            mCmasInfo = null;
+        } else if (isCmasMessage()) {
+            int messageClass = getCmasMessageClass();
+            int severity = getCmasSeverity();
+            int urgency = getCmasUrgency();
+            int certainty = getCmasCertainty();
+            mEtwsInfo = null;
+            mCmasInfo = new SmsCbCmasInfo(messageClass, SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN,
+                    SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, severity, urgency, certainty);
+        } else {
+            mEtwsInfo = null;
+            mCmasInfo = null;
+        }
+    }
+
+    int getGeographicalScope() {
+        return geographicalScope;
+    }
+
+    int getSerialNumber() {
+        return serialNumber;
+    }
+
+    int getServiceCategory() {
+        return messageIdentifier;
+    }
+
+    int getDataCodingScheme() {
+        return dataCodingScheme;
+    }
+
+    int getPageIndex() {
+        return pageIndex;
+    }
+
+    int getNumberOfPages() {
+        return nrOfPages;
+    }
+
+    SmsCbEtwsInfo getEtwsInfo() {
+        return mEtwsInfo;
+    }
+
+    SmsCbCmasInfo getCmasInfo() {
+        return mCmasInfo;
+    }
+
+    /**
+     * Return whether this broadcast is an emergency (PWS) message type.
+     * @return true if this message is emergency type; false otherwise
+     */
+    boolean isEmergencyMessage() {
+        return messageIdentifier >= SmsCbConstants.MESSAGE_ID_PWS_FIRST_IDENTIFIER
+                && messageIdentifier <= SmsCbConstants.MESSAGE_ID_PWS_LAST_IDENTIFIER;
+    }
+
+    /**
+     * Return whether this broadcast is an ETWS emergency message type.
+     * @return true if this message is ETWS emergency type; false otherwise
+     */
+    private boolean isEtwsMessage() {
+        return (messageIdentifier & SmsCbConstants.MESSAGE_ID_ETWS_TYPE_MASK)
+                == SmsCbConstants.MESSAGE_ID_ETWS_TYPE;
     }
 
     /**
-     * Return whether the specified message ID is an emergency (PWS) message type.
-     * This method is static and takes an argument so that it can be used by
-     * CellBroadcastReceiver, which stores message ID's in SQLite rather than PDU.
-     * @param id the message identifier to check
-     * @return true if the message is emergency type; false otherwise
+     * Return whether this broadcast is an ETWS primary notification.
+     * @return true if this message is an ETWS primary notification; false otherwise
      */
-    public static boolean isEmergencyMessage(int id) {
-        return id >= MESSAGE_ID_PWS_FIRST_IDENTIFIER && id <= MESSAGE_ID_PWS_LAST_IDENTIFIER;
+    boolean isEtwsPrimaryNotification() {
+        return format == FORMAT_ETWS_PRIMARY;
     }
 
     /**
-     * Return whether the specified message ID is an ETWS emergency message type.
-     * This method is static and takes an argument so that it can be used by
-     * CellBroadcastReceiver, which stores message ID's in SQLite rather than PDU.
-     * @param id the message identifier to check
-     * @return true if the message is ETWS emergency type; false otherwise
+     * Return whether this broadcast is in UMTS format.
+     * @return true if this message is in UMTS format; false otherwise
      */
-    public static boolean isEtwsMessage(int id) {
-        return (id & MESSAGE_ID_ETWS_TYPE_MASK) == MESSAGE_ID_ETWS_TYPE;
+    boolean isUmtsFormat() {
+        return format == FORMAT_UMTS;
     }
 
     /**
-     * Return whether the specified message ID is a CMAS emergency message type.
-     * This method is static and takes an argument so that it can be used by
-     * CellBroadcastReceiver, which stores message ID's in SQLite rather than PDU.
-     * @param id the message identifier to check
-     * @return true if the message is CMAS emergency type; false otherwise
+     * Return whether this message is a CMAS emergency message type.
+     * @return true if this message is CMAS emergency type; false otherwise
      */
-    public static boolean isCmasMessage(int id) {
-        return id >= MESSAGE_ID_CMAS_FIRST_IDENTIFIER && id <= MESSAGE_ID_CMAS_LAST_IDENTIFIER;
+    private boolean isCmasMessage() {
+        return messageIdentifier >= SmsCbConstants.MESSAGE_ID_CMAS_FIRST_IDENTIFIER
+                && messageIdentifier <= SmsCbConstants.MESSAGE_ID_CMAS_LAST_IDENTIFIER;
     }
 
     /**
-     * Return whether the specified message code indicates an ETWS popup alert.
-     * This method is static and takes an argument so that it can be used by
-     * CellBroadcastReceiver, which stores message codes in SQLite rather than PDU.
+     * Return whether the popup alert flag is set for an ETWS warning notification.
      * This method assumes that the message ID has already been checked for ETWS type.
      *
-     * @param messageCode the message code to check
      * @return true if the message code indicates a popup alert should be displayed
      */
-    public static boolean isEtwsPopupAlert(int messageCode) {
-        return (messageCode & MESSAGE_CODE_ETWS_ACTIVATE_POPUP) != 0;
+    private boolean isEtwsPopupAlert() {
+        return (serialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_ACTIVATE_POPUP) != 0;
     }
 
     /**
-     * Return whether the specified message code indicates an ETWS emergency user alert.
-     * This method is static and takes an argument so that it can be used by
-     * CellBroadcastReceiver, which stores message codes in SQLite rather than PDU.
+     * Return whether the emergency user alert flag is set for an ETWS warning notification.
      * This method assumes that the message ID has already been checked for ETWS type.
      *
-     * @param messageCode the message code to check
      * @return true if the message code indicates an emergency user alert
      */
-    public static boolean isEtwsEmergencyUserAlert(int messageCode) {
-        return (messageCode & MESSAGE_CODE_ETWS_EMERGENCY_USER_ALERT) != 0;
+    private boolean isEtwsEmergencyUserAlert() {
+        return (serialNumber & SmsCbConstants.SERIAL_NUMBER_ETWS_EMERGENCY_USER_ALERT) != 0;
+    }
+
+    /**
+     * Returns the warning type for an ETWS warning notification.
+     * This method assumes that the message ID has already been checked for ETWS type.
+     *
+     * @return the ETWS warning type defined in 3GPP TS 23.041 section 9.3.24
+     */
+    private int getEtwsWarningType() {
+        return messageIdentifier - SmsCbConstants.MESSAGE_ID_ETWS_EARTHQUAKE_WARNING;
+    }
+
+    /**
+     * Returns the message class for a CMAS warning notification.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS message class as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasMessageClass() {
+        switch (messageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_PRESIDENTIAL_LEVEL:
+                return SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+                return SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+                return SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_CHILD_ABDUCTION_EMERGENCY:
+                return SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_REQUIRED_MONTHLY_TEST:
+                return SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXERCISE:
+                return SmsCbCmasInfo.CMAS_CLASS_CMAS_EXERCISE;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_OPERATOR_DEFINED_USE:
+                return SmsCbCmasInfo.CMAS_CLASS_OPERATOR_DEFINED_USE;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CLASS_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the severity for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS severity as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasSeverity() {
+        switch (messageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+                return SmsCbCmasInfo.CMAS_SEVERITY_EXTREME;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+                return SmsCbCmasInfo.CMAS_SEVERITY_SEVERE;
+
+            default:
+                return SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the urgency for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS urgency as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasUrgency() {
+        switch (messageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+                return SmsCbCmasInfo.CMAS_URGENCY_IMMEDIATE;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+                return SmsCbCmasInfo.CMAS_URGENCY_EXPECTED;
+
+            default:
+                return SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN;
+        }
+    }
+
+    /**
+     * Returns the certainty for a CMAS warning notification. This is only available for extreme
+     * and severe alerts, not for other types such as Presidential Level and AMBER alerts.
+     * This method assumes that the message ID has already been checked for CMAS type.
+     * @return the CMAS certainty as defined in {@link SmsCbCmasInfo}
+     */
+    private int getCmasCertainty() {
+        switch (messageIdentifier) {
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_OBSERVED:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_OBSERVED:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_OBSERVED;
+
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_EXTREME_EXPECTED_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_IMMEDIATE_LIKELY:
+            case SmsCbConstants.MESSAGE_ID_CMAS_ALERT_SEVERE_EXPECTED_LIKELY:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY;
+
+            default:
+                return SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN;
+        }
     }
 
     @Override
     public String toString() {
-        return "SmsCbHeader{GS=" + geographicalScope + ", messageCode=0x" +
-                Integer.toHexString(messageCode) + ", updateNumber=" + updateNumber +
+        return "SmsCbHeader{GS=" + geographicalScope + ", serialNumber=0x" +
+                Integer.toHexString(serialNumber) +
                 ", messageIdentifier=0x" + Integer.toHexString(messageIdentifier) +
                 ", DCS=0x" + Integer.toHexString(dataCodingScheme) +
                 ", page " + pageIndex + " of " + nrOfPages + '}';
diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaSmsCbTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaSmsCbTest.java
new file mode 100644 (file)
index 0000000..81f7905
--- /dev/null
@@ -0,0 +1,570 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.cdma;
+
+import android.os.Parcel;
+import android.telephony.SmsCbCmasInfo;
+import android.telephony.SmsCbMessage;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import com.android.internal.telephony.GsmAlphabet;
+import com.android.internal.telephony.IccUtils;
+import com.android.internal.telephony.cdma.sms.BearerData;
+import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
+import com.android.internal.telephony.cdma.sms.SmsEnvelope;
+import com.android.internal.telephony.cdma.sms.UserData;
+import com.android.internal.util.BitwiseOutputStream;
+
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Test cases for basic SmsCbMessage operation for CDMA.
+ */
+public class CdmaSmsCbTest extends AndroidTestCase {
+
+    /* Copy of private subparameter identifier constants from BearerData class. */
+    private static final byte SUBPARAM_MESSAGE_IDENTIFIER   = (byte) 0x00;
+    private static final byte SUBPARAM_USER_DATA            = (byte) 0x01;
+    private static final byte SUBPARAM_PRIORITY_INDICATOR   = (byte) 0x08;
+    private static final byte SUBPARAM_LANGUAGE_INDICATOR   = (byte) 0x0D;
+
+    /**
+     * Initialize a Parcel for an incoming CDMA cell broadcast. The caller will write the
+     * bearer data and then convert it to an SmsMessage.
+     * @param serviceCategory the CDMA service category
+     * @return the initialized Parcel
+     */
+    private static Parcel createBroadcastParcel(int serviceCategory) {
+        Parcel p = Parcel.obtain();
+
+        p.writeInt(SmsEnvelope.TELESERVICE_NOT_SET);
+        p.writeByte((byte) 1);  // non-zero for MESSAGE_TYPE_BROADCAST
+        p.writeInt(serviceCategory);
+
+        // dummy address (RIL may generate a different dummy address for broadcasts)
+        p.writeInt(CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF);            // sAddress.digit_mode
+        p.writeInt(CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK);    // sAddress.number_mode
+        p.writeInt(CdmaSmsAddress.TON_UNKNOWN);                     // sAddress.number_type
+        p.writeInt(CdmaSmsAddress.NUMBERING_PLAN_ISDN_TELEPHONY);   // sAddress.number_plan
+        p.writeByte((byte) 0);      // sAddress.number_of_digits
+        p.writeInt((byte) 0);       // sSubAddress.subaddressType
+        p.writeByte((byte) 0);      // sSubAddress.odd
+        p.writeByte((byte) 0);      // sSubAddress.number_of_digits
+        return p;
+    }
+
+    /**
+     * Initialize a BitwiseOutputStream with the CDMA bearer data subparameters except for
+     * user data. The caller will append the user data and add it to the parcel.
+     * @param messageId the 16-bit message identifier
+     * @param priority message priority
+     * @param language message language code
+     * @return the initialized BitwiseOutputStream
+     */
+    private static BitwiseOutputStream createBearerDataStream(int messageId, int priority,
+            int language) throws BitwiseOutputStream.AccessException {
+        BitwiseOutputStream bos = new BitwiseOutputStream(10);
+        bos.write(8, SUBPARAM_MESSAGE_IDENTIFIER);
+        bos.write(8, 3);    // length: 3 bytes
+        bos.write(4, BearerData.MESSAGE_TYPE_DELIVER);
+        bos.write(8, ((messageId >>> 8) & 0xff));
+        bos.write(8, (messageId & 0xff));
+        bos.write(1, 0);    // no User Data Header
+        bos.write(3, 0);    // reserved
+
+        if (priority != -1) {
+            bos.write(8, SUBPARAM_PRIORITY_INDICATOR);
+            bos.write(8, 1);    // length: 1 byte
+            bos.write(2, (priority & 0x03));
+            bos.write(6, 0);    // reserved
+        }
+
+        if (language != -1) {
+            bos.write(8, SUBPARAM_LANGUAGE_INDICATOR);
+            bos.write(8, 1);    // length: 1 byte
+            bos.write(8, (language & 0xff));
+        }
+
+        return bos;
+    }
+
+    /**
+     * Write the bearer data array to the parcel, then return a new SmsMessage from the parcel.
+     * @param p the parcel containing the CDMA SMS headers
+     * @param bearerData the bearer data byte array to append to the parcel
+     * @return the new SmsMessage created from the parcel
+     */
+    private static SmsMessage createMessageFromParcel(Parcel p, byte[] bearerData) {
+        p.writeInt(bearerData.length);
+        for (byte b : bearerData) {
+            p.writeByte(b);
+        }
+        p.setDataPosition(0);   // reset position for reading
+        SmsMessage message = SmsMessage.newFromParcel(p);
+        p.recycle();
+        return message;
+    }
+
+    /**
+     * Create a parcel for an incoming CMAS broadcast, then return a new SmsMessage created
+     * from the parcel.
+     * @param serviceCategory the CDMA service category
+     * @param messageId the 16-bit message identifier
+     * @param priority message priority
+     * @param language message language code
+     * @param body message body
+     * @param cmasCategory CMAS category (or -1 to skip adding CMAS type 1 elements record)
+     * @param responseType CMAS response type
+     * @param severity CMAS severity
+     * @param urgency CMAS urgency
+     * @param certainty CMAS certainty
+     * @return the newly created SmsMessage object
+     */
+    private static SmsMessage createCmasSmsMessage(int serviceCategory, int messageId, int priority,
+            int language, int encoding, String body, int cmasCategory, int responseType,
+            int severity, int urgency, int certainty) throws Exception {
+        BitwiseOutputStream cmasBos = new BitwiseOutputStream(10);
+        cmasBos.write(8, 0);    // CMAE protocol version 0
+
+        if (body != null) {
+            cmasBos.write(8, 0);        // Type 0 elements (alert text)
+            encodeBody(encoding, body, true, cmasBos);
+        }
+
+        if (cmasCategory != SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN) {
+            cmasBos.write(8, 1);    // Type 1 elements
+            cmasBos.write(8, 4);    // length: 4 bytes
+            cmasBos.write(8, (cmasCategory & 0xff));
+            cmasBos.write(8, (responseType & 0xff));
+            cmasBos.write(4, (severity & 0x0f));
+            cmasBos.write(4, (urgency & 0x0f));
+            cmasBos.write(4, (certainty & 0x0f));
+            cmasBos.write(4, 0);    // pad to octet boundary
+        }
+
+        byte[] cmasUserData = cmasBos.toByteArray();
+
+        Parcel p = createBroadcastParcel(serviceCategory);
+        BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language);
+
+        bos.write(8, SUBPARAM_USER_DATA);
+        bos.write(8, cmasUserData.length + 2);  // add 2 bytes for msg_encoding and num_fields
+        bos.write(5, UserData.ENCODING_OCTET);
+        bos.write(8, cmasUserData.length);
+        bos.writeByteArray(cmasUserData.length * 8, cmasUserData);
+        bos.write(3, 0);    // pad to byte boundary
+
+        return createMessageFromParcel(p, bos.toByteArray());
+    }
+
+    /**
+     * Create a parcel for an incoming CDMA cell broadcast, then return a new SmsMessage created
+     * from the parcel.
+     * @param serviceCategory the CDMA service category
+     * @param messageId the 16-bit message identifier
+     * @param priority message priority
+     * @param language message language code
+     * @param encoding user data encoding method
+     * @param body the message body
+     * @return the newly created SmsMessage object
+     */
+    private static SmsMessage createBroadcastSmsMessage(int serviceCategory, int messageId,
+            int priority, int language, int encoding, String body) throws Exception {
+        Parcel p = createBroadcastParcel(serviceCategory);
+        BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language);
+
+        bos.write(8, SUBPARAM_USER_DATA);
+        encodeBody(encoding, body, false, bos);
+
+        return createMessageFromParcel(p, bos.toByteArray());
+    }
+
+    /**
+     * Append the message length, encoding, and body to the BearerData output stream.
+     * This is used for writing the User Data subparameter for non-CMAS broadcasts and for
+     * writing the alert text for CMAS broadcasts.
+     * @param encoding one of the CDMA UserData encoding values
+     * @param body the message body
+     * @param isCmasRecord true if this is a CMAS type 0 elements record; false for user data
+     * @param bos the BitwiseOutputStream to write to
+     * @throws Exception on any encoding error
+     */
+    private static void encodeBody(int encoding, String body, boolean isCmasRecord,
+            BitwiseOutputStream bos) throws Exception {
+        if (encoding == UserData.ENCODING_7BIT_ASCII || encoding == UserData.ENCODING_IA5) {
+            int charCount = body.length();
+            int recordBits = (charCount * 7) + 5;       // add 5 bits for char set field
+            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
+            int padBits = (recordOctets * 8) - recordBits;
+
+            if (!isCmasRecord) {
+                recordOctets++;                         // add 8 bits for num_fields
+            }
+
+            bos.write(8, recordOctets);
+            bos.write(5, (encoding & 0x1f));
+
+            if (!isCmasRecord) {
+                bos.write(8, charCount);
+            }
+
+            for (int i = 0; i < charCount; i++) {
+                bos.write(7, body.charAt(i));
+            }
+
+            bos.write(padBits, 0);      // pad to octet boundary
+        } else if (encoding == UserData.ENCODING_GSM_7BIT_ALPHABET
+                || encoding == UserData.ENCODING_GSM_DCS) {
+            // convert to 7-bit packed encoding with septet count in index 0 of byte array
+            byte[] encodedBody = GsmAlphabet.stringToGsm7BitPacked(body);
+
+            int charCount = encodedBody[0];             // septet count
+            int recordBits = (charCount * 7) + 5;       // add 5 bits for char set field
+            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
+            int padBits = (recordOctets * 8) - recordBits;
+
+            if (!isCmasRecord) {
+                recordOctets++;                         // add 8 bits for num_fields
+                if (encoding == UserData.ENCODING_GSM_DCS) {
+                    recordOctets++;                     // add 8 bits for DCS (message type)
+                }
+            }
+
+            bos.write(8, recordOctets);
+            bos.write(5, (encoding & 0x1f));
+
+            if (!isCmasRecord && encoding == UserData.ENCODING_GSM_DCS) {
+                bos.write(8, 0);        // GSM DCS: 7 bit default alphabet, no msg class
+            }
+
+            if (!isCmasRecord) {
+                bos.write(8, charCount);
+            }
+            byte[] bodySeptets = Arrays.copyOfRange(encodedBody, 1, encodedBody.length);
+            bos.writeByteArray(charCount * 7, bodySeptets);
+            bos.write(padBits, 0);      // pad to octet boundary
+        } else if (encoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) {
+            // 6 bit packed encoding with 0x20 offset (ASCII 0x20 - 0x60)
+            int charCount = body.length();
+            int recordBits = (charCount * 6) + 21;      // add 21 bits for header fields
+            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
+            int padBits = (recordOctets * 8) - recordBits;
+
+            bos.write(8, recordOctets);
+
+            bos.write(5, (encoding & 0x1f));
+            bos.write(8, UserData.IS91_MSG_TYPE_SHORT_MESSAGE);
+            bos.write(8, charCount);
+
+            for (int i = 0; i < charCount; i++) {
+                bos.write(6, ((int) body.charAt(i) - 0x20));
+            }
+
+            bos.write(padBits, 0);      // pad to octet boundary
+        } else {
+            byte[] encodedBody;
+            switch (encoding) {
+                case UserData.ENCODING_UNICODE_16:
+                    encodedBody = body.getBytes("UTF-16BE");
+                    break;
+
+                case UserData.ENCODING_SHIFT_JIS:
+                    encodedBody = body.getBytes("Shift_JIS");
+                    break;
+
+                case UserData.ENCODING_KOREAN:
+                    encodedBody = body.getBytes("KSC5601");
+                    break;
+
+                case UserData.ENCODING_LATIN_HEBREW:
+                    encodedBody = body.getBytes("ISO-8859-8");
+                    break;
+
+                case UserData.ENCODING_LATIN:
+                default:
+                    encodedBody = body.getBytes("ISO-8859-1");
+                    break;
+            }
+            int charCount = body.length();              // use actual char count for num fields
+            int recordOctets = encodedBody.length + 1;  // add 1 byte for encoding and pad bits
+            if (!isCmasRecord) {
+                recordOctets++;                         // add 8 bits for num_fields
+            }
+            bos.write(8, recordOctets);
+            bos.write(5, (encoding & 0x1f));
+            if (!isCmasRecord) {
+                bos.write(8, charCount);
+            }
+            bos.writeByteArray(encodedBody.length * 8, encodedBody);
+            bos.write(3, 0);            // pad to octet boundary
+        }
+    }
+
+    private static final String TEST_TEXT = "This is a test CDMA cell broadcast message..."
+            + "678901234567890123456789012345678901234567890";
+
+    private static final String PRES_ALERT =
+            "THE PRESIDENT HAS ISSUED AN EMERGENCY ALERT. CHECK LOCAL MEDIA FOR MORE DETAILS";
+
+    private static final String EXTREME_ALERT = "FLASH FLOOD WARNING FOR SOUTH COCONINO COUNTY"
+            + " - NORTH CENTRAL ARIZONA UNTIL 415 PM MST";
+
+    private static final String SEVERE_ALERT = "SEVERE WEATHER WARNING FOR SOMERSET COUNTY"
+            + " - NEW JERSEY UNTIL 415 PM MST";
+
+    private static final String AMBER_ALERT =
+            "AMBER ALERT:Mountain View,CA VEH'07 Blue Honda Civic CA LIC 5ABC123";
+
+    private static final String MONTHLY_TEST_ALERT = "This is a test of the emergency alert system."
+            + " This is only a test. 89012345678901234567890";
+
+    private static final String IS91_TEXT = "IS91 SHORT MSG";   // max length 14 chars
+
+    /**
+     * Verify that the SmsCbMessage has the correct values for CDMA.
+     * @param cbMessage the message to test
+     */
+    private static void verifyCbValues(SmsCbMessage cbMessage) {
+        assertEquals(SmsCbMessage.MESSAGE_FORMAT_3GPP2, cbMessage.getMessageFormat());
+        assertEquals(SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, cbMessage.getGeographicalScope());
+        assertEquals(false, cbMessage.isEtwsMessage()); // ETWS on CDMA not currently supported
+    }
+
+    private static void doTestNonEmergencyBroadcast(int encoding) throws Exception {
+        SmsMessage msg = createBroadcastSmsMessage(123, 456, BearerData.PRIORITY_NORMAL,
+                BearerData.LANGUAGE_ENGLISH, encoding, TEST_TEXT);
+
+        SmsCbMessage cbMessage = msg.parseBroadcastSms();
+        verifyCbValues(cbMessage);
+        assertEquals(123, cbMessage.getServiceCategory());
+        assertEquals(456, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_NORMAL, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(TEST_TEXT, cbMessage.getMessageBody());
+        assertEquals(false, cbMessage.isEmergencyMessage());
+        assertEquals(false, cbMessage.isCmasMessage());
+    }
+
+    public void testNonEmergencyBroadcast7bitAscii() throws Exception {
+        doTestNonEmergencyBroadcast(UserData.ENCODING_7BIT_ASCII);
+    }
+
+    public void testNonEmergencyBroadcast7bitGsm() throws Exception {
+        doTestNonEmergencyBroadcast(UserData.ENCODING_GSM_7BIT_ALPHABET);
+    }
+
+    public void testNonEmergencyBroadcast16bitUnicode() throws Exception {
+        doTestNonEmergencyBroadcast(UserData.ENCODING_UNICODE_16);
+    }
+
+    public void testNonEmergencyBroadcastIs91Extended() throws Exception {
+        // IS-91 doesn't support language or priority subparameters, max 14 chars text
+        SmsMessage msg = createBroadcastSmsMessage(987, 654, -1, -1,
+                UserData.ENCODING_IS91_EXTENDED_PROTOCOL, IS91_TEXT);
+
+        SmsCbMessage cbMessage = msg.parseBroadcastSms();
+        verifyCbValues(cbMessage);
+        assertEquals(987, cbMessage.getServiceCategory());
+        assertEquals(654, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_NORMAL, cbMessage.getMessagePriority());
+        assertEquals(null, cbMessage.getLanguageCode());
+        assertEquals(IS91_TEXT, cbMessage.getMessageBody());
+        assertEquals(false, cbMessage.isEmergencyMessage());
+        assertEquals(false, cbMessage.isCmasMessage());
+    }
+
+    private static void doTestCmasBroadcast(int serviceCategory, int messageClass, String body)
+            throws Exception {
+        SmsMessage msg = createCmasSmsMessage(
+                serviceCategory, 1234, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                UserData.ENCODING_7BIT_ASCII, body, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage = msg.parseBroadcastSms();
+        verifyCbValues(cbMessage);
+        assertEquals(serviceCategory, cbMessage.getServiceCategory());
+        assertEquals(1234, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(body, cbMessage.getMessageBody());
+        assertEquals(true, cbMessage.isEmergencyMessage());
+        assertEquals(true, cbMessage.isCmasMessage());
+        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
+        assertEquals(messageClass, cmasInfo.getMessageClass());
+        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory());
+        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType());
+        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity());
+        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency());
+        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty());
+    }
+
+    public void testCmasPresidentialAlert() throws Exception {
+        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
+                SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, PRES_ALERT);
+    }
+
+    public void testCmasExtremeAlert() throws Exception {
+        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
+                SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, EXTREME_ALERT);
+    }
+
+    public void testCmasSevereAlert() throws Exception {
+        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT,
+                SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT, SEVERE_ALERT);
+    }
+
+    public void testCmasAmberAlert() throws Exception {
+        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY,
+                SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY, AMBER_ALERT);
+    }
+
+    public void testCmasTestMessage() throws Exception {
+        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE,
+                SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST, MONTHLY_TEST_ALERT);
+    }
+
+    public void testCmasExtremeAlertType1Elements() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
+                5678, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                UserData.ENCODING_7BIT_ASCII, EXTREME_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_ENV,
+                SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, SmsCbCmasInfo.CMAS_SEVERITY_SEVERE,
+                SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY);
+
+        SmsCbMessage cbMessage = msg.parseBroadcastSms();
+        verifyCbValues(cbMessage);
+        assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
+                cbMessage.getServiceCategory());
+        assertEquals(5678, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(EXTREME_ALERT, cbMessage.getMessageBody());
+        assertEquals(true, cbMessage.isEmergencyMessage());
+        assertEquals(true, cbMessage.isCmasMessage());
+        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
+        assertEquals(SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, cmasInfo.getMessageClass());
+        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_ENV, cmasInfo.getCategory());
+        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, cmasInfo.getResponseType());
+        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_SEVERE, cmasInfo.getSeverity());
+        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, cmasInfo.getUrgency());
+        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY, cmasInfo.getCertainty());
+    }
+
+    // VZW requirement is to discard message with unsupported charset. Verify that we return null
+    // for this unsupported character set.
+    public void testCmasUnsupportedCharSet() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
+                12345, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                UserData.ENCODING_GSM_DCS, EXTREME_ALERT, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage = msg.parseBroadcastSms();
+        assertNull("expected null for unsupported charset", cbMessage);
+    }
+
+    // VZW requirement is to discard message with unsupported charset. Verify that we return null
+    // for this unsupported character set.
+    public void testCmasUnsupportedCharSet2() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
+                67890, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                UserData.ENCODING_KOREAN, EXTREME_ALERT, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage = msg.parseBroadcastSms();
+        assertNull("expected null for unsupported charset", cbMessage);
+    }
+
+    // VZW requirement is to discard message without record type 0. The framework will decode it
+    // and the app will discard it.
+    public void testCmasNoRecordType0() throws Exception {
+        SmsMessage msg = createCmasSmsMessage(
+                SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT, 1234,
+                BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
+                UserData.ENCODING_7BIT_ASCII, null, -1, -1, -1, -1, -1);
+
+        SmsCbMessage cbMessage = msg.parseBroadcastSms();
+        verifyCbValues(cbMessage);
+        assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
+                cbMessage.getServiceCategory());
+        assertEquals(1234, cbMessage.getSerialNumber());
+        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
+        assertEquals("en", cbMessage.getLanguageCode());
+        assertEquals(null, cbMessage.getMessageBody());
+        assertEquals(true, cbMessage.isEmergencyMessage());
+        assertEquals(true, cbMessage.isCmasMessage());
+        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
+        assertEquals(SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, cmasInfo.getMessageClass());
+        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory());
+        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType());
+        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity());
+        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency());
+        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty());
+    }
+
+    // Make sure we don't throw an exception if we feed completely random data to BearerStream.
+    public void testRandomBearerStreamData() {
+        Random r = new Random(54321);
+        for (int run = 0; run < 10000; run++) {
+            int len = r.nextInt(140);
+            byte[] data = new byte[len];
+            for (int i = 0; i < len; i++) {
+                data[i] = (byte) r.nextInt(256);
+            }
+            // Log.d("CdmaSmsCbTest", "trying random bearer data run " + run + " length " + len);
+            try {
+                int category = 0x0ff0 + r.nextInt(32);  // half CMAS, half non-CMAS
+                Parcel p = createBroadcastParcel(category);
+                SmsMessage msg = createMessageFromParcel(p, data);
+                SmsCbMessage cbMessage = msg.parseBroadcastSms();
+                // with random input, cbMessage will almost always be null (log when it isn't)
+                if (cbMessage != null) {
+                    Log.d("CdmaSmsCbTest", "success: " + cbMessage);
+                }
+            } catch (Exception e) {
+                Log.d("CdmaSmsCbTest", "exception thrown", e);
+                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
+            }
+        }
+    }
+
+    // Make sure we don't throw an exception if we put random data in the UserData subparam.
+    public void testRandomUserData() {
+        Random r = new Random(94040);
+        for (int run = 0; run < 10000; run++) {
+            int category = 0x0ff0 + r.nextInt(32);  // half CMAS, half non-CMAS
+            Parcel p = createBroadcastParcel(category);
+            int len = r.nextInt(140);
+            // Log.d("CdmaSmsCbTest", "trying random user data run " + run + " length " + len);
+
+            try {
+                BitwiseOutputStream bos = createBearerDataStream(r.nextInt(65536), r.nextInt(4),
+                        r.nextInt(256));
+
+                bos.write(8, SUBPARAM_USER_DATA);
+                bos.write(8, len);
+
+                for (int i = 0; i < len; i++) {
+                    bos.write(8, r.nextInt(256));
+                }
+
+                SmsMessage msg = createMessageFromParcel(p, bos.toByteArray());
+                SmsCbMessage cbMessage = msg.parseBroadcastSms();
+            } catch (Exception e) {
+                Log.d("CdmaSmsCbTest", "exception thrown", e);
+                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
+            }
+        }
+    }
+}
  * limitations under the License.
  */
 
-package com.android.internal.telephony;
+package com.android.internal.telephony.gsm;
 
+import android.telephony.SmsCbEtwsInfo;
+import android.telephony.SmsCbLocation;
 import android.telephony.SmsCbMessage;
 import android.test.AndroidTestCase;
 import android.util.Log;
 
+import com.android.internal.telephony.IccUtils;
+
+import java.util.Random;
+
 /**
  * Test cases for basic SmsCbMessage operations
  */
 public class GsmSmsCbTest extends AndroidTestCase {
 
-    private void doTestGeographicalScopeValue(byte[] pdu, byte b, int expectedGs) {
+    private static final String TAG = "GsmSmsCbTest";
+
+    private static final SmsCbLocation sTestLocation = new SmsCbLocation("94040", 1234, 5678);
+
+    private static SmsCbMessage createFromPdu(byte[] pdu) {
+        try {
+            SmsCbHeader header = new SmsCbHeader(pdu);
+            byte[][] pdus = new byte[1][];
+            pdus[0] = pdu;
+            return GsmSmsCbMessage.createSmsCbMessage(header, sTestLocation, pdus);
+        } catch (IllegalArgumentException e) {
+            return null;
+        }
+    }
+
+    private static void doTestGeographicalScopeValue(byte[] pdu, byte b, int expectedGs) {
         pdu[0] = b;
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected geographical scope decoded", expectedGs, msg
                 .getGeographicalScope());
     }
 
     public void testCreateNullPdu() {
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(null);
-
+        SmsCbMessage msg = createFromPdu(null);
         assertNull("createFromPdu(byte[] with null pdu should return null", msg);
     }
 
     public void testCreateTooShortPdu() {
         byte[] pdu = new byte[4];
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertNull("createFromPdu(byte[] with too short pdu should return null", msg);
     }
@@ -94,7 +114,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x34
         };
 
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected geographical scope decoded",
                 SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE, msg.getGeographicalScope());
@@ -116,7 +136,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D,
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A GSM default alphabet message with carriage return padding",
@@ -146,7 +166,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
 
                 (byte)0x34
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A GSM default alphabet message with carriage return padding",
@@ -193,7 +213,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
 
                 (byte)0x0A
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected multipage 7-bit string decoded",
                 "First page+Second page",
@@ -216,7 +236,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x90, (byte)0xFB, (byte)0x0D, (byte)0x82, (byte)0x87, (byte)0xC9, (byte)0xE4,
                 (byte)0xB4, (byte)0xFB, (byte)0x1C, (byte)0x02
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals(
                 "Unexpected 7-bit string decoded",
@@ -248,7 +268,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
 
                 (byte)0x52
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals(
                 "Unexpected 7-bit string decoded",
@@ -273,7 +293,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D,
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A GSM default alphabet message with carriage return padding",
@@ -298,7 +318,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x68, (byte)0x34, (byte)0x1A, (byte)0x8D,
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A GSM default alphabet message with carriage return padding",
@@ -330,7 +350,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
 
                 (byte)0x37
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A GSM default alphabet message with carriage return padding",
@@ -355,7 +375,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45, (byte)0x46, (byte)0x47, (byte)0x41,
                 (byte)0x42, (byte)0x43, (byte)0x44, (byte)0x45
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("8-bit message body should be empty", "", msg.getMessageBody());
     }
@@ -376,7 +396,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x63, (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x65, (byte)0x00, (byte)0x72,
                 (byte)0x00, (byte)0x0D, (byte)0x00, (byte)0x0D
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A UCS2 message containing a \u0434 character", msg.getMessageBody());
@@ -405,7 +425,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
 
                 (byte)0x4E
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A UCS2 message containing a \u0434 character", msg.getMessageBody());
@@ -451,7 +471,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
 
                 (byte)0x06
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected multipage UCS2 string decoded",
                 "AAABBB", msg.getMessageBody());
@@ -473,7 +493,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x61, (byte)0x00, (byte)0x63, (byte)0x00, (byte)0x74, (byte)0x00, (byte)0x65,
                 (byte)0x00, (byte)0x72, (byte)0x00, (byte)0x0D
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A UCS2 message containing a \u0434 character", msg.getMessageBody());
@@ -504,7 +524,7 @@ public class GsmSmsCbTest extends AndroidTestCase {
 
                 (byte)0x50
         };
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
         assertEquals("Unexpected 7-bit string decoded",
                 "A UCS2 message containing a \u0434 character", msg.getMessageBody());
@@ -529,9 +549,9 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00
         };
 
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
-        assertEquals("Unexpected message identifier decoded", 12345, msg.getMessageIdentifier());
+        assertEquals("Unexpected message identifier decoded", 12345, msg.getServiceCategory());
     }
 
     public void testGetMessageIdentifierUmts() {
@@ -558,9 +578,9 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x34
         };
 
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
 
-        assertEquals("Unexpected message identifier decoded", 12345, msg.getMessageIdentifier());
+        assertEquals("Unexpected message identifier decoded", 12345, msg.getServiceCategory());
     }
 
     public void testGetMessageCode() {
@@ -580,9 +600,10 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00
         };
 
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
+        int messageCode = (msg.getSerialNumber() & 0x3ff0) >> 4;
 
-        assertEquals("Unexpected message code decoded", 682, msg.getMessageCode());
+        assertEquals("Unexpected message code decoded", 682, messageCode);
     }
 
     public void testGetMessageCodeUmts() {
@@ -609,9 +630,10 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x34
         };
 
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
+        int messageCode = (msg.getSerialNumber() & 0x3ff0) >> 4;
 
-        assertEquals("Unexpected message code decoded", 682, msg.getMessageCode());
+        assertEquals("Unexpected message code decoded", 682, messageCode);
     }
 
     public void testGetUpdateNumber() {
@@ -631,9 +653,10 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x46, (byte)0xA3, (byte)0xD1, (byte)0x00
         };
 
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
+        int updateNumber = msg.getSerialNumber() & 0x000f;
 
-        assertEquals("Unexpected update number decoded", 5, msg.getUpdateNumber());
+        assertEquals("Unexpected update number decoded", 5, updateNumber);
     }
 
     public void testGetUpdateNumberUmts() {
@@ -660,9 +683,10 @@ public class GsmSmsCbTest extends AndroidTestCase {
                 (byte)0x34
         };
 
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(pdu);
+        SmsCbMessage msg = createFromPdu(pdu);
+        int updateNumber = msg.getSerialNumber() & 0x000f;
 
-        assertEquals("Unexpected update number decoded", 5, msg.getUpdateNumber());
+        assertEquals("Unexpected update number decoded", 5, updateNumber);
     }
 
     /* ETWS Test message including header */
@@ -684,29 +708,51 @@ public class GsmSmsCbTest extends AndroidTestCase {
     // FIXME: add example of ETWS primary notification PDU
 
     public void testEtwsMessageNormal() {
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(etwsMessageNormal);
-        Log.d("GsmSmsCbTest", msg.toString());
+        SmsCbMessage msg = createFromPdu(etwsMessageNormal);
+        Log.d(TAG, msg.toString());
         assertEquals("GS mismatch", 0, msg.getGeographicalScope());
-        assertEquals("message code mismatch", 0, msg.getMessageCode());
-        assertEquals("update number mismatch", 0, msg.getUpdateNumber());
-        assertEquals("message ID mismatch", 0x1100, msg.getMessageIdentifier());
+        assertEquals("serial number mismatch", 0, msg.getSerialNumber());
+        assertEquals("message ID mismatch", 0x1100, msg.getServiceCategory());
+        assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE,
+                msg.getEtwsWarningInfo().getWarningType());
     }
 
     public void testEtwsMessageCancel() {
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(etwsMessageCancel);
-        Log.d("GsmSmsCbTest", msg.toString());
+        SmsCbMessage msg = createFromPdu(etwsMessageCancel);
+        Log.d(TAG, msg.toString());
         assertEquals("GS mismatch", 0, msg.getGeographicalScope());
-        assertEquals("message code mismatch", 0, msg.getMessageCode());
-        assertEquals("update number mismatch", 0, msg.getUpdateNumber());
-        assertEquals("message ID mismatch", 0x1100, msg.getMessageIdentifier());
+        assertEquals("serial number mismatch", 0, msg.getSerialNumber());
+        assertEquals("message ID mismatch", 0x1100, msg.getServiceCategory());
+        assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_EARTHQUAKE,
+                msg.getEtwsWarningInfo().getWarningType());
     }
 
     public void testEtwsMessageTest() {
-        SmsCbMessage msg = SmsCbMessage.createFromPdu(etwsMessageTest);
-        Log.d("GsmSmsCbTest", msg.toString());
+        SmsCbMessage msg = createFromPdu(etwsMessageTest);
+        Log.d(TAG, msg.toString());
         assertEquals("GS mismatch", 0, msg.getGeographicalScope());
-        assertEquals("message code mismatch", 0, msg.getMessageCode());
-        assertEquals("update number mismatch", 0, msg.getUpdateNumber());
-        assertEquals("message ID mismatch", 0x1103, msg.getMessageIdentifier());
+        assertEquals("serial number mismatch", 0, msg.getSerialNumber());
+        assertEquals("message ID mismatch", 0x1103, msg.getServiceCategory());
+        assertEquals("warning type mismatch", SmsCbEtwsInfo.ETWS_WARNING_TYPE_TEST_MESSAGE,
+                msg.getEtwsWarningInfo().getWarningType());
+    }
+
+    // Make sure we don't throw an exception if we feed random data to the PDU parser.
+    public void testRandomPdus() {
+        Random r = new Random(94040);
+        for (int run = 0; run < 10000; run++) {
+            int len = r.nextInt(140);
+            byte[] data = new byte[len];
+            for (int i = 0; i < len; i++) {
+                data[i] = (byte) r.nextInt(256);
+            }
+            try {
+                // this should return a SmsCbMessage object or null for invalid data
+                SmsCbMessage msg = createFromPdu(data);
+            } catch (Exception e) {
+                Log.d(TAG, "exception thrown", e);
+                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
+            }
+        }
     }
 }