blob: 9295773d921f37f2479cab9451c9d7a5348bf61f [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.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);
}
}
}