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