/* * 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.ServiceState; import android.telephony.SmsCbMessage; import android.telephony.gsm.GsmCellLocation; import android.util.Config; import android.util.Log; import com.android.internal.telephony.BaseCommands; import com.android.internal.telephony.CommandsInterface; import com.android.internal.telephony.IccUtils; import com.android.internal.telephony.SMSDispatcher; import com.android.internal.telephony.SmsHeader; import com.android.internal.telephony.SmsMessageBase; import com.android.internal.telephony.SmsMessageBase.TextEncodingDetails; import com.android.internal.telephony.TelephonyProperties; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import static android.telephony.SmsMessage.MessageClass; final class GsmSMSDispatcher extends SMSDispatcher { private static final String TAG = "GSM"; private GSMPhone mGsmPhone; GsmSMSDispatcher(GSMPhone phone) { super(phone); mGsmPhone = phone; ((BaseCommands)mCm).setOnNewGsmBroadcastSms(this, EVENT_NEW_BROADCAST_SMS, null); } /** * 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. */ protected 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)); 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} */ protected int dispatchMessage(SmsMessageBase smsb) { // If sms is null, means there was a parsing error. if (smsb == null) { return Intents.RESULT_SMS_GENERIC_ERROR; } SmsMessage sms = (SmsMessage) smsb; boolean handled = false; 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; } // Special case the message waiting indicator messages if (sms.isMWISetMessage()) { mGsmPhone.updateMessageWaitingIndicator(true); handled = sms.isMwiDontStore(); if (Config.LOGD) { Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled); } } else if (sms.isMWIClearMessage()) { mGsmPhone.updateMessageWaitingIndicator(false); handled = sms.isMwiDontStore(); if (Config.LOGD) { Log.d(TAG, "Received voice mail indicator clear SMS shouldStore=" + !handled); } } if (handled) { return Intents.RESULT_SMS_HANDLED; } if (!mStorageAvailable && (sms.getMessageClass() != 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; } SmsHeader smsHeader = sms.getUserDataHeader(); // See if message is partial or port addressed. if ((smsHeader == null) || (smsHeader.concatRef == null)) { // Message is not partial (not part of concatenated sequence). byte[][] pdus = new byte[1][]; pdus[0] = sms.getPdu(); if (smsHeader != null && smsHeader.portAddrs != null) { if (smsHeader.portAddrs.destPort == SmsHeader.PORT_WAP_PUSH) { return mWapPush.dispatchWapPdu(sms.getUserData()); } else { // The message was sent to a port, so concoct a URI for it. dispatchPortAddressedPdus(pdus, smsHeader.portAddrs.destPort); } } else { // Normal short and non-port-addressed message, dispatch it. dispatchPdus(pdus); } return Activity.RESULT_OK; } else { // Process the message part. return processMessagePart(sms, smsHeader.concatRef, smsHeader.portAddrs); } } /** {@inheritDoc} */ 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)); sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); } /** {@inheritDoc} */ protected void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent, PendingIntent deliveryIntent) { SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu( scAddr, destAddr, text, (deliveryIntent != null)); sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent); } /** {@inheritDoc} */ protected void sendMultipartText(String destinationAddress, String scAddress, ArrayList<String> parts, ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { int refNumber = getNextConcatenatedRef() & 0x00FF; int msgCount = parts.size(); int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN; mRemainingMessages = msgCount; TextEncodingDetails[] encodingForParts = new TextEncodingDetails[msgCount]; for (int i = 0; i < msgCount; i++) { TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false); if (encoding != details.codeUnitSize && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) { encoding = details.codeUnitSize; } encodingForParts[i] = details; } for (int i = 0; i < msgCount; i++) { SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); concatRef.refNumber = refNumber; concatRef.seqNumber = i + 1; // 1-based sequence concatRef.msgCount = msgCount; // TODO: We currently set this to true since our messaging app will never // send more than 255 parts (it converts the message to MMS well before that). // However, we should support 3rd party messaging apps that might need 16-bit // references // Note: It's not sufficient to just flip this bit to true; it will have // ripple effects (several calculations assume 8-bit ref). concatRef.isEightBits = true; SmsHeader smsHeader = new SmsHeader(); smsHeader.concatRef = concatRef; if (encoding == android.telephony.SmsMessage.ENCODING_7BIT) { smsHeader.languageTable = encodingForParts[i].languageTable; smsHeader.languageShiftTable = encodingForParts[i].languageShiftTable; } PendingIntent sentIntent = null; if (sentIntents != null && sentIntents.size() > i) { sentIntent = sentIntents.get(i); } PendingIntent deliveryIntent = null; if (deliveryIntents != null && deliveryIntents.size() > i) { deliveryIntent = deliveryIntents.get(i); } SmsMessage.SubmitPdu pdus = SmsMessage.getSubmitPdu(scAddress, destinationAddress, parts.get(i), deliveryIntent != null, SmsHeader.toByteArray(smsHeader), encoding, smsHeader.languageTable, smsHeader.languageShiftTable); sendRawPdu(pdus.encodedScAddress, pdus.encodedMessage, sentIntent, deliveryIntent); } } /** * Send a multi-part text based SMS which already passed SMS control check. * * It is the working function for sendMultipartText(). * * @param destinationAddress the address to send the message to * @param scAddress is the service center address or null to use * the current default SMSC * @param parts an <code>ArrayList</code> of strings that, in order, * comprise the original message * @param sentIntents if not null, an <code>ArrayList</code> of * <code>PendingIntent</code>s (one for each message part) that is * broadcast when the corresponding message part has been sent. * The result code will be <code>Activity.RESULT_OK<code> for success, * or one of these errors: * <code>RESULT_ERROR_GENERIC_FAILURE</code> * <code>RESULT_ERROR_RADIO_OFF</code> * <code>RESULT_ERROR_NULL_PDU</code>. * @param deliveryIntents if not null, an <code>ArrayList</code> of * <code>PendingIntent</code>s (one for each message part) that is * broadcast when the corresponding message part has been delivered * to the recipient. The raw pdu of the status report is in the * extended data ("pdu"). */ private void sendMultipartTextWithPermit(String destinationAddress, String scAddress, ArrayList<String> parts, ArrayList<PendingIntent> sentIntents, ArrayList<PendingIntent> deliveryIntents) { // check if in service int ss = mPhone.getServiceState().getState(); if (ss != ServiceState.STATE_IN_SERVICE) { for (int i = 0, count = parts.size(); i < count; i++) { PendingIntent sentIntent = null; if (sentIntents != null && sentIntents.size() > i) { sentIntent = sentIntents.get(i); } SmsTracker tracker = SmsTrackerFactory(null, sentIntent, null); handleNotInService(ss, tracker); } return; } int refNumber = getNextConcatenatedRef() & 0x00FF; int msgCount = parts.size(); int encoding = android.telephony.SmsMessage.ENCODING_UNKNOWN; mRemainingMessages = msgCount; TextEncodingDetails[] encodingForParts = new TextEncodingDetails[msgCount]; for (int i = 0; i < msgCount; i++) { TextEncodingDetails details = SmsMessage.calculateLength(parts.get(i), false); if (encoding != details.codeUnitSize && (encoding == android.telephony.SmsMessage.ENCODING_UNKNOWN || encoding == android.telephony.SmsMessage.ENCODING_7BIT)) { encoding = details.codeUnitSize; } encodingForParts[i] = details; } for (int i = 0; i < msgCount; i++) { SmsHeader.ConcatRef concatRef = new SmsHeader.ConcatRef(); concatRef.refNumber = refNumber; concatRef.seqNumber = i + 1; // 1-based sequence concatRef.msgCount = msgCount; concatRef.isEightBits = false; SmsHeader smsHeader = new SmsHeader(); smsHeader.concatRef = concatRef; if (encoding == android.telephony.SmsMessage.ENCODING_7BIT) { smsHeader.languageTable = encodingForParts[i].languageTable; smsHeader.languageShiftTable = encodingForParts[i].languageShiftTable; } PendingIntent sentIntent = null; if (sentIntents != null && sentIntents.size() > i) { sentIntent = sentIntents.get(i); } PendingIntent deliveryIntent = null; if (deliveryIntents != null && deliveryIntents.size() > i) { deliveryIntent = deliveryIntents.get(i); } SmsMessage.SubmitPdu pdus = SmsMessage.getSubmitPdu(scAddress, destinationAddress, parts.get(i), deliveryIntent != null, SmsHeader.toByteArray(smsHeader), encoding, smsHeader.languageTable, smsHeader.languageShiftTable); HashMap<String, Object> map = new HashMap<String, Object>(); map.put("smsc", pdus.encodedScAddress); map.put("pdu", pdus.encodedMessage); SmsTracker tracker = SmsTrackerFactory(map, sentIntent, deliveryIntent); sendSms(tracker); } } /** {@inheritDoc} */ protected void sendSms(SmsTracker tracker) { HashMap 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); } /** * Send the multi-part SMS based on multipart Sms tracker * * @param tracker holds the multipart Sms tracker ready to be sent */ @Override protected void sendMultipartSms (SmsTracker tracker) { ArrayList<String> parts; ArrayList<PendingIntent> sentIntents; ArrayList<PendingIntent> deliveryIntents; HashMap map = tracker.mData; String destinationAddress = (String) map.get("destination"); String scAddress = (String) map.get("scaddress"); parts = (ArrayList<String>) map.get("parts"); sentIntents = (ArrayList<PendingIntent>) map.get("sentIntents"); deliveryIntents = (ArrayList<PendingIntent>) map.get("deliveryIntents"); sendMultipartTextWithPermit(destinationAddress, scAddress, parts, sentIntents, deliveryIntents); } /** {@inheritDoc} */ @Override protected void acknowledgeLastIncomingSms(boolean success, int result, Message response){ // FIXME unit test leaves cm == null. this should change if (mCm != null) { mCm.acknowledgeLastIncomingGsmSms(success, resultToCause(result), response); } } private 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 String mPlmn; private final int mLac; private final int mCid; public SmsCbConcatInfo(SmsCbHeader header, String plmn, int lac, int cid) { mHeader = header; mPlmn = plmn; mLac = lac; mCid = cid; } @Override public int hashCode() { return mHeader.messageIdentifier * 31 + mHeader.updateNumber; } @Override public boolean equals(Object obj) { if (obj instanceof SmsCbConcatInfo) { SmsCbConcatInfo other = (SmsCbConcatInfo)obj; // Two pages match if all header attributes (except the page // index) are identical, and both pages belong to the same // location (which is also determined by the scope parameter) if (mHeader.geographicalScope == other.mHeader.geographicalScope && mHeader.messageCode == other.mHeader.messageCode && mHeader.updateNumber == other.mHeader.updateNumber && mHeader.messageIdentifier == other.mHeader.messageIdentifier && mHeader.dataCodingScheme == other.mHeader.dataCodingScheme && mHeader.nrOfPages == other.mHeader.nrOfPages) { return matchesLocation(other.mPlmn, other.mLac, other.mCid); } } return false; } /** * Checks if this concatenation info matches the given location. The * granularity of the match depends on the geographical scope. * * @param plmn PLMN * @param lac Location area code * @param cid Cell ID * @return true if matching, false otherwise */ public boolean matchesLocation(String plmn, int lac, int cid) { switch (mHeader.geographicalScope) { case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE: case SmsCbMessage.GEOGRAPHICAL_SCOPE_CELL_WIDE_IMMEDIATE: if (mCid != cid) { return false; } // deliberate fall-through case SmsCbMessage.GEOGRAPHICAL_SCOPE_LA_WIDE: if (mLac != lac) { return false; } // deliberate fall-through case SmsCbMessage.GEOGRAPHICAL_SCOPE_PLMN_WIDE: return mPlmn != null && mPlmn.equals(plmn); } return false; } } // This map holds incomplete concatenated messages waiting for assembly private final HashMap<SmsCbConcatInfo, byte[][]> mSmsCbPageMap = new HashMap<SmsCbConcatInfo, byte[][]>(); @Override protected void handleBroadcastSms(AsyncResult ar) { try { byte[][] pdus = null; byte[] receivedPdu = (byte[])ar.result; if (Config.LOGD) { 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); GsmCellLocation cellLocation = (GsmCellLocation)mGsmPhone.getCellLocation(); int lac = cellLocation.getLac(); int cid = cellLocation.getCid(); if (header.nrOfPages > 1) { // Multi-page message SmsCbConcatInfo concatInfo = new SmsCbConcatInfo(header, plmn, lac, cid); // Try to find other pages of the same message pdus = mSmsCbPageMap.get(concatInfo); if (pdus == null) { // This it the first page of this message, make room for all // pages and keep until complete pdus = new byte[header.nrOfPages][]; mSmsCbPageMap.put(concatInfo, pdus); } // Page parameter is one-based pdus[header.pageIndex - 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; } boolean isEmergencyMessage = SmsCbHeader.isEmergencyMessage(header.messageIdentifier); dispatchBroadcastPdus(pdus, isEmergencyMessage); // 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); } } }