| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.internal.telephony.gsm; |
| |
| import android.app.Activity; |
| import android.app.PendingIntent; |
| import android.app.PendingIntent.CanceledException; |
| import android.content.Intent; |
| import android.os.AsyncResult; |
| import android.os.Message; |
| import android.os.SystemProperties; |
| import android.provider.Telephony.Sms; |
| import android.provider.Telephony.Sms.Intents; |
| import android.telephony.PhoneNumberUtils; |
| import android.telephony.SmsCbLocation; |
| import android.telephony.SmsCbMessage; |
| import android.telephony.SmsManager; |
| import android.telephony.gsm.GsmCellLocation; |
| import android.util.Log; |
| |
| import com.android.internal.telephony.CommandsInterface; |
| import com.android.internal.telephony.GsmAlphabet; |
| import com.android.internal.telephony.IccUtils; |
| import com.android.internal.telephony.PhoneBase; |
| import com.android.internal.telephony.SmsConstants; |
| import com.android.internal.telephony.SMSDispatcher; |
| import com.android.internal.telephony.SmsHeader; |
| import com.android.internal.telephony.SmsMessageBase; |
| import com.android.internal.telephony.SmsStorageMonitor; |
| import com.android.internal.telephony.SmsUsageMonitor; |
| import com.android.internal.telephony.TelephonyProperties; |
| |
| import java.util.HashMap; |
| import java.util.Iterator; |
| |
| public final class GsmSMSDispatcher extends SMSDispatcher { |
| private static final String TAG = "GSM"; |
| |
| /** Status report received */ |
| private static final int EVENT_NEW_SMS_STATUS_REPORT = 100; |
| |
| /** New broadcast SMS */ |
| private static final int EVENT_NEW_BROADCAST_SMS = 101; |
| |
| /** Result of writing SM to UICC (when SMS-PP service is not available). */ |
| private static final int EVENT_WRITE_SMS_COMPLETE = 102; |
| |
| /** Handler for SMS-PP data download messages to UICC. */ |
| private final UsimDataDownloadHandler mDataDownloadHandler; |
| |
| public GsmSMSDispatcher(PhoneBase phone, SmsStorageMonitor storageMonitor, |
| SmsUsageMonitor usageMonitor) { |
| super(phone, storageMonitor, usageMonitor); |
| mDataDownloadHandler = new UsimDataDownloadHandler(mCm); |
| mCm.setOnNewGsmSms(this, EVENT_NEW_SMS, null); |
| mCm.setOnSmsStatus(this, EVENT_NEW_SMS_STATUS_REPORT, null); |
| mCm.setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null); |
| } |
| |
| @Override |
| public void dispose() { |
| mCm.unSetOnNewGsmSms(this); |
| mCm.unSetOnSmsStatus(this); |
| mCm.unSetOnNewGsmBroadcastSms(this); |
| } |
| |
| @Override |
| protected String getFormat() { |
| return SmsConstants.FORMAT_3GPP; |
| } |
| |
| /** |
| * Handles 3GPP format-specific events coming from the phone stack. |
| * Other events are handled by {@link SMSDispatcher#handleMessage}. |
| * |
| * @param msg the message to handle |
| */ |
| @Override |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case EVENT_NEW_SMS_STATUS_REPORT: |
| handleStatusReport((AsyncResult) msg.obj); |
| break; |
| |
| case EVENT_NEW_BROADCAST_SMS: |
| handleBroadcastSms((AsyncResult)msg.obj); |
| break; |
| |
| case EVENT_WRITE_SMS_COMPLETE: |
| AsyncResult ar = (AsyncResult) msg.obj; |
| if (ar.exception == null) { |
| Log.d(TAG, "Successfully wrote SMS-PP message to UICC"); |
| mCm.acknowledgeLastIncomingGsmSms(true, 0, null); |
| } else { |
| Log.d(TAG, "Failed to write SMS-PP message to UICC", ar.exception); |
| mCm.acknowledgeLastIncomingGsmSms(false, |
| CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR, null); |
| } |
| break; |
| |
| default: |
| super.handleMessage(msg); |
| } |
| } |
| |
| /** |
| * Called when a status report is received. This should correspond to |
| * a previously successful SEND. |
| * |
| * @param ar AsyncResult passed into the message handler. ar.result should |
| * be a String representing the status report PDU, as ASCII hex. |
| */ |
| private void handleStatusReport(AsyncResult ar) { |
| String pduString = (String) ar.result; |
| SmsMessage sms = SmsMessage.newFromCDS(pduString); |
| |
| if (sms != null) { |
| int tpStatus = sms.getStatus(); |
| int messageRef = sms.messageRef; |
| for (int i = 0, count = deliveryPendingList.size(); i < count; i++) { |
| SmsTracker tracker = deliveryPendingList.get(i); |
| if (tracker.mMessageRef == messageRef) { |
| // Found it. Remove from list and broadcast. |
| if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) { |
| deliveryPendingList.remove(i); |
| } |
| PendingIntent intent = tracker.mDeliveryIntent; |
| Intent fillIn = new Intent(); |
| fillIn.putExtra("pdu", IccUtils.hexStringToBytes(pduString)); |
| fillIn.putExtra("format", SmsConstants.FORMAT_3GPP); |
| try { |
| intent.send(mContext, Activity.RESULT_OK, fillIn); |
| } catch (CanceledException ex) {} |
| |
| // Only expect to see one tracker matching this messageref |
| break; |
| } |
| } |
| } |
| acknowledgeLastIncomingSms(true, Intents.RESULT_SMS_HANDLED, null); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public int dispatchMessage(SmsMessageBase smsb) { |
| |
| // If sms is null, means there was a parsing error. |
| if (smsb == null) { |
| Log.e(TAG, "dispatchMessage: message is null"); |
| return Intents.RESULT_SMS_GENERIC_ERROR; |
| } |
| |
| SmsMessage sms = (SmsMessage) smsb; |
| |
| if (sms.isTypeZero()) { |
| // As per 3GPP TS 23.040 9.2.3.9, Type Zero messages should not be |
| // Displayed/Stored/Notified. They should only be acknowledged. |
| Log.d(TAG, "Received short message type 0, Don't display or store it. Send Ack"); |
| return Intents.RESULT_SMS_HANDLED; |
| } |
| |
| // Send SMS-PP data download messages to UICC. See 3GPP TS 31.111 section 7.1.1. |
| if (sms.isUsimDataDownload()) { |
| UsimServiceTable ust = mPhone.getUsimServiceTable(); |
| // If we receive an SMS-PP message before the UsimServiceTable has been loaded, |
| // assume that the data download service is not present. This is very unlikely to |
| // happen because the IMS connection will not be established until after the ISIM |
| // records have been loaded, after the USIM service table has been loaded. |
| if (ust != null && ust.isAvailable( |
| UsimServiceTable.UsimService.DATA_DL_VIA_SMS_PP)) { |
| Log.d(TAG, "Received SMS-PP data download, sending to UICC."); |
| return mDataDownloadHandler.startDataDownload(sms); |
| } else { |
| Log.d(TAG, "DATA_DL_VIA_SMS_PP service not available, storing message to UICC."); |
| String smsc = IccUtils.bytesToHexString( |
| PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength( |
| sms.getServiceCenterAddress())); |
| mCm.writeSmsToSim(SmsManager.STATUS_ON_ICC_UNREAD, smsc, |
| IccUtils.bytesToHexString(sms.getPdu()), |
| obtainMessage(EVENT_WRITE_SMS_COMPLETE)); |
| return Activity.RESULT_OK; // acknowledge after response from write to USIM |
| } |
| } |
| |
| if (mSmsReceiveDisabled) { |
| // Device doesn't support SMS service, |
| Log.d(TAG, "Received short message on device which doesn't support " |
| + "SMS service. Ignored."); |
| return Intents.RESULT_SMS_HANDLED; |
| } |
| |
| // Special case the message waiting indicator messages |
| boolean handled = false; |
| if (sms.isMWISetMessage()) { |
| mPhone.setVoiceMessageWaiting(1, -1); // line 1: unknown number of msgs waiting |
| handled = sms.isMwiDontStore(); |
| if (false) { |
| Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled); |
| } |
| } else if (sms.isMWIClearMessage()) { |
| mPhone.setVoiceMessageWaiting(1, 0); // line 1: no msgs waiting |
| handled = sms.isMwiDontStore(); |
| if (false) { |
| Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled); |
| } |
| } |
| |
| if (handled) { |
| return Intents.RESULT_SMS_HANDLED; |
| } |
| |
| if (!mStorageMonitor.isStorageAvailable() && |
| sms.getMessageClass() != SmsConstants.MessageClass.CLASS_0) { |
| // It's a storable message and there's no storage available. Bail. |
| // (See TS 23.038 for a description of class 0 messages.) |
| return Intents.RESULT_SMS_OUT_OF_MEMORY; |
| } |
| |
| return dispatchNormalMessage(smsb); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void sendData(String destAddr, String scAddr, int destPort, |
| byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) { |
| SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( |
| scAddr, destAddr, destPort, data, (deliveryIntent != null)); |
| if (pdu != null) { |
| sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, |
| destAddr); |
| } else { |
| Log.e(TAG, "GsmSMSDispatcher.sendData(): getSubmitPdu() returned null"); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void sendText(String destAddr, String scAddr, String text, |
| PendingIntent sentIntent, PendingIntent deliveryIntent) { |
| SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( |
| scAddr, destAddr, text, (deliveryIntent != null)); |
| if (pdu != null) { |
| sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, |
| destAddr); |
| } else { |
| Log.e(TAG, "GsmSMSDispatcher.sendText(): getSubmitPdu() returned null"); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected GsmAlphabet.TextEncodingDetails calculateLength(CharSequence messageBody, |
| boolean use7bitOnly) { |
| return SmsMessage.calculateLength(messageBody, use7bitOnly); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void sendNewSubmitPdu(String destinationAddress, String scAddress, |
| String message, SmsHeader smsHeader, int encoding, |
| PendingIntent sentIntent, PendingIntent deliveryIntent, boolean lastPart) { |
| SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddress, destinationAddress, |
| message, deliveryIntent != null, SmsHeader.toByteArray(smsHeader), |
| encoding, smsHeader.languageTable, smsHeader.languageShiftTable); |
| if (pdu != null) { |
| sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent, |
| destinationAddress); |
| } else { |
| Log.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null"); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void sendSms(SmsTracker tracker) { |
| HashMap<String, Object> map = tracker.mData; |
| |
| byte smsc[] = (byte[]) map.get("smsc"); |
| byte pdu[] = (byte[]) map.get("pdu"); |
| |
| Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker); |
| mCm.sendSMS(IccUtils.bytesToHexString(smsc), IccUtils.bytesToHexString(pdu), reply); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) { |
| mCm.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response); |
| } |
| |
| private static int resultToCause(int rc) { |
| switch (rc) { |
| case Activity.RESULT_OK: |
| case Intents.RESULT_SMS_HANDLED: |
| // Cause code is ignored on success. |
| return 0; |
| case Intents.RESULT_SMS_OUT_OF_MEMORY: |
| return CommandsInterface.GSM_SMS_FAIL_CAUSE_MEMORY_CAPACITY_EXCEEDED; |
| case Intents.RESULT_SMS_GENERIC_ERROR: |
| default: |
| return CommandsInterface.GSM_SMS_FAIL_CAUSE_UNSPECIFIED_ERROR; |
| } |
| } |
| |
| /** |
| * Holds all info about a message page needed to assemble a complete |
| * concatenated message |
| */ |
| private static final class SmsCbConcatInfo { |
| |
| private final SmsCbHeader mHeader; |
| private final SmsCbLocation mLocation; |
| |
| public SmsCbConcatInfo(SmsCbHeader header, SmsCbLocation location) { |
| mHeader = header; |
| mLocation = location; |
| } |
| |
| @Override |
| public int hashCode() { |
| return (mHeader.getSerialNumber() * 31) + mLocation.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj instanceof SmsCbConcatInfo) { |
| SmsCbConcatInfo other = (SmsCbConcatInfo)obj; |
| |
| // Two pages match if they have the same serial number (which includes the |
| // geographical scope and update number), and both pages belong to the same |
| // location (PLMN, plus LAC and CID if these are part of the geographical scope). |
| return mHeader.getSerialNumber() == other.mHeader.getSerialNumber() |
| && mLocation.equals(other.mLocation); |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Compare the location code for this message to the current location code. The match is |
| * relative to the geographical scope of the message, which determines whether the LAC |
| * and Cell ID are saved in mLocation or set to -1 to match all values. |
| * |
| * @param plmn the current PLMN |
| * @param lac the current Location Area (GSM) or Service Area (UMTS) |
| * @param cid the current Cell ID |
| * @return true if this message is valid for the current location; false otherwise |
| */ |
| public boolean matchesLocation(String plmn, int lac, int cid) { |
| return mLocation.isInLocationArea(plmn, lac, cid); |
| } |
| } |
| |
| // This map holds incomplete concatenated messages waiting for assembly |
| private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = |
| new HashMap<SmsCbConcatInfo, byte[][]>(); |
| |
| /** |
| * Handle 3GPP format SMS-CB message. |
| * @param ar the AsyncResult containing the received PDUs |
| */ |
| private void handleBroadcastSms(AsyncResult ar) { |
| try { |
| byte[] receivedPdu = (byte[])ar.result; |
| |
| if (false) { |
| for (int i = 0; i < receivedPdu.length; i += 8) { |
| StringBuilder sb = new StringBuilder("SMS CB pdu data: "); |
| for (int j = i; j < i + 8 && j < receivedPdu.length; j++) { |
| int b = receivedPdu[j] & 0xff; |
| if (b < 0x10) { |
| sb.append('0'); |
| } |
| sb.append(Integer.toHexString(b)).append(' '); |
| } |
| Log.d(TAG, sb.toString()); |
| } |
| } |
| |
| SmsCbHeader header = new SmsCbHeader(receivedPdu); |
| String plmn = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_NUMERIC); |
| int lac = -1; |
| int cid = -1; |
| android.telephony.CellLocation cl = mPhone.getCellLocation(); |
| // Check if cell location is GsmCellLocation. This is required to support |
| // dual-mode devices such as CDMA/LTE devices that require support for |
| // both 3GPP and 3GPP2 format messages |
| if (cl instanceof GsmCellLocation) { |
| GsmCellLocation cellLocation = (GsmCellLocation)cl; |
| lac = cellLocation.getLac(); |
| cid = cellLocation.getCid(); |
| } |
| |
| SmsCbLocation location; |
| switch (header.getGeographicalScope()) { |
| case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE: |
| location = new SmsCbLocation(plmn, lac, -1); |
| break; |
| |
| case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: |
| case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: |
| location = new SmsCbLocation(plmn, lac, cid); |
| break; |
| |
| case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: |
| default: |
| location = new SmsCbLocation(plmn); |
| break; |
| } |
| |
| byte[][] pdus; |
| int pageCount = header.getNumberOfPages(); |
| if (pageCount > 1) { |
| // Multi-page message |
| SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, location); |
| |
| // Try to find other pages of the same message |
| pdus = mSmsCbPageMap.get(concatInfo); |
| |
| if (pdus == null) { |
| // This is the first page of this message, make room for all |
| // pages and keep until complete |
| pdus = new byte[pageCount][]; |
| |
| mSmsCbPageMap.put(concatInfo, pdus); |
| } |
| |
| // Page parameter is one-based |
| pdus[header.getPageIndex() - 1] = receivedPdu; |
| |
| for (int i = 0; i < pdus.length; i++) { |
| if (pdus[i] == null) { |
| // Still missing pages, exit |
| return; |
| } |
| } |
| |
| // Message complete, remove and dispatch |
| mSmsCbPageMap.remove(concatInfo); |
| } else { |
| // Single page message |
| pdus = new byte[1][]; |
| pdus[0] = receivedPdu; |
| } |
| |
| SmsCbMessage message = GsmSmsCbMessage.createSmsCbMessage(header, location, pdus); |
| dispatchBroadcastMessage(message); |
| |
| // Remove messages that are out of scope to prevent the map from |
| // growing indefinitely, containing incomplete messages that were |
| // never assembled |
| Iterator<SmsCbConcatInfo> iter = mSmsCbPageMap.keySet().iterator(); |
| |
| while (iter.hasNext()) { |
| SmsCbConcatInfo info = iter.next(); |
| |
| if (!info.matchesLocation(plmn, lac, cid)) { |
| iter.remove(); |
| } |
| } |
| } catch (RuntimeException e) { |
| Log.e(TAG, "Error in decoding SMS CB pdu", e); |
| } |
| } |
| } |