1599 lines
50 KiB
Java
1599 lines
50 KiB
Java
/*
|
|
* 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 android.telephony.gsm;
|
|
|
|
import android.telephony.PhoneNumberUtils;
|
|
import android.util.Config;
|
|
import android.util.Log;
|
|
import android.telephony.PhoneNumberUtils;
|
|
import android.text.format.Time;
|
|
|
|
import com.android.internal.telephony.gsm.EncodeException;
|
|
import com.android.internal.telephony.gsm.GsmAlphabet;
|
|
import com.android.internal.telephony.gsm.SimUtils;
|
|
import com.android.internal.telephony.gsm.SmsHeader;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.UnsupportedEncodingException;
|
|
import java.util.Arrays;
|
|
|
|
class SmsAddress {
|
|
// From TS 23.040 9.1.2.5 and TS 24.008 table 10.5.118
|
|
static final int TON_UNKNOWN = 0;
|
|
|
|
static final int TON_INTERNATIONAL = 1;
|
|
|
|
static final int TON_NATIONAL = 2;
|
|
|
|
static final int TON_NETWORK = 3;
|
|
|
|
static final int TON_SUBSCRIBER = 4;
|
|
|
|
static final int TON_ALPHANUMERIC = 5;
|
|
|
|
static final int TON_APPREVIATED = 6;
|
|
|
|
static final int OFFSET_ADDRESS_LENGTH = 0;
|
|
|
|
static final int OFFSET_TOA = 1;
|
|
|
|
static final int OFFSET_ADDRESS_VALUE = 2;
|
|
|
|
int ton;
|
|
|
|
String address;
|
|
|
|
byte[] origBytes;
|
|
|
|
/**
|
|
* New SmsAddress from TS 23.040 9.1.2.5 Address Field
|
|
*
|
|
* @param offset the offset of the Address-Length byte
|
|
* @param length the length in bytes rounded up, e.g. "2 +
|
|
* (addressLength + 1) / 2"
|
|
*/
|
|
|
|
SmsAddress(byte[] data, int offset, int length) {
|
|
origBytes = new byte[length];
|
|
System.arraycopy(data, offset, origBytes, 0, length);
|
|
|
|
// addressLength is the count of semi-octets, not bytes
|
|
int addressLength = origBytes[OFFSET_ADDRESS_LENGTH] & 0xff;
|
|
|
|
int toa = origBytes[OFFSET_TOA] & 0xff;
|
|
ton = 0x7 & (toa >> 4);
|
|
|
|
// TOA must have its high bit set
|
|
if ((toa & 0x80) != 0x80) {
|
|
throw new RuntimeException("Invalid TOA - high bit must be set");
|
|
}
|
|
|
|
if (isAlphanumeric()) {
|
|
// An alphanumeric address
|
|
int countSeptets = addressLength * 4 / 7;
|
|
|
|
address = GsmAlphabet.gsm7BitPackedToString(origBytes,
|
|
OFFSET_ADDRESS_VALUE, countSeptets);
|
|
} else {
|
|
// TS 23.040 9.1.2.5 says
|
|
// that "the MS shall interpret reserved values as 'Unknown'
|
|
// but shall store them exactly as received"
|
|
|
|
byte lastByte = origBytes[length - 1];
|
|
|
|
if ((addressLength & 1) == 1) {
|
|
// Make sure the final unused BCD digit is 0xf
|
|
origBytes[length - 1] |= 0xf0;
|
|
}
|
|
address = PhoneNumberUtils.calledPartyBCDToString(origBytes,
|
|
OFFSET_TOA, length - OFFSET_TOA);
|
|
|
|
// And restore origBytes
|
|
origBytes[length - 1] = lastByte;
|
|
}
|
|
}
|
|
|
|
public String getAddressString() {
|
|
return address;
|
|
}
|
|
|
|
/**
|
|
* Returns true if this is an alphanumeric addres
|
|
*/
|
|
public boolean isAlphanumeric() {
|
|
return ton == TON_ALPHANUMERIC;
|
|
}
|
|
|
|
public boolean isNetworkSpecific() {
|
|
return ton == TON_NETWORK;
|
|
}
|
|
|
|
/**
|
|
* Returns true of this is a valid CPHS voice message waiting indicator
|
|
* address
|
|
*/
|
|
public boolean isCphsVoiceMessageIndicatorAddress() {
|
|
// CPHS-style MWI message
|
|
// See CPHS 4.7 B.4.2.1
|
|
//
|
|
// Basically:
|
|
//
|
|
// - Originating address should be 4 bytes long and alphanumeric
|
|
// - Decode will result with two chars:
|
|
// - Char 1
|
|
// 76543210
|
|
// ^ set/clear indicator (0 = clear)
|
|
// ^^^ type of indicator (000 = voice)
|
|
// ^^^^ must be equal to 0001
|
|
// - Char 2:
|
|
// 76543210
|
|
// ^ line number (0 = line 1)
|
|
// ^^^^^^^ set to 0
|
|
//
|
|
// Remember, since the alpha address is stored in 7-bit compact form,
|
|
// the "line number" is really the top bit of the first address value
|
|
// byte
|
|
|
|
return (origBytes[OFFSET_ADDRESS_LENGTH] & 0xff) == 4
|
|
&& isAlphanumeric() && (origBytes[OFFSET_TOA] & 0x0f) == 0;
|
|
}
|
|
|
|
/**
|
|
* Returns true if this is a valid CPHS voice message waiting indicator
|
|
* address indicating a "set" of "indicator 1" of type "voice message
|
|
* waiting"
|
|
*/
|
|
public boolean isCphsVoiceMessageSet() {
|
|
// 0x11 means "set" "voice message waiting" "indicator 1"
|
|
return isCphsVoiceMessageIndicatorAddress()
|
|
&& (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x11;
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns true if this is a valid CPHS voice message waiting indicator
|
|
* address indicationg a "clear" of "indicator 1" of type "voice message
|
|
* waiting"
|
|
*/
|
|
public boolean isCphsVoiceMessageClear() {
|
|
// 0x10 means "clear" "voice message waiting" "indicator 1"
|
|
return isCphsVoiceMessageIndicatorAddress()
|
|
&& (origBytes[OFFSET_ADDRESS_VALUE] & 0xff) == 0x10;
|
|
|
|
}
|
|
|
|
public boolean couldBeEmailGateway() {
|
|
// Some carriers seems to send email gateway messages in this form:
|
|
// from: an UNKNOWN TON, 3 or 4 digits long, beginning with a 5
|
|
// PID: 0x00, Data coding scheme 0x03
|
|
// So we just attempt to treat any message from an address length <= 4
|
|
// as an email gateway
|
|
|
|
return address.length() <= 4;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* A Short Message Service message.
|
|
*
|
|
*/
|
|
public class SmsMessage {
|
|
static final String LOG_TAG = "GSM";
|
|
|
|
/**
|
|
* SMS Class enumeration.
|
|
* See TS 23.038.
|
|
*
|
|
*/
|
|
public enum MessageClass {
|
|
UNKNOWN, CLASS_0, CLASS_1, CLASS_2, CLASS_3;
|
|
}
|
|
|
|
/** Unknown encoding scheme (see TS 23.038) */
|
|
public static final int ENCODING_UNKNOWN = 0;
|
|
/** 7-bit encoding scheme (see TS 23.038) */
|
|
public static final int ENCODING_7BIT = 1;
|
|
/** 8-bit encoding scheme (see TS 23.038) */
|
|
public static final int ENCODING_8BIT = 2;
|
|
/** 16-bit encoding scheme (see TS 23.038) */
|
|
public static final int ENCODING_16BIT = 3;
|
|
|
|
/** The maximum number of payload bytes per message */
|
|
public static final int MAX_USER_DATA_BYTES = 140;
|
|
|
|
/**
|
|
* The maximum number of payload bytes per message if a user data header
|
|
* is present. This assumes the header only contains the
|
|
* CONCATENATED_8_BIT_REFERENCE element.
|
|
*
|
|
* @hide pending API Council approval to extend the public API
|
|
*/
|
|
static final int MAX_USER_DATA_BYTES_WITH_HEADER = 134;
|
|
|
|
/** The maximum number of payload septets per message */
|
|
public static final int MAX_USER_DATA_SEPTETS = 160;
|
|
|
|
/**
|
|
* The maximum number of payload septets per message if a user data header
|
|
* is present. This assumes the header only contains the
|
|
* CONCATENATED_8_BIT_REFERENCE element.
|
|
*/
|
|
public static final int MAX_USER_DATA_SEPTETS_WITH_HEADER = 153;
|
|
|
|
/** The address of the SMSC. May be null */
|
|
String scAddress;
|
|
|
|
/** The address of the sender */
|
|
SmsAddress originatingAddress;
|
|
|
|
/** The message body as a string. May be null if the message isn't text */
|
|
String messageBody;
|
|
|
|
String pseudoSubject;
|
|
|
|
/** Non-null this is an email gateway message */
|
|
String emailFrom;
|
|
|
|
/** Non-null if this is an email gateway message */
|
|
String emailBody;
|
|
|
|
boolean isEmail;
|
|
|
|
long scTimeMillis;
|
|
|
|
/** The raw PDU of the message */
|
|
byte[] mPdu;
|
|
|
|
/** The raw bytes for the user data section of the message */
|
|
byte[] userData;
|
|
|
|
SmsHeader userDataHeader;
|
|
|
|
/**
|
|
* TP-Message-Type-Indicator
|
|
* 9.2.3
|
|
*/
|
|
int mti;
|
|
|
|
/** TP-Protocol-Identifier (TP-PID) */
|
|
int protocolIdentifier;
|
|
|
|
// TP-Data-Coding-Scheme
|
|
// see TS 23.038
|
|
int dataCodingScheme;
|
|
|
|
// TP-Reply-Path
|
|
// e.g. 23.040 9.2.2.1
|
|
boolean replyPathPresent = false;
|
|
|
|
// "Message Marked for Automatic Deletion Group"
|
|
// 23.038 Section 4
|
|
boolean automaticDeletion;
|
|
|
|
// "Message Waiting Indication Group"
|
|
// 23.038 Section 4
|
|
private boolean isMwi;
|
|
|
|
private boolean mwiSense;
|
|
|
|
private boolean mwiDontStore;
|
|
|
|
MessageClass messageClass;
|
|
|
|
/**
|
|
* Indicates status for messages stored on the SIM.
|
|
*/
|
|
int statusOnSim = -1;
|
|
|
|
/**
|
|
* Record index of message in the EF.
|
|
*/
|
|
int indexOnSim = -1;
|
|
|
|
/** TP-Message-Reference - Message Reference of sent message. @hide */
|
|
public int messageRef;
|
|
|
|
/** True if Status Report is for SMS-SUBMIT; false for SMS-COMMAND. */
|
|
boolean forSubmit;
|
|
|
|
/** The address of the receiver. */
|
|
SmsAddress recipientAddress;
|
|
|
|
/** Time when SMS-SUBMIT was delivered from SC to MSE. */
|
|
long dischargeTimeMillis;
|
|
|
|
/**
|
|
* TP-Status - status of a previously submitted SMS.
|
|
* This field applies to SMS-STATUS-REPORT messages. 0 indicates success;
|
|
* see TS 23.040, 9.2.3.15 for description of other possible values.
|
|
*/
|
|
int status;
|
|
|
|
/**
|
|
* TP-Status - status of a previously submitted SMS.
|
|
* This field is true iff the message is a SMS-STATUS-REPORT message.
|
|
*/
|
|
boolean isStatusReportMessage = false;
|
|
|
|
/**
|
|
* This class represents the encoded form of an outgoing SMS.
|
|
*/
|
|
public static class SubmitPdu {
|
|
public byte[] encodedScAddress; // Null if not applicable.
|
|
public byte[] encodedMessage;
|
|
|
|
public String toString() {
|
|
return "SubmitPdu: encodedScAddress = "
|
|
+ Arrays.toString(encodedScAddress)
|
|
+ ", encodedMessage = "
|
|
+ Arrays.toString(encodedMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an SmsMessage from a raw PDU.
|
|
*/
|
|
public static SmsMessage createFromPdu(byte[] pdu) {
|
|
try {
|
|
SmsMessage msg = new SmsMessage();
|
|
msg.parsePdu(pdu);
|
|
return msg;
|
|
} catch (RuntimeException ex) {
|
|
Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TS 27.005 3.4.1 lines[0] and lines[1] are the two lines read from the
|
|
* +CMT unsolicited response (PDU mode, of course)
|
|
* +CMT: [<alpha>],<length><CR><LF><pdu>
|
|
*
|
|
* Only public for debugging
|
|
*
|
|
* {@hide}
|
|
*/
|
|
/* package */ public static SmsMessage newFromCMT(String[] lines) {
|
|
try {
|
|
SmsMessage msg = new SmsMessage();
|
|
msg.parsePdu(SimUtils.hexStringToBytes(lines[1]));
|
|
return msg;
|
|
} catch (RuntimeException ex) {
|
|
Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/* pacakge */ static SmsMessage newFromCMTI(String line) {
|
|
// the thinking here is not to read the message immediately
|
|
// FTA test case
|
|
Log.e(LOG_TAG, "newFromCMTI: not yet supported");
|
|
return null;
|
|
}
|
|
|
|
/** @hide */
|
|
/* package */ public static SmsMessage newFromCDS(String line) {
|
|
try {
|
|
SmsMessage msg = new SmsMessage();
|
|
msg.parsePdu(SimUtils.hexStringToBytes(line));
|
|
return msg;
|
|
} catch (RuntimeException ex) {
|
|
Log.e(LOG_TAG, "CDS SMS PDU parsing failed: ", ex);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create an SmsMessage from an SMS EF record.
|
|
*
|
|
* @param index Index of SMS record. This should be index in ArrayList
|
|
* returned by SmsManager.getAllMessagesFromSim + 1.
|
|
* @param data Record data.
|
|
* @return An SmsMessage representing the record.
|
|
*
|
|
* @hide
|
|
*/
|
|
public static SmsMessage createFromEfRecord(int index, byte[] data) {
|
|
try {
|
|
SmsMessage msg = new SmsMessage();
|
|
|
|
msg.indexOnSim = index;
|
|
|
|
// First byte is status: RECEIVED_READ, RECEIVED_UNREAD, STORED_SENT,
|
|
// or STORED_UNSENT
|
|
// See TS 51.011 10.5.3
|
|
if ((data[0] & 1) == 0) {
|
|
Log.w(LOG_TAG,
|
|
"SMS parsing failed: Trying to parse a free record");
|
|
return null;
|
|
} else {
|
|
msg.statusOnSim = data[0] & 0x07;
|
|
}
|
|
|
|
int size = data.length - 1;
|
|
|
|
// Note: Data may include trailing FF's. That's OK; message
|
|
// should still parse correctly.
|
|
byte[] pdu = new byte[size];
|
|
System.arraycopy(data, 1, pdu, 0, size);
|
|
msg.parsePdu(pdu);
|
|
return msg;
|
|
} catch (RuntimeException ex) {
|
|
Log.e(LOG_TAG, "SMS PDU parsing failed: ", ex);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the TP-Layer-Length for the given SMS-SUBMIT PDU Basically, the
|
|
* length in bytes (not hex chars) less the SMSC header
|
|
*/
|
|
public static int getTPLayerLengthForPDU(String pdu) {
|
|
int len = pdu.length() / 2;
|
|
int smscLen = 0;
|
|
|
|
smscLen = Integer.parseInt(pdu.substring(0, 2), 16);
|
|
|
|
return len - smscLen - 1;
|
|
}
|
|
|
|
/**
|
|
* Calculates the number of SMS's required to encode the message body and
|
|
* the number of characters remaining until the next message, given the
|
|
* current encoding.
|
|
*
|
|
* @param messageBody the message to encode
|
|
* @param use7bitOnly if true, characters that are not part of the GSM
|
|
* alphabet are counted as a single space char. If false, a
|
|
* messageBody containing non-GSM alphabet characters is calculated
|
|
* for 16-bit encoding.
|
|
* @return an int[4] with int[0] being the number of SMS's required, int[1]
|
|
* the number of code units used, and int[2] is the number of code
|
|
* units remaining until the next message. int[3] is the encoding
|
|
* type that should be used for the message.
|
|
*/
|
|
public static int[] calculateLength(String messageBody, boolean use7bitOnly) {
|
|
int ret[] = new int[4];
|
|
|
|
try {
|
|
// Try GSM alphabet
|
|
int septets = GsmAlphabet.countGsmSeptets(messageBody, !use7bitOnly);
|
|
ret[1] = septets;
|
|
if (septets > MAX_USER_DATA_SEPTETS) {
|
|
ret[0] = (septets / MAX_USER_DATA_SEPTETS_WITH_HEADER) + 1;
|
|
ret[2] = MAX_USER_DATA_SEPTETS_WITH_HEADER
|
|
- (septets % MAX_USER_DATA_SEPTETS_WITH_HEADER);
|
|
} else {
|
|
ret[0] = 1;
|
|
ret[2] = MAX_USER_DATA_SEPTETS - septets;
|
|
}
|
|
ret[3] = ENCODING_7BIT;
|
|
} catch (EncodeException ex) {
|
|
// fall back to UCS-2
|
|
int octets = messageBody.length() * 2;
|
|
ret[1] = messageBody.length();
|
|
if (octets > MAX_USER_DATA_BYTES) {
|
|
// 6 is the size of the user data header
|
|
ret[0] = (octets / MAX_USER_DATA_BYTES_WITH_HEADER) + 1;
|
|
ret[2] = (MAX_USER_DATA_BYTES_WITH_HEADER
|
|
- (octets % MAX_USER_DATA_BYTES_WITH_HEADER))/2;
|
|
} else {
|
|
ret[0] = 1;
|
|
ret[2] = (MAX_USER_DATA_BYTES - octets)/2;
|
|
}
|
|
ret[3] = ENCODING_16BIT;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get an SMS-SUBMIT PDU for a destination address and a message
|
|
*
|
|
* @param scAddress Service Centre address. Null means use default.
|
|
* @return a <code>SubmitPdu</code> containing the encoded SC
|
|
* address, if applicable, and the encoded message.
|
|
* Returns null on encode error.
|
|
* @hide
|
|
*/
|
|
public static SubmitPdu getSubmitPdu(String scAddress,
|
|
String destinationAddress, String message,
|
|
boolean statusReportRequested, byte[] header) {
|
|
|
|
// Perform null parameter checks.
|
|
if (message == null || destinationAddress == null) {
|
|
return null;
|
|
}
|
|
|
|
SubmitPdu ret = new SubmitPdu();
|
|
// MTI = SMS-SUBMIT, UDHI = header != null
|
|
byte mtiByte = (byte)(0x01 | (header != null ? 0x40 : 0x00));
|
|
ByteArrayOutputStream bo = getSubmitPduHead(
|
|
scAddress, destinationAddress, mtiByte,
|
|
statusReportRequested, ret);
|
|
|
|
try {
|
|
// First, try encoding it with the GSM alphabet
|
|
|
|
// User Data (and length)
|
|
byte[] userData = GsmAlphabet.stringToGsm7BitPackedWithHeader(message, header);
|
|
|
|
if ((0xff & userData[0]) > MAX_USER_DATA_SEPTETS) {
|
|
// Message too long
|
|
return null;
|
|
}
|
|
|
|
// TP-Data-Coding-Scheme
|
|
// Default encoding, uncompressed
|
|
bo.write(0x00);
|
|
|
|
// (no TP-Validity-Period)
|
|
|
|
bo.write(userData, 0, userData.length);
|
|
} catch (EncodeException ex) {
|
|
byte[] userData, textPart;
|
|
// Encoding to the 7-bit alphabet failed. Let's see if we can
|
|
// send it as a UCS-2 encoded message
|
|
|
|
try {
|
|
textPart = message.getBytes("utf-16be");
|
|
} catch (UnsupportedEncodingException uex) {
|
|
Log.e(LOG_TAG,
|
|
"Implausible UnsupportedEncodingException ",
|
|
uex);
|
|
return null;
|
|
}
|
|
|
|
if (header != null) {
|
|
userData = new byte[header.length + textPart.length];
|
|
|
|
System.arraycopy(header, 0, userData, 0, header.length);
|
|
System.arraycopy(textPart, 0, userData, header.length, textPart.length);
|
|
} else {
|
|
userData = textPart;
|
|
}
|
|
|
|
if (userData.length > MAX_USER_DATA_BYTES) {
|
|
// Message too long
|
|
return null;
|
|
}
|
|
|
|
// TP-Data-Coding-Scheme
|
|
// Class 3, UCS-2 encoding, uncompressed
|
|
bo.write(0x0b);
|
|
|
|
// (no TP-Validity-Period)
|
|
|
|
// TP-UDL
|
|
bo.write(userData.length);
|
|
|
|
bo.write(userData, 0, userData.length);
|
|
}
|
|
|
|
ret.encodedMessage = bo.toByteArray();
|
|
return ret;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get an SMS-SUBMIT PDU for a destination address and a message
|
|
*
|
|
* @param scAddress Service Centre address. Null means use default.
|
|
* @return a <code>SubmitPdu</code> containing the encoded SC
|
|
* address, if applicable, and the encoded message.
|
|
* Returns null on encode error.
|
|
*/
|
|
public static SubmitPdu getSubmitPdu(String scAddress,
|
|
String destinationAddress, String message,
|
|
boolean statusReportRequested) {
|
|
|
|
return getSubmitPdu(scAddress, destinationAddress, message, statusReportRequested, null);
|
|
}
|
|
|
|
/**
|
|
* Get an SMS-SUBMIT PDU for a data message to a destination address & port
|
|
*
|
|
* @param scAddress Service Centre address. null == use default
|
|
* @param destinationAddress the address of the destination for the message
|
|
* @param destinationPort the port to deliver the message to at the
|
|
* destination
|
|
* @param data the dat for the message
|
|
* @return a <code>SubmitPdu</code> containing the encoded SC
|
|
* address, if applicable, and the encoded message.
|
|
* Returns null on encode error.
|
|
*/
|
|
public static SubmitPdu getSubmitPdu(String scAddress,
|
|
String destinationAddress, short destinationPort, byte[] data,
|
|
boolean statusReportRequested) {
|
|
if (data.length > (MAX_USER_DATA_BYTES - 7 /* UDH size */)) {
|
|
Log.e(LOG_TAG, "SMS data message may only contain "
|
|
+ (MAX_USER_DATA_BYTES - 7) + " bytes");
|
|
return null;
|
|
}
|
|
|
|
SubmitPdu ret = new SubmitPdu();
|
|
ByteArrayOutputStream bo = getSubmitPduHead(
|
|
scAddress, destinationAddress, (byte) 0x41, // MTI = SMS-SUBMIT,
|
|
// TP-UDHI = true
|
|
statusReportRequested, ret);
|
|
|
|
// TP-Data-Coding-Scheme
|
|
// No class, 8 bit data
|
|
bo.write(0x04);
|
|
|
|
// (no TP-Validity-Period)
|
|
|
|
// User data size
|
|
bo.write(data.length + 7);
|
|
|
|
// User data header size
|
|
bo.write(0x06); // header is 6 octets
|
|
|
|
// User data header, indicating the destination port
|
|
bo.write(SmsHeader.APPLICATION_PORT_ADDRESSING_16_BIT); // port
|
|
// addressing
|
|
// header
|
|
bo.write(0x04); // each port is 2 octets
|
|
bo.write((destinationPort >> 8) & 0xFF); // MSB of destination port
|
|
bo.write(destinationPort & 0xFF); // LSB of destination port
|
|
bo.write(0x00); // MSB of originating port
|
|
bo.write(0x00); // LSB of originating port
|
|
|
|
// User data
|
|
bo.write(data, 0, data.length);
|
|
|
|
ret.encodedMessage = bo.toByteArray();
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Create the beginning of a SUBMIT PDU. This is the part of the
|
|
* SUBMIT PDU that is common to the two versions of {@link #getSubmitPdu},
|
|
* one of which takes a byte array and the other of which takes a
|
|
* <code>String</code>.
|
|
*
|
|
* @param scAddress Service Centre address. null == use default
|
|
* @param destinationAddress the address of the destination for the message
|
|
* @param mtiByte
|
|
* @param ret <code>SubmitPdu</code> containing the encoded SC
|
|
* address, if applicable, and the encoded message
|
|
*/
|
|
private static ByteArrayOutputStream getSubmitPduHead(
|
|
String scAddress, String destinationAddress, byte mtiByte,
|
|
boolean statusReportRequested, SubmitPdu ret) {
|
|
ByteArrayOutputStream bo = new ByteArrayOutputStream(
|
|
MAX_USER_DATA_BYTES + 40);
|
|
|
|
// SMSC address with length octet, or 0
|
|
if (scAddress == null) {
|
|
ret.encodedScAddress = null;
|
|
} else {
|
|
ret.encodedScAddress = PhoneNumberUtils.networkPortionToCalledPartyBCDWithLength(
|
|
scAddress);
|
|
}
|
|
|
|
// TP-Message-Type-Indicator (and friends)
|
|
if (statusReportRequested) {
|
|
// Set TP-Status-Report-Request bit.
|
|
mtiByte |= 0x20;
|
|
if (Config.LOGD) Log.d(LOG_TAG, "SMS status report requested");
|
|
}
|
|
bo.write(mtiByte);
|
|
|
|
// space for TP-Message-Reference
|
|
bo.write(0);
|
|
|
|
byte[] daBytes;
|
|
|
|
daBytes = PhoneNumberUtils.networkPortionToCalledPartyBCD(destinationAddress);
|
|
|
|
// destination address length in BCD digits, ignoring TON byte and pad
|
|
// TODO Should be better.
|
|
bo.write((daBytes.length - 1) * 2
|
|
- ((daBytes[daBytes.length - 1] & 0xf0) == 0xf0 ? 1 : 0));
|
|
|
|
// destination address
|
|
bo.write(daBytes, 0, daBytes.length);
|
|
|
|
// TP-Protocol-Identifier
|
|
bo.write(0);
|
|
return bo;
|
|
}
|
|
|
|
static class PduParser {
|
|
byte pdu[];
|
|
|
|
int cur;
|
|
|
|
SmsHeader userDataHeader;
|
|
|
|
byte[] userData;
|
|
|
|
int mUserDataSeptetPadding;
|
|
|
|
int mUserDataSize;
|
|
|
|
PduParser(String s) {
|
|
this(SimUtils.hexStringToBytes(s));
|
|
}
|
|
|
|
PduParser(byte[] pdu) {
|
|
this.pdu = pdu;
|
|
cur = 0;
|
|
mUserDataSeptetPadding = 0;
|
|
}
|
|
|
|
/**
|
|
* Parse and return the SC address prepended to SMS messages coming via
|
|
* the TS 27.005 / AT interface. Returns null on invalid address
|
|
*/
|
|
String getSCAddress() {
|
|
int len;
|
|
String ret;
|
|
|
|
// length of SC Address
|
|
len = getByte();
|
|
|
|
if (len == 0) {
|
|
// no SC address
|
|
ret = null;
|
|
} else {
|
|
// SC address
|
|
try {
|
|
ret = PhoneNumberUtils
|
|
.calledPartyBCDToString(pdu, cur, len);
|
|
} catch (RuntimeException tr) {
|
|
Log.d(LOG_TAG, "invalid SC address: ", tr);
|
|
ret = null;
|
|
}
|
|
}
|
|
|
|
cur += len;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* returns non-sign-extended byte value
|
|
*/
|
|
int getByte() {
|
|
return pdu[cur++] & 0xff;
|
|
}
|
|
|
|
/**
|
|
* Any address except the SC address (eg, originating address) See TS
|
|
* 23.040 9.1.2.5
|
|
*/
|
|
SmsAddress getAddress() {
|
|
SmsAddress ret;
|
|
|
|
// "The Address-Length field is an integer representation of
|
|
// the number field, i.e. excludes any semi octet containing only
|
|
// fill bits."
|
|
// The TOA field is not included as part of this
|
|
int addressLength = pdu[cur] & 0xff;
|
|
int lengthBytes = 2 + (addressLength + 1) / 2;
|
|
|
|
ret = new SmsAddress(pdu, cur, lengthBytes);
|
|
|
|
cur += lengthBytes;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Parses an SC timestamp and returns a currentTimeMillis()-style
|
|
* timestamp
|
|
*/
|
|
|
|
long getSCTimestampMillis() {
|
|
// TP-Service-Centre-Time-Stamp
|
|
int year = SimUtils.bcdByteToInt(pdu[cur++]);
|
|
int month = SimUtils.bcdByteToInt(pdu[cur++]);
|
|
int day = SimUtils.bcdByteToInt(pdu[cur++]);
|
|
int hour = SimUtils.bcdByteToInt(pdu[cur++]);
|
|
int minute = SimUtils.bcdByteToInt(pdu[cur++]);
|
|
int second = SimUtils.bcdByteToInt(pdu[cur++]);
|
|
|
|
// For the timezone, the most significant bit of the
|
|
// least signficant nibble is the sign byte
|
|
// (meaning the max range of this field is 79 quarter-hours,
|
|
// which is more than enough)
|
|
|
|
byte tzByte = pdu[cur++];
|
|
|
|
// Mask out sign bit.
|
|
int timezoneOffset = SimUtils
|
|
.bcdByteToInt((byte) (tzByte & (~0x08)));
|
|
|
|
timezoneOffset = ((tzByte & 0x08) == 0) ? timezoneOffset
|
|
: -timezoneOffset;
|
|
|
|
Time time = new Time(Time.TIMEZONE_UTC);
|
|
|
|
// It's 2006. Should I really support years < 2000?
|
|
time.year = year >= 90 ? year + 1900 : year + 2000;
|
|
time.month = month - 1;
|
|
time.monthDay = day;
|
|
time.hour = hour;
|
|
time.minute = minute;
|
|
time.second = second;
|
|
|
|
// Timezone offset is in quarter hours.
|
|
return time.toMillis(true) - (timezoneOffset * 15 * 60 * 1000);
|
|
}
|
|
|
|
/**
|
|
* Pulls the user data out of the PDU, and separates the payload from
|
|
* the header if there is one.
|
|
*
|
|
* @param hasUserDataHeader true if there is a user data header
|
|
* @param dataInSeptets true if the data payload is in septets instead
|
|
* of octets
|
|
* @return the number of septets or octets in the user data payload
|
|
*/
|
|
int constructUserData(boolean hasUserDataHeader, boolean dataInSeptets)
|
|
{
|
|
int offset = cur;
|
|
int userDataLength = pdu[offset++] & 0xff;
|
|
int headerSeptets = 0;
|
|
|
|
if (hasUserDataHeader) {
|
|
int userDataHeaderLength = pdu[offset++] & 0xff;
|
|
|
|
byte[] udh = new byte[userDataHeaderLength];
|
|
System.arraycopy(pdu, offset, udh, 0, userDataHeaderLength);
|
|
userDataHeader = SmsHeader.parse(udh);
|
|
offset += userDataHeaderLength;
|
|
|
|
int headerBits = (userDataHeaderLength + 1) * 8;
|
|
headerSeptets = headerBits / 7;
|
|
headerSeptets += (headerBits % 7) > 0 ? 1 : 0;
|
|
mUserDataSeptetPadding = (headerSeptets * 7) - headerBits;
|
|
}
|
|
|
|
/*
|
|
* Here we just create the user data length to be the remainder of
|
|
* the pdu minus the user data hearder. This is because the count
|
|
* could mean the number of uncompressed sepets if the userdata is
|
|
* encoded in 7-bit.
|
|
*/
|
|
userData = new byte[pdu.length - offset];
|
|
System.arraycopy(pdu, offset, userData, 0, userData.length);
|
|
cur = offset;
|
|
|
|
if (dataInSeptets) {
|
|
// Return the number of septets
|
|
int count = userDataLength - headerSeptets;
|
|
// If count < 0, return 0 (means UDL was probably incorrect)
|
|
return count < 0 ? 0 : count;
|
|
} else {
|
|
// Return the number of octets
|
|
return userData.length;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the user data payload, not including the headers
|
|
*
|
|
* @return the user data payload, not including the headers
|
|
*/
|
|
byte[] getUserData() {
|
|
return userData;
|
|
}
|
|
|
|
/**
|
|
* Returns the number of padding bits at the begining of the user data
|
|
* array before the start of the septets.
|
|
*
|
|
* @return the number of padding bits at the begining of the user data
|
|
* array before the start of the septets
|
|
*/
|
|
int getUserDataSeptetPadding() {
|
|
return mUserDataSeptetPadding;
|
|
}
|
|
|
|
/**
|
|
* Returns an object representing the user data headers
|
|
*
|
|
* @return an object representing the user data headers
|
|
*
|
|
* {@hide}
|
|
*/
|
|
SmsHeader getUserDataHeader() {
|
|
return userDataHeader;
|
|
}
|
|
|
|
/*
|
|
XXX Not sure what this one is supposed to be doing, and no one is using
|
|
it.
|
|
String getUserDataGSM8bit() {
|
|
// System.out.println("remainder of pud:" +
|
|
// HexDump.dumpHexString(pdu, cur, pdu.length - cur));
|
|
int count = pdu[cur++] & 0xff;
|
|
int size = pdu[cur++];
|
|
|
|
// skip over header for now
|
|
cur += size;
|
|
|
|
if (pdu[cur - 1] == 0x01) {
|
|
int tid = pdu[cur++] & 0xff;
|
|
int type = pdu[cur++] & 0xff;
|
|
|
|
size = pdu[cur++] & 0xff;
|
|
|
|
int i = cur;
|
|
|
|
while (pdu[i++] != '\0') {
|
|
}
|
|
|
|
int length = i - cur;
|
|
String mimeType = new String(pdu, cur, length);
|
|
|
|
cur += length;
|
|
|
|
if (false) {
|
|
System.out.println("tid = 0x" + HexDump.toHexString(tid));
|
|
System.out.println("type = 0x" + HexDump.toHexString(type));
|
|
System.out.println("header size = " + size);
|
|
System.out.println("mimeType = " + mimeType);
|
|
System.out.println("remainder of header:" +
|
|
HexDump.dumpHexString(pdu, cur, (size - mimeType.length())));
|
|
}
|
|
|
|
cur += size - mimeType.length();
|
|
|
|
// System.out.println("data count = " + count + " cur = " + cur
|
|
// + " :" + HexDump.dumpHexString(pdu, cur, pdu.length - cur));
|
|
|
|
MMSMessage msg = MMSMessage.parseEncoding(mContext, pdu, cur,
|
|
pdu.length - cur);
|
|
} else {
|
|
System.out.println(new String(pdu, cur, pdu.length - cur - 1));
|
|
}
|
|
|
|
return SimUtils.bytesToHexString(pdu);
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Interprets the user data payload as pack GSM 7bit characters, and
|
|
* decodes them into a String.
|
|
*
|
|
* @param septetCount the number of septets in the user data payload
|
|
* @return a String with the decoded characters
|
|
*/
|
|
String getUserDataGSM7Bit(int septetCount) {
|
|
String ret;
|
|
|
|
ret = GsmAlphabet.gsm7BitPackedToString(pdu, cur, septetCount,
|
|
mUserDataSeptetPadding);
|
|
|
|
cur += (septetCount * 7) / 8;
|
|
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* Interprets the user data payload as UCS2 characters, and
|
|
* decodes them into a String.
|
|
*
|
|
* @param byteCount the number of bytes in the user data payload
|
|
* @return a String with the decoded characters
|
|
*/
|
|
String getUserDataUCS2(int byteCount) {
|
|
String ret;
|
|
|
|
try {
|
|
ret = new String(pdu, cur, byteCount, "utf-16");
|
|
} catch (UnsupportedEncodingException ex) {
|
|
ret = "";
|
|
Log.e(LOG_TAG, "implausible UnsupportedEncodingException", ex);
|
|
}
|
|
|
|
cur += byteCount;
|
|
return ret;
|
|
}
|
|
|
|
boolean moreDataPresent() {
|
|
return (pdu.length > cur);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the address of the SMS service center that relayed this message
|
|
* or null if there is none.
|
|
*/
|
|
public String getServiceCenterAddress() {
|
|
return scAddress;
|
|
}
|
|
|
|
/**
|
|
* Returns the originating address (sender) of this SMS message in String
|
|
* form or null if unavailable
|
|
*/
|
|
public String getOriginatingAddress() {
|
|
if (originatingAddress == null) {
|
|
return null;
|
|
}
|
|
|
|
return originatingAddress.getAddressString();
|
|
}
|
|
|
|
/**
|
|
* Returns the originating address, or email from address if this message
|
|
* was from an email gateway. Returns null if originating address
|
|
* unavailable.
|
|
*/
|
|
public String getDisplayOriginatingAddress() {
|
|
if (isEmail) {
|
|
return emailFrom;
|
|
} else {
|
|
return getOriginatingAddress();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the message body as a String, if it exists and is text based.
|
|
* @return message body is there is one, otherwise null
|
|
*/
|
|
public String getMessageBody() {
|
|
return messageBody;
|
|
}
|
|
|
|
/**
|
|
* Returns the class of this message.
|
|
*/
|
|
public MessageClass getMessageClass() {
|
|
return messageClass;
|
|
}
|
|
|
|
/**
|
|
* Returns the message body, or email message body if this message was from
|
|
* an email gateway. Returns null if message body unavailable.
|
|
*/
|
|
public String getDisplayMessageBody() {
|
|
if (isEmail) {
|
|
return emailBody;
|
|
} else {
|
|
return getMessageBody();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unofficial convention of a subject line enclosed in parens empty string
|
|
* if not present
|
|
*/
|
|
public String getPseudoSubject() {
|
|
return pseudoSubject == null ? "" : pseudoSubject;
|
|
}
|
|
|
|
/**
|
|
* Returns the service centre timestamp in currentTimeMillis() format
|
|
*/
|
|
public long getTimestampMillis() {
|
|
return scTimeMillis;
|
|
}
|
|
|
|
/**
|
|
* Returns true if message is an email.
|
|
*
|
|
* @return true if this message came through an email gateway and email
|
|
* sender / subject / parsed body are available
|
|
*/
|
|
public boolean isEmail() {
|
|
return isEmail;
|
|
}
|
|
|
|
/**
|
|
* @return if isEmail() is true, body of the email sent through the gateway.
|
|
* null otherwise
|
|
*/
|
|
public String getEmailBody() {
|
|
return emailBody;
|
|
}
|
|
|
|
/**
|
|
* @return if isEmail() is true, email from address of email sent through
|
|
* the gateway. null otherwise
|
|
*/
|
|
public String getEmailFrom() {
|
|
return emailFrom;
|
|
}
|
|
|
|
/**
|
|
* Get protocol identifier.
|
|
*/
|
|
public int getProtocolIdentifier() {
|
|
return protocolIdentifier;
|
|
}
|
|
|
|
/**
|
|
* See TS 23.040 9.2.3.9 returns true if this is a "replace short message"
|
|
* SMS
|
|
*/
|
|
public boolean isReplace() {
|
|
return (protocolIdentifier & 0xc0) == 0x40
|
|
&& (protocolIdentifier & 0x3f) > 0
|
|
&& (protocolIdentifier & 0x3f) < 8;
|
|
}
|
|
|
|
/**
|
|
* Returns true for CPHS MWI toggle message.
|
|
*
|
|
* @return true if this is a CPHS MWI toggle message See CPHS 4.2 section
|
|
* B.4.2
|
|
*/
|
|
public boolean isCphsMwiMessage() {
|
|
return originatingAddress.isCphsVoiceMessageClear()
|
|
|| originatingAddress.isCphsVoiceMessageSet();
|
|
}
|
|
|
|
/**
|
|
* returns true if this message is a CPHS voicemail / message waiting
|
|
* indicator (MWI) clear message
|
|
*/
|
|
public boolean isMWIClearMessage() {
|
|
if (isMwi && (mwiSense == false)) {
|
|
return true;
|
|
}
|
|
|
|
return originatingAddress != null
|
|
&& originatingAddress.isCphsVoiceMessageClear();
|
|
}
|
|
|
|
/**
|
|
* returns true if this message is a CPHS voicemail / message waiting
|
|
* indicator (MWI) set message
|
|
*/
|
|
public boolean isMWISetMessage() {
|
|
if (isMwi && (mwiSense == true)) {
|
|
return true;
|
|
}
|
|
|
|
return originatingAddress != null
|
|
&& originatingAddress.isCphsVoiceMessageSet();
|
|
}
|
|
|
|
/**
|
|
* returns true if this message is a "Message Waiting Indication Group:
|
|
* Discard Message" notification and should not be stored.
|
|
*/
|
|
public boolean isMwiDontStore() {
|
|
if (isMwi && mwiDontStore) {
|
|
return true;
|
|
}
|
|
|
|
if (isCphsMwiMessage()) {
|
|
// See CPHS 4.2 Section B.4.2.1
|
|
// If the user data is a single space char, do not store
|
|
// the message. Otherwise, store and display as usual
|
|
if (" ".equals(getMessageBody())) {
|
|
;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* returns the user data section minus the user data header if one was
|
|
* present.
|
|
*/
|
|
public byte[] getUserData() {
|
|
return userData;
|
|
}
|
|
|
|
/**
|
|
* Returns an object representing the user data header
|
|
*
|
|
* @return an object representing the user data header
|
|
*
|
|
* {@hide}
|
|
*/
|
|
public SmsHeader getUserDataHeader() {
|
|
return userDataHeader;
|
|
}
|
|
|
|
/**
|
|
* Returns the raw PDU for the message.
|
|
*
|
|
* @return the raw PDU for the message.
|
|
*/
|
|
public byte[] getPdu() {
|
|
return mPdu;
|
|
}
|
|
|
|
/**
|
|
* Returns the status of the message on the SIM (read, unread, sent, unsent).
|
|
*
|
|
* @return the status of the message on the SIM. These are:
|
|
* SmsManager.STATUS_ON_SIM_FREE
|
|
* SmsManager.STATUS_ON_SIM_READ
|
|
* SmsManager.STATUS_ON_SIM_UNREAD
|
|
* SmsManager.STATUS_ON_SIM_SEND
|
|
* SmsManager.STATUS_ON_SIM_UNSENT
|
|
*/
|
|
public int getStatusOnSim() {
|
|
return statusOnSim;
|
|
}
|
|
|
|
/**
|
|
* Returns the record index of the message on the SIM (1-based index).
|
|
* @return the record index of the message on the SIM, or -1 if this
|
|
* SmsMessage was not created from a SIM SMS EF record.
|
|
*/
|
|
public int getIndexOnSim() {
|
|
return indexOnSim;
|
|
}
|
|
|
|
/**
|
|
* For an SMS-STATUS-REPORT message, this returns the status field from
|
|
* the status report. This field indicates the status of a previousely
|
|
* submitted SMS, if requested. See TS 23.040, 9.2.3.15 TP-Status for a
|
|
* description of values.
|
|
*
|
|
* @return 0 indicates the previously sent message was received.
|
|
* See TS 23.040, 9.9.2.3.15 for a description of other possible
|
|
* values.
|
|
*/
|
|
public int getStatus() {
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Return true iff the message is a SMS-STATUS-REPORT message.
|
|
*/
|
|
public boolean isStatusReportMessage() {
|
|
return isStatusReportMessage;
|
|
}
|
|
|
|
/**
|
|
* Returns true iff the <code>TP-Reply-Path</code> bit is set in
|
|
* this message.
|
|
*/
|
|
public boolean isReplyPathPresent() {
|
|
return replyPathPresent;
|
|
}
|
|
|
|
/**
|
|
* TS 27.005 3.1, <pdu> definition "In the case of SMS: 3GPP TS 24.011 [6]
|
|
* SC address followed by 3GPP TS 23.040 [3] TPDU in hexadecimal format:
|
|
* ME/TA converts each octet of TP data unit into two IRA character long
|
|
* hexad number (e.g. octet with integer value 42 is presented to TE as two
|
|
* characters 2A (IRA 50 and 65))" ...in the case of cell broadcast,
|
|
* something else...
|
|
*/
|
|
private void parsePdu(byte[] pdu) {
|
|
mPdu = pdu;
|
|
// Log.d(LOG_TAG, "raw sms mesage:");
|
|
// Log.d(LOG_TAG, s);
|
|
|
|
PduParser p = new PduParser(pdu);
|
|
|
|
scAddress = p.getSCAddress();
|
|
|
|
if (scAddress != null) {
|
|
if (Config.LOGD) Log.d(LOG_TAG, "SMS SC address: " + scAddress);
|
|
}
|
|
|
|
// TODO(mkf) support reply path, user data header indicator
|
|
|
|
// TP-Message-Type-Indicator
|
|
// 9.2.3
|
|
int firstByte = p.getByte();
|
|
|
|
mti = firstByte & 0x3;
|
|
switch (mti) {
|
|
// TP-Message-Type-Indicator
|
|
// 9.2.3
|
|
case 0:
|
|
parseSmsDeliver(p, firstByte);
|
|
break;
|
|
case 2:
|
|
parseSmsStatusReport(p, firstByte);
|
|
break;
|
|
default:
|
|
// TODO(mkf) the rest of these
|
|
throw new RuntimeException("Unsupported message type");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a SMS-STATUS-REPORT message.
|
|
*
|
|
* @param p A PduParser, cued past the first byte.
|
|
* @param firstByte The first byte of the PDU, which contains MTI, etc.
|
|
*/
|
|
private void parseSmsStatusReport(PduParser p, int firstByte) {
|
|
isStatusReportMessage = true;
|
|
|
|
// TP-Status-Report-Qualifier bit == 0 for SUBMIT
|
|
forSubmit = (firstByte & 0x20) == 0x00;
|
|
// TP-Message-Reference
|
|
messageRef = p.getByte();
|
|
// TP-Recipient-Address
|
|
recipientAddress = p.getAddress();
|
|
// TP-Service-Centre-Time-Stamp
|
|
scTimeMillis = p.getSCTimestampMillis();
|
|
// TP-Discharge-Time
|
|
dischargeTimeMillis = p.getSCTimestampMillis();
|
|
// TP-Status
|
|
status = p.getByte();
|
|
|
|
// The following are optional fields that may or may not be present.
|
|
if (p.moreDataPresent()) {
|
|
// TP-Parameter-Indicator
|
|
int extraParams = p.getByte();
|
|
int moreExtraParams = extraParams;
|
|
while ((moreExtraParams & 0x80) != 0) {
|
|
// We only know how to parse a few extra parameters, all
|
|
// indicated in the first TP-PI octet, so skip over any
|
|
// additional TP-PI octets.
|
|
moreExtraParams = p.getByte();
|
|
}
|
|
// TP-Protocol-Identifier
|
|
if ((extraParams & 0x01) != 0) {
|
|
protocolIdentifier = p.getByte();
|
|
}
|
|
// TP-Data-Coding-Scheme
|
|
if ((extraParams & 0x02) != 0) {
|
|
dataCodingScheme = p.getByte();
|
|
}
|
|
// TP-User-Data-Length (implies existence of TP-User-Data)
|
|
if ((extraParams & 0x04) != 0) {
|
|
boolean hasUserDataHeader = (firstByte & 0x40) == 0x40;
|
|
parseUserData(p, hasUserDataHeader);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void parseSmsDeliver(PduParser p, int firstByte) {
|
|
replyPathPresent = (firstByte & 0x80) == 0x80;
|
|
|
|
originatingAddress = p.getAddress();
|
|
|
|
if (originatingAddress != null) {
|
|
if (Config.LOGV) Log.v(LOG_TAG, "SMS originating address: "
|
|
+ originatingAddress.address);
|
|
}
|
|
|
|
// TP-Protocol-Identifier (TP-PID)
|
|
// TS 23.040 9.2.3.9
|
|
protocolIdentifier = p.getByte();
|
|
|
|
// TP-Data-Coding-Scheme
|
|
// see TS 23.038
|
|
dataCodingScheme = p.getByte();
|
|
|
|
if (Config.LOGV) {
|
|
Log.v(LOG_TAG, "SMS TP-PID:" + protocolIdentifier
|
|
+ " data coding scheme: " + dataCodingScheme);
|
|
}
|
|
|
|
scTimeMillis = p.getSCTimestampMillis();
|
|
|
|
if (Config.LOGD) Log.d(LOG_TAG, "SMS SC timestamp: " + scTimeMillis);
|
|
|
|
boolean hasUserDataHeader = (firstByte & 0x40) == 0x40;
|
|
|
|
parseUserData(p, hasUserDataHeader);
|
|
}
|
|
|
|
/**
|
|
* Parses the User Data of an SMS.
|
|
*
|
|
* @param p The current PduParser.
|
|
* @param hasUserDataHeader Indicates whether a header is present in the
|
|
* User Data.
|
|
*/
|
|
private void parseUserData(PduParser p, boolean hasUserDataHeader) {
|
|
boolean hasMessageClass = false;
|
|
boolean userDataCompressed = false;
|
|
|
|
int encodingType = ENCODING_UNKNOWN;
|
|
|
|
// Look up the data encoding scheme
|
|
if ((dataCodingScheme & 0x80) == 0) {
|
|
// Bits 7..4 == 0xxx
|
|
automaticDeletion = (0 != (dataCodingScheme & 0x40));
|
|
userDataCompressed = (0 != (dataCodingScheme & 0x20));
|
|
hasMessageClass = (0 != (dataCodingScheme & 0x10));
|
|
|
|
if (userDataCompressed) {
|
|
Log.w(LOG_TAG, "4 - Unsupported SMS data coding scheme "
|
|
+ "(compression) " + (dataCodingScheme & 0xff));
|
|
} else {
|
|
switch ((dataCodingScheme >> 2) & 0x3) {
|
|
case 0: // GSM 7 bit default alphabet
|
|
encodingType = ENCODING_7BIT;
|
|
break;
|
|
|
|
case 2: // UCS 2 (16bit)
|
|
encodingType = ENCODING_16BIT;
|
|
break;
|
|
|
|
case 1: // 8 bit data
|
|
case 3: // reserved
|
|
Log.w(LOG_TAG, "1 - Unsupported SMS data coding scheme "
|
|
+ (dataCodingScheme & 0xff));
|
|
encodingType = ENCODING_8BIT;
|
|
break;
|
|
}
|
|
}
|
|
} else if ((dataCodingScheme & 0xf0) == 0xf0) {
|
|
automaticDeletion = false;
|
|
hasMessageClass = true;
|
|
userDataCompressed = false;
|
|
|
|
if (0 == (dataCodingScheme & 0x04)) {
|
|
// GSM 7 bit default alphabet
|
|
encodingType = ENCODING_7BIT;
|
|
} else {
|
|
// 8 bit data
|
|
encodingType = ENCODING_8BIT;
|
|
}
|
|
} else if ((dataCodingScheme & 0xF0) == 0xC0
|
|
|| (dataCodingScheme & 0xF0) == 0xD0
|
|
|| (dataCodingScheme & 0xF0) == 0xE0) {
|
|
// 3GPP TS 23.038 V7.0.0 (2006-03) section 4
|
|
|
|
// 0xC0 == 7 bit, don't store
|
|
// 0xD0 == 7 bit, store
|
|
// 0xE0 == UCS-2, store
|
|
|
|
if ((dataCodingScheme & 0xF0) == 0xE0) {
|
|
encodingType = ENCODING_16BIT;
|
|
} else {
|
|
encodingType = ENCODING_7BIT;
|
|
}
|
|
|
|
userDataCompressed = false;
|
|
boolean active = ((dataCodingScheme & 0x08) == 0x08);
|
|
|
|
// bit 0x04 reserved
|
|
|
|
if ((dataCodingScheme & 0x03) == 0x00) {
|
|
isMwi = true;
|
|
mwiSense = active;
|
|
mwiDontStore = ((dataCodingScheme & 0xF0) == 0xC0);
|
|
} else {
|
|
isMwi = false;
|
|
|
|
Log.w(LOG_TAG, "MWI for fax, email, or other "
|
|
+ (dataCodingScheme & 0xff));
|
|
}
|
|
} else {
|
|
Log.w(LOG_TAG, "3 - Unsupported SMS data coding scheme "
|
|
+ (dataCodingScheme & 0xff));
|
|
}
|
|
|
|
// set both the user data and the user data header.
|
|
int count = p.constructUserData(hasUserDataHeader,
|
|
encodingType == ENCODING_7BIT);
|
|
this.userData = p.getUserData();
|
|
this.userDataHeader = p.getUserDataHeader();
|
|
|
|
switch (encodingType) {
|
|
case ENCODING_UNKNOWN:
|
|
case ENCODING_8BIT:
|
|
messageBody = null;
|
|
break;
|
|
|
|
case ENCODING_7BIT:
|
|
messageBody = p.getUserDataGSM7Bit(count);
|
|
break;
|
|
|
|
case ENCODING_16BIT:
|
|
messageBody = p.getUserDataUCS2(count);
|
|
break;
|
|
}
|
|
|
|
if (Config.LOGV) Log.v(LOG_TAG, "SMS message body (raw): '" + messageBody + "'");
|
|
|
|
if (messageBody != null) {
|
|
parseMessageBody();
|
|
}
|
|
|
|
if (!hasMessageClass) {
|
|
messageClass = MessageClass.UNKNOWN;
|
|
} else {
|
|
switch (dataCodingScheme & 0x3) {
|
|
case 0:
|
|
messageClass = MessageClass.CLASS_0;
|
|
break;
|
|
case 1:
|
|
messageClass = MessageClass.CLASS_1;
|
|
break;
|
|
case 2:
|
|
messageClass = MessageClass.CLASS_2;
|
|
break;
|
|
case 3:
|
|
messageClass = MessageClass.CLASS_3;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void parseMessageBody() {
|
|
if (originatingAddress.couldBeEmailGateway()) {
|
|
extractEmailAddressFromMessageBody();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Try to parse this message as an email gateway message -> Neither
|
|
* of the standard ways are currently supported: There are two ways
|
|
* specified in TS 23.040 Section 3.8 (not supported via this mechanism) -
|
|
* SMS message "may have its TP-PID set for internet electronic mail - MT
|
|
* SMS format: [<from-address><space>]<message> - "Depending on the
|
|
* nature of the gateway, the destination/origination address is either
|
|
* derived from the content of the SMS TP-OA or TP-DA field, or the
|
|
* TP-OA/TP-DA field contains a generic gateway address and the to/from
|
|
* address is added at the beginning as shown above." - multiple addreses
|
|
* separated by commas, no spaces - subject field delimited by '()' or '##'
|
|
* and '#' Section 9.2.3.24.11
|
|
*/
|
|
private void extractEmailAddressFromMessageBody() {
|
|
|
|
/*
|
|
* a little guesswork here. I haven't found doc for this.
|
|
* the format could be either
|
|
*
|
|
* 1. [x@y][ ]/[subject][ ]/[body]
|
|
* -or-
|
|
* 2. [x@y][ ]/[body]
|
|
*/
|
|
int slash = 0, slash2 = 0, atSymbol = 0;
|
|
|
|
try {
|
|
slash = messageBody.indexOf(" /");
|
|
if (slash == -1) {
|
|
return;
|
|
}
|
|
|
|
atSymbol = messageBody.indexOf('@');
|
|
if (atSymbol == -1 || atSymbol > slash) {
|
|
return;
|
|
}
|
|
|
|
emailFrom = messageBody.substring(0, slash);
|
|
|
|
slash2 = messageBody.indexOf(" /", slash + 2);
|
|
|
|
if (slash2 == -1) {
|
|
pseudoSubject = null;
|
|
emailBody = messageBody.substring(slash + 2);
|
|
} else {
|
|
pseudoSubject = messageBody.substring(slash + 2, slash2);
|
|
emailBody = messageBody.substring(slash2 + 2);
|
|
}
|
|
|
|
isEmail = true;
|
|
} catch (Exception ex) {
|
|
Log.w(LOG_TAG,
|
|
"extractEmailAddressFromMessageBody: exception slash="
|
|
+ slash + ", atSymbol=" + atSymbol + ", slash2="
|
|
+ slash2, ex);
|
|
}
|
|
}
|
|
|
|
}
|