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