blob: 61377dc2a1b56f01fd28839b63cccf33e37fa8b0 [file] [log] [blame]
/*
* Copyright (C) 2013 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.cdma;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.Message;
import android.os.RemoteCallback;
import android.os.SystemProperties;
import android.provider.Telephony.Sms.Intents;
import android.telephony.PhoneNumberUtils;
import android.telephony.cdma.CdmaSmsCbProgramResults;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.InboundSmsHandler;
import com.android.internal.telephony.InboundSmsTracker;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.SmsConstants;
import com.android.internal.telephony.SmsMessageBase;
import com.android.internal.telephony.SmsStorageMonitor;
import com.android.internal.telephony.TelephonyComponentFactory;
import com.android.internal.telephony.WspTypeDecoder;
import com.android.internal.telephony.cdma.sms.BearerData;
import com.android.internal.telephony.cdma.sms.CdmaSmsAddress;
import com.android.internal.telephony.cdma.sms.SmsEnvelope;
import com.android.internal.util.HexDump;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Subclass of {@link InboundSmsHandler} for 3GPP2 type messages.
*/
public class CdmaInboundSmsHandler extends InboundSmsHandler {
private final CdmaSMSDispatcher mSmsDispatcher;
private static CdmaCbTestBroadcastReceiver sTestBroadcastReceiver;
private static CdmaScpTestBroadcastReceiver sTestScpBroadcastReceiver;
private byte[] mLastDispatchedSmsFingerprint;
private byte[] mLastAcknowledgedSmsFingerprint;
// Callback used to process the result of an SCP message
private RemoteCallback mScpCallback;
private final boolean mCheckForDuplicatePortsInOmadmWapPush = Resources.getSystem().getBoolean(
com.android.internal.R.bool.config_duplicate_port_omadm_wappush);
// When TEST_MODE is on we allow the test intent to trigger an SMS CB alert
private static final boolean TEST_MODE = SystemProperties.getInt("ro.debuggable", 0) == 1;
private static final String TEST_ACTION = "com.android.internal.telephony.cdma"
+ ".TEST_TRIGGER_CELL_BROADCAST";
private static final String SCP_TEST_ACTION = "com.android.internal.telephony.cdma"
+ ".TEST_TRIGGER_SCP_MESSAGE";
/**
* Create a new inbound SMS handler for CDMA.
*/
private CdmaInboundSmsHandler(Context context, SmsStorageMonitor storageMonitor,
Phone phone, CdmaSMSDispatcher smsDispatcher) {
super("CdmaInboundSmsHandler", context, storageMonitor, phone);
mSmsDispatcher = smsDispatcher;
phone.mCi.setOnNewCdmaSms(getHandler(), EVENT_NEW_SMS, null);
mCellBroadcastServiceManager.enable();
mScpCallback = new RemoteCallback(result -> {
if (result == null) {
loge("SCP results error: missing extras");
return;
}
String sender = result.getString("sender");
if (sender == null) {
loge("SCP results error: missing sender extra.");
return;
}
ArrayList<CdmaSmsCbProgramResults> results = result.getParcelableArrayList("results");
if (results == null) {
loge("SCP results error: missing results extra.");
return;
}
BearerData bData = new BearerData();
bData.messageType = BearerData.MESSAGE_TYPE_SUBMIT;
bData.messageId = SmsMessage.getNextMessageId();
bData.serviceCategoryProgramResults = results;
byte[] encodedBearerData = BearerData.encode(bData);
ByteArrayOutputStream baos = new ByteArrayOutputStream(100);
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeInt(SmsEnvelope.TELESERVICE_SCPT);
dos.writeInt(0); //servicePresent
dos.writeInt(0); //serviceCategory
CdmaSmsAddress destAddr = CdmaSmsAddress.parse(
PhoneNumberUtils.cdmaCheckAndProcessPlusCodeForSms(sender));
dos.write(destAddr.digitMode);
dos.write(destAddr.numberMode);
dos.write(destAddr.ton); // number_type
dos.write(destAddr.numberPlan);
dos.write(destAddr.numberOfDigits);
dos.write(destAddr.origBytes, 0, destAddr.origBytes.length); // digits
// Subaddress is not supported.
dos.write(0); //subaddressType
dos.write(0); //subaddr_odd
dos.write(0); //subaddr_nbr_of_digits
dos.write(encodedBearerData.length);
dos.write(encodedBearerData, 0, encodedBearerData.length);
// Ignore the RIL response. TODO: implement retry if SMS send fails.
mPhone.mCi.sendCdmaSms(baos.toByteArray(), null);
} catch (IOException e) {
loge("exception creating SCP results PDU", e);
} finally {
try {
dos.close();
} catch (IOException ignored) {
}
}
});
if (TEST_MODE) {
if (sTestBroadcastReceiver == null) {
sTestBroadcastReceiver = new CdmaCbTestBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(TEST_ACTION);
context.registerReceiver(sTestBroadcastReceiver, filter);
}
if (sTestScpBroadcastReceiver == null) {
sTestScpBroadcastReceiver = new CdmaScpTestBroadcastReceiver();
IntentFilter filter = new IntentFilter();
filter.addAction(SCP_TEST_ACTION);
context.registerReceiver(sTestScpBroadcastReceiver, filter);
}
}
}
/**
* Unregister for CDMA SMS.
*/
@Override
protected void onQuitting() {
mPhone.mCi.unSetOnNewCdmaSms(getHandler());
if (DBG) log("unregistered for 3GPP2 SMS");
super.onQuitting();
}
/**
* Wait for state machine to enter startup state. We can't send any messages until then.
*/
public static CdmaInboundSmsHandler makeInboundSmsHandler(Context context,
SmsStorageMonitor storageMonitor, Phone phone, CdmaSMSDispatcher smsDispatcher) {
CdmaInboundSmsHandler handler = new CdmaInboundSmsHandler(context, storageMonitor,
phone, smsDispatcher);
handler.start();
return handler;
}
/**
* Return true if this handler is for 3GPP2 messages; false for 3GPP format.
*
* @return true (3GPP2)
*/
@Override
protected boolean is3gpp2() {
return true;
}
/**
* Process Cell Broadcast, Voicemail Notification, and other 3GPP/3GPP2-specific messages.
*
* @param smsb the SmsMessageBase object from the RIL
* @return true if the message was handled here; false to continue processing
*/
@Override
protected int dispatchMessageRadioSpecific(SmsMessageBase smsb) {
SmsMessage sms = (SmsMessage) smsb;
boolean isBroadcastType = (SmsEnvelope.MESSAGE_TYPE_BROADCAST == sms.getMessageType());
// Handle CMAS emergency broadcast messages.
if (isBroadcastType) {
log("Broadcast type message");
mCellBroadcastServiceManager.sendCdmaMessageToHandler(sms);
return Intents.RESULT_SMS_HANDLED;
}
// Initialize fingerprint field, and see if we have a network duplicate SMS.
mLastDispatchedSmsFingerprint = sms.getIncomingSmsFingerprint();
if (mLastAcknowledgedSmsFingerprint != null &&
Arrays.equals(mLastDispatchedSmsFingerprint, mLastAcknowledgedSmsFingerprint)) {
return Intents.RESULT_SMS_HANDLED;
}
// Decode BD stream and set sms variables.
sms.parseSms();
int teleService = sms.getTeleService();
switch (teleService) {
case SmsEnvelope.TELESERVICE_VMN:
case SmsEnvelope.TELESERVICE_MWI:
// handle voicemail indication
handleVoicemailTeleservice(sms);
return Intents.RESULT_SMS_HANDLED;
case SmsEnvelope.TELESERVICE_WMT:
case SmsEnvelope.TELESERVICE_WEMT:
if (sms.isStatusReportMessage()) {
mSmsDispatcher.sendStatusReportMessage(sms);
return Intents.RESULT_SMS_HANDLED;
}
break;
case SmsEnvelope.TELESERVICE_SCPT:
mCellBroadcastServiceManager.sendCdmaScpMessageToHandler(sms, mScpCallback);
return Intents.RESULT_SMS_HANDLED;
case SmsEnvelope.TELESERVICE_FDEA_WAP:
if (!sms.preprocessCdmaFdeaWap()) {
return Intents.RESULT_SMS_HANDLED;
}
teleService = SmsEnvelope.TELESERVICE_WAP;
// fall through
case SmsEnvelope.TELESERVICE_WAP:
// handled below, after storage check
break;
default:
loge("unsupported teleservice 0x" + Integer.toHexString(teleService));
return Intents.RESULT_SMS_UNSUPPORTED;
}
if (!mStorageMonitor.isStorageAvailable() &&
sms.getMessageClass() != SmsConstants.MessageClass.CLASS_0) {
// It's a storable message and there's no storage available. Bail.
// (See C.S0015-B v2.0 for a description of "Immediate Display"
// messages, which we represent as CLASS_0.)
return Intents.RESULT_SMS_OUT_OF_MEMORY;
}
if (SmsEnvelope.TELESERVICE_WAP == teleService) {
return processCdmaWapPdu(sms.getUserData(), sms.mMessageRef,
sms.getOriginatingAddress(), sms.getDisplayOriginatingAddress(),
sms.getTimestampMillis());
}
return dispatchNormalMessage(smsb);
}
/**
* Send an acknowledge message.
*
* @param success indicates that last message was successfully received.
* @param result result code indicating any error
* @param response callback message sent when operation completes.
*/
@Override
protected void acknowledgeLastIncomingSms(boolean success, int result, Message response) {
int causeCode = resultToCause(result);
mPhone.mCi.acknowledgeLastIncomingCdmaSms(success, causeCode, response);
if (causeCode == 0) {
mLastAcknowledgedSmsFingerprint = mLastDispatchedSmsFingerprint;
}
mLastDispatchedSmsFingerprint = null;
}
/**
* Convert Android result code to CDMA SMS failure cause.
*
* @param rc the Android SMS intent result value
* @return 0 for success, or a CDMA SMS failure cause value
*/
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.CDMA_SMS_FAIL_CAUSE_RESOURCE_SHORTAGE;
case Intents.RESULT_SMS_UNSUPPORTED:
return CommandsInterface.CDMA_SMS_FAIL_CAUSE_INVALID_TELESERVICE_ID;
case Intents.RESULT_SMS_GENERIC_ERROR:
default:
return CommandsInterface.CDMA_SMS_FAIL_CAUSE_OTHER_TERMINAL_PROBLEM;
}
}
/**
* Handle {@link SmsEnvelope#TELESERVICE_VMN} and {@link SmsEnvelope#TELESERVICE_MWI}.
*
* @param sms the message to process
*/
private void handleVoicemailTeleservice(SmsMessage sms) {
int voicemailCount = sms.getNumOfVoicemails();
if (DBG) log("Voicemail count=" + voicemailCount);
// range check
if (voicemailCount < 0) {
voicemailCount = -1;
} else if (voicemailCount > 99) {
// C.S0015-B v2, 4.5.12
// range: 0-99
voicemailCount = 99;
}
// update voice mail count in phone
mPhone.setVoiceMessageCount(voicemailCount);
// update metrics
addVoicemailSmsToMetrics();
}
/**
* Processes inbound messages that are in the WAP-WDP PDU format. See
* wap-259-wdp-20010614-a section 6.5 for details on the WAP-WDP PDU format.
* WDP segments are gathered until a datagram completes and gets dispatched.
*
* @param pdu The WAP-WDP PDU segment
* @return a result code from {@link android.provider.Telephony.Sms.Intents}, or
* {@link Activity#RESULT_OK} if the message has been broadcast
* to applications
*/
private int processCdmaWapPdu(byte[] pdu, int referenceNumber, String address, String dispAddr,
long timestamp) {
int index = 0;
int msgType = (0xFF & pdu[index++]);
if (msgType != 0) {
log("Received a WAP SMS which is not WDP. Discard.");
return Intents.RESULT_SMS_HANDLED;
}
int totalSegments = (0xFF & pdu[index++]); // >= 1
int segment = (0xFF & pdu[index++]); // >= 0
if (segment >= totalSegments) {
loge("WDP bad segment #" + segment + " expecting 0-" + (totalSegments - 1));
return Intents.RESULT_SMS_HANDLED;
}
// Only the first segment contains sourcePort and destination Port
int sourcePort = 0;
int destinationPort = 0;
if (segment == 0) {
//process WDP segment
sourcePort = (0xFF & pdu[index++]) << 8;
sourcePort |= 0xFF & pdu[index++];
destinationPort = (0xFF & pdu[index++]) << 8;
destinationPort |= 0xFF & pdu[index++];
// Some carriers incorrectly send duplicate port fields in omadm wap pushes.
// If configured, check for that here
if (mCheckForDuplicatePortsInOmadmWapPush) {
if (checkDuplicatePortOmadmWapPush(pdu, index)) {
index = index + 4; // skip duplicate port fields
}
}
}
// Lookup all other related parts
log("Received WAP PDU. Type = " + msgType + ", originator = " + address
+ ", src-port = " + sourcePort + ", dst-port = " + destinationPort
+ ", ID = " + referenceNumber + ", segment# = " + segment + '/' + totalSegments);
// pass the user data portion of the PDU to the shared handler in SMSDispatcher
byte[] userData = new byte[pdu.length - index];
System.arraycopy(pdu, index, userData, 0, pdu.length - index);
InboundSmsTracker tracker = TelephonyComponentFactory.getInstance()
.inject(InboundSmsTracker.class.getName()).makeInboundSmsTracker(mContext,
userData, timestamp, destinationPort, true, address, dispAddr,
referenceNumber,
segment, totalSegments, true, HexDump.toHexString(userData),
false /* isClass0 */,
mPhone.getSubId());
// de-duping is done only for text messages
return addTrackerToRawTableAndSendMessage(tracker, false /* don't de-dup */);
}
/**
* Optional check to see if the received WapPush is an OMADM notification with erroneous
* extra port fields.
* - Some carriers make this mistake.
* ex: MSGTYPE-TotalSegments-CurrentSegment
* -SourcePortDestPort-SourcePortDestPort-OMADM PDU
*
* @param origPdu The WAP-WDP PDU segment
* @param index Current Index while parsing the PDU.
* @return True if OrigPdu is OmaDM Push Message which has duplicate ports.
* False if OrigPdu is NOT OmaDM Push Message which has duplicate ports.
*/
private static boolean checkDuplicatePortOmadmWapPush(byte[] origPdu, int index) {
index += 4;
byte[] omaPdu = new byte[origPdu.length - index];
System.arraycopy(origPdu, index, omaPdu, 0, omaPdu.length);
WspTypeDecoder pduDecoder = new WspTypeDecoder(omaPdu);
int wspIndex = 2;
// Process header length field
if (!pduDecoder.decodeUintvarInteger(wspIndex)) {
return false;
}
wspIndex += pduDecoder.getDecodedDataLength(); // advance to next field
// Process content type field
if (!pduDecoder.decodeContentType(wspIndex)) {
return false;
}
String mimeType = pduDecoder.getValueString();
return (WspTypeDecoder.CONTENT_TYPE_B_PUSH_SYNCML_NOTI.equals(mimeType));
}
/**
* Add voicemail indication SMS 0 to metrics.
*/
private void addVoicemailSmsToMetrics() {
mMetrics.writeIncomingVoiceMailSms(mPhone.getPhoneId(),
android.telephony.SmsMessage.FORMAT_3GPP2);
}
/**
* A broadcast receiver used for testing emergency cell broadcasts. To trigger test CDMA cell
* broadcasts with adb run e.g:
*
* adb shell am broadcast -a com.android.internal.telephony.cdma.TEST_TRIGGER_CELL_BROADCAST \
* --ei service_category 4097 \
* --es bearer_data_string 00031303900801C00D0101015C02D00002BFD1931054D208119313D3D10815D05 \
* 493925391C81193D48814D3D555120810D3D0D3D3925393C810D3D5539516480B481393D495120810D1539514 \
* 9053081054925693D390481553951253080D0C4D481413481354D500
*
* adb shell am broadcast -a com.android.internal.telephony.cdma.TEST_TRIGGER_CELL_BROADCAST \
* --ei service_category 4097 \
* --es bearer_data_string 00031303900801C00D0101015C02D00002BFD1931054D208119313D3D10815D05 \
* 493925391C81193D48814D3D555120810D3D0D3D3925393C810D3D5539516480B481393D495120810D1539514 \
* 9053081054925693D390481553951253080D0C4D481413481354D500 \
* --ei phone_id 0 \
*/
private class CdmaCbTestBroadcastReceiver extends CbTestBroadcastReceiver {
CdmaCbTestBroadcastReceiver() {
super(TEST_ACTION);
}
@Override
protected void handleTestAction(Intent intent) {
SmsEnvelope envelope = new SmsEnvelope();
// the CdmaSmsAddress is not used for a test cell broadcast message, but needs to be
// supplied to avoid a null pointer exception in the platform
CdmaSmsAddress nonNullAddress = new CdmaSmsAddress();
nonNullAddress.origBytes = new byte[]{(byte) 0xFF};
envelope.origAddress = nonNullAddress;
// parse service category from intent
envelope.serviceCategory = intent.getIntExtra("service_category", -1);
if (envelope.serviceCategory == -1) {
log("No service category, ignoring CB test intent");
return;
}
// parse bearer data from intent
String bearerDataString = intent.getStringExtra("bearer_data_string");
envelope.bearerData = decodeHexString(bearerDataString);
if (envelope.bearerData == null) {
log("No bearer data, ignoring CB test intent");
return;
}
SmsMessage sms = new SmsMessage(new CdmaSmsAddress(), envelope);
mCellBroadcastServiceManager.sendCdmaMessageToHandler(sms);
}
}
/**
* A broadcast receiver used for testing CDMA SCP messages. To trigger test CDMA SCP messages
* with adb run e.g:
*
* adb shell am broadcast -a com.android.internal.telephony.cdma.TEST_TRIGGER_SCP_MESSAGE \
* --es originating_address_string 1234567890 \
* --es bearer_data_string 00031007B0122610880080B2091C5F1D3965DB95054D1CB2E1E883A6F41334E \
* 6CA830EEC882872DFC32F2E9E40
*/
private class CdmaScpTestBroadcastReceiver extends CbTestBroadcastReceiver {
CdmaScpTestBroadcastReceiver() {
super(SCP_TEST_ACTION);
}
@Override
protected void handleTestAction(Intent intent) {
SmsEnvelope envelope = new SmsEnvelope();
// the CdmaSmsAddress is not used for a test SCP message, but needs to be supplied to
// avoid a null pointer exception in the platform
CdmaSmsAddress nonNullAddress = new CdmaSmsAddress();
nonNullAddress.origBytes = new byte[]{(byte) 0xFF};
envelope.origAddress = nonNullAddress;
// parse bearer data from intent
String bearerDataString = intent.getStringExtra("bearer_data_string");
envelope.bearerData = decodeHexString(bearerDataString);
if (envelope.bearerData == null) {
log("No bearer data, ignoring SCP test intent");
return;
}
CdmaSmsAddress origAddr = new CdmaSmsAddress();
String addressString = intent.getStringExtra("originating_address_string");
origAddr.origBytes = decodeHexString(addressString);
if (origAddr.origBytes == null) {
log("No address data, ignoring SCP test intent");
return;
}
SmsMessage sms = new SmsMessage(origAddr, envelope);
sms.parseSms();
mCellBroadcastServiceManager.sendCdmaScpMessageToHandler(sms, mScpCallback);
}
}
}