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