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