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.contacts.compat;
18 
19 import android.content.ContentResolver;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.net.Uri;
23 import android.os.Build;
24 import android.provider.BaseColumns;
25 import android.provider.Telephony;
26 import android.text.TextUtils;
27 import android.util.Log;
28 import android.util.Patterns;
29 
30 import java.util.HashSet;
31 import java.util.Set;
32 import java.util.regex.Matcher;
33 import java.util.regex.Pattern;
34 
35 /**
36  * This class contains static utility methods and variables extracted from Telephony and
37  * SqliteWrapper, and the methods were made visible in API level 23. In this way, we could
38  * enable the corresponding functionality for pre-M devices. We need maintain this class and keep
39  * it synced with Telephony and SqliteWrapper.
40  */
41 public class TelephonyThreadsCompat {
42     /**
43      * Not instantiable.
44      */
TelephonyThreadsCompat()45     private TelephonyThreadsCompat() {}
46 
47     private static final String TAG = "TelephonyThreadsCompat";
48 
getOrCreateThreadId(Context context, String recipient)49     public static long getOrCreateThreadId(Context context, String recipient) {
50         if (SdkVersionOverride.getSdkVersion(Build.VERSION_CODES.M) >= Build.VERSION_CODES.M) {
51             return Telephony.Threads.getOrCreateThreadId(context, recipient);
52         } else {
53             return getOrCreateThreadIdInternal(context, recipient);
54         }
55     }
56 
57     // Below is code copied from Telephony and SqliteWrapper
58     /**
59      * Private {@code content://} style URL for this table. Used by
60      * {@link #getOrCreateThreadId(Context, Set)}.
61      */
62     private static final Uri THREAD_ID_CONTENT_URI = Uri.parse("content://mms-sms/threadID");
63 
64     private static final String[] ID_PROJECTION = { BaseColumns._ID };
65 
66     /**
67      * Regex pattern for names and email addresses.
68      * <ul>
69      *     <li><em>mailbox</em> = {@code name-addr}</li>
70      *     <li><em>name-addr</em> = {@code [display-name] angle-addr}</li>
71      *     <li><em>angle-addr</em> = {@code [CFWS] "<" addr-spec ">" [CFWS]}</li>
72      * </ul>
73      */
74     private static final Pattern NAME_ADDR_EMAIL_PATTERN =
75             Pattern.compile("\\s*(\"[^\"]*\"|[^<>\"]+)\\s*<([^<>]+)>\\s*");
76 
77     /**
78      * Copied from {@link Telephony.Threads#getOrCreateThreadId(Context, String)}
79      */
getOrCreateThreadIdInternal(Context context, String recipient)80     private static long getOrCreateThreadIdInternal(Context context, String recipient) {
81         Set<String> recipients = new HashSet<String>();
82 
83         recipients.add(recipient);
84         return getOrCreateThreadIdInternal(context, recipients);
85     }
86 
87     /**
88      * Given the recipients list and subject of an unsaved message,
89      * return its thread ID.  If the message starts a new thread,
90      * allocate a new thread ID.  Otherwise, use the appropriate
91      * existing thread ID.
92      *
93      * <p>Find the thread ID of the same set of recipients (in any order,
94      * without any additions). If one is found, return it. Otherwise,
95      * return a unique thread ID.</p>
96      */
getOrCreateThreadIdInternal(Context context, Set<String> recipients)97     private static long getOrCreateThreadIdInternal(Context context, Set<String> recipients) {
98         Uri.Builder uriBuilder = THREAD_ID_CONTENT_URI.buildUpon();
99 
100         for (String recipient : recipients) {
101             if (isEmailAddress(recipient)) {
102                 recipient = extractAddrSpec(recipient);
103             }
104 
105             uriBuilder.appendQueryParameter("recipient", recipient);
106         }
107 
108         Uri uri = uriBuilder.build();
109 
110         Cursor cursor = query(
111                 context.getContentResolver(), uri, ID_PROJECTION, null, null, null);
112         if (cursor != null) {
113             try {
114                 if (cursor.moveToFirst()) {
115                     return cursor.getLong(0);
116                 } else {
117                     Log.e(TAG, "getOrCreateThreadId returned no rows!");
118                 }
119             } finally {
120                 cursor.close();
121             }
122         }
123 
124         Log.e(TAG, "getOrCreateThreadId failed with uri " + uri.toString());
125         throw new IllegalArgumentException("Unable to find or allocate a thread ID.");
126     }
127 
128     /**
129      * Copied from {@link SqliteWrapper#query}
130      */
query(ContentResolver resolver, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)131     private static Cursor query(ContentResolver resolver, Uri uri, String[] projection,
132             String selection, String[] selectionArgs, String sortOrder) {
133         try {
134             return resolver.query(uri, projection, selection, selectionArgs, sortOrder);
135         } catch (Exception e) {
136             Log.e(TAG, "Catch an exception when query: ", e);
137             return null;
138         }
139     }
140 
141     /**
142      * Is the specified address an email address?
143      *
144      * @param address the input address to test
145      * @return true if address is an email address; false otherwise.
146      */
isEmailAddress(String address)147     private static boolean isEmailAddress(String address) {
148         if (TextUtils.isEmpty(address)) {
149             return false;
150         }
151 
152         String s = extractAddrSpec(address);
153         Matcher match = Patterns.EMAIL_ADDRESS.matcher(s);
154         return match.matches();
155     }
156 
157     /**
158      * Helper method to extract email address from address string.
159      */
extractAddrSpec(String address)160     private static String extractAddrSpec(String address) {
161         Matcher match = NAME_ADDR_EMAIL_PATTERN.matcher(address);
162 
163         if (match.matches()) {
164             return match.group(2);
165         }
166         return address;
167     }
168 }
169