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