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