blob: 545e1856d7a8089a0818bb6b8c56f5b42f488b97 [file] [log] [blame]
Wenyi Wang13e6b2e2015-12-02 16:48:44 -08001/*
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
Gary Mai69c182a2016-12-05 13:07:03 -080017package com.android.contacts.compat;
Wenyi Wang13e6b2e2015-12-02 16:48:44 -080018
19import android.content.ContentResolver;
20import android.content.Context;
21import android.database.Cursor;
22import android.net.Uri;
23import android.os.Build;
24import android.provider.BaseColumns;
25import android.provider.Telephony;
26import android.text.TextUtils;
27import android.util.Log;
28import android.util.Patterns;
29
30import java.util.HashSet;
31import java.util.Set;
32import java.util.regex.Matcher;
33import 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 */
41public class TelephonyThreadsCompat {
42 /**
43 * Not instantiable.
44 */
45 private TelephonyThreadsCompat() {}
46
47 private static final String TAG = "TelephonyThreadsCompat";
48
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 */
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 */
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 */
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 */
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 */
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}