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