1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.telephony.cdma;
18
19import android.os.Parcel;
20import android.telephony.SmsCbCmasInfo;
21import android.telephony.SmsCbMessage;
22import android.telephony.cdma.CdmaSmsCbProgramData;
23import android.test.AndroidTestCase;
24import android.telephony.Rlog;
25import android.test.suitebuilder.annotation.SmallTest;
26
27import com.android.internal.telephony.GsmAlphabet;
28import com.android.internal.telephony.cdma.sms.BearerData;
29import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
30import com.android.internal.telephony.cdma.sms.SmsEnvelope;
31import com.android.internal.telephony.cdma.sms.UserData;
32import com.android.internal.telephony.uicc.IccUtils;
33import com.android.internal.util.BitwiseOutputStream;
34
35import org.junit.Test;
36
37import java.util.Arrays;
38import java.util.List;
39import java.util.Random;
40
41/**
42 * Test cases for basic SmsCbMessage operation for CDMA.
43 */
44public class CdmaSmsCbTest extends AndroidTestCase {
45
46    /* Copy of private subparameter identifier constants from BearerData class. */
47    private static final byte SUBPARAM_MESSAGE_IDENTIFIER   = (byte) 0x00;
48    private static final byte SUBPARAM_USER_DATA            = (byte) 0x01;
49    private static final byte SUBPARAM_PRIORITY_INDICATOR   = (byte) 0x08;
50    private static final byte SUBPARAM_LANGUAGE_INDICATOR   = (byte) 0x0D;
51    private static final byte SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA    = 0x12;
52
53    /**
54     * Initialize a Parcel for an incoming CDMA cell broadcast. The caller will write the
55     * bearer data and then convert it to an SmsMessage.
56     * @param serviceCategory the CDMA service category
57     * @return the initialized Parcel
58     */
59    private static Parcel createBroadcastParcel(int serviceCategory) {
60        Parcel p = Parcel.obtain();
61
62        p.writeInt(SmsEnvelope.TELESERVICE_NOT_SET);
63        p.writeByte((byte) 1);  // non-zero for MESSAGE_TYPE_BROADCAST
64        p.writeInt(serviceCategory);
65
66        // dummy address (RIL may generate a different dummy address for broadcasts)
67        p.writeInt(CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF);            // sAddress.digit_mode
68        p.writeInt(CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK);    // sAddress.number_mode
69        p.writeInt(CdmaSmsAddress.TON_UNKNOWN);                     // sAddress.number_type
70        p.writeInt(CdmaSmsAddress.NUMBERING_PLAN_ISDN_TELEPHONY);   // sAddress.number_plan
71        p.writeByte((byte) 0);      // sAddress.number_of_digits
72        p.writeInt((byte) 0);       // sSubAddress.subaddressType
73        p.writeByte((byte) 0);      // sSubAddress.odd
74        p.writeByte((byte) 0);      // sSubAddress.number_of_digits
75        return p;
76    }
77
78    /**
79     * Initialize a BitwiseOutputStream with the CDMA bearer data subparameters except for
80     * user data. The caller will append the user data and add it to the parcel.
81     * @param messageId the 16-bit message identifier
82     * @param priority message priority
83     * @param language message language code
84     * @return the initialized BitwiseOutputStream
85     */
86    private static BitwiseOutputStream createBearerDataStream(int messageId, int priority,
87            int language) throws BitwiseOutputStream.AccessException {
88        BitwiseOutputStream bos = new BitwiseOutputStream(10);
89        bos.write(8, SUBPARAM_MESSAGE_IDENTIFIER);
90        bos.write(8, 3);    // length: 3 bytes
91        bos.write(4, BearerData.MESSAGE_TYPE_DELIVER);
92        bos.write(8, ((messageId >>> 8) & 0xff));
93        bos.write(8, (messageId & 0xff));
94        bos.write(1, 0);    // no User Data Header
95        bos.write(3, 0);    // reserved
96
97        if (priority != -1) {
98            bos.write(8, SUBPARAM_PRIORITY_INDICATOR);
99            bos.write(8, 1);    // length: 1 byte
100            bos.write(2, (priority & 0x03));
101            bos.write(6, 0);    // reserved
102        }
103
104        if (language != -1) {
105            bos.write(8, SUBPARAM_LANGUAGE_INDICATOR);
106            bos.write(8, 1);    // length: 1 byte
107            bos.write(8, (language & 0xff));
108        }
109
110        return bos;
111    }
112
113    /**
114     * Write the bearer data array to the parcel, then return a new SmsMessage from the parcel.
115     * @param p the parcel containing the CDMA SMS headers
116     * @param bearerData the bearer data byte array to append to the parcel
117     * @return the new SmsMessage created from the parcel
118     */
119    private static SmsMessage createMessageFromParcel(Parcel p, byte[] bearerData) {
120        p.writeInt(bearerData.length);
121        for (byte b : bearerData) {
122            p.writeByte(b);
123        }
124        p.setDataPosition(0);   // reset position for reading
125        SmsMessage message = SmsMessage.newFromParcel(p);
126        p.recycle();
127        return message;
128    }
129
130    /**
131     * Create a parcel for an incoming CMAS broadcast, then return a new SmsMessage created
132     * from the parcel.
133     * @param serviceCategory the CDMA service category
134     * @param messageId the 16-bit message identifier
135     * @param priority message priority
136     * @param language message language code
137     * @param body message body
138     * @param cmasCategory CMAS category (or -1 to skip adding CMAS type 1 elements record)
139     * @param responseType CMAS response type
140     * @param severity CMAS severity
141     * @param urgency CMAS urgency
142     * @param certainty CMAS certainty
143     * @return the newly created SmsMessage object
144     */
145    private static SmsMessage createCmasSmsMessage(int serviceCategory, int messageId, int priority,
146            int language, int encoding, String body, int cmasCategory, int responseType,
147            int severity, int urgency, int certainty) throws Exception {
148        BitwiseOutputStream cmasBos = new BitwiseOutputStream(10);
149        cmasBos.write(8, 0);    // CMAE protocol version 0
150
151        if (body != null) {
152            cmasBos.write(8, 0);        // Type 0 elements (alert text)
153            encodeBody(encoding, body, true, cmasBos);
154        }
155
156        if (cmasCategory != SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN) {
157            cmasBos.write(8, 1);    // Type 1 elements
158            cmasBos.write(8, 4);    // length: 4 bytes
159            cmasBos.write(8, (cmasCategory & 0xff));
160            cmasBos.write(8, (responseType & 0xff));
161            cmasBos.write(4, (severity & 0x0f));
162            cmasBos.write(4, (urgency & 0x0f));
163            cmasBos.write(4, (certainty & 0x0f));
164            cmasBos.write(4, 0);    // pad to octet boundary
165        }
166
167        byte[] cmasUserData = cmasBos.toByteArray();
168
169        Parcel p = createBroadcastParcel(serviceCategory);
170        BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language);
171
172        bos.write(8, SUBPARAM_USER_DATA);
173        bos.write(8, cmasUserData.length + 2);  // add 2 bytes for msg_encoding and num_fields
174        bos.write(5, UserData.ENCODING_OCTET);
175        bos.write(8, cmasUserData.length);
176        bos.writeByteArray(cmasUserData.length * 8, cmasUserData);
177        bos.write(3, 0);    // pad to byte boundary
178
179        return createMessageFromParcel(p, bos.toByteArray());
180    }
181
182    /**
183     * Create a parcel for an incoming CDMA cell broadcast, then return a new SmsMessage created
184     * from the parcel.
185     * @param serviceCategory the CDMA service category
186     * @param messageId the 16-bit message identifier
187     * @param priority message priority
188     * @param language message language code
189     * @param encoding user data encoding method
190     * @param body the message body
191     * @return the newly created SmsMessage object
192     */
193    private static SmsMessage createBroadcastSmsMessage(int serviceCategory, int messageId,
194            int priority, int language, int encoding, String body) throws Exception {
195        Parcel p = createBroadcastParcel(serviceCategory);
196        BitwiseOutputStream bos = createBearerDataStream(messageId, priority, language);
197
198        bos.write(8, SUBPARAM_USER_DATA);
199        encodeBody(encoding, body, false, bos);
200
201        return createMessageFromParcel(p, bos.toByteArray());
202    }
203
204    /**
205     * Append the message length, encoding, and body to the BearerData output stream.
206     * This is used for writing the User Data subparameter for non-CMAS broadcasts and for
207     * writing the alert text for CMAS broadcasts.
208     * @param encoding one of the CDMA UserData encoding values
209     * @param body the message body
210     * @param isCmasRecord true if this is a CMAS type 0 elements record; false for user data
211     * @param bos the BitwiseOutputStream to write to
212     * @throws Exception on any encoding error
213     */
214    private static void encodeBody(int encoding, String body, boolean isCmasRecord,
215            BitwiseOutputStream bos) throws Exception {
216        if (encoding == UserData.ENCODING_7BIT_ASCII || encoding == UserData.ENCODING_IA5) {
217            int charCount = body.length();
218            int recordBits = (charCount * 7) + 5;       // add 5 bits for char set field
219            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
220            int padBits = (recordOctets * 8) - recordBits;
221
222            if (!isCmasRecord) {
223                recordOctets++;                         // add 8 bits for num_fields
224            }
225
226            bos.write(8, recordOctets);
227            bos.write(5, (encoding & 0x1f));
228
229            if (!isCmasRecord) {
230                bos.write(8, charCount);
231            }
232
233            for (int i = 0; i < charCount; i++) {
234                bos.write(7, body.charAt(i));
235            }
236
237            bos.write(padBits, 0);      // pad to octet boundary
238        } else if (encoding == UserData.ENCODING_GSM_7BIT_ALPHABET
239                || encoding == UserData.ENCODING_GSM_DCS) {
240            // convert to 7-bit packed encoding with septet count in index 0 of byte array
241            byte[] encodedBody = GsmAlphabet.stringToGsm7BitPacked(body);
242
243            int charCount = encodedBody[0];             // septet count
244            int recordBits = (charCount * 7) + 5;       // add 5 bits for char set field
245            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
246            int padBits = (recordOctets * 8) - recordBits;
247
248            if (!isCmasRecord) {
249                recordOctets++;                         // add 8 bits for num_fields
250                if (encoding == UserData.ENCODING_GSM_DCS) {
251                    recordOctets++;                     // add 8 bits for DCS (message type)
252                }
253            }
254
255            bos.write(8, recordOctets);
256            bos.write(5, (encoding & 0x1f));
257
258            if (!isCmasRecord && encoding == UserData.ENCODING_GSM_DCS) {
259                bos.write(8, 0);        // GSM DCS: 7 bit default alphabet, no msg class
260            }
261
262            if (!isCmasRecord) {
263                bos.write(8, charCount);
264            }
265            byte[] bodySeptets = Arrays.copyOfRange(encodedBody, 1, encodedBody.length);
266            bos.writeByteArray(charCount * 7, bodySeptets);
267            bos.write(padBits, 0);      // pad to octet boundary
268        } else if (encoding == UserData.ENCODING_IS91_EXTENDED_PROTOCOL) {
269            // 6 bit packed encoding with 0x20 offset (ASCII 0x20 - 0x60)
270            int charCount = body.length();
271            int recordBits = (charCount * 6) + 21;      // add 21 bits for header fields
272            int recordOctets = (recordBits + 7) / 8;    // round up to octet boundary
273            int padBits = (recordOctets * 8) - recordBits;
274
275            bos.write(8, recordOctets);
276
277            bos.write(5, (encoding & 0x1f));
278            bos.write(8, UserData.IS91_MSG_TYPE_SHORT_MESSAGE);
279            bos.write(8, charCount);
280
281            for (int i = 0; i < charCount; i++) {
282                bos.write(6, ((int) body.charAt(i) - 0x20));
283            }
284
285            bos.write(padBits, 0);      // pad to octet boundary
286        } else {
287            byte[] encodedBody;
288            switch (encoding) {
289                case UserData.ENCODING_UNICODE_16:
290                    encodedBody = body.getBytes("UTF-16BE");
291                    break;
292
293                case UserData.ENCODING_SHIFT_JIS:
294                    encodedBody = body.getBytes("Shift_JIS");
295                    break;
296
297                case UserData.ENCODING_KOREAN:
298                    encodedBody = body.getBytes("KSC5601");
299                    break;
300
301                case UserData.ENCODING_LATIN_HEBREW:
302                    encodedBody = body.getBytes("ISO-8859-8");
303                    break;
304
305                case UserData.ENCODING_LATIN:
306                default:
307                    encodedBody = body.getBytes("ISO-8859-1");
308                    break;
309            }
310            int charCount = body.length();              // use actual char count for num fields
311            int recordOctets = encodedBody.length + 1;  // add 1 byte for encoding and pad bits
312            if (!isCmasRecord) {
313                recordOctets++;                         // add 8 bits for num_fields
314            }
315            bos.write(8, recordOctets);
316            bos.write(5, (encoding & 0x1f));
317            if (!isCmasRecord) {
318                bos.write(8, charCount);
319            }
320            bos.writeByteArray(encodedBody.length * 8, encodedBody);
321            bos.write(3, 0);            // pad to octet boundary
322        }
323    }
324
325    private static final String TEST_TEXT = "This is a test CDMA cell broadcast message..."
326            + "678901234567890123456789012345678901234567890";
327
328    private static final String PRES_ALERT =
329            "THE PRESIDENT HAS ISSUED AN EMERGENCY ALERT. CHECK LOCAL MEDIA FOR MORE DETAILS";
330
331    private static final String EXTREME_ALERT = "FLASH FLOOD WARNING FOR SOUTH COCONINO COUNTY"
332            + " - NORTH CENTRAL ARIZONA UNTIL 415 PM MST";
333
334    private static final String SEVERE_ALERT = "SEVERE WEATHER WARNING FOR SOMERSET COUNTY"
335            + " - NEW JERSEY UNTIL 415 PM MST";
336
337    private static final String AMBER_ALERT =
338            "AMBER ALERT:Mountain View,CA VEH'07 Blue Honda Civic CA LIC 5ABC123";
339
340    private static final String MONTHLY_TEST_ALERT = "This is a test of the emergency alert system."
341            + " This is only a test. 89012345678901234567890";
342
343    private static final String IS91_TEXT = "IS91 SHORT MSG";   // max length 14 chars
344
345    /**
346     * Verify that the SmsCbMessage has the correct values for CDMA.
347     * @param cbMessage the message to test
348     */
349    private static void verifyCbValues(SmsCbMessage cbMessage) {
350        assertEquals(SmsCbMessage.MESSAGE_FORMAT_3GPP2, cbMessage.getMessageFormat());
351        assertEquals(SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE, cbMessage.getGeographicalScope());
352        assertEquals(false, cbMessage.isEtwsMessage()); // ETWS on CDMA not currently supported
353    }
354
355    private static void doTestNonEmergencyBroadcast(int encoding) throws Exception {
356        SmsMessage msg = createBroadcastSmsMessage(123, 456, BearerData.PRIORITY_NORMAL,
357                BearerData.LANGUAGE_ENGLISH, encoding, TEST_TEXT);
358
359        SmsCbMessage cbMessage = msg.parseBroadcastSms();
360        verifyCbValues(cbMessage);
361        assertEquals(123, cbMessage.getServiceCategory());
362        assertEquals(456, cbMessage.getSerialNumber());
363        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_NORMAL, cbMessage.getMessagePriority());
364        assertEquals("en", cbMessage.getLanguageCode());
365        assertEquals(TEST_TEXT, cbMessage.getMessageBody());
366        assertEquals(false, cbMessage.isEmergencyMessage());
367        assertEquals(false, cbMessage.isCmasMessage());
368    }
369
370    @Test @SmallTest
371    public void testNonEmergencyBroadcast7bitAscii() throws Exception {
372        doTestNonEmergencyBroadcast(UserData.ENCODING_7BIT_ASCII);
373    }
374
375    @Test @SmallTest
376    public void testNonEmergencyBroadcast7bitGsm() throws Exception {
377        doTestNonEmergencyBroadcast(UserData.ENCODING_GSM_7BIT_ALPHABET);
378    }
379
380    @Test @SmallTest
381    public void testNonEmergencyBroadcast16bitUnicode() throws Exception {
382        doTestNonEmergencyBroadcast(UserData.ENCODING_UNICODE_16);
383    }
384
385    @Test @SmallTest
386    public void testNonEmergencyBroadcastIs91Extended() throws Exception {
387        // IS-91 doesn't support language or priority subparameters, max 14 chars text
388        SmsMessage msg = createBroadcastSmsMessage(987, 654, -1, -1,
389                UserData.ENCODING_IS91_EXTENDED_PROTOCOL, IS91_TEXT);
390
391        SmsCbMessage cbMessage = msg.parseBroadcastSms();
392        verifyCbValues(cbMessage);
393        assertEquals(987, cbMessage.getServiceCategory());
394        assertEquals(654, cbMessage.getSerialNumber());
395        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_NORMAL, cbMessage.getMessagePriority());
396        assertEquals(null, cbMessage.getLanguageCode());
397        assertEquals(IS91_TEXT, cbMessage.getMessageBody());
398        assertEquals(false, cbMessage.isEmergencyMessage());
399        assertEquals(false, cbMessage.isCmasMessage());
400    }
401
402    private static void doTestCmasBroadcast(int serviceCategory, int messageClass, String body)
403            throws Exception {
404        SmsMessage msg = createCmasSmsMessage(
405                serviceCategory, 1234, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
406                UserData.ENCODING_7BIT_ASCII, body, -1, -1, -1, -1, -1);
407
408        SmsCbMessage cbMessage = msg.parseBroadcastSms();
409        verifyCbValues(cbMessage);
410        assertEquals(serviceCategory, cbMessage.getServiceCategory());
411        assertEquals(1234, cbMessage.getSerialNumber());
412        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
413        assertEquals("en", cbMessage.getLanguageCode());
414        assertEquals(body, cbMessage.getMessageBody());
415        assertEquals(true, cbMessage.isEmergencyMessage());
416        assertEquals(true, cbMessage.isCmasMessage());
417        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
418        assertEquals(messageClass, cmasInfo.getMessageClass());
419        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory());
420        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType());
421        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity());
422        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency());
423        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty());
424    }
425
426    @Test @SmallTest
427    public void testCmasPresidentialAlert() throws Exception {
428        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
429                SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, PRES_ALERT);
430    }
431
432    @Test @SmallTest
433    public void testCmasExtremeAlert() throws Exception {
434        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
435                SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, EXTREME_ALERT);
436    }
437
438    @Test @SmallTest
439    public void testCmasSevereAlert() throws Exception {
440        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT,
441                SmsCbCmasInfo.CMAS_CLASS_SEVERE_THREAT, SEVERE_ALERT);
442    }
443
444    @Test @SmallTest
445    public void testCmasAmberAlert() throws Exception {
446        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY,
447                SmsCbCmasInfo.CMAS_CLASS_CHILD_ABDUCTION_EMERGENCY, AMBER_ALERT);
448    }
449
450    @Test @SmallTest
451    public void testCmasTestMessage() throws Exception {
452        doTestCmasBroadcast(SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE,
453                SmsCbCmasInfo.CMAS_CLASS_REQUIRED_MONTHLY_TEST, MONTHLY_TEST_ALERT);
454    }
455
456    @Test @SmallTest
457    public void testCmasExtremeAlertType1Elements() throws Exception {
458        SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
459                5678, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
460                UserData.ENCODING_7BIT_ASCII, EXTREME_ALERT, SmsCbCmasInfo.CMAS_CATEGORY_ENV,
461                SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, SmsCbCmasInfo.CMAS_SEVERITY_SEVERE,
462                SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY);
463
464        SmsCbMessage cbMessage = msg.parseBroadcastSms();
465        verifyCbValues(cbMessage);
466        assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
467                cbMessage.getServiceCategory());
468        assertEquals(5678, cbMessage.getSerialNumber());
469        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
470        assertEquals("en", cbMessage.getLanguageCode());
471        assertEquals(EXTREME_ALERT, cbMessage.getMessageBody());
472        assertEquals(true, cbMessage.isEmergencyMessage());
473        assertEquals(true, cbMessage.isCmasMessage());
474        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
475        assertEquals(SmsCbCmasInfo.CMAS_CLASS_EXTREME_THREAT, cmasInfo.getMessageClass());
476        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_ENV, cmasInfo.getCategory());
477        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_MONITOR, cmasInfo.getResponseType());
478        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_SEVERE, cmasInfo.getSeverity());
479        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_EXPECTED, cmasInfo.getUrgency());
480        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY, cmasInfo.getCertainty());
481    }
482
483    // VZW requirement is to discard message with unsupported charset. Verify that we return null
484    // for this unsupported character set.
485    @Test @SmallTest
486    public void testCmasUnsupportedCharSet() throws Exception {
487        SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
488                12345, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
489                UserData.ENCODING_GSM_DCS, EXTREME_ALERT, -1, -1, -1, -1, -1);
490
491        SmsCbMessage cbMessage = msg.parseBroadcastSms();
492        assertNull("expected null for unsupported charset", cbMessage);
493    }
494
495    // VZW requirement is to discard message with unsupported charset. Verify that we return null
496    // for this unsupported character set.
497    @Test @SmallTest
498    public void testCmasUnsupportedCharSet2() throws Exception {
499        SmsMessage msg = createCmasSmsMessage(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT,
500                67890, BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
501                UserData.ENCODING_KOREAN, EXTREME_ALERT, -1, -1, -1, -1, -1);
502
503        SmsCbMessage cbMessage = msg.parseBroadcastSms();
504        assertNull("expected null for unsupported charset", cbMessage);
505    }
506
507    // VZW requirement is to discard message without record type 0. The framework will decode it
508    // and the app will discard it.
509    @Test @SmallTest
510    public void testCmasNoRecordType0() throws Exception {
511        SmsMessage msg = createCmasSmsMessage(
512                SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT, 1234,
513                BearerData.PRIORITY_EMERGENCY, BearerData.LANGUAGE_ENGLISH,
514                UserData.ENCODING_7BIT_ASCII, null, -1, -1, -1, -1, -1);
515
516        SmsCbMessage cbMessage = msg.parseBroadcastSms();
517        verifyCbValues(cbMessage);
518        assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_PRESIDENTIAL_LEVEL_ALERT,
519                cbMessage.getServiceCategory());
520        assertEquals(1234, cbMessage.getSerialNumber());
521        assertEquals(SmsCbMessage.MESSAGE_PRIORITY_EMERGENCY, cbMessage.getMessagePriority());
522        assertEquals("en", cbMessage.getLanguageCode());
523        assertEquals(null, cbMessage.getMessageBody());
524        assertEquals(true, cbMessage.isEmergencyMessage());
525        assertEquals(true, cbMessage.isCmasMessage());
526        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
527        assertEquals(SmsCbCmasInfo.CMAS_CLASS_PRESIDENTIAL_LEVEL_ALERT, cmasInfo.getMessageClass());
528        assertEquals(SmsCbCmasInfo.CMAS_CATEGORY_UNKNOWN, cmasInfo.getCategory());
529        assertEquals(SmsCbCmasInfo.CMAS_RESPONSE_TYPE_UNKNOWN, cmasInfo.getResponseType());
530        assertEquals(SmsCbCmasInfo.CMAS_SEVERITY_UNKNOWN, cmasInfo.getSeverity());
531        assertEquals(SmsCbCmasInfo.CMAS_URGENCY_UNKNOWN, cmasInfo.getUrgency());
532        assertEquals(SmsCbCmasInfo.CMAS_CERTAINTY_UNKNOWN, cmasInfo.getCertainty());
533    }
534
535    // Make sure we don't throw an exception if we feed completely random data to BearerStream.
536    @Test @SmallTest
537    public void testRandomBearerStreamData() {
538        Random r = new Random(54321);
539        for (int run = 0; run < 1000; run++) {
540            int len = r.nextInt(140);
541            byte[] data = new byte[len];
542            for (int i = 0; i < len; i++) {
543                data[i] = (byte) r.nextInt(256);
544            }
545            // Rlog.d("CdmaSmsCbTest", "trying random bearer data run " + run + " length " + len);
546            try {
547                int category = 0x0ff0 + r.nextInt(32);  // half CMAS, half non-CMAS
548                Parcel p = createBroadcastParcel(category);
549                SmsMessage msg = createMessageFromParcel(p, data);
550                SmsCbMessage cbMessage = msg.parseBroadcastSms();
551                // with random input, cbMessage will almost always be null (log when it isn't)
552                if (cbMessage != null) {
553                    Rlog.d("CdmaSmsCbTest", "success: " + cbMessage);
554                }
555            } catch (Exception e) {
556                Rlog.d("CdmaSmsCbTest", "exception thrown", e);
557                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
558            }
559        }
560    }
561
562    // Make sure we don't throw an exception if we put random data in the UserData subparam.
563    @Test @SmallTest
564    public void testRandomUserData() {
565        Random r = new Random(94040);
566        for (int run = 0; run < 1000; run++) {
567            int category = 0x0ff0 + r.nextInt(32);  // half CMAS, half non-CMAS
568            Parcel p = createBroadcastParcel(category);
569            int len = r.nextInt(140);
570            // Rlog.d("CdmaSmsCbTest", "trying random user data run " + run + " length " + len);
571
572            try {
573                BitwiseOutputStream bos = createBearerDataStream(r.nextInt(65536), r.nextInt(4),
574                        r.nextInt(256));
575
576                bos.write(8, SUBPARAM_USER_DATA);
577                bos.write(8, len);
578
579                for (int i = 0; i < len; i++) {
580                    bos.write(8, r.nextInt(256));
581                }
582
583                SmsMessage msg = createMessageFromParcel(p, bos.toByteArray());
584                SmsCbMessage cbMessage = msg.parseBroadcastSms();
585            } catch (Exception e) {
586                Rlog.d("CdmaSmsCbTest", "exception thrown", e);
587                fail("Exception in decoder at run " + run + " length " + len + ": " + e);
588            }
589        }
590    }
591
592    /**
593     * Initialize a Parcel for incoming Service Category Program Data teleservice. The caller will
594     * write the bearer data and then convert it to an SmsMessage.
595     * @return the initialized Parcel
596     */
597    private static Parcel createServiceCategoryProgramDataParcel() {
598        Parcel p = Parcel.obtain();
599
600        p.writeInt(SmsEnvelope.TELESERVICE_SCPT);
601        p.writeByte((byte) 0);  // non-zero for MESSAGE_TYPE_BROADCAST
602        p.writeInt(0);
603
604        // dummy address (RIL may generate a different dummy address for broadcasts)
605        p.writeInt(CdmaSmsAddress.DIGIT_MODE_4BIT_DTMF);            // sAddress.digit_mode
606        p.writeInt(CdmaSmsAddress.NUMBER_MODE_NOT_DATA_NETWORK);    // sAddress.number_mode
607        p.writeInt(CdmaSmsAddress.TON_UNKNOWN);                     // sAddress.number_type
608        p.writeInt(CdmaSmsAddress.NUMBERING_PLAN_ISDN_TELEPHONY);   // sAddress.number_plan
609        p.writeByte((byte) 0);      // sAddress.number_of_digits
610        p.writeInt((byte) 0);       // sSubAddress.subaddressType
611        p.writeByte((byte) 0);      // sSubAddress.odd
612        p.writeByte((byte) 0);      // sSubAddress.number_of_digits
613        return p;
614    }
615
616    private static final String CAT_EXTREME_THREAT = "Extreme Threat to Life and Property";
617    private static final String CAT_SEVERE_THREAT = "Severe Threat to Life and Property";
618    private static final String CAT_AMBER_ALERTS = "AMBER Alerts";
619
620    @Test @SmallTest
621    public void testServiceCategoryProgramDataAddCategory() throws Exception {
622        Parcel p = createServiceCategoryProgramDataParcel();
623        BitwiseOutputStream bos = createBearerDataStream(123, -1, -1);
624
625        int categoryNameLength = CAT_EXTREME_THREAT.length();
626        int subparamLengthBits = (53 + (categoryNameLength * 7));
627        int subparamLengthBytes = (subparamLengthBits + 7) / 8;
628        int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits;
629
630        bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA);
631        bos.write(8, subparamLengthBytes);
632        bos.write(5, UserData.ENCODING_7BIT_ASCII);
633
634        bos.write(4, CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY);
635        bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT >>> 8));
636        bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT & 0xff));
637        bos.write(8, BearerData.LANGUAGE_ENGLISH);
638        bos.write(8, 100);  // max messages
639        bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT);
640
641        bos.write(8, categoryNameLength);
642        for (int i = 0; i < categoryNameLength; i++) {
643            bos.write(7, CAT_EXTREME_THREAT.charAt(i));
644        }
645        bos.write(subparamPadBits, 0);
646
647        SmsMessage msg = createMessageFromParcel(p, bos.toByteArray());
648        assertNotNull(msg);
649        msg.parseSms();
650        List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData();
651        assertNotNull(programDataList);
652        assertEquals(1, programDataList.size());
653        CdmaSmsCbProgramData programData = programDataList.get(0);
654        assertEquals(CdmaSmsCbProgramData.OPERATION_ADD_CATEGORY, programData.getOperation());
655        assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_EXTREME_THREAT, programData.getCategory());
656        assertEquals(CAT_EXTREME_THREAT, programData.getCategoryName());
657        assertEquals(BearerData.LANGUAGE_ENGLISH, programData.getLanguage());
658        assertEquals(100, programData.getMaxMessages());
659        assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_DEFAULT_ALERT, programData.getAlertOption());
660    }
661
662    @Test @SmallTest
663    public void testServiceCategoryProgramDataDeleteTwoCategories() throws Exception {
664        Parcel p = createServiceCategoryProgramDataParcel();
665        BitwiseOutputStream bos = createBearerDataStream(456, -1, -1);
666
667        int category1NameLength = CAT_SEVERE_THREAT.length();
668        int category2NameLength = CAT_AMBER_ALERTS.length();
669
670        int subparamLengthBits = (101 + (category1NameLength * 7) + (category2NameLength * 7));
671        int subparamLengthBytes = (subparamLengthBits + 7) / 8;
672        int subparamPadBits = (subparamLengthBytes * 8) - subparamLengthBits;
673
674        bos.write(8, SUBPARAM_SERVICE_CATEGORY_PROGRAM_DATA);
675        bos.write(8, subparamLengthBytes);
676        bos.write(5, UserData.ENCODING_7BIT_ASCII);
677
678        bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY);
679        bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT >>> 8));
680        bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT & 0xff));
681        bos.write(8, BearerData.LANGUAGE_ENGLISH);
682        bos.write(8, 0);  // max messages
683        bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT);
684
685        bos.write(8, category1NameLength);
686        for (int i = 0; i < category1NameLength; i++) {
687            bos.write(7, CAT_SEVERE_THREAT.charAt(i));
688        }
689
690        bos.write(4, CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY);
691        bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY >>> 8));
692        bos.write(8, (SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY & 0xff));
693        bos.write(8, BearerData.LANGUAGE_ENGLISH);
694        bos.write(8, 0);  // max messages
695        bos.write(4, CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT);
696
697        bos.write(8, category2NameLength);
698        for (int i = 0; i < category2NameLength; i++) {
699            bos.write(7, CAT_AMBER_ALERTS.charAt(i));
700        }
701
702        bos.write(subparamPadBits, 0);
703
704        SmsMessage msg = createMessageFromParcel(p, bos.toByteArray());
705        assertNotNull(msg);
706        msg.parseSms();
707        List<CdmaSmsCbProgramData> programDataList = msg.getSmsCbProgramData();
708        assertNotNull(programDataList);
709        assertEquals(2, programDataList.size());
710
711        CdmaSmsCbProgramData programData = programDataList.get(0);
712        assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation());
713        assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_SEVERE_THREAT, programData.getCategory());
714        assertEquals(CAT_SEVERE_THREAT, programData.getCategoryName());
715        assertEquals(BearerData.LANGUAGE_ENGLISH, programData.getLanguage());
716        assertEquals(0, programData.getMaxMessages());
717        assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption());
718
719        programData = programDataList.get(1);
720        assertEquals(CdmaSmsCbProgramData.OPERATION_DELETE_CATEGORY, programData.getOperation());
721        assertEquals(SmsEnvelope.SERVICE_CATEGORY_CMAS_CHILD_ABDUCTION_EMERGENCY,
722                programData.getCategory());
723        assertEquals(CAT_AMBER_ALERTS, programData.getCategoryName());
724        assertEquals(BearerData.LANGUAGE_ENGLISH, programData.getLanguage());
725        assertEquals(0, programData.getMaxMessages());
726        assertEquals(CdmaSmsCbProgramData.ALERT_OPTION_NO_ALERT, programData.getAlertOption());
727    }
728
729    private static final byte[] CMAS_TEST_BEARER_DATA = {
730        0x00, 0x03, 0x1C, 0x78, 0x00, 0x01, 0x59, 0x02, (byte) 0xB8, 0x00, 0x02, 0x10, (byte) 0xAA,
731        0x68, (byte) 0xD3, (byte) 0xCD, 0x06, (byte) 0x9E, 0x68, 0x30, (byte) 0xA0, (byte) 0xE9,
732        (byte) 0x97, (byte) 0x9F, 0x44, 0x1B, (byte) 0xF3, 0x20, (byte) 0xE9, (byte) 0xA3,
733        0x2A, 0x08, 0x7B, (byte) 0xF6, (byte) 0xED, (byte) 0xCB, (byte) 0xCB, 0x1E, (byte) 0x9C,
734        0x3B, 0x10, 0x4D, (byte) 0xDF, (byte) 0x8B, 0x4E,
735        (byte) 0xCC, (byte) 0xA8, 0x20, (byte) 0xEC, (byte) 0xCB, (byte) 0xCB, (byte) 0xA2, 0x0A,
736        0x7E, 0x79, (byte) 0xF4, (byte) 0xCB, (byte) 0xB5, 0x72, 0x0A, (byte) 0x9A, 0x34,
737        (byte) 0xF3, 0x41, (byte) 0xA7, (byte) 0x9A, 0x0D, (byte) 0xFB, (byte) 0xB6, 0x79, 0x41,
738        (byte) 0x85, 0x07, 0x4C, (byte) 0xBC, (byte) 0xFA, 0x2E, 0x00, 0x08, 0x20, 0x58, 0x38,
739        (byte) 0x88, (byte) 0x80, 0x10, 0x54, 0x06, 0x38, 0x20, 0x60,
740        0x30, (byte) 0xA8, (byte) 0x81, (byte) 0x90, 0x20, 0x08
741    };
742
743    // Test case for CMAS test message received on the Sprint network.
744    @Test @SmallTest
745    public void testDecodeRawBearerData() throws Exception {
746        Parcel p = createBroadcastParcel(SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE);
747        SmsMessage msg = createMessageFromParcel(p, CMAS_TEST_BEARER_DATA);
748
749        SmsCbMessage cbMessage = msg.parseBroadcastSms();
750        assertNotNull("expected non-null for bearer data", cbMessage);
751        assertEquals("geoScope", cbMessage.getGeographicalScope(), 1);
752        assertEquals("serialNumber", cbMessage.getSerialNumber(), 51072);
753        assertEquals("serviceCategory", cbMessage.getServiceCategory(),
754                SmsEnvelope.SERVICE_CATEGORY_CMAS_TEST_MESSAGE);
755        assertEquals("payload", cbMessage.getMessageBody(),
756                "This is a test of the Commercial Mobile Alert System. This is only a test.");
757
758        SmsCbCmasInfo cmasInfo = cbMessage.getCmasWarningInfo();
759        assertNotNull("expected non-null for CMAS info", cmasInfo);
760        assertEquals("category", cmasInfo.getCategory(), SmsCbCmasInfo.CMAS_CATEGORY_OTHER);
761        assertEquals("responseType", cmasInfo.getResponseType(),
762                SmsCbCmasInfo.CMAS_RESPONSE_TYPE_NONE);
763        assertEquals("severity", cmasInfo.getSeverity(), SmsCbCmasInfo.CMAS_SEVERITY_SEVERE);
764        assertEquals("urgency", cmasInfo.getUrgency(), SmsCbCmasInfo.CMAS_URGENCY_EXPECTED);
765        assertEquals("certainty", cmasInfo.getCertainty(), SmsCbCmasInfo.CMAS_CERTAINTY_LIKELY);
766    }
767}
768