531 lines
18 KiB
Java
531 lines
18 KiB
Java
/*
|
|
* Copyright (C) 2008 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;
|
|
|
|
import static com.android.internal.telephony.SmsConstants.ENCODING_UNKNOWN;
|
|
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.os.Build;
|
|
import android.telephony.SmsMessage;
|
|
import android.text.TextUtils;
|
|
import android.util.Patterns;
|
|
|
|
import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
|
|
|
|
import java.text.BreakIterator;
|
|
import java.util.Arrays;
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
/**
|
|
* Base class declaring the specific methods and members for SmsMessage.
|
|
* {@hide}
|
|
*/
|
|
public abstract class SmsMessageBase {
|
|
// Copied from Telephony.Mms.NAME_ADDR_EMAIL_PATTERN
|
|
public static final Pattern NAME_ADDR_EMAIL_PATTERN =
|
|
Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
|
|
|
|
/** {@hide} The address of the SMSC. May be null */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
protected String mScAddress;
|
|
|
|
/** {@hide} The address of the sender */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
protected SmsAddress mOriginatingAddress;
|
|
|
|
/** {@hide} The address of the receiver */
|
|
protected SmsAddress mRecipientAddress;
|
|
|
|
/** {@hide} The message body as a string. May be null if the message isn't text */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
protected String mMessageBody;
|
|
|
|
/** {@hide} */
|
|
protected String mPseudoSubject;
|
|
|
|
/** {@hide} Non-null if this is an email gateway message */
|
|
protected String mEmailFrom;
|
|
|
|
/** {@hide} Non-null if this is an email gateway message */
|
|
protected String mEmailBody;
|
|
|
|
/** {@hide} */
|
|
protected boolean mIsEmail;
|
|
|
|
/** {@hide} Time when SC (service centre) received the message */
|
|
protected long mScTimeMillis;
|
|
|
|
/** {@hide} The raw PDU of the message */
|
|
@UnsupportedAppUsage
|
|
protected byte[] mPdu;
|
|
|
|
/** {@hide} The raw bytes for the user data section of the message */
|
|
protected byte[] mUserData;
|
|
|
|
/** {@hide} */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
protected SmsHeader mUserDataHeader;
|
|
|
|
// "Message Waiting Indication Group"
|
|
// 23.038 Section 4
|
|
/** {@hide} */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
protected boolean mIsMwi;
|
|
|
|
/** {@hide} */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
protected boolean mMwiSense;
|
|
|
|
/** {@hide} */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
protected boolean mMwiDontStore;
|
|
|
|
/**
|
|
* The encoding type of a received SMS message, which is specified using ENCODING_*
|
|
* GSM: defined in android.telephony.SmsConstants
|
|
* CDMA: defined in android.telephony.cdma.UserData
|
|
*
|
|
* @hide
|
|
*/
|
|
protected int mReceivedEncodingType = ENCODING_UNKNOWN;
|
|
|
|
/**
|
|
* Indicates status for messages stored on the ICC.
|
|
*/
|
|
protected int mStatusOnIcc = -1;
|
|
|
|
/**
|
|
* Record index of message in the EF.
|
|
*/
|
|
protected int mIndexOnIcc = -1;
|
|
|
|
/** TP-Message-Reference - Message Reference of sent message. @hide */
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public int mMessageRef;
|
|
|
|
@UnsupportedAppUsage
|
|
public SmsMessageBase() {
|
|
}
|
|
|
|
// TODO(): This class is duplicated in SmsMessage.java. Refactor accordingly.
|
|
public static abstract class SubmitPduBase {
|
|
@UnsupportedAppUsage
|
|
public byte[] encodedScAddress; // Null if not applicable.
|
|
@UnsupportedAppUsage
|
|
public byte[] encodedMessage;
|
|
|
|
@Override
|
|
public String toString() {
|
|
return "SubmitPdu: encodedScAddress = "
|
|
+ Arrays.toString(encodedScAddress)
|
|
+ ", encodedMessage = "
|
|
+ Arrays.toString(encodedMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the address of the SMS service center that relayed this message
|
|
* or null if there is none.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
public String getServiceCenterAddress() {
|
|
return mScAddress;
|
|
}
|
|
|
|
/**
|
|
* Returns the originating address (sender) of this SMS message in String
|
|
* form or null if unavailable
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public String getOriginatingAddress() {
|
|
if (mOriginatingAddress == null) {
|
|
return null;
|
|
}
|
|
|
|
return mOriginatingAddress.getAddressString();
|
|
}
|
|
|
|
/**
|
|
* Returns the originating address, or email from address if this message
|
|
* was from an email gateway. Returns null if originating address
|
|
* unavailable.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public String getDisplayOriginatingAddress() {
|
|
if (mIsEmail) {
|
|
return mEmailFrom;
|
|
} 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
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public String getMessageBody() {
|
|
return mMessageBody;
|
|
}
|
|
|
|
/**
|
|
* Returns the class of this message.
|
|
*/
|
|
public abstract SmsConstants.MessageClass getMessageClass();
|
|
|
|
/**
|
|
* Returns the message body, or email message body if this message was from
|
|
* an email gateway. Returns null if message body unavailable.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public String getDisplayMessageBody() {
|
|
if (mIsEmail) {
|
|
return mEmailBody;
|
|
} else {
|
|
return getMessageBody();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unofficial convention of a subject line enclosed in parens empty string
|
|
* if not present
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public String getPseudoSubject() {
|
|
return mPseudoSubject == null ? "" : mPseudoSubject;
|
|
}
|
|
|
|
/**
|
|
* Returns the service centre timestamp in currentTimeMillis() format
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public long getTimestampMillis() {
|
|
return mScTimeMillis;
|
|
}
|
|
|
|
/**
|
|
* 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 mIsEmail;
|
|
}
|
|
|
|
/**
|
|
* @return if isEmail() is true, body of the email sent through the gateway.
|
|
* null otherwise
|
|
*/
|
|
public String getEmailBody() {
|
|
return mEmailBody;
|
|
}
|
|
|
|
/**
|
|
* @return if isEmail() is true, email from address of email sent through
|
|
* the gateway. null otherwise
|
|
*/
|
|
public String getEmailFrom() {
|
|
return mEmailFrom;
|
|
}
|
|
|
|
/**
|
|
* Get protocol identifier.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
public abstract int getProtocolIdentifier();
|
|
|
|
/**
|
|
* See TS 23.040 9.2.3.9 returns true if this is a "replace short message"
|
|
* SMS
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
public abstract boolean isReplace();
|
|
|
|
/**
|
|
* 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 abstract boolean isCphsMwiMessage();
|
|
|
|
/**
|
|
* returns true if this message is a CPHS voicemail / message waiting
|
|
* indicator (MWI) clear message
|
|
*/
|
|
public abstract boolean isMWIClearMessage();
|
|
|
|
/**
|
|
* returns true if this message is a CPHS voicemail / message waiting
|
|
* indicator (MWI) set message
|
|
*/
|
|
public abstract boolean isMWISetMessage();
|
|
|
|
/**
|
|
* returns true if this message is a "Message Waiting Indication Group:
|
|
* Discard Message" notification and should not be stored.
|
|
*/
|
|
public abstract boolean isMwiDontStore();
|
|
|
|
/**
|
|
* returns the user data section minus the user data header if one was
|
|
* present.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public byte[] getUserData() {
|
|
return mUserData;
|
|
}
|
|
|
|
/**
|
|
* Returns an object representing the user data header
|
|
*
|
|
* {@hide}
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public SmsHeader getUserDataHeader() {
|
|
return mUserDataHeader;
|
|
}
|
|
|
|
/**
|
|
* TODO(cleanup): The term PDU is used in a seemingly non-unique
|
|
* manner -- for example, what is the difference between this byte
|
|
* array and the contents of SubmitPdu objects. Maybe a more
|
|
* illustrative term would be appropriate.
|
|
*/
|
|
|
|
/**
|
|
* Returns the raw PDU for the message.
|
|
*/
|
|
public byte[] getPdu() {
|
|
return mPdu;
|
|
}
|
|
|
|
/**
|
|
* For an SMS-STATUS-REPORT message, this returns the status field from
|
|
* the status report. This field indicates the status of a previously
|
|
* 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.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
public abstract int getStatus();
|
|
|
|
/**
|
|
* Return true iff the message is a SMS-STATUS-REPORT message.
|
|
*/
|
|
@UnsupportedAppUsage
|
|
public abstract boolean isStatusReportMessage();
|
|
|
|
/**
|
|
* Returns true iff the <code>TP-Reply-Path</code> bit is set in
|
|
* this message.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
|
|
public abstract boolean isReplyPathPresent();
|
|
|
|
/**
|
|
* Returns the status of the message on the ICC (read, unread, sent, unsent).
|
|
*
|
|
* @return the status of the message on the ICC. These are:
|
|
* SmsManager.STATUS_ON_ICC_FREE
|
|
* SmsManager.STATUS_ON_ICC_READ
|
|
* SmsManager.STATUS_ON_ICC_UNREAD
|
|
* SmsManager.STATUS_ON_ICC_SEND
|
|
* SmsManager.STATUS_ON_ICC_UNSENT
|
|
*/
|
|
public int getStatusOnIcc() {
|
|
return mStatusOnIcc;
|
|
}
|
|
|
|
/**
|
|
* Returns the record index of the message on the ICC (1-based index).
|
|
* @return the record index of the message on the ICC, or -1 if this
|
|
* SmsMessage was not created from a ICC SMS EF record.
|
|
*/
|
|
public int getIndexOnIcc() {
|
|
return mIndexOnIcc;
|
|
}
|
|
|
|
protected void parseMessageBody() {
|
|
// originatingAddress could be null if this message is from a status
|
|
// report.
|
|
if (mOriginatingAddress != null && mOriginatingAddress.couldBeEmailGateway()) {
|
|
extractEmailAddressFromMessageBody();
|
|
}
|
|
}
|
|
|
|
private static String extractAddrSpec(String messageHeader) {
|
|
Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(messageHeader);
|
|
|
|
if (match.matches()) {
|
|
return match.group(2);
|
|
}
|
|
return messageHeader;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the message header string indicates that the message is from a email address.
|
|
*
|
|
* @param messageHeader message header
|
|
* @return {@code true} if it's a message from an email address, {@code false} otherwise.
|
|
*/
|
|
public static boolean isEmailAddress(String messageHeader) {
|
|
if (TextUtils.isEmpty(messageHeader)) {
|
|
return false;
|
|
}
|
|
|
|
String s = extractAddrSpec(messageHeader);
|
|
Matcher match = Patterns.EMAIL_ADDRESS.matcher(s);
|
|
return match.matches();
|
|
}
|
|
|
|
/**
|
|
* Try to parse this message as an email gateway message
|
|
* There are two ways specified in TS 23.040 Section 3.8 :
|
|
* - 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." (which is supported here)
|
|
* - Multiple addresses separated by commas, no spaces, Subject field delimited
|
|
* by '()' or '##' and '#' Section 9.2.3.24.11 (which are NOT supported here)
|
|
*/
|
|
protected void extractEmailAddressFromMessageBody() {
|
|
|
|
/* Some carriers may use " /" delimiter as below
|
|
*
|
|
* 1. [x@y][ ]/[subject][ ]/[body]
|
|
* -or-
|
|
* 2. [x@y][ ]/[body]
|
|
*/
|
|
String[] parts = mMessageBody.split("( /)|( )", 2);
|
|
if (parts.length < 2) return;
|
|
mEmailFrom = parts[0];
|
|
mEmailBody = parts[1];
|
|
mIsEmail = isEmailAddress(mEmailFrom);
|
|
}
|
|
|
|
/**
|
|
* Find the next position to start a new fragment of a multipart SMS.
|
|
*
|
|
* @param currentPosition current start position of the fragment
|
|
* @param byteLimit maximum number of bytes in the fragment
|
|
* @param msgBody text of the SMS in UTF-16 encoding
|
|
* @return the position to start the next fragment
|
|
*/
|
|
public static int findNextUnicodePosition(
|
|
int currentPosition, int byteLimit, CharSequence msgBody) {
|
|
int nextPos = Math.min(currentPosition + byteLimit / 2, msgBody.length());
|
|
// Check whether the fragment ends in a character boundary. Some characters take 4-bytes
|
|
// in UTF-16 encoding. Many carriers cannot handle
|
|
// a fragment correctly if it does not end at a character boundary.
|
|
if (nextPos < msgBody.length()) {
|
|
BreakIterator breakIterator = BreakIterator.getCharacterInstance();
|
|
breakIterator.setText(msgBody.toString());
|
|
if (!breakIterator.isBoundary(nextPos)) {
|
|
int breakPos = breakIterator.preceding(nextPos);
|
|
while (breakPos + 4 <= nextPos
|
|
&& isRegionalIndicatorSymbol(
|
|
Character.codePointAt(msgBody, breakPos))
|
|
&& isRegionalIndicatorSymbol(
|
|
Character.codePointAt(msgBody, breakPos + 2))) {
|
|
// skip forward over flags (pairs of Regional Indicator Symbol)
|
|
breakPos += 4;
|
|
}
|
|
if (breakPos > currentPosition) {
|
|
nextPos = breakPos;
|
|
} else if (Character.isHighSurrogate(msgBody.charAt(nextPos - 1))) {
|
|
// no character boundary in this fragment, try to at least land on a code point
|
|
nextPos -= 1;
|
|
}
|
|
}
|
|
}
|
|
return nextPos;
|
|
}
|
|
|
|
private static boolean isRegionalIndicatorSymbol(int codePoint) {
|
|
/** Based on http://unicode.org/Public/emoji/3.0/emoji-sequences.txt */
|
|
return 0x1F1E6 <= codePoint && codePoint <= 0x1F1FF;
|
|
}
|
|
|
|
/**
|
|
* Calculate the TextEncodingDetails of a message encoded in Unicode.
|
|
*/
|
|
public static TextEncodingDetails calcUnicodeEncodingDetails(CharSequence msgBody) {
|
|
TextEncodingDetails ted = new TextEncodingDetails();
|
|
int octets = msgBody.length() * 2;
|
|
ted.codeUnitSize = SmsConstants.ENCODING_16BIT;
|
|
ted.codeUnitCount = msgBody.length();
|
|
if (octets > SmsConstants.MAX_USER_DATA_BYTES) {
|
|
// If EMS is not supported, break down EMS into single segment SMS
|
|
// and add page info " x/y".
|
|
// In the case of UCS2 encoding type, we need 8 bytes for this
|
|
// but we only have 6 bytes from UDH, so truncate the limit for
|
|
// each segment by 2 bytes (1 char).
|
|
int maxUserDataBytesWithHeader = SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER;
|
|
if (!SmsMessage.hasEmsSupport()) {
|
|
// make sure total number of segments is less than 10
|
|
if (octets <= 9 * (maxUserDataBytesWithHeader - 2)) {
|
|
maxUserDataBytesWithHeader -= 2;
|
|
}
|
|
}
|
|
|
|
int pos = 0; // Index in code units.
|
|
int msgCount = 0;
|
|
while (pos < msgBody.length()) {
|
|
int nextPos = findNextUnicodePosition(pos, maxUserDataBytesWithHeader,
|
|
msgBody);
|
|
if (nextPos == msgBody.length()) {
|
|
ted.codeUnitsRemaining = pos + maxUserDataBytesWithHeader / 2 -
|
|
msgBody.length();
|
|
}
|
|
pos = nextPos;
|
|
msgCount++;
|
|
}
|
|
ted.msgCount = msgCount;
|
|
} else {
|
|
ted.msgCount = 1;
|
|
ted.codeUnitsRemaining = (SmsConstants.MAX_USER_DATA_BYTES - octets) / 2;
|
|
}
|
|
|
|
return ted;
|
|
}
|
|
|
|
/**
|
|
* {@hide}
|
|
* Returns the receiver address of this SMS message in String
|
|
* form or null if unavailable
|
|
*/
|
|
public String getRecipientAddress() {
|
|
if (mRecipientAddress == null) {
|
|
return null;
|
|
}
|
|
|
|
return mRecipientAddress.getAddressString();
|
|
}
|
|
|
|
public int getReceivedEncodingType() {
|
|
return mReceivedEncodingType;
|
|
}
|
|
}
|