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      * Returns true if the number is a Phone number
105      *
106      * @param number the input number to be tested
107      * @return true if number is a Phone number
108      */
isPhoneNumber(final String number)109     public static boolean isPhoneNumber(final String number) {
110         if (TextUtils.isEmpty(number)) {
111             return false;
112         }
113 
114         final Matcher match = Patterns.PHONE.matcher(number);
115         return match.matches();
116     }
117 
118     /**
119      * Check if MMS is required when sending to email address
120      *
121      * @param destinationHasEmailAddress destination includes an email address
122      * @return true if MMS is required.
123      */
getRequireMmsForEmailAddress(final boolean destinationHasEmailAddress, final int subId)124     public static boolean getRequireMmsForEmailAddress(final boolean destinationHasEmailAddress,
125             final int subId) {
126         if (!TextUtils.isEmpty(MmsConfig.get(subId).getEmailGateway())) {
127             return false;
128         } else {
129             return destinationHasEmailAddress;
130         }
131     }
132 
133     /**
134      * Helper functions for the "threads" table used by MMS and SMS.
135      */
136     public static final class Threads implements android.provider.Telephony.ThreadsColumns {
137         private static final String[] ID_PROJECTION = { BaseColumns._ID };
138         private static final Uri THREAD_ID_CONTENT_URI = Uri.parse(
139                 "content://mms-sms/threadID");
140         public static final Uri CONTENT_URI = Uri.withAppendedPath(
141                 android.provider.Telephony.MmsSms.CONTENT_URI, "conversations");
142 
143         // No one should construct an instance of this class.
Threads()144         private Threads() {
145         }
146 
147         /**
148          * This is a single-recipient version of
149          * getOrCreateThreadId.  It's convenient for use with SMS
150          * messages.
151          */
getOrCreateThreadId(final Context context, final String recipient)152         public static long getOrCreateThreadId(final Context context, final String recipient) {
153             final Set<String> recipients = new HashSet<String>();
154 
155             recipients.add(recipient);
156             return getOrCreateThreadId(context, recipients);
157         }
158 
159         /**
160          * Given the recipients list and subject of an unsaved message,
161          * return its thread ID.  If the message starts a new thread,
162          * allocate a new thread ID.  Otherwise, use the appropriate
163          * existing thread ID.
164          *
165          * Find the thread ID of the same set of recipients (in
166          * any order, without any additions). If one
167          * is found, return it.  Otherwise, return a unique thread ID.
168          */
getOrCreateThreadId( final Context context, final Set<String> recipients)169         public static long getOrCreateThreadId(
170                 final Context context, final Set<String> recipients) {
171             final Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
172 
173             for (String recipient : recipients) {
174                 if (isEmailAddress(recipient)) {
175                     recipient = extractAddrSpec(recipient);
176                 }
177 
178                 uriBuilder.appendQueryParameter("recipient", recipient);
179             }
180 
181             final Uri uri = uriBuilder.build();
182             //if (DEBUG) Rlog.v(TAG, "getOrCreateThreadId uri: " + uri);
183 
184             final Cursor cursor = SqliteWrapper.query(context, context.getContentResolver(),
185                     uri, ID_PROJECTION, null, null, null);
186             if (cursor != null) {
187                 try {
188                     if (cursor.moveToFirst()) {
189                         return cursor.getLong(0);
190                     } else {
191                         LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG,
192                                 "getOrCreateThreadId returned no rows!");
193                     }
194                 } finally {
195                     cursor.close();
196                 }
197             }
198 
199             LogUtil.e(LogUtil.BUGLE_DATAMODEL_TAG, "getOrCreateThreadId failed with "
200                     + LogUtil.sanitizePII(recipients.toString()));
201             throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
202         }
203     }
204 }
205