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 46 private static final String TAG = "SmsMmsContacts"; 47 48 private HashMap<Long, String> mPhoneNumbers = null; 49 50 @VisibleForTesting 51 final HashMap<String, MapContact> mNames = new HashMap<String, MapContact>(10); 52 53 private static final Uri ADDRESS_URI = 54 MmsSms.CONTENT_URI.buildUpon().appendPath("canonical-addresses").build(); 55 56 @VisibleForTesting 57 static final String[] ADDRESS_PROJECTION = { 58 CanonicalAddressesColumns._ID, CanonicalAddressesColumns.ADDRESS 59 }; 60 61 private static final int COL_ADDR_ID = 62 Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns._ID); 63 private static final int COL_ADDR_ADDR = 64 Arrays.asList(ADDRESS_PROJECTION).indexOf(CanonicalAddressesColumns.ADDRESS); 65 66 @VisibleForTesting 67 static final String[] CONTACT_PROJECTION = {Contacts._ID, Contacts.DISPLAY_NAME}; 68 69 private static final String CONTACT_SEL_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 70 private static final int COL_CONTACT_ID = 71 Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts._ID); 72 private static final int COL_CONTACT_NAME = 73 Arrays.asList(CONTACT_PROJECTION).indexOf(Contacts.DISPLAY_NAME); 74 75 /** 76 * Get a contacts phone number based on the canonical addresses id of the contact. (The ID 77 * listed in the Threads table.) 78 * 79 * @param resolver the ContantResolver to be used. 80 * @param id the id of the contact, as listed in the Threads table 81 * @return the phone number of the contact - or null if id does not exist. 82 */ getPhoneNumber(ContentResolver resolver, long id)83 public String getPhoneNumber(ContentResolver resolver, long id) { 84 String number; 85 if (mPhoneNumbers != null && (number = mPhoneNumbers.get(id)) != null) { 86 return number; 87 } 88 fillPhoneCache(resolver); 89 return mPhoneNumbers.get(id); 90 } 91 getPhoneNumberUncached(ContentResolver resolver, long id)92 public static String getPhoneNumberUncached(ContentResolver resolver, long id) { 93 String where = CanonicalAddressesColumns._ID + " = " + id; 94 Cursor c = 95 BluetoothMethodProxy.getInstance() 96 .contentResolverQuery( 97 resolver, ADDRESS_URI, ADDRESS_PROJECTION, where, null, null); 98 try { 99 if (c != null) { 100 if (c.moveToPosition(0)) { 101 return c.getString(COL_ADDR_ADDR); 102 } 103 } 104 Log.e(TAG, "query failed"); 105 ContentProfileErrorReportUtils.report( 106 BluetoothProfile.MAP, 107 BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS, 108 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 109 0); 110 } finally { 111 if (c != null) { 112 c.close(); 113 } 114 } 115 return null; 116 } 117 118 /** Clears the local cache. Call after a listing is complete, to avoid using invalid data. */ clearCache()119 public void clearCache() { 120 if (mPhoneNumbers != null) { 121 mPhoneNumbers.clear(); 122 } 123 if (mNames != null) { 124 mNames.clear(); 125 } 126 } 127 128 /** 129 * Refreshes the cache, by clearing all cached values and fill the cache with the result of a 130 * new query. 131 * 132 * @param resolver the ContantResolver to be used. 133 */ 134 @VisibleForTesting fillPhoneCache(ContentResolver resolver)135 void fillPhoneCache(ContentResolver resolver) { 136 Cursor c = 137 BluetoothMethodProxy.getInstance() 138 .contentResolverQuery( 139 resolver, ADDRESS_URI, ADDRESS_PROJECTION, null, null, null); 140 if (mPhoneNumbers == null) { 141 int size = 0; 142 if (c != null) { 143 size = c.getCount(); 144 } 145 mPhoneNumbers = new HashMap<Long, String>(size); 146 } else { 147 mPhoneNumbers.clear(); 148 } 149 try { 150 if (c != null) { 151 long id; 152 String addr; 153 c.moveToPosition(-1); 154 while (c.moveToNext()) { 155 id = c.getLong(COL_ADDR_ID); 156 addr = c.getString(COL_ADDR_ADDR); 157 mPhoneNumbers.put(id, addr); 158 } 159 } else { 160 Log.e(TAG, "query failed"); 161 ContentProfileErrorReportUtils.report( 162 BluetoothProfile.MAP, 163 BluetoothProtoEnums.BLUETOOTH_SMS_MMS_CONTACTS, 164 BluetoothStatsLog.BLUETOOTH_CONTENT_PROFILE_ERROR_REPORTED__TYPE__LOG_ERROR, 165 1); 166 } 167 } finally { 168 if (c != null) { 169 c.close(); 170 } 171 } 172 } 173 getContactNameFromPhone(String phone, ContentResolver resolver)174 public MapContact getContactNameFromPhone(String phone, ContentResolver resolver) { 175 return getContactNameFromPhone(phone, resolver, null); 176 } 177 178 /** 179 * Lookup a contacts name in the Android Contacts database. 180 * 181 * @param phone the phone number of the contact 182 * @param resolver the ContentResolver to use. 183 * @return the name of the contact or null, if no contact was found. 184 */ getContactNameFromPhone( String phone, ContentResolver resolver, String contactNameFilter)185 public MapContact getContactNameFromPhone( 186 String phone, ContentResolver resolver, String contactNameFilter) { 187 MapContact contact = mNames.get(phone); 188 189 if (contact != null) { 190 if (contact.getId() < 0) { 191 return null; 192 } 193 if (contactNameFilter == null) { 194 return contact; 195 } 196 // Validate filter 197 String searchString = contactNameFilter.replace("*", ".*"); 198 searchString = ".*" + searchString + ".*"; 199 Pattern p = Pattern.compile(Pattern.quote(searchString), Pattern.CASE_INSENSITIVE); 200 if (p.matcher(contact.getName()).find()) { 201 return contact; 202 } 203 return null; 204 } 205 206 // TODO: Should we change to extract both formatted name, and display name? 207 208 Uri uri = 209 Uri.withAppendedPath(PhoneLookup.ENTERPRISE_CONTENT_FILTER_URI, Uri.encode(phone)); 210 String selection = CONTACT_SEL_VISIBLE; 211 String[] selectionArgs = null; 212 if (contactNameFilter != null) { 213 selection += "AND " + ContactsContract.Contacts.DISPLAY_NAME + " like ?"; 214 selectionArgs = new String[] {"%" + contactNameFilter.replace("*", "%") + "%"}; 215 } 216 217 Cursor c = 218 BluetoothMethodProxy.getInstance() 219 .contentResolverQuery( 220 resolver, uri, CONTACT_PROJECTION, selection, selectionArgs, null); 221 try { 222 if (c != null && c.getCount() >= 1) { 223 c.moveToFirst(); 224 long id = c.getLong(COL_CONTACT_ID); 225 String name = c.getString(COL_CONTACT_NAME); 226 contact = MapContact.create(id, name); 227 mNames.put(phone, contact); 228 } else { 229 contact = MapContact.create(-1, null); 230 mNames.put(phone, contact); 231 contact = null; 232 } 233 } finally { 234 if (c != null) { 235 c.close(); 236 } 237 } 238 return contact; 239 } 240 } 241