diff options
| author | 2012-03-14 13:25:18 -0700 | |
|---|---|---|
| committer | 2012-03-14 13:25:18 -0700 | |
| commit | f98f68d5ade7af04bf8544e9ff7b3213a908bbf5 (patch) | |
| tree | fe8f303464bc6fdf86863a1bbe3e2fe9c9f8d238 | |
| parent | 89ea4ca9c26f3c7e365525a0b83500e85517a457 (diff) | |
| parent | 3e3c3f80a90b156ff500076f8655647dfb317acf (diff) | |
Merge "Add support for CMAS warning notifications over CDMA."
20 files changed, 3063 insertions, 579 deletions
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 0e6d07d8dd18..961215184170 100755 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -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. diff --git a/core/java/com/android/internal/util/BitwiseOutputStream.java b/core/java/com/android/internal/util/BitwiseOutputStream.java index 70c0be81a56a..ddecbed1d97c 100644 --- a/core/java/com/android/internal/util/BitwiseOutputStream.java +++ b/core/java/com/android/internal/util/BitwiseOutputStream.java @@ -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; } /** diff --git a/core/tests/coretests/src/com/android/internal/util/BitwiseStreamsTest.java b/core/tests/coretests/src/com/android/internal/util/BitwiseStreamsTest.java index a304b68145df..306f58fcce2e 100644 --- a/core/tests/coretests/src/com/android/internal/util/BitwiseStreamsTest.java +++ b/core/tests/coretests/src/com/android/internal/util/BitwiseStreamsTest.java @@ -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 index 000000000000..7a89d94ab2ef --- /dev/null +++ b/telephony/java/android/telephony/SmsCbCmasInfo.java @@ -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 index 000000000000..0890d528f866 --- /dev/null +++ b/telephony/java/android/telephony/SmsCbEtwsInfo.java @@ -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 index 000000000000..7b5bd0d4bb71 --- /dev/null +++ b/telephony/java/android/telephony/SmsCbLocation.java @@ -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; + } +} diff --git a/telephony/java/android/telephony/SmsCbMessage.java b/telephony/java/android/telephony/SmsCbMessage.java index 383e0f9c90c4..046bf8c700eb 100644 --- a/telephony/java/android/telephony/SmsCbMessage.java +++ b/telephony/java/android/telephony/SmsCbMessage.java @@ -16,444 +16,367 @@ 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 index 000000000000..f94efd8f56d7 --- /dev/null +++ b/telephony/java/android/telephony/cdma/CdmaSmsCbProgramData.java @@ -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]; + } + }; +} diff --git a/telephony/java/com/android/internal/telephony/SMSDispatcher.java b/telephony/java/com/android/internal/telephony/SMSDispatcher.java index a42a267a1fda..d6f96ff6fe96 100644 --- a/telephony/java/com/android/internal/telephony/SMSDispatcher.java +++ b/telephony/java/com/android/internal/telephony/SMSDispatcher.java @@ -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; @@ -339,7 +340,7 @@ public abstract class SMSDispatcher extends Handler { * @param intent intent to broadcast * @param permission Receivers are required to have this permission */ - void dispatch(Intent intent, String permission) { + public void dispatch(Intent intent, String permission) { // Hold a wake lock for WAKE_LOCK_TIMEOUT seconds, enough to give any // receivers time to take their own wake locks. mWakeLock.acquire(WAKE_LOCK_TIMEOUT); @@ -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); } } diff --git a/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java b/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java index ca8d9ae11617..bfa23e7be8f3 100755 --- a/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java +++ b/telephony/java/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java @@ -30,8 +30,10 @@ 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.telephony.cdma.CdmaSmsCbProgramData; import android.util.Log; import com.android.internal.telephony.CommandsInterface; @@ -50,6 +52,7 @@ import com.android.internal.util.HexDump; import java.io.ByteArrayOutputStream; import java.util.Arrays; import java.util.HashMap; +import java.util.List; import android.content.res.Resources; @@ -97,6 +100,24 @@ final class CdmaSMSDispatcher extends SMSDispatcher { } } + /** + * Dispatch service category program data to the CellBroadcastReceiver app, which filters + * the broadcast alerts to display. + * @param sms the SMS message containing one or more + * {@link android.telephony.cdma.CdmaSmsCbProgramData} objects. + */ + private void handleServiceCategoryProgramData(SmsMessage sms) { + List<CdmaSmsCbProgramData> programDataList = sms.getSmsCbProgramData(); + if (programDataList == null) { + Log.e(TAG, "handleServiceCategoryProgramData: program data list is null!"); + return; + } + + Intent intent = new Intent(Intents.SMS_SERVICE_CATEGORY_PROGRAM_DATA_RECEIVED_ACTION); + intent.putExtra("program_data_list", (CdmaSmsCbProgramData[]) programDataList.toArray()); + dispatch(intent, RECEIVE_SMS_PERMISSION); + } + /** {@inheritDoc} */ @Override public int dispatchMessage(SmsMessageBase smsb) { @@ -119,8 +140,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 +181,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"); diff --git a/telephony/java/com/android/internal/telephony/cdma/SmsMessage.java b/telephony/java/com/android/internal/telephony/cdma/SmsMessage.java index 1409cab2aa2a..a723de3543c3 100644 --- a/telephony/java/com/android/internal/telephony/cdma/SmsMessage.java +++ b/telephony/java/com/android/internal/telephony/cdma/SmsMessage.java @@ -19,7 +19,11 @@ 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.telephony.cdma.CdmaSmsCbProgramData; import android.util.Log; + import com.android.internal.telephony.IccUtils; import com.android.internal.telephony.SmsHeader; import com.android.internal.telephony.SmsMessageBase; @@ -38,6 +42,9 @@ import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.util.List; + +import static android.telephony.SmsMessage.MessageClass; /** * TODO(cleanup): these constants are disturbing... are they not just @@ -47,12 +54,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 +193,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 +734,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() { @@ -961,4 +986,12 @@ public class SmsMessage extends SmsMessageBase { return output.toByteArray(); } + + /** + * Returns the list of service category program data, if present. + * @return a list of CdmaSmsCbProgramData objects, or null if not present + */ + List<CdmaSmsCbProgramData> getSmsCbProgramData() { + return mBearerData.serviceCategoryProgramData; + } } diff --git a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java index 6743da01f1d3..e70ff1816980 100755 --- a/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java +++ b/telephony/java/com/android/internal/telephony/cdma/sms/BearerData.java @@ -16,28 +16,29 @@ 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)) diff --git a/telephony/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java b/telephony/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java index 4a60231a451e..f73df56a9ba6 100644 --- a/telephony/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java +++ b/telephony/java/com/android/internal/telephony/cdma/sms/SmsEnvelope.java @@ -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 diff --git a/telephony/java/com/android/internal/telephony/cdma/sms/UserData.java b/telephony/java/com/android/internal/telephony/cdma/sms/UserData.java index 189d97db0f75..599c2b393a6b 100644 --- a/telephony/java/com/android/internal/telephony/cdma/sms/UserData.java +++ b/telephony/java/com/android/internal/telephony/cdma/sms/UserData.java @@ -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; diff --git a/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java index 8a75f51ff5ff..931c662d27e9 100644 --- a/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java +++ b/telephony/java/com/android/internal/telephony/gsm/GsmSMSDispatcher.java @@ -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 index 000000000000..dc9554a56d80 --- /dev/null +++ b/telephony/java/com/android/internal/telephony/gsm/GsmSmsCbMessage.java @@ -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); + } +} diff --git a/telephony/java/android/telephony/SmsCbConstants.java b/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java index a1b4adf19baf..ebb4666f91f3 100644 --- a/telephony/java/android/telephony/SmsCbConstants.java +++ b/telephony/java/com/android/internal/telephony/gsm/SmsCbConstants.java @@ -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. @@ -14,14 +14,22 @@ * 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; diff --git a/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java index 8e6b79b322da..569204489c45 100644 --- a/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java +++ b/telephony/java/com/android/internal/telephony/gsm/SmsCbHeader.java @@ -16,28 +16,42 @@ 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 index 000000000000..3c9c0b2cbaa3 --- /dev/null +++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/cdma/CdmaSmsCbTest.java @@ -0,0 +1,708 @@ +/* + * 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.telephony.cdma.CdmaSmsCbProgramData; +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.List; +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; + private static final byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA = 0x12; + + /** + * 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 < 1000; 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 < 1000; 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); + } + } + } + + /** + * Initialize a Parcel for incoming Service Category Program Data teleservice. The caller will + * write the bearer data and then convert it to an SmsMessage. + * @return the initialized Parcel + */ + private static Parcel createServiceCategoryProgramDataParcel() { + Parcel p = Parcel.obtain(); + + p.writeInt(SmsEnvelope.TELESERVICE_SCPT); + p.writeByte((byte) 0); // non-zero for MESSAGE_TYPE_BROADCAST + p.writeInt(0); + + // 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; + } + + private static final String CAT_EXTREME_THREAT = "Extreme Threat to Life and Property"; + private static final String CAT_SEVERE_THREAT = "Severe Threat to Life and Property"; + private static final String CAT_AMBER_ALERTS = "AMBER Alerts"; + + public void testServiceCategoryProgramDataAddCategory() throws Exception { + Parcel p = createServiceCategoryProgramDataParcel(); + BitwiseOutputStream bos = createBearerDataStream(123, -1, -1); + + int categoryNameLength = CAT_EXTREME_THREAT.length(); + int subparamLengthBits = (53 + (categoryNameLength * 7)); + int subparamLengthBytes = (subparamLengthBits + 7) / 8; + int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits; + + bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA); + bos.write(8, subparamLengthBytes); + bos.write(5, UserData.ENCODING_7BIT_ASCII); + + bos.write(4, CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT >>> 8)); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT & 0xff)); + bos.write(8, BearerData.LANGUAGE_ENGLISH); + bos.write(8, 100); // max messages + bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT); + + bos.write(8, categoryNameLength); + for (int i = 0; i < categoryNameLength; i++) { + bos.write(7, CAT_EXTREME_THREAT.charAt(i)); + } + bos.write(subparamPadBits, 0); + + SmsMessage msg = createMessageFromParcel(p, bos.toByteArray()); + assertNotNull(msg); + msg.parseSms(); + List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData(); + assertNotNull(programDataList); + assertEquals(1, programDataList.size()); + CdmaSmsCbProgramData programData = programDataList.get(0); + assertEquals(CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY, programData.getOperation()); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, programData.getCategory()); + assertEquals(CAT_EXTREME_THREAT, programData.getCategoryName()); + assertEquals("en", programData.getLanguageCode()); + assertEquals(100, programData.getMaxMessages()); + assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT, programData.getAlertOption()); + } + + public void testServiceCategoryProgramDataDeleteTwoCategories() throws Exception { + Parcel p = createServiceCategoryProgramDataParcel(); + BitwiseOutputStream bos = createBearerDataStream(456, -1, -1); + + int category1NameLength = CAT_SEVERE_THREAT.length(); + int category2NameLength = CAT_AMBER_ALERTS.length(); + + int subparamLengthBits = (101 + (category1NameLength * 7) + (category2NameLength * 7)); + int subparamLengthBytes = (subparamLengthBits + 7) / 8; + int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits; + + bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA); + bos.write(8, subparamLengthBytes); + bos.write(5, UserData.ENCODING_7BIT_ASCII); + + bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT >>> 8)); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT & 0xff)); + bos.write(8, BearerData.LANGUAGE_ENGLISH); + bos.write(8, 0); // max messages + bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT); + + bos.write(8, category1NameLength); + for (int i = 0; i < category1NameLength; i++) { + bos.write(7, CAT_SEVERE_THREAT.charAt(i)); + } + + bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY >>> 8)); + bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY & 0xff)); + bos.write(8, BearerData.LANGUAGE_ENGLISH); + bos.write(8, 0); // max messages + bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT); + + bos.write(8, category2NameLength); + for (int i = 0; i < category2NameLength; i++) { + bos.write(7, CAT_AMBER_ALERTS.charAt(i)); + } + + bos.write(subparamPadBits, 0); + + SmsMessage msg = createMessageFromParcel(p, bos.toByteArray()); + assertNotNull(msg); + msg.parseSms(); + List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData(); + assertNotNull(programDataList); + assertEquals(2, programDataList.size()); + + CdmaSmsCbProgramData programData = programDataList.get(0); + assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation()); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT, programData.getCategory()); + assertEquals(CAT_SEVERE_THREAT, programData.getCategoryName()); + assertEquals("en", programData.getLanguageCode()); + assertEquals(0, programData.getMaxMessages()); + assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption()); + + programData = programDataList.get(1); + assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation()); + assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY, + programData.getCategory()); + assertEquals(CAT_AMBER_ALERTS, programData.getCategoryName()); + assertEquals("en", programData.getLanguageCode()); + assertEquals(0, programData.getMaxMessages()); + assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption()); + } +} diff --git a/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsCbTest.java b/telephony/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmSmsCbTest.java index 417aac4cca92..82c6944e13f6 100644 --- a/telephony/tests/telephonytests/src/com/android/internal/telephony/GsmSmsCbTest.java +++ b/telephony/tests/telephonytests/src/com/android/internal/telephony/gsm/GsmSmsCbTest.java @@ -14,34 +14,54 @@ * 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); + } + } } } |