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
17package com.android.messaging.sms;
18
19import android.content.Context;
20import android.database.Cursor;
21import android.net.Uri;
22import android.provider.BaseColumns;
23import android.text.TextUtils;
24import android.util.Patterns;
25
26import com.android.messaging.mmslib.SqliteWrapper;
27import com.android.messaging.util.LogUtil;
28
29import java.util.HashSet;
30import java.util.Set;
31import java.util.regex.Matcher;
32import java.util.regex.Pattern;
33
34/**
35 * Utility functions for the Messaging Service
36 */
37public class 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.
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
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     */
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     */
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     */
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.
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         */
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         */
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