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