1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.internal.telephony; 18 19 import static com.android.internal.telephony.SmsConstants.ENCODING_UNKNOWN; 20 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.os.Build; 23 import android.telephony.SmsMessage; 24 import android.text.TextUtils; 25 import android.util.Patterns; 26 27 import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails; 28 29 import java.text.BreakIterator; 30 import java.util.Arrays; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 34 /** 35 * Base class declaring the specific methods and members for SmsMessage. 36 * {@hide} 37 */ 38 public abstract class SmsMessageBase { 39 // Copied from Telephony.Mms.NAME_ADDR_EMAIL_PATTERN 40 public static final Pattern NAME_ADDR_EMAIL_PATTERN = 41 Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*"); 42 43 /** {@hide} The address of the SMSC. May be null */ 44 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 45 protected String mScAddress; 46 47 /** {@hide} The address of the sender */ 48 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 49 protected SmsAddress mOriginatingAddress; 50 51 /** {@hide} The address of the receiver */ 52 protected SmsAddress mRecipientAddress; 53 54 /** {@hide} The message body as a string. May be null if the message isn't text */ 55 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 56 protected String mMessageBody; 57 58 /** {@hide} */ 59 protected String mPseudoSubject; 60 61 /** {@hide} Non-null if this is an email gateway message */ 62 protected String mEmailFrom; 63 64 /** {@hide} Non-null if this is an email gateway message */ 65 protected String mEmailBody; 66 67 /** {@hide} */ 68 protected boolean mIsEmail; 69 70 /** {@hide} Time when SC (service centre) received the message */ 71 protected long mScTimeMillis; 72 73 /** {@hide} The raw PDU of the message */ 74 @UnsupportedAppUsage 75 protected byte[] mPdu; 76 77 /** {@hide} The raw bytes for the user data section of the message */ 78 protected byte[] mUserData; 79 80 /** {@hide} */ 81 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 82 protected SmsHeader mUserDataHeader; 83 84 // "Message Waiting Indication Group" 85 // 23.038 Section 4 86 /** {@hide} */ 87 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 88 protected boolean mIsMwi; 89 90 /** {@hide} */ 91 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 92 protected boolean mMwiSense; 93 94 /** {@hide} */ 95 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 96 protected boolean mMwiDontStore; 97 98 /** 99 * The encoding type of a received SMS message, which is specified using ENCODING_* 100 * GSM: defined in android.telephony.SmsConstants 101 * CDMA: defined in android.telephony.cdma.UserData 102 * 103 * @hide 104 */ 105 protected int mReceivedEncodingType = ENCODING_UNKNOWN; 106 107 /** 108 * Indicates status for messages stored on the ICC. 109 */ 110 protected int mStatusOnIcc = -1; 111 112 /** 113 * Record index of message in the EF. 114 */ 115 protected int mIndexOnIcc = -1; 116 117 /** TP-Message-Reference - Message Reference of sent message. @hide */ 118 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 119 public int mMessageRef; 120 121 @UnsupportedAppUsage SmsMessageBase()122 public SmsMessageBase() { 123 } 124 125 // TODO(): This class is duplicated in SmsMessage.java. Refactor accordingly. 126 public static abstract class SubmitPduBase { 127 @UnsupportedAppUsage 128 public byte[] encodedScAddress; // Null if not applicable. 129 @UnsupportedAppUsage 130 public byte[] encodedMessage; 131 132 @Override toString()133 public String toString() { 134 return "SubmitPdu: encodedScAddress = " 135 + Arrays.toString(encodedScAddress) 136 + ", encodedMessage = " 137 + Arrays.toString(encodedMessage); 138 } 139 } 140 141 /** 142 * Returns the address of the SMS service center that relayed this message 143 * or null if there is none. 144 */ 145 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) getServiceCenterAddress()146 public String getServiceCenterAddress() { 147 return mScAddress; 148 } 149 150 /** 151 * Returns the originating address (sender) of this SMS message in String 152 * form or null if unavailable 153 */ 154 @UnsupportedAppUsage getOriginatingAddress()155 public String getOriginatingAddress() { 156 if (mOriginatingAddress == null) { 157 return null; 158 } 159 160 return mOriginatingAddress.getAddressString(); 161 } 162 163 /** 164 * Returns the originating address, or email from address if this message 165 * was from an email gateway. Returns null if originating address 166 * unavailable. 167 */ 168 @UnsupportedAppUsage getDisplayOriginatingAddress()169 public String getDisplayOriginatingAddress() { 170 if (mIsEmail) { 171 return mEmailFrom; 172 } else { 173 return getOriginatingAddress(); 174 } 175 } 176 177 /** 178 * Returns the message body as a String, if it exists and is text based. 179 * @return message body is there is one, otherwise null 180 */ 181 @UnsupportedAppUsage getMessageBody()182 public String getMessageBody() { 183 return mMessageBody; 184 } 185 186 /** 187 * Returns the class of this message. 188 */ getMessageClass()189 public abstract SmsConstants.MessageClass getMessageClass(); 190 191 /** 192 * Returns the message body, or email message body if this message was from 193 * an email gateway. Returns null if message body unavailable. 194 */ 195 @UnsupportedAppUsage getDisplayMessageBody()196 public String getDisplayMessageBody() { 197 if (mIsEmail) { 198 return mEmailBody; 199 } else { 200 return getMessageBody(); 201 } 202 } 203 204 /** 205 * Unofficial convention of a subject line enclosed in parens empty string 206 * if not present 207 */ 208 @UnsupportedAppUsage getPseudoSubject()209 public String getPseudoSubject() { 210 return mPseudoSubject == null ? "" : mPseudoSubject; 211 } 212 213 /** 214 * Returns the service centre timestamp in currentTimeMillis() format 215 */ 216 @UnsupportedAppUsage getTimestampMillis()217 public long getTimestampMillis() { 218 return mScTimeMillis; 219 } 220 221 /** 222 * Returns true if message is an email. 223 * 224 * @return true if this message came through an email gateway and email 225 * sender / subject / parsed body are available 226 */ isEmail()227 public boolean isEmail() { 228 return mIsEmail; 229 } 230 231 /** 232 * @return if isEmail() is true, body of the email sent through the gateway. 233 * null otherwise 234 */ getEmailBody()235 public String getEmailBody() { 236 return mEmailBody; 237 } 238 239 /** 240 * @return if isEmail() is true, email from address of email sent through 241 * the gateway. null otherwise 242 */ getEmailFrom()243 public String getEmailFrom() { 244 return mEmailFrom; 245 } 246 247 /** 248 * Get protocol identifier. 249 */ 250 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) getProtocolIdentifier()251 public abstract int getProtocolIdentifier(); 252 253 /** 254 * See TS 23.040 9.2.3.9 returns true if this is a "replace short message" 255 * SMS 256 */ 257 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) isReplace()258 public abstract boolean isReplace(); 259 260 /** 261 * Returns true for CPHS MWI toggle message. 262 * 263 * @return true if this is a CPHS MWI toggle message See CPHS 4.2 section 264 * B.4.2 265 */ isCphsMwiMessage()266 public abstract boolean isCphsMwiMessage(); 267 268 /** 269 * returns true if this message is a CPHS voicemail / message waiting 270 * indicator (MWI) clear message 271 */ isMWIClearMessage()272 public abstract boolean isMWIClearMessage(); 273 274 /** 275 * returns true if this message is a CPHS voicemail / message waiting 276 * indicator (MWI) set message 277 */ isMWISetMessage()278 public abstract boolean isMWISetMessage(); 279 280 /** 281 * returns true if this message is a "Message Waiting Indication Group: 282 * Discard Message" notification and should not be stored. 283 */ isMwiDontStore()284 public abstract boolean isMwiDontStore(); 285 286 /** 287 * returns the user data section minus the user data header if one was 288 * present. 289 */ 290 @UnsupportedAppUsage getUserData()291 public byte[] getUserData() { 292 return mUserData; 293 } 294 295 /** 296 * Returns an object representing the user data header 297 * 298 * {@hide} 299 */ 300 @UnsupportedAppUsage getUserDataHeader()301 public SmsHeader getUserDataHeader() { 302 return mUserDataHeader; 303 } 304 305 /** 306 * TODO(cleanup): The term PDU is used in a seemingly non-unique 307 * manner -- for example, what is the difference between this byte 308 * array and the contents of SubmitPdu objects. Maybe a more 309 * illustrative term would be appropriate. 310 */ 311 312 /** 313 * Returns the raw PDU for the message. 314 */ getPdu()315 public byte[] getPdu() { 316 return mPdu; 317 } 318 319 /** 320 * For an SMS-STATUS-REPORT message, this returns the status field from 321 * the status report. This field indicates the status of a previously 322 * submitted SMS, if requested. See TS 23.040, 9.2.3.15 TP-Status for a 323 * description of values. 324 * 325 * @return 0 indicates the previously sent message was received. 326 * See TS 23.040, 9.9.2.3.15 for a description of other possible 327 * values. 328 */ 329 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) getStatus()330 public abstract int getStatus(); 331 332 /** 333 * Return true iff the message is a SMS-STATUS-REPORT message. 334 */ 335 @UnsupportedAppUsage isStatusReportMessage()336 public abstract boolean isStatusReportMessage(); 337 338 /** 339 * Returns true iff the <code>TP-Reply-Path</code> bit is set in 340 * this message. 341 */ 342 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) isReplyPathPresent()343 public abstract boolean isReplyPathPresent(); 344 345 /** 346 * Returns the status of the message on the ICC (read, unread, sent, unsent). 347 * 348 * @return the status of the message on the ICC. These are: 349 * SmsManager.STATUS_ON_ICC_FREE 350 * SmsManager.STATUS_ON_ICC_READ 351 * SmsManager.STATUS_ON_ICC_UNREAD 352 * SmsManager.STATUS_ON_ICC_SEND 353 * SmsManager.STATUS_ON_ICC_UNSENT 354 */ getStatusOnIcc()355 public int getStatusOnIcc() { 356 return mStatusOnIcc; 357 } 358 359 /** 360 * Returns the record index of the message on the ICC (1-based index). 361 * @return the record index of the message on the ICC, or -1 if this 362 * SmsMessage was not created from a ICC SMS EF record. 363 */ getIndexOnIcc()364 public int getIndexOnIcc() { 365 return mIndexOnIcc; 366 } 367 parseMessageBody()368 protected void parseMessageBody() { 369 // originatingAddress could be null if this message is from a status 370 // report. 371 if (mOriginatingAddress != null && mOriginatingAddress.couldBeEmailGateway()) { 372 extractEmailAddressFromMessageBody(); 373 } 374 } 375 extractAddrSpec(String messageHeader)376 private static String extractAddrSpec(String messageHeader) { 377 Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(messageHeader); 378 379 if (match.matches()) { 380 return match.group(2); 381 } 382 return messageHeader; 383 } 384 385 /** 386 * Returns true if the message header string indicates that the message is from a email address. 387 * 388 * @param messageHeader message header 389 * @return {@code true} if it's a message from an email address, {@code false} otherwise. 390 */ isEmailAddress(String messageHeader)391 public static boolean isEmailAddress(String messageHeader) { 392 if (TextUtils.isEmpty(messageHeader)) { 393 return false; 394 } 395 396 String s = extractAddrSpec(messageHeader); 397 Matcher match = Patterns.EMAIL_ADDRESS.matcher(s); 398 return match.matches(); 399 } 400 401 /** 402 * Try to parse this message as an email gateway message 403 * There are two ways specified in TS 23.040 Section 3.8 : 404 * - SMS message "may have its TP-PID set for Internet electronic mail - MT 405 * SMS format: [<from-address><space>]<message> - "Depending on the 406 * nature of the gateway, the destination/origination address is either 407 * derived from the content of the SMS TP-OA or TP-DA field, or the 408 * TP-OA/TP-DA field contains a generic gateway address and the to/from 409 * address is added at the beginning as shown above." (which is supported here) 410 * - Multiple addresses separated by commas, no spaces, Subject field delimited 411 * by '()' or '##' and '#' Section 9.2.3.24.11 (which are NOT supported here) 412 */ extractEmailAddressFromMessageBody()413 protected void extractEmailAddressFromMessageBody() { 414 415 /* Some carriers may use " /" delimiter as below 416 * 417 * 1. [x@y][ ]/[subject][ ]/[body] 418 * -or- 419 * 2. [x@y][ ]/[body] 420 */ 421 String[] parts = mMessageBody.split("( /)|( )", 2); 422 if (parts.length < 2) return; 423 mEmailFrom = parts[0]; 424 mEmailBody = parts[1]; 425 mIsEmail = isEmailAddress(mEmailFrom); 426 } 427 428 /** 429 * Find the next position to start a new fragment of a multipart SMS. 430 * 431 * @param currentPosition current start position of the fragment 432 * @param byteLimit maximum number of bytes in the fragment 433 * @param msgBody text of the SMS in UTF-16 encoding 434 * @return the position to start the next fragment 435 */ findNextUnicodePosition( int currentPosition, int byteLimit, CharSequence msgBody)436 public static int findNextUnicodePosition( 437 int currentPosition, int byteLimit, CharSequence msgBody) { 438 int nextPos = Math.min(currentPosition + byteLimit / 2, msgBody.length()); 439 // Check whether the fragment ends in a character boundary. Some characters take 4-bytes 440 // in UTF-16 encoding. Many carriers cannot handle 441 // a fragment correctly if it does not end at a character boundary. 442 if (nextPos < msgBody.length()) { 443 BreakIterator breakIterator = BreakIterator.getCharacterInstance(); 444 breakIterator.setText(msgBody.toString()); 445 if (!breakIterator.isBoundary(nextPos)) { 446 int breakPos = breakIterator.preceding(nextPos); 447 while (breakPos + 4 <= nextPos 448 && isRegionalIndicatorSymbol( 449 Character.codePointAt(msgBody, breakPos)) 450 && isRegionalIndicatorSymbol( 451 Character.codePointAt(msgBody, breakPos + 2))) { 452 // skip forward over flags (pairs of Regional Indicator Symbol) 453 breakPos += 4; 454 } 455 if (breakPos > currentPosition) { 456 nextPos = breakPos; 457 } else if (Character.isHighSurrogate(msgBody.charAt(nextPos - 1))) { 458 // no character boundary in this fragment, try to at least land on a code point 459 nextPos -= 1; 460 } 461 } 462 } 463 return nextPos; 464 } 465 isRegionalIndicatorSymbol(int codePoint)466 private static boolean isRegionalIndicatorSymbol(int codePoint) { 467 /** Based on http://unicode.org/Public/emoji/3.0/emoji-sequences.txt */ 468 return 0x1F1E6 <= codePoint && codePoint <= 0x1F1FF; 469 } 470 471 /** 472 * Calculate the TextEncodingDetails of a message encoded in Unicode. 473 */ calcUnicodeEncodingDetails(CharSequence msgBody)474 public static TextEncodingDetails calcUnicodeEncodingDetails(CharSequence msgBody) { 475 TextEncodingDetails ted = new TextEncodingDetails(); 476 int octets = msgBody.length() * 2; 477 ted.codeUnitSize = SmsConstants.ENCODING_16BIT; 478 ted.codeUnitCount = msgBody.length(); 479 if (octets > SmsConstants.MAX_USER_DATA_BYTES) { 480 // If EMS is not supported, break down EMS into single segment SMS 481 // and add page info " x/y". 482 // In the case of UCS2 encoding type, we need 8 bytes for this 483 // but we only have 6 bytes from UDH, so truncate the limit for 484 // each segment by 2 bytes (1 char). 485 int maxUserDataBytesWithHeader = SmsConstants.MAX_USER_DATA_BYTES_WITH_HEADER; 486 if (!SmsMessage.hasEmsSupport()) { 487 // make sure total number of segments is less than 10 488 if (octets <= 9 * (maxUserDataBytesWithHeader - 2)) { 489 maxUserDataBytesWithHeader -= 2; 490 } 491 } 492 493 int pos = 0; // Index in code units. 494 int msgCount = 0; 495 while (pos < msgBody.length()) { 496 int nextPos = findNextUnicodePosition(pos, maxUserDataBytesWithHeader, 497 msgBody); 498 if (nextPos == msgBody.length()) { 499 ted.codeUnitsRemaining = pos + maxUserDataBytesWithHeader / 2 - 500 msgBody.length(); 501 } 502 pos = nextPos; 503 msgCount++; 504 } 505 ted.msgCount = msgCount; 506 } else { 507 ted.msgCount = 1; 508 ted.codeUnitsRemaining = (SmsConstants.MAX_USER_DATA_BYTES - octets) / 2; 509 } 510 511 return ted; 512 } 513 514 /** 515 * {@hide} 516 * Returns the receiver address of this SMS message in String 517 * form or null if unavailable 518 */ getRecipientAddress()519 public String getRecipientAddress() { 520 if (mRecipientAddress == null) { 521 return null; 522 } 523 524 return mRecipientAddress.getAddressString(); 525 } 526 getReceivedEncodingType()527 public int getReceivedEncodingType() { 528 return mReceivedEncodingType; 529 } 530 } 531