GsmSMSDispatcher.java revision ab79ee4adcbc6eb9fb9c509766753f65d3857739
1/*
2 * Copyright (C) 2006 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.gsm;
18
19import android.app.Activity;
20import android.app.PendingIntent;
21import android.app.PendingIntent.CanceledException;
22import android.content.Intent;
23import android.os.AsyncResult;
24import android.os.Message;
25import android.os.SystemProperties;
26import android.provider.Telephony.Sms;
27import android.provider.Telephony.Sms.Intents;
28import android.telephony.ServiceState;
29import android.telephony.SmsCbMessage;
30import android.telephony.gsm.GsmCellLocation;
31import android.util.Config;
32import android.util.Log;
33
34import com.android.internal.telephony.BaseCommands;
35import com.android.internal.telephony.CommandsInterface;
36import com.android.internal.telephony.IccUtils;
37import com.android.internal.telephony.SMSDispatcher;
38import com.android.internal.telephony.SmsHeader;
39import com.android.internal.telephony.SmsMessageBase;
40import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails;
41import com.android.internal.telephony.TelephonyProperties;
42
43import java.util.ArrayList;
44import java.util.HashMap;
45import java.util.Iterator;
46
47import static android.telephony.SmsMessage.MessageClass;
48
49final class GsmSMSDispatcher extends SMSDispatcher {
50    private static final String TAG = "GSM";
51
52    private GSMPhone mGsmPhone;
53
54    GsmSMSDispatcher(GSMPhone phone) {
55        super(phone);
56        mGsmPhone = phone;
57
58        ((BaseCommands)mCm).setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null);
59    }
60
61    /**
62     * Called when a status report is received.  This should correspond to
63     * a previously successful SEND.
64     *
65     * @param ar AsyncResult passed into the message handler.  ar.result should
66     *           be a String representing the status report PDU, as ASCII hex.
67     */
68    protected void handleStatusReport(AsyncResult ar) {
69        String pduString = (String) ar.result;
70        SmsMessage sms = SmsMessage.newFromCDS(pduString);
71
72        if (sms != null) {
73            int tpStatus = sms.getStatus();
74            int messageRef = sms.messageRef;
75            for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
76                SmsTracker tracker = deliveryPendingList.get(i);
77                if (tracker.mMessageRef == messageRef) {
78                    // Found it.  Remove from list and broadcast.
79                    if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) {
80                       deliveryPendingList.remove(i);
81                    }
82                    PendingIntent intent = tracker.mDeliveryIntent;
83                    Intent fillIn = new Intent();
84                    fillIn.putExtra("pdu", IccUtils.hexStringToBytes(pduString));
85                    try {
86                        intent.send(mContext, Activity.RESULT_OK, fillIn);
87                    } catch (CanceledException ex) {}
88
89                    // Only expect to see one tracker matching this messageref
90                    break;
91                }
92            }
93        }
94        acknowledgeLastIncomingSms(true, Intents.RESULT_SMS_HANDLED, null);
95    }
96
97
98    /** {@inheritDoc} */
99    protected int dispatchMessage(SmsMessageBase smsb) {
100
101        // If sms is null, means there was a parsing error.
102        if (smsb == null) {
103            return Intents.RESULT_SMS_GENERIC_ERROR;
104        }
105        SmsMessage sms = (SmsMessage) smsb;
106        boolean handled = false;
107
108        if (sms.isTypeZero()) {
109            // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be
110            // Displayed/Stored/Notified. They should only be acknowledged.
111            Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack");
112            return Intents.RESULT_SMS_HANDLED;
113        }
114
115        // Special case the message waiting indicator messages
116        if (sms.isMWISetMessage()) {
117            mGsmPhone.updateMessageWaitingIndicator(true);
118            handled = sms.isMwiDontStore();
119            if (Config.LOGD) {
120                Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled);
121            }
122        } else if (sms.isMWIClearMessage()) {
123            mGsmPhone.updateMessageWaitingIndicator(false);
124            handled = sms.isMwiDontStore();
125            if (Config.LOGD) {
126                Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled);
127            }
128        }
129
130        if (handled) {
131            return Intents.RESULT_SMS_HANDLED;
132        }
133
134        if (!mStorageAvailable && (sms.getMessageClass() != MessageClass.CLASS_0)) {
135            // It's a storable message and there's no storage available.  Bail.
136            // (See TS 23.038 for a description of class 0 messages.)
137            return Intents.RESULT_SMS_OUT_OF_MEMORY;
138        }
139
140        SmsHeader smsHeader = sms.getUserDataHeader();
141         // See if message is partial or port addressed.
142        if ((smsHeader == null) || (smsHeader.concatRef == null)) {
143            // Message is not partial (not part of concatenated sequence).
144            byte[][] pdus = new byte[1][];
145            pdus[0] = sms.getPdu();
146
147            if (smsHeader != null && smsHeader.portAddrs != null) {
148                if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) {
149                    return mWapPush.dispatchWapPdu(sms.getUserData());
150                } else {
151                    // The message was sent to a port, so concoct a URI for it.
152                    dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort);
153                }
154            } else {
155                // Normal short and non-port-addressed message, dispatch it.
156                dispatchPdus(pdus);
157            }
158            return Activity.RESULT_OK;
159        } else {
160            // Process the message part.
161            return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs);
162        }
163    }
164
165    /** {@inheritDoc} */
166    protected void sendData(String destAddr, String scAddr, int destPort,
167            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
168        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
169                scAddr, destAddr, destPort, data, (deliveryIntent != null));
170        sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
171    }
172
173    /** {@inheritDoc} */
174    protected void sendText(String destAddr, String scAddr, String text,
175            PendingIntent sentIntent, PendingIntent deliveryIntent) {
176        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
177                scAddr, destAddr, text, (deliveryIntent != null));
178        sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
179    }
180
181    /** {@inheritDoc} */
182    protected void sendMultipartText(String destinationAddress, String scAddress,
183            ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
184            ArrayList<PendingIntent> deliveryIntents) {
185
186        int refNumber = getNextConcatenatedRef() & 0x00FF;
187        int msgCount = parts.size();
188        int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN;
189
190        mRemainingMessages = msgCount;
191
192        TextEncodingDetails[] encodingForParts = new TextEncodingDetails[msgCount];
193        for (int i = 0; i < msgCount; i++) {
194            TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false);
195            if (encoding != details.codeUnitSize
196                    && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN
197                            || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) {
198                encoding = details.codeUnitSize;
199            }
200            encodingForParts[i] = details;
201        }
202
203        for (int i = 0; i < msgCount; i++) {
204            SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
205            concatRef.refNumber = refNumber;
206            concatRef.seqNumber = i + 1;  // 1-based sequence
207            concatRef.msgCount = msgCount;
208            // TODO: We currently set this to true since our messaging app will never
209            // send more than 255 parts (it converts the message to MMS well before that).
210            // However, we should support 3rd party messaging apps that might need 16-bit
211            // references
212            // Note:  It's not sufficient to just flip this bit to true; it will have
213            // ripple effects (several calculations assume 8-bit ref).
214            concatRef.isEightBits = true;
215            SmsHeader smsHeader = new SmsHeader();
216            smsHeader.concatRef = concatRef;
217            if (encoding == android.telephony.SmsMessage.ENCODING_7BIT) {
218                smsHeader.languageTable = encodingForParts[i].languageTable;
219                smsHeader.languageShiftTable = encodingForParts[i].languageShiftTable;
220            }
221
222            PendingIntent sentIntent = null;
223            if (sentIntents != null && sentIntents.size() > i) {
224                sentIntent = sentIntents.get(i);
225            }
226
227            PendingIntent deliveryIntent = null;
228            if (deliveryIntents != null && deliveryIntents.size() > i) {
229                deliveryIntent = deliveryIntents.get(i);
230            }
231
232            SmsMessage.SubmitPdu pdus = SmsMessage.getSubmitPdu(scAddress, destinationAddress,
233                    parts.get(i), deliveryIntent != null, SmsHeader.toByteArray(smsHeader),
234                    encoding, smsHeader.languageTable, smsHeader.languageShiftTable);
235
236            sendRawPdu(pdus.encodedScAddress, pdus.encodedMessage, sentIntent, deliveryIntent);
237        }
238    }
239
240    /**
241     * Send a multi-part text based SMS which already passed SMS control check.
242     *
243     * It is the working function for sendMultipartText().
244     *
245     * @param destinationAddress the address to send the message to
246     * @param scAddress is the service center address or null to use
247     *   the current default SMSC
248     * @param parts an <code>ArrayList</code> of strings that, in order,
249     *   comprise the original message
250     * @param sentIntents if not null, an <code>ArrayList</code> of
251     *   <code>PendingIntent</code>s (one for each message part) that is
252     *   broadcast when the corresponding message part has been sent.
253     *   The result code will be <code>Activity.RESULT_OK<code> for success,
254     *   or one of these errors:
255     *   <code>RESULT_ERROR_GENERIC_FAILURE</code>
256     *   <code>RESULT_ERROR_RADIO_OFF</code>
257     *   <code>RESULT_ERROR_NULL_PDU</code>.
258     * @param deliveryIntents if not null, an <code>ArrayList</code> of
259     *   <code>PendingIntent</code>s (one for each message part) that is
260     *   broadcast when the corresponding message part has been delivered
261     *   to the recipient.  The raw pdu of the status report is in the
262     *   extended data ("pdu").
263     */
264    private void sendMultipartTextWithPermit(String destinationAddress,
265            String scAddress, ArrayList<String> parts,
266            ArrayList<PendingIntent> sentIntents,
267            ArrayList<PendingIntent> deliveryIntents) {
268
269        // check if in service
270        int ss = mPhone.getServiceState().getState();
271        if (ss != ServiceState.STATE_IN_SERVICE) {
272            for (int i = 0, count = parts.size(); i < count; i++) {
273                PendingIntent sentIntent = null;
274                if (sentIntents != null && sentIntents.size() > i) {
275                    sentIntent = sentIntents.get(i);
276                }
277                SmsTracker tracker = SmsTrackerFactory(null, sentIntent, null);
278                handleNotInService(ss, tracker);
279            }
280            return;
281        }
282
283        int refNumber = getNextConcatenatedRef() & 0x00FF;
284        int msgCount = parts.size();
285        int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN;
286
287        mRemainingMessages = msgCount;
288
289        TextEncodingDetails[] encodingForParts = new TextEncodingDetails[msgCount];
290        for (int i = 0; i < msgCount; i++) {
291            TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false);
292            if (encoding != details.codeUnitSize
293                    && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN
294                            || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) {
295                encoding = details.codeUnitSize;
296            }
297            encodingForParts[i] = details;
298        }
299
300        for (int i = 0; i < msgCount; i++) {
301            SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef();
302            concatRef.refNumber = refNumber;
303            concatRef.seqNumber = i + 1;  // 1-based sequence
304            concatRef.msgCount = msgCount;
305            concatRef.isEightBits = false;
306            SmsHeader smsHeader = new SmsHeader();
307            smsHeader.concatRef = concatRef;
308            if (encoding == android.telephony.SmsMessage.ENCODING_7BIT) {
309                smsHeader.languageTable = encodingForParts[i].languageTable;
310                smsHeader.languageShiftTable = encodingForParts[i].languageShiftTable;
311            }
312
313            PendingIntent sentIntent = null;
314            if (sentIntents != null && sentIntents.size() > i) {
315                sentIntent = sentIntents.get(i);
316            }
317
318            PendingIntent deliveryIntent = null;
319            if (deliveryIntents != null && deliveryIntents.size() > i) {
320                deliveryIntent = deliveryIntents.get(i);
321            }
322
323            SmsMessage.SubmitPdu pdus = SmsMessage.getSubmitPdu(scAddress, destinationAddress,
324                    parts.get(i), deliveryIntent != null, SmsHeader.toByteArray(smsHeader),
325                    encoding, smsHeader.languageTable, smsHeader.languageShiftTable);
326
327            HashMap<String, Object> map = new HashMap<String, Object>();
328            map.put("smsc", pdus.encodedScAddress);
329            map.put("pdu", pdus.encodedMessage);
330
331            SmsTracker tracker = SmsTrackerFactory(map, sentIntent, deliveryIntent);
332            sendSms(tracker);
333        }
334    }
335
336    /** {@inheritDoc} */
337    protected void sendSms(SmsTracker tracker) {
338        HashMap map = tracker.mData;
339
340        byte smsc[] = (byte[]) map.get("smsc");
341        byte pdu[] = (byte[]) map.get("pdu");
342
343        Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);
344        mCm.sendSMS(IccUtils.bytesToHexString(smsc),
345                IccUtils.bytesToHexString(pdu), reply);
346    }
347
348    /**
349     * Send the multi-part SMS based on multipart Sms tracker
350     *
351     * @param tracker holds the multipart Sms tracker ready to be sent
352     */
353    @Override
354    protected void sendMultipartSms (SmsTracker tracker) {
355        ArrayList<String> parts;
356        ArrayList<PendingIntent> sentIntents;
357        ArrayList<PendingIntent> deliveryIntents;
358
359        HashMap map = tracker.mData;
360
361        String destinationAddress = (String) map.get("destination");
362        String scAddress = (String) map.get("scaddress");
363
364        parts = (ArrayList<String>) map.get("parts");
365        sentIntents = (ArrayList<PendingIntent>) map.get("sentIntents");
366        deliveryIntents = (ArrayList<PendingIntent>) map.get("deliveryIntents");
367
368        sendMultipartTextWithPermit(destinationAddress,
369                scAddress, parts, sentIntents, deliveryIntents);
370
371    }
372
373    /** {@inheritDoc} */
374    @Override
375    protected void acknowledgeLastIncomingSms(boolean success, int result, Message response){
376        // FIXME unit test leaves cm == null. this should change
377        if (mCm != null) {
378            mCm.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response);
379        }
380    }
381
382    private int resultToCause(int rc) {
383        switch (rc) {
384            case Activity.RESULT_OK:
385            case Intents.RESULT_SMS_HANDLED:
386                // Cause code is ignored on success.
387                return 0;
388            case Intents.RESULT_SMS_OUT_OF_MEMORY:
389                return CommandsInterface.GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED;
390            case Intents.RESULT_SMS_GENERIC_ERROR:
391            default:
392                return CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR;
393        }
394    }
395
396    /**
397     * Holds all info about a message page needed to assemble a complete
398     * concatenated message
399     */
400    private static final class SmsCbConcatInfo {
401        private final SmsCbHeader mHeader;
402
403        private final String mPlmn;
404
405        private final int mLac;
406
407        private final int mCid;
408
409        public SmsCbConcatInfo(SmsCbHeader header, String plmn, int lac, int cid) {
410            mHeader = header;
411            mPlmn = plmn;
412            mLac = lac;
413            mCid = cid;
414        }
415
416        @Override
417        public int hashCode() {
418            return mHeader.messageIdentifier * 31 + mHeader.updateNumber;
419        }
420
421        @Override
422        public boolean equals(Object obj) {
423            if (obj instanceof SmsCbConcatInfo) {
424                SmsCbConcatInfo other = (SmsCbConcatInfo)obj;
425
426                // Two pages match if all header attributes (except the page
427                // index) are identical, and both pages belong to the same
428                // location (which is also determined by the scope parameter)
429                if (mHeader.geographicalScope == other.mHeader.geographicalScope
430                        && mHeader.messageCode == other.mHeader.messageCode
431                        && mHeader.updateNumber == other.mHeader.updateNumber
432                        && mHeader.messageIdentifier == other.mHeader.messageIdentifier
433                        && mHeader.dataCodingScheme == other.mHeader.dataCodingScheme
434                        && mHeader.nrOfPages == other.mHeader.nrOfPages) {
435                    return matchesLocation(other.mPlmn, other.mLac, other.mCid);
436                }
437            }
438
439            return false;
440        }
441
442        /**
443         * Checks if this concatenation info matches the given location. The
444         * granularity of the match depends on the geographical scope.
445         *
446         * @param plmn PLMN
447         * @param lac Location area code
448         * @param cid Cell ID
449         * @return true if matching, false otherwise
450         */
451        public boolean matchesLocation(String plmn, int lac, int cid) {
452            switch (mHeader.geographicalScope) {
453                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
454                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
455                    if (mCid != cid) {
456                        return false;
457                    }
458                    // deliberate fall-through
459                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
460                    if (mLac != lac) {
461                        return false;
462                    }
463                    // deliberate fall-through
464                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
465                    return mPlmn != null && mPlmn.equals(plmn);
466            }
467
468            return false;
469        }
470    }
471
472    // This map holds incomplete concatenated messages waiting for assembly
473    private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
474            new HashMap<SmsCbConcatInfo, byte[][]>();
475
476    @Override
477    protected void handleBroadcastSms(AsyncResult ar) {
478        try {
479            byte[][] pdus = null;
480            byte[] receivedPdu = (byte[])ar.result;
481
482            if (Config.LOGD) {
483                for (int i = 0; i < receivedPdu.length; i += 8) {
484                    StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
485                    for (int j = i; j < i + 8 && j < receivedPdu.length; j++) {
486                        int b = receivedPdu[j] & 0xff;
487                        if (b < 0x10) {
488                            sb.append('0');
489                        }
490                        sb.append(Integer.toHexString(b)).append(' ');
491                    }
492                    Log.d(TAG, sb.toString());
493                }
494            }
495
496            SmsCbHeader header = new SmsCbHeader(receivedPdu);
497            String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC);
498            GsmCellLocation cellLocation = (GsmCellLocation)mGsmPhone.getCellLocation();
499            int lac = cellLocation.getLac();
500            int cid = cellLocation.getCid();
501
502            if (header.nrOfPages > 1) {
503                // Multi-page message
504                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, plmn, lac, cid);
505
506                // Try to find other pages of the same message
507                pdus = mSmsCbPageMap.get(concatInfo);
508
509                if (pdus == null) {
510                    // This it the first page of this message, make room for all
511                    // pages and keep until complete
512                    pdus = new byte[header.nrOfPages][];
513
514                    mSmsCbPageMap.put(concatInfo, pdus);
515                }
516
517                // Page parameter is one-based
518                pdus[header.pageIndex - 1] = receivedPdu;
519
520                for (int i = 0; i < pdus.length; i++) {
521                    if (pdus[i] == null) {
522                        // Still missing pages, exit
523                        return;
524                    }
525                }
526
527                // Message complete, remove and dispatch
528                mSmsCbPageMap.remove(concatInfo);
529            } else {
530                // Single page message
531                pdus = new byte[1][];
532                pdus[0] = receivedPdu;
533            }
534
535            boolean isEmergencyMessage = SmsCbHeader.isEmergencyMessage(header.messageIdentifier);
536            dispatchBroadcastPdus(pdus, isEmergencyMessage);
537
538            // Remove messages that are out of scope to prevent the map from
539            // growing indefinitely, containing incomplete messages that were
540            // never assembled
541            Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
542
543            while (iter.hasNext()) {
544                SmsCbConcatInfo info = iter.next();
545
546                if (!info.matchesLocation(plmn, lac, cid)) {
547                    iter.remove();
548                }
549            }
550        } catch (RuntimeException e) {
551            Log.e(TAG, "Error in decoding SMS CB pdu", e);
552        }
553    }
554
555}
556