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