blob: 52ca4530471403f8df0ae6d89eb6f042c5c07632 [file] [log] [blame]
/*
* 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.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.
*/
@Override
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} */
@Override
public 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;
}
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
if (sms.isMWISetMessage()) {
mGsmPhone.updateMessageWaitingIndicator(true);
handled = sms.isMwiDontStore();
if (false) {
Log.d(TAG, "Received voice mail indicator set SMS shouldStore=" + !handled);
}
} else if (sms.isMWIClearMessage()) {
mGsmPhone.updateMessageWaitingIndicator(false);
handled = sms.isMwiDontStore();
if (false) {
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} */
@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));
sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
}
/** {@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));
sendRawPdu(pdu.encodedScAddress, pdu.encodedMessage, sentIntent, deliveryIntent);
}
/** {@inheritDoc} */
@Override
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} */
@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);
}
/**
* 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<String, Object> 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 (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);
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);
}
}
}