1 /* 2 * Copyright (C) 2015 Samsung System LSI 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package com.android.bluetooth.map; 17 18 import android.bluetooth.BluetoothProfile; 19 import android.bluetooth.BluetoothProtoEnums; 20 import android.content.ContentResolver; 21 import android.database.Cursor; 22 import android.net.Uri; 23 import android.provider.ContactsContract; 24 import android.provider.ContactsContract.Contacts; 25 import android.provider.ContactsContract.PhoneLookup; 26 import android.provider.Telephony.CanonicalAddressesColumns; 27 import android.provider.Telephony.MmsSms; 28 import android.util.Log; 29 30 import com.android.bluetooth.BluetoothMethodProxy; 31 import com.android.bluetooth.BluetoothStatsLog; 32 import com.android.bluetooth.content_profiles.ContentProfileErrorReportUtils; 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import java.util.Arrays; 36 import java.util.HashMap; 37 import java.util.regex.Pattern; 38 39 /** 40 * Use these functions when extracting data for listings. It caches frequently used data to speed up 41 * building large listings - e.g. before applying filtering. 42 */ 43 // Next tag value for ContentProfileErrorReportUtils.report(): 2 44 public class SmsMmsContacts { 45 private static final String TAG = SmsMmsContacts.class.getSimpleName(); 46 47 private HashMap<Long, String> mPhoneNumbers = null; 48 49 @VisibleForTesting 50 final HashMap<String, MapContact> mNames = new HashMap<String, MapContact>(10); 51 52 private static final Uri ADDRESS_URI = 53 MmsSms.CONTENT_URI.buildUpon().appendPath("canonical-addresses").build(); 54 55 @VisibleForTesting 56 static final String[] ADDRESS_PROJECTION = { 57 CanonicalAddressesColumns._ID, CanonicalAddressesColumns.ADDRESS 58 }; 59 60 private static final int COL_ADDR_ID = 61 Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns._ID); 62 private static final int COL_ADDR_ADDR = 63 Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns.ADDRESS); 64 65 @VisibleForTesting 66 static final String[] CONTACT_PROJECTION = {Contacts._ID, Contacts.DISPLAY_NAME}; 67 68 private static final String CONTACT_SEL_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 69 private static final int COL_CONTACT_ID = 70 Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts._ID); 71 private static final int COL_CONTACT_NAME = 72 Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts.DISPLAY_NAME); 73 74 /** 75 * Get a contacts phone number based on the canonical addresses id of the contact. (The ID 76 * listed in the Threads table.) 77 * 78 * @param resolver the ContentResolver to be used. 79 * @param id the id of the contact, as listed in the Threads table 80 * @return the phone number of the contact - or null if id does not exist. 81 */ getPhoneNumber(ContentResolver resolver, long id)82 public String getPhoneNumber(ContentResolver resolver, long id) { 83 String number; 84 if (mPhoneNumbers != null && (number = mPhoneNumbers.get(id)) != null) { 85 return number; 86 } 87 fillPhoneCache(resolver); 88 return mPhoneNumbers.get(id); 89 } 90 getPhoneNumberUncached(ContentResolver resolver, long id)91 public static String getPhoneNumberUncached(ContentResolver resolver, long id) { 92 String where = CanonicalAddressesColumns._ID + " = " + id; 93 Cursor c = 94 BluetoothMethodProxy.getInstance() 95 .contentResolverQuery( 96 resolver, ADDRESS_URI, ADDRESS_PROJECTION, where, null, null); 97 try { 98 if (c != null) { 99 if (c.moveToPosition(0)) { 100 return c.getString(COL_ADDR_ADDR); 101 } 102 } 103 Log.e(TAG, "query failed"); 104 ContentProfileErrorReportUtils.report( 105 BluetoothProfile.MAP, 106 BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS, 107 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 108 0); 109 } finally { 110 if (c != null) { 111 c.close(); 112 } 113 } 114 return null; 115 } 116 117 /** Clears the local cache. Call after a listing is complete, to avoid using invalid data. */ clearCache()118 public void clearCache() { 119 if (mPhoneNumbers != null) { 120 mPhoneNumbers.clear(); 121 } 122 if (mNames != null) { 123 mNames.clear(); 124 } 125 } 126 127 /** 128 * Refreshes the cache, by clearing all cached values and fill the cache with the result of a 129 * new query. 130 * 131 * @param resolver the ContentResolver to be used. 132 */ 133 @VisibleForTesting fillPhoneCache(ContentResolver resolver)134 void fillPhoneCache(ContentResolver resolver) { 135 Cursor c = 136 BluetoothMethodProxy.getInstance() 137 .contentResolverQuery( 138 resolver, ADDRESS_URI, ADDRESS_PROJECTION, null, null, null); 139 if (mPhoneNumbers == null) { 140 int size = 0; 141 if (c != null) { 142 size = c.getCount(); 143 } 144 mPhoneNumbers = new HashMap<Long, String>(size); 145 } else { 146 mPhoneNumbers.clear(); 147 } 148 try { 149 if (c != null) { 150 long id; 151 String addr; 152 c.moveToPosition(-1); 153 while (c.moveToNext()) { 154 id = c.getLong(COL_ADDR_ID); 155 addr = c.getString(COL_ADDR_ADDR); 156 mPhoneNumbers.put(id, addr); 157 } 158 } else { 159 Log.e(TAG, "query failed"); 160 ContentProfileErrorReportUtils.report( 161 BluetoothProfile.MAP, 162 BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS, 163 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 164 1); 165 } 166 } finally { 167 if (c != null) { 168 c.close(); 169 } 170 } 171 } 172 getContactNameFromPhone(String phone, ContentResolver resolver)173 public MapContact getContactNameFromPhone(String phone, ContentResolver resolver) { 174 return getContactNameFromPhone(phone, resolver, null); 175 } 176 177 /** 178 * Lookup a contacts name in the Android Contacts database. 179 * 180 * @param phone the phone number of the contact 181 * @param resolver the ContentResolver to use. 182 * @return the name of the contact or null, if no contact was found. 183 */ getContactNameFromPhone( String phone, ContentResolver resolver, String contactNameFilter)184 public MapContact getContactNameFromPhone( 185 String phone, ContentResolver resolver, String contactNameFilter) { 186 MapContact contact = mNames.get(phone); 187 188 if (contact != null) { 189 if (contact.id() < 0) { 190 return null; 191 } 192 if (contactNameFilter == null) { 193 return contact; 194 } 195 // Validate filter 196 String searchString = contactNameFilter.replace("*", ".*"); 197 searchString = ".*" + searchString + ".*"; 198 Pattern p = Pattern.compile(Pattern.quote(searchString), Pattern.CASE_INSENSITIVE); 199 if (p.matcher(contact.name()).find()) { 200 return contact; 201 } 202 return null; 203 } 204 205 // TODO: Should we change to extract both formatted name, and display name? 206 207 Uri uri = 208 Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(phone)); 209 String selection = CONTACT_SEL_VISIBLE; 210 String[] selectionArgs = null; 211 if (contactNameFilter != null) { 212 selection = selection + "AND " + ContactsContract.Contacts.DISPLAY_NAME + " like ?"; 213 selectionArgs = new String[] {"%" + contactNameFilter.replace("*", "%") + "%"}; 214 } 215 216 Cursor c = 217 BluetoothMethodProxy.getInstance() 218 .contentResolverQuery( 219 resolver, uri, CONTACT_PROJECTION, selection, selectionArgs, null); 220 try { 221 if (c != null && c.getCount() >= 1) { 222 c.moveToFirst(); 223 long id = c.getLong(COL_CONTACT_ID); 224 String name = c.getString(COL_CONTACT_NAME); 225 contact = new MapContact(id, name); 226 mNames.put(phone, contact); 227 } else { 228 contact = new MapContact(-1, null); 229 mNames.put(phone, contact); 230 contact = null; 231 } 232 } finally { 233 if (c != null) { 234 c.close(); 235 } 236 } 237 return contact; 238 } 239 } 240