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