1 /*
2  * Copyright (C) 2015 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.messaging.sms;
18 
19 import android.content.Context;
20 import android.database.Cursor;
21 import android.net.Uri;
22 import android.provider.BaseColumns;
23 import android.text.TextUtils;
24 import android.util.Patterns;
25 
26 import com.android.messaging.mmslib.SqliteWrapper;
27 import com.android.messaging.util.LogUtil;
28 
29 import java.util.HashSet;
30 import java.util.Set;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33 
34 /**
35  * Utility functions for the Messaging Service
36  */
37 public class MmsSmsUtils {
MmsSmsUtils()38     private MmsSmsUtils() {
39         // Forbidden being instantiated.
40     }
41 
42     // An alias (or commonly called "nickname") is:
43     // Nickname must begin with a letter.
44     // Only letters a-z, numbers 0-9, or . are allowed in Nickname field.
isAlias(final String string, final int subId)45     public static boolean isAlias(final String string, final int subId) {
46         if (!MmsConfig.get(subId).isAliasEnabled()) {
47             return false;
48         }
49 
50         final int len = string == null ? 0 : string.length();
51 
52         if (len < MmsConfig.get(subId).getAliasMinChars() ||
53                 len > MmsConfig.get(subId).getAliasMaxChars()) {
54             return false;
55         }
56 
57         if (!Character.isLetter(string.charAt(0))) {    // Nickname begins with a letter
58             return false;
59         }
60         for (int i = 1; i < len; i++) {
61             final char c = string.charAt(i);
62             if (!(Character.isLetterOrDigit(c) || c == '.')) {
63                 return false;
64             }
65         }
66 
67         return true;
68     }
69 
70     /**
71      * mailbox         =       name-addr
72      * name-addr       =       [display-name] angle-addr
73      * angle-addr      =       [CFWS] "<" addr-spec ">" [CFWS]
74      */
75     public static final Pattern NAME_ADDR_EMAIL_PATTERN =
76             Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
77 
extractAddrSpec(final String address)78     public static String extractAddrSpec(final String address) {
79         final Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address);
80 
81         if (match.matches()) {
82             return match.group(2);
83         }
84         return address;
85     }
86 
87     /**
88      * Returns true if the address is an email address
89      *
90      * @param address the input address to be tested
91      * @return true if address is an email address
92      */
isEmailAddress(final String address)93     public static boolean isEmailAddress(final String address) {
94         if (TextUtils.isEmpty(address)) {
95             return false;
96         }
97 
98         final String s = extractAddrSpec(address);
99         final Matcher match = Patterns.EMAIL_ADDRESS.matcher(s);
100         return match.matches();
101     }
102 
103     /**
104      * This pattern is intended for searching for carrier specific phone numbers starting with star
105      * sign and digits such as the voice mail number.
106      * (e.g. *20 Chile Claro)
107      */
108     private static final Pattern PHONE_NUMBER_STARTING_WITH_STAR_PATTERN =
109             Pattern.compile("\\*[0-9]+");
110 
111     /**
112      * Returns true if the number is a Phone number
113      *
114      * @param number the input number to be tested
115      * @return true if number is a Phone number
116      */
isPhoneNumber(final String number)117     public static boolean isPhoneNumber(final String number) {
118         if (TextUtils.isEmpty(number)) {
119             return false;
120         }
121 
122         Matcher match = Patterns.PHONE.matcher(number);
123         if (!match.matches()) {
124             match = PHONE_NUMBER_STARTING_WITH_STAR_PATTERN.matcher(number);
125             return match.matches();
126         }
127         return true;
128     }
129 
130     /**
131      * Check if MMS is required when sending to email address
132      *
133      * @param destinationHasEmailAddress destination includes an email address
134      * @return true if MMS is required.
135      */
getRequireMmsForEmailAddress(final boolean destinationHasEmailAddress, final int subId)136     public static boolean getRequireMmsForEmailAddress(final boolean destinationHasEmailAddress,
137             final int subId) {
138         if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway())) {
139             return false;
140         } else {
141             return destinationHasEmailAddress;
142         }
143     }
144 
145     /**
146      * Helper functions for the "threads" table used by MMS and SMS.
147      */
148     public static final class Threads implements android.provider.Telephony.ThreadsColumns {
149         private static final String[] ID_PROJECTION = { BaseColumns._ID };
150         private static final Uri THREAD_ID_CONTENT_URI = Uri.parse(
151                 "content://mms-sms/threadID");
152         public static final Uri CONTENT_URI = Uri.withAppendedPath(
153                 android.provider.Telephony.MmsSms.CONTENT_URI, "conversations");
154 
155         // No one should construct an instance of this class.
Threads()156         private Threads() {
157         }
158 
159         /**
160          * This is a single-recipient version of
161          * getOrCreateThreadId.  It's convenient for use with SMS
162          * messages.
163          */
getOrCreateThreadId(final Context context, final String recipient)164         public static long getOrCreateThreadId(final Context context, final String recipient) {
165             final Set<String> recipients = new HashSet<String>();
166 
167             recipients.add(recipient);
168             return getOrCreateThreadId(context, recipients);
169         }
170 
171         /**
172          * Given the recipients list and subject of an unsaved message,
173          * return its thread ID.  If the message starts a new thread,
174          * allocate a new thread ID.  Otherwise, use the appropriate
175          * existing thread ID.
176          *
177          * Find the thread ID of the same set of recipients (in
178          * any order, without any additions). If one
179          * is found, return it.  Otherwise, return a unique thread ID.
180          */
getOrCreateThreadId( final Context context, final Set<String> recipients)181         public static long getOrCreateThreadId(
182                 final Context context, final Set<String> recipients) {
183             final Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
184 
185             for (String recipient : recipients) {
186                 if (isEmailAddress(recipient)) {
187                     recipient = extractAddrSpec(recipient);
188                 }
189 
190                 uriBuilder.appendQueryParameter("recipient", recipient);
191             }
192 
193             final Uri uri = uriBuilder.build();
194             //if (DEBUG) Rlog.v(TAG, "getOrCreateThreadId uri: " + uri);
195 
196             final Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
197                     uri, ID_PROJECTION, null, null, null);
198             if (cursor != null) {
199                 try {
200                     if (cursor.moveToFirst()) {
201                         return cursor.getLong(0);
202                     } else {
203                         LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG,
204                                 "getOrCreateThreadId returned no rows!");
205                     }
206                 } finally {
207                     cursor.close();
208                 }
209             }
210 
211             LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "getOrCreateThreadId failed with "
212                     + LogUtil.sanitizePII(recipients.toString()));
213             throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
214         }
215     }
216 }
217