GsmSMSDispatcher.java revision 87d14a1756f2ff4abdc107de35d06739245a606e
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.CommandsInterface;
34import com.android.internal.telephony.IccUtils;
35import com.android.internal.telephony.PhoneBase;
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.SmsStorageMonitor;
41import com.android.internal.telephony.SmsUsageMonitor;
42import com.android.internal.telephony.TelephonyProperties;
43
44import java.util.ArrayList;
45import java.util.HashMap;
46import java.util.Iterator;
47
48import static android.telephony.SmsMessage.MessageClass;
49
50public final class GsmSMSDispatcher extends SMSDispatcher {
51    private static final String TAG = "GSM";
52
53    /** Status report received */
54    private static final int EVENT_NEW_SMS_STATUS_REPORT = 100;
55
56    /** New broadcast SMS */
57    private static final int EVENT_NEW_BROADCAST_SMS = 101;
58
59    public GsmSMSDispatcher(PhoneBase phone, SmsStorageMonitor storageMonitor,
60            SmsUsageMonitor usageMonitor) {
61        super(phone, storageMonitor, usageMonitor);
62        mCm.setOnNewGsmSms(this, EVENT_NEW_SMS, null);
63        mCm.setOnSmsStatus(this, EVENT_NEW_SMS_STATUS_REPORT, null);
64        mCm.setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null);
65    }
66
67    @Override
68    public void dispose() {
69        mCm.unSetOnNewGsmSms(this);
70        mCm.unSetOnSmsStatus(this);
71        mCm.unSetOnNewGsmBroadcastSms(this);
72    }
73
74    @Override
75    protected String getFormat() {
76        return android.telephony.SmsMessage.FORMAT_3GPP;
77    }
78
79    /**
80     * Handles 3GPP format-specific events coming from the phone stack.
81     * Other events are handled by {@link SMSDispatcher#handleMessage}.
82     *
83     * @param msg the message to handle
84     */
85    @Override
86    public void handleMessage(Message msg) {
87        switch (msg.what) {
88        case EVENT_NEW_SMS_STATUS_REPORT:
89            handleStatusReport((AsyncResult) msg.obj);
90            break;
91
92        case EVENT_NEW_BROADCAST_SMS:
93            handleBroadcastSms((AsyncResult)msg.obj);
94            break;
95
96        default:
97            super.handleMessage(msg);
98        }
99    }
100
101    /**
102     * Called when a status report is received.  This should correspond to
103     * a previously successful SEND.
104     *
105     * @param ar AsyncResult passed into the message handler.  ar.result should
106     *           be a String representing the status report PDU, as ASCII hex.
107     */
108    private void handleStatusReport(AsyncResult ar) {
109        String pduString = (String) ar.result;
110        SmsMessage sms = SmsMessage.newFromCDS(pduString);
111
112        if (sms != null) {
113            int tpStatus = sms.getStatus();
114            int messageRef = sms.messageRef;
115            for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
116                SmsTracker tracker = deliveryPendingList.get(i);
117                if (tracker.mMessageRef == messageRef) {
118                    // Found it.  Remove from list and broadcast.
119                    if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) {
120                       deliveryPendingList.remove(i);
121                    }
122                    PendingIntent intent = tracker.mDeliveryIntent;
123                    Intent fillIn = new Intent();
124                    fillIn.putExtra("pdu", IccUtils.hexStringToBytes(pduString));
125                    fillIn.putExtra("format", android.telephony.SmsMessage.FORMAT_3GPP);
126                    try {
127                        intent.send(mContext, Activity.RESULT_OK, fillIn);
128                    } catch (CanceledException ex) {}
129
130                    // Only expect to see one tracker matching this messageref
131                    break;
132                }
133            }
134        }
135        acknowledgeLastIncomingSms(true, Intents.RESULT_SMS_HANDLED, null);
136    }
137
138    /** {@inheritDoc} */
139    @Override
140    public int dispatchMessage(SmsMessageBase smsb) {
141
142        // If sms is null, means there was a parsing error.
143        if (smsb == null) {
144            Log.e(TAG, "dispatchMessage: message is null");
145            return Intents.RESULT_SMS_GENERIC_ERROR;
146        }
147
148        SmsMessage sms = (SmsMessage) smsb;
149
150        if (sms.isTypeZero()) {
151            // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be
152            // Displayed/Stored/Notified. They should only be acknowledged.
153            Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack");
154            return Intents.RESULT_SMS_HANDLED;
155        }
156
157        if (mSmsReceiveDisabled) {
158            // Device doesn't support SMS service,
159            Log.d(TAG, "Received short message on device which doesn't support "
160                    + "SMS service. Ignored.");
161            return Intents.RESULT_SMS_HANDLED;
162        }
163
164        // Special case the message waiting indicator messages
165        boolean handled = false;
166        if (sms.isMWISetMessage()) {
167            mPhone.setVoiceMessageWaiting(1, -1);  // line 1: unknown number of msgs waiting
168            handled = sms.isMwiDontStore();
169            if (false) {
170                Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled);
171            }
172        } else if (sms.isMWIClearMessage()) {
173            mPhone.setVoiceMessageWaiting(1, 0);   // line 1: no msgs waiting
174            handled = sms.isMwiDontStore();
175            if (false) {
176                Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled);
177            }
178        }
179
180        if (handled) {
181            return Intents.RESULT_SMS_HANDLED;
182        }
183
184        if (!mStorageMonitor.isStorageAvailable() &&
185                sms.getMessageClass() != MessageClass.CLASS_0) {
186            // It's a storable message and there's no storage available.  Bail.
187            // (See TS 23.038 for a description of class 0 messages.)
188            return Intents.RESULT_SMS_OUT_OF_MEMORY;
189        }
190
191        return dispatchNormalMessage(smsb);
192    }
193
194    /** {@inheritDoc} */
195    @Override
196    protected void sendData(String destAddr, String scAddr, int destPort,
197            byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
198        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
199                scAddr, destAddr, destPort, data, (deliveryIntent != null));
200        if (pdu != null) {
201            sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
202        } else {
203            Log.e(TAG, "GsmSMSDispatcher.sendData(): getSubmitPdu() returned null");
204        }
205    }
206
207    /** {@inheritDoc} */
208    @Override
209    protected void sendText(String destAddr, String scAddr, String text,
210            PendingIntent sentIntent, PendingIntent deliveryIntent) {
211        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
212                scAddr, destAddr, text, (deliveryIntent != null));
213        if (pdu != null) {
214            sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
215        } else {
216            Log.e(TAG, "GsmSMSDispatcher.sendText(): getSubmitPdu() returned null");
217        }
218    }
219
220    /** {@inheritDoc} */
221    @Override
222    protected TextEncodingDetails calculateLength(CharSequence messageBody,
223            boolean use7bitOnly) {
224        return SmsMessage.calculateLength(messageBody, use7bitOnly);
225    }
226
227    /** {@inheritDoc} */
228    @Override
229    protected void sendNewSubmitPdu(String destinationAddress, String scAddress,
230            String message, SmsHeader smsHeader, int encoding,
231            PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart) {
232        SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddress, destinationAddress,
233                message, deliveryIntent != null, SmsHeader.toByteArray(smsHeader),
234                encoding, smsHeader.languageTable, smsHeader.languageShiftTable);
235        if (pdu != null) {
236            sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
237        } else {
238            Log.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null");
239        }
240    }
241
242    /** {@inheritDoc} */
243    @Override
244    protected void sendSms(SmsTracker tracker) {
245        HashMap<String, Object> map = tracker.mData;
246
247        byte smsc[] = (byte[]) map.get("smsc");
248        byte pdu[] = (byte[]) map.get("pdu");
249
250        Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);
251        mCm.sendSMS(IccUtils.bytesToHexString(smsc), IccUtils.bytesToHexString(pdu), reply);
252    }
253
254    /** {@inheritDoc} */
255    @Override
256    protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) {
257        mCm.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response);
258    }
259
260    private static int resultToCause(int rc) {
261        switch (rc) {
262            case Activity.RESULT_OK:
263            case Intents.RESULT_SMS_HANDLED:
264                // Cause code is ignored on success.
265                return 0;
266            case Intents.RESULT_SMS_OUT_OF_MEMORY:
267                return CommandsInterface.GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED;
268            case Intents.RESULT_SMS_GENERIC_ERROR:
269            default:
270                return CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR;
271        }
272    }
273
274    /**
275     * Holds all info about a message page needed to assemble a complete
276     * concatenated message
277     */
278    private static final class SmsCbConcatInfo {
279        private final SmsCbHeader mHeader;
280
281        private final String mPlmn;
282
283        private final int mLac;
284
285        private final int mCid;
286
287        public SmsCbConcatInfo(SmsCbHeader header, String plmn, int lac, int cid) {
288            mHeader = header;
289            mPlmn = plmn;
290            mLac = lac;
291            mCid = cid;
292        }
293
294        @Override
295        public int hashCode() {
296            return mHeader.messageIdentifier * 31 + mHeader.updateNumber;
297        }
298
299        @Override
300        public boolean equals(Object obj) {
301            if (obj instanceof SmsCbConcatInfo) {
302                SmsCbConcatInfo other = (SmsCbConcatInfo)obj;
303
304                // Two pages match if all header attributes (except the page
305                // index) are identical, and both pages belong to the same
306                // location (which is also determined by the scope parameter)
307                if (mHeader.geographicalScope == other.mHeader.geographicalScope
308                        && mHeader.messageCode == other.mHeader.messageCode
309                        && mHeader.updateNumber == other.mHeader.updateNumber
310                        && mHeader.messageIdentifier == other.mHeader.messageIdentifier
311                        && mHeader.dataCodingScheme == other.mHeader.dataCodingScheme
312                        && mHeader.nrOfPages == other.mHeader.nrOfPages) {
313                    return matchesLocation(other.mPlmn, other.mLac, other.mCid);
314                }
315            }
316
317            return false;
318        }
319
320        /**
321         * Checks if this concatenation info matches the given location. The
322         * granularity of the match depends on the geographical scope.
323         *
324         * @param plmn PLMN
325         * @param lac Location area code
326         * @param cid Cell ID
327         * @return true if matching, false otherwise
328         */
329        public boolean matchesLocation(String plmn, int lac, int cid) {
330            switch (mHeader.geographicalScope) {
331                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE:
332                case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE:
333                    if (mCid != cid) {
334                        return false;
335                    }
336                    // deliberate fall-through
337                case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE:
338                    if (mLac != lac) {
339                        return false;
340                    }
341                    // deliberate fall-through
342                case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE:
343                    return mPlmn != null && mPlmn.equals(plmn);
344            }
345
346            return false;
347        }
348    }
349
350    // This map holds incomplete concatenated messages waiting for assembly
351    private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap =
352            new HashMap<SmsCbConcatInfo, byte[][]>();
353
354    /**
355     * Handle 3GPP format SMS-CB message.
356     * @param ar the AsyncResult containing the received PDUs
357     */
358    private void handleBroadcastSms(AsyncResult ar) {
359        try {
360            byte[] receivedPdu = (byte[])ar.result;
361
362            if (false) {
363                for (int i = 0; i < receivedPdu.length; i += 8) {
364                    StringBuilder sb = new StringBuilder("SMS CB pdu data: ");
365                    for (int j = i; j < i + 8 && j < receivedPdu.length; j++) {
366                        int b = receivedPdu[j] & 0xff;
367                        if (b < 0x10) {
368                            sb.append('0');
369                        }
370                        sb.append(Integer.toHexString(b)).append(' ');
371                    }
372                    Log.d(TAG, sb.toString());
373                }
374            }
375
376            SmsCbHeader header = new SmsCbHeader(receivedPdu);
377            String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC);
378            GsmCellLocation cellLocation = (GsmCellLocation) mPhone.getCellLocation();
379            int lac = cellLocation.getLac();
380            int cid = cellLocation.getCid();
381
382            byte[][] pdus;
383            if (header.nrOfPages > 1) {
384                // Multi-page message
385                SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, plmn, lac, cid);
386
387                // Try to find other pages of the same message
388                pdus = mSmsCbPageMap.get(concatInfo);
389
390                if (pdus == null) {
391                    // This it the first page of this message, make room for all
392                    // pages and keep until complete
393                    pdus = new byte[header.nrOfPages][];
394
395                    mSmsCbPageMap.put(concatInfo, pdus);
396                }
397
398                // Page parameter is one-based
399                pdus[header.pageIndex - 1] = receivedPdu;
400
401                for (int i = 0; i < pdus.length; i++) {
402                    if (pdus[i] == null) {
403                        // Still missing pages, exit
404                        return;
405                    }
406                }
407
408                // Message complete, remove and dispatch
409                mSmsCbPageMap.remove(concatInfo);
410            } else {
411                // Single page message
412                pdus = new byte[1][];
413                pdus[0] = receivedPdu;
414            }
415
416            boolean isEmergencyMessage = SmsCbHeader.isEmergencyMessage(header.messageIdentifier);
417            dispatchBroadcastPdus(pdus, isEmergencyMessage);
418
419            // Remove messages that are out of scope to prevent the map from
420            // growing indefinitely, containing incomplete messages that were
421            // never assembled
422            Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator();
423
424            while (iter.hasNext()) {
425                SmsCbConcatInfo info = iter.next();
426
427                if (!info.matchesLocation(plmn, lac, cid)) {
428                    iter.remove();
429                }
430            }
431        } catch (RuntimeException e) {
432            Log.e(TAG, "Error in decoding SMS CB pdu", e);
433        }
434    }
435}
436