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