CdmaSMSDispatcher.java revision d027329cd6105c05c157e3858a170e4f04eb0e3e
1/*
2 * Copyright (C) 2008 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
19
20import android.app.Activity;
21import android.app.PendingIntent;
22import android.app.PendingIntent.CanceledException;
23import android.content.ContentValues;
24import android.content.Intent;
25import android.content.SharedPreferences;
26import android.database.Cursor;
27import android.database.SQLException;
28import android.os.AsyncResult;
29import android.os.Message;
30import android.os.SystemProperties;
31import android.preference.PreferenceManager;
32import android.provider.Telephony;
33import android.provider.Telephony.Sms.Intents;
34import android.telephony.SmsManager;
35import android.telephony.SmsMessage.MessageClass;
36import android.util.Config;
37import android.util.Log;
38
39import com.android.internal.telephony.CommandsInterface;
40import com.android.internal.telephony.SMSDispatcher;
41import com.android.internal.telephony.SmsHeader;
42import com.android.internal.telephony.SmsMessageBase;
43import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails;
44import com.android.internal.telephony.TelephonyProperties;
45import com.android.internal.telephony.WspTypeDecoder;
46import com.android.internal.telephony.cdma.sms.SmsEnvelope;
47import com.android.internal.telephony.cdma.sms.UserData;
48import com.android.internal.util.HexDump;
49
50import java.io.ByteArrayOutputStream;
51import java.util.ArrayList;
52import java.util.Arrays;
53import java.util.HashMap;
54
55import android.content.res.Resources;
56
57
58final class CdmaSMSDispatcher extends SMSDispatcher {
59    private static final String TAG = "CDMA";
60
61    private byte[] mLastDispatchedSmsFingerprint;
62    private byte[] mLastAcknowledgedSmsFingerprint;
63
64    private boolean mCheckForDuplicatePortsInOmadmWapPush = Resources.getSystem().getBoolean(
65            com.android.internal.R.bool.config_duplicate_port_omadm_wappush);
66
67    CdmaSMSDispatcher(CDMAPhone phone) {
68        super(phone);
69    }
70
71    /**
72     * Called when a status report is received.  This should correspond to
73     * a previously successful SEND.
74     * Is a special GSM function, should never be called in CDMA!!
75     *
76     * @param ar AsyncResult passed into the message handler.  ar.result should
77     *           be a String representing the status report PDU, as ASCII hex.
78     */
79    protected void handleStatusReport(AsyncResult ar) {
80        Log.d(TAG, "handleStatusReport is a special GSM function, should never be called in CDMA!");
81    }
82
83    private void handleCdmaStatusReport(SmsMessage sms) {
84        for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
85            SmsTracker tracker = deliveryPendingList.get(i);
86            if (tracker.mMessageRef == sms.messageRef) {
87                // Found it.  Remove from list and broadcast.
88                deliveryPendingList.remove(i);
89                PendingIntent intent = tracker.mDeliveryIntent;
90                Intent fillIn = new Intent();
91                fillIn.putExtra("pdu", sms.getPdu());
92                try {
93                    intent.send(mContext, Activity.RESULT_OK, fillIn);
94                } catch (CanceledException ex) {}
95                break;  // Only expect to see one tracker matching this message.
96            }
97        }
98    }
99
100    /** {@inheritDoc} */
101    protected int dispatchMessage(SmsMessageBase smsb) {
102
103        // If sms is null, means there was a parsing error.
104        if (smsb == null) {
105            Log.e(TAG, "dispatchMessage: message is null");
106            return Intents.RESULT_SMS_GENERIC_ERROR;
107        }
108
109        String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false");
110        if (inEcm.equals("true")) {
111            return Activity.RESULT_OK;
112        }
113
114        // See if we have a network duplicate SMS.
115        SmsMessage sms = (SmsMessage) smsb;
116        mLastDispatchedSmsFingerprint = sms.getIncomingSmsFingerprint();
117        if (mLastAcknowledgedSmsFingerprint != null &&
118                Arrays.equals(mLastDispatchedSmsFingerprint, mLastAcknowledgedSmsFingerprint)) {
119            return Intents.RESULT_SMS_HANDLED;
120        }
121        // Decode BD stream and set sms variables.
122        sms.parseSms();
123        int teleService = sms.getTeleService();
124        boolean handled = false;
125
126        if ((SmsEnvelope.TELESERVICE_VMN == teleService) ||
127                (SmsEnvelope.TELESERVICE_MWI == teleService)) {
128            // handling Voicemail
129            int voicemailCount = sms.getNumOfVoicemails();
130            Log.d(TAG, "Voicemail count=" + voicemailCount);
131            // Store the voicemail count in preferences.
132            SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(
133                    mPhone.getContext());
134            SharedPreferences.Editor editor = sp.edit();
135            editor.putInt(CDMAPhone.VM_COUNT_CDMA, voicemailCount);
136            editor.apply();
137            ((CDMAPhone) mPhone).updateMessageWaitingIndicator(voicemailCount);
138            handled = true;
139        } else if (((SmsEnvelope.TELESERVICE_WMT == teleService) ||
140                (SmsEnvelope.TELESERVICE_WEMT == teleService)) &&
141                sms.isStatusReportMessage()) {
142            handleCdmaStatusReport(sms);
143            handled = true;
144        } else if ((sms.getUserData() == null)) {
145            if (Config.LOGD) {
146                Log.d(TAG, "Received SMS without user data");
147            }
148            handled = true;
149        }
150
151        if (handled) {
152            return Intents.RESULT_SMS_HANDLED;
153        }
154
155        if (!mStorageAvailable && (sms.getMessageClass() != MessageClass.CLASS_0)) {
156            // It's a storable message and there's no storage available.  Bail.
157            // (See C.S0015-B v2.0 for a description of "Immediate Display"
158            // messages, which we represent as CLASS_0.)
159            return Intents.RESULT_SMS_OUT_OF_MEMORY;
160        }
161
162        if (SmsEnvelope.TELESERVICE_WAP == teleService) {
163            return processCdmaWapPdu(sms.getUserData(), sms.messageRef,
164                    sms.getOriginatingAddress());
165        }
166
167        // Reject (NAK) any messages with teleservice ids that have
168        // not yet been handled and also do not correspond to the two
169        // kinds that are processed below.
170        if ((SmsEnvelope.TELESERVICE_WMT != teleService) &&
171                (SmsEnvelope.TELESERVICE_WEMT != teleService) &&
172                (SmsEnvelope.MESSAGE_TYPE_BROADCAST != sms.getMessageType())) {
173            return Intents.RESULT_SMS_UNSUPPORTED;
174        }
175
176        /*
177         * TODO(cleanup): Why are we using a getter method for this
178         * (and for so many other sms fields)?  Trivial getters and
179         * setters like this are direct violations of the style guide.
180         * If the purpose is to protect against writes (by not
181         * providing a setter) then any protection is illusory (and
182         * hence bad) for cases where the values are not primitives,
183         * such as this call for the header.  Since this is an issue
184         * with the public API it cannot be changed easily, but maybe
185         * something can be done eventually.
186         */
187        SmsHeader smsHeader = sms.getUserDataHeader();
188
189        /*
190         * TODO(cleanup): Since both CDMA and GSM use the same header
191         * format, this dispatch processing is naturally identical,
192         * and code should probably not be replicated explicitly.
193         */
194
195        // See if message is partial or port addressed.
196        if ((smsHeader == null) || (smsHeader.concatRef == null)) {
197            // Message is not partial (not part of concatenated sequence).
198            byte[][] pdus = new byte[1][];
199            pdus[0] = sms.getPdu();
200
201            if (smsHeader != null && smsHeader.portAddrs != null) {
202                if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) {
203                    // GSM-style WAP indication
204                    return mWapPush.dispatchWapPdu(sms.getUserData());
205                } else {
206                    // The message was sent to a port, so concoct a URI for it.
207                    dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort);
208                }
209            } else {
210                // Normal short and non-port-addressed message, dispatch it.
211                dispatchPdus(pdus);
212            }
213            return Activity.RESULT_OK;
214        } else {
215            // Process the message part.
216            return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs);
217        }
218    }
219
220    /**
221     * Processes inbound messages that are in the WAP-WDP PDU format. See
222     * wap-259-wdp-20010614-a section 6.5 for details on the WAP-WDP PDU format.
223     * WDP segments are gathered until a datagram completes and gets dispatched.
224     *
225     * @param pdu The WAP-WDP PDU segment
226     * @return a result code from {@link Telephony.Sms.Intents}, or
227     *         {@link Activity#RESULT_OK} if the message has been broadcast
228     *         to applications
229     */
230    protected int processCdmaWapPdu(byte[] pdu, int referenceNumber, String address) {
231        int segment;
232        int totalSegments;
233        int index = 0;
234        int msgType;
235
236        int sourcePort = 0;
237        int destinationPort = 0;
238
239        msgType = pdu[index++];
240        if (msgType != 0){
241            Log.w(TAG, "Received a WAP SMS which is not WDP. Discard.");
242            return Intents.RESULT_SMS_HANDLED;
243        }
244        totalSegments = pdu[index++]; // >=1
245        segment = pdu[index++]; // >=0
246
247        // Only the first segment contains sourcePort and destination Port
248        if (segment == 0) {
249            //process WDP segment
250            sourcePort = (0xFF & pdu[index++]) << 8;
251            sourcePort |= 0xFF & pdu[index++];
252            destinationPort = (0xFF & pdu[index++]) << 8;
253            destinationPort |= 0xFF & pdu[index++];
254            // Some carriers incorrectly send duplicate port fields in omadm wap pushes.
255            // If configured, check for that here
256            if (mCheckForDuplicatePortsInOmadmWapPush) {
257                if (checkDuplicatePortOmadmWappush(pdu,index)) {
258                    index = index + 4; // skip duplicate port fields
259                }
260            }
261        }
262
263        // Lookup all other related parts
264        StringBuilder where = new StringBuilder("reference_number =");
265        where.append(referenceNumber);
266        where.append(" AND address = ?");
267        String[] whereArgs = new String[] {address};
268
269        Log.i(TAG, "Received WAP PDU. Type = " + msgType + ", originator = " + address
270                + ", src-port = " + sourcePort + ", dst-port = " + destinationPort
271                + ", ID = " + referenceNumber + ", segment# = " + segment + "/" + totalSegments);
272
273        byte[][] pdus = null;
274        Cursor cursor = null;
275        try {
276            cursor = mResolver.query(mRawUri, RAW_PROJECTION, where.toString(), whereArgs, null);
277            int cursorCount = cursor.getCount();
278            if (cursorCount != totalSegments - 1) {
279                // We don't have all the parts yet, store this one away
280                ContentValues values = new ContentValues();
281                values.put("date", (long) 0);
282                values.put("pdu", HexDump.toHexString(pdu, index, pdu.length - index));
283                values.put("address", address);
284                values.put("reference_number", referenceNumber);
285                values.put("count", totalSegments);
286                values.put("sequence", segment);
287                values.put("destination_port", destinationPort);
288
289                mResolver.insert(mRawUri, values);
290
291                return Intents.RESULT_SMS_HANDLED;
292            }
293
294            // All the parts are in place, deal with them
295            int pduColumn = cursor.getColumnIndex("pdu");
296            int sequenceColumn = cursor.getColumnIndex("sequence");
297
298            pdus = new byte[totalSegments][];
299            for (int i = 0; i < cursorCount; i++) {
300                cursor.moveToNext();
301                int cursorSequence = (int)cursor.getLong(sequenceColumn);
302                // Read the destination port from the first segment
303                if (cursorSequence == 0) {
304                    int destinationPortColumn = cursor.getColumnIndex("destination_port");
305                    destinationPort = (int)cursor.getLong(destinationPortColumn);
306                }
307                pdus[cursorSequence] = HexDump.hexStringToByteArray(
308                        cursor.getString(pduColumn));
309            }
310            // The last part will be added later
311
312            // Remove the parts from the database
313            mResolver.delete(mRawUri, where.toString(), whereArgs);
314        } catch (SQLException e) {
315            Log.e(TAG, "Can't access multipart SMS database", e);
316            return Intents.RESULT_SMS_GENERIC_ERROR;
317        } finally {
318            if (cursor != null) cursor.close();
319        }
320
321        // Build up the data stream
322        ByteArrayOutputStream output = new ByteArrayOutputStream();
323        for (int i = 0; i < totalSegments; i++) {
324            // reassemble the (WSP-)pdu
325            if (i == segment) {
326                // This one isn't in the DB, so add it
327                output.write(pdu, index, pdu.length - index);
328            } else {
329                output.write(pdus[i], 0, pdus[i].length);
330            }
331        }
332
333        byte[] datagram = output.toByteArray();
334        // Dispatch the PDU to applications
335        switch (destinationPort) {
336        case SmsHeader.PORT_WAP_PUSH:
337            // Handle the PUSH
338            return mWapPush.dispatchWapPdu(datagram);
339
340        default:{
341            pdus = new byte[1][];
342            pdus[0] = datagram;
343            // The messages were sent to any other WAP port
344            dispatchPortAddressedPdus(pdus, destinationPort);
345            return Activity.RESULT_OK;
346        }
347        }
348    }
349
350    /** {@inheritDoc} */
351    protected void sendData(String destAddr, String scAddr, int destPort,
352            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
353        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
354                scAddr, destAddr, destPort, data, (deliveryIntent != null));
355        sendSubmitPdu(pdu, sentIntent, deliveryIntent);
356    }
357
358    /** {@inheritDoc} */
359    protected void sendText(String destAddr, String scAddr, String text,
360            PendingIntent sentIntent, PendingIntent deliveryIntent) {
361        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
362                scAddr, destAddr, text, (deliveryIntent != null), null);
363        sendSubmitPdu(pdu, sentIntent, deliveryIntent);
364    }
365
366    /** {@inheritDoc} */
367    protected void sendMultipartText(String destAddr, String scAddr,
368            ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
369            ArrayList<PendingIntent> deliveryIntents) {
370
371        /**
372         * TODO(cleanup): There is no real code difference between
373         * this and the GSM version, and hence it should be moved to
374         * the base class or consolidated somehow, provided calling
375         * the proper submitpdu stuff can be arranged.
376         */
377
378        int refNumber = getNextConcatenatedRef() & 0x00FF;
379        int msgCount = parts.size();
380        int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN;
381
382        for (int i = 0; i < msgCount; i++) {
383            TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false);
384            if (encoding != details.codeUnitSize
385                    && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN
386                            || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) {
387                encoding = details.codeUnitSize;
388            }
389        }
390
391        for (int i = 0; i < msgCount; i++) {
392            SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
393            concatRef.refNumber = refNumber;
394            concatRef.seqNumber = i + 1;  // 1-based sequence
395            concatRef.msgCount = msgCount;
396            concatRef.isEightBits = true;
397            SmsHeader smsHeader = new SmsHeader();
398            smsHeader.concatRef = concatRef;
399
400            PendingIntent sentIntent = null;
401            if (sentIntents != null && sentIntents.size() > i) {
402                sentIntent = sentIntents.get(i);
403            }
404
405            PendingIntent deliveryIntent = null;
406            if (deliveryIntents != null && deliveryIntents.size() > i) {
407                deliveryIntent = deliveryIntents.get(i);
408            }
409
410            UserData uData = new UserData();
411            uData.payloadStr = parts.get(i);
412            uData.userDataHeader = smsHeader;
413            if (encoding == android.telephony.SmsMessage.ENCODING_7BIT) {
414                uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
415            } else { // assume UTF-16
416                uData.msgEncoding = UserData.ENCODING_UNICODE_16;
417            }
418            uData.msgEncodingSet = true;
419
420            /* By setting the statusReportRequested bit only for the
421             * last message fragment, this will result in only one
422             * callback to the sender when that last fragment delivery
423             * has been acknowledged. */
424            SmsMessage.SubmitPdu submitPdu = SmsMessage.getSubmitPdu(destAddr,
425                    uData, (deliveryIntent != null) && (i == (msgCount - 1)));
426
427            sendSubmitPdu(submitPdu, sentIntent, deliveryIntent);
428        }
429    }
430
431    protected void sendSubmitPdu(SmsMessage.SubmitPdu pdu,
432            PendingIntent sentIntent, PendingIntent deliveryIntent) {
433        if (SystemProperties.getBoolean(TelephonyProperties.PROPERTY_INECM_MODE, false)) {
434            if (sentIntent != null) {
435                try {
436                    sentIntent.send(SmsManager.RESULT_ERROR_NO_SERVICE);
437                } catch (CanceledException ex) {}
438            }
439            if (Config.LOGD) {
440                Log.d(TAG, "Block SMS in Emergency Callback mode");
441            }
442            return;
443        }
444        sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
445    }
446
447    /** {@inheritDoc} */
448    protected void sendSms(SmsTracker tracker) {
449        HashMap map = tracker.mData;
450
451        // byte smsc[] = (byte[]) map.get("smsc");  // unused for CDMA
452        byte pdu[] = (byte[]) map.get("pdu");
453
454        Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);
455
456        mCm.sendCdmaSms(pdu, reply);
457    }
458
459     /** {@inheritDoc} */
460    protected void sendMultipartSms (SmsTracker tracker) {
461        Log.d(TAG, "TODO: CdmaSMSDispatcher.sendMultipartSms not implemented");
462    }
463
464    /** {@inheritDoc} */
465    protected void acknowledgeLastIncomingSms(boolean success, int result, Message response){
466        // FIXME unit test leaves cm == null. this should change
467
468        String inEcm=SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE, "false");
469        if (inEcm.equals("true")) {
470            return;
471        }
472
473        if (mCm != null) {
474            int causeCode = resultToCause(result);
475            mCm.acknowledgeLastIncomingCdmaSms(success, causeCode, response);
476
477            if (causeCode == 0) {
478                mLastAcknowledgedSmsFingerprint = mLastDispatchedSmsFingerprint;
479            }
480            mLastDispatchedSmsFingerprint = null;
481        }
482    }
483
484    protected void handleBroadcastSms(AsyncResult ar) {
485        // Not supported
486        Log.e(TAG, "Error! Not implemented for CDMA.");
487    }
488
489    private int resultToCause(int rc) {
490        switch (rc) {
491        case Activity.RESULT_OK:
492        case Intents.RESULT_SMS_HANDLED:
493            // Cause code is ignored on success.
494            return 0;
495        case Intents.RESULT_SMS_OUT_OF_MEMORY:
496            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE;
497        case Intents.RESULT_SMS_UNSUPPORTED:
498            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID;
499        case Intents.RESULT_SMS_GENERIC_ERROR:
500        default:
501            return CommandsInterface.CDMA_SMS_FAIL_CAUSE_ENCODING_PROBLEM;
502        }
503    }
504
505    /**
506     * Optional check to see if the received WapPush is an OMADM notification with erroneous
507     * extra port fields.
508     * - Some carriers make this mistake.
509     * ex: MSGTYPE-TotalSegments-CurrentSegment
510     *       -SourcePortDestPort-SourcePortDestPort-OMADM PDU
511     * @param origPdu The WAP-WDP PDU segment
512     * @param index Current Index while parsing the PDU.
513     * @return True if OrigPdu is OmaDM Push Message which has duplicate ports.
514     *         False if OrigPdu is NOT OmaDM Push Message which has duplicate ports.
515     */
516    private boolean checkDuplicatePortOmadmWappush(byte[] origPdu, int index) {
517        index += 4;
518        byte[] omaPdu = new byte[origPdu.length - index];
519        System.arraycopy(origPdu, index, omaPdu, 0, omaPdu.length);
520
521        WspTypeDecoder pduDecoder = new WspTypeDecoder(omaPdu);
522        int wspIndex = 2;
523
524        // Process header length field
525        if (pduDecoder.decodeUintvarInteger(wspIndex) == false) {
526            return false;
527        }
528
529        wspIndex += pduDecoder.getDecodedDataLength(); // advance to next field
530
531        // Process content type field
532        if (pduDecoder.decodeContentType(wspIndex) == false) {
533            return false;
534        }
535
536        String mimeType = pduDecoder.getValueString();
537        if (mimeType == null) {
538            int binaryContentType = (int)pduDecoder.getValue32();
539            if (binaryContentType == WspTypeDecoder.CONTENT_TYPE_B_PUSH_SYNCML_NOTI) {
540                return true;
541            }
542        }
543        return false;
544    }
545}
546