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