ContactDirectoryManager.java revision ac6f0b8a92fccb3611a7841d0f04e4ac24a58c91
1/* 2 * Copyright (C) 2010 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 17package com.android.providers.contacts; 18 19import com.android.providers.contacts.ContactsDatabaseHelper.DirectoryColumns; 20import com.android.providers.contacts.ContactsDatabaseHelper.Tables; 21import com.google.android.collect.Lists; 22import com.google.android.collect.Sets; 23import com.google.common.annotations.VisibleForTesting; 24 25import android.content.ContentValues; 26import android.content.Context; 27import android.content.pm.PackageInfo; 28import android.content.pm.PackageManager; 29import android.content.pm.PackageManager.NameNotFoundException; 30import android.content.pm.ProviderInfo; 31import android.content.res.Resources; 32import android.content.res.Resources.NotFoundException; 33import android.database.Cursor; 34import android.database.sqlite.SQLiteDatabase; 35import android.net.Uri; 36import android.os.Bundle; 37import android.os.SystemClock; 38import android.provider.ContactsContract; 39import android.provider.ContactsContract.Directory; 40import android.text.TextUtils; 41import android.util.Log; 42 43import java.util.ArrayList; 44import java.util.List; 45import java.util.Set; 46 47/** 48 * Manages the contents of the {@link Directory} table. 49 */ 50// TODO: Determine whether directories need to be aware of data sets under the account. 51public class ContactDirectoryManager { 52 53 private static final String TAG = "ContactDirectoryManager"; 54 private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE 55 56 public static final String PROPERTY_DIRECTORY_SCAN_COMPLETE = "directoryScanComplete"; 57 public static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory"; 58 59 public class DirectoryInfo { 60 long id; 61 String packageName; 62 String authority; 63 String accountName; 64 String accountType; 65 String displayName; 66 int typeResourceId; 67 int exportSupport = Directory.EXPORT_SUPPORT_NONE; 68 int shortcutSupport = Directory.SHORTCUT_SUPPORT_NONE; 69 int photoSupport = Directory.PHOTO_SUPPORT_NONE; 70 @Override 71 public String toString() { 72 return "DirectoryInfo:" 73 + "id=" + id 74 + " packageName=" + accountType 75 + " authority=" + authority 76 + " accountName=***" 77 + " accountType=" + accountType; 78 } 79 } 80 81 private final static class DirectoryQuery { 82 public static final String[] PROJECTION = { 83 Directory.ACCOUNT_NAME, 84 Directory.ACCOUNT_TYPE, 85 Directory.DISPLAY_NAME, 86 Directory.TYPE_RESOURCE_ID, 87 Directory.EXPORT_SUPPORT, 88 Directory.SHORTCUT_SUPPORT, 89 Directory.PHOTO_SUPPORT, 90 }; 91 92 public static final int ACCOUNT_NAME = 0; 93 public static final int ACCOUNT_TYPE = 1; 94 public static final int DISPLAY_NAME = 2; 95 public static final int TYPE_RESOURCE_ID = 3; 96 public static final int EXPORT_SUPPORT = 4; 97 public static final int SHORTCUT_SUPPORT = 5; 98 public static final int PHOTO_SUPPORT = 6; 99 } 100 101 private final ContactsProvider2 mContactsProvider; 102 private final Context mContext; 103 private final PackageManager mPackageManager; 104 105 public ContactDirectoryManager(ContactsProvider2 contactsProvider) { 106 mContactsProvider = contactsProvider; 107 mContext = contactsProvider.getContext(); 108 mPackageManager = mContext.getPackageManager(); 109 } 110 111 public ContactsDatabaseHelper getDbHelper() { 112 return (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper(); 113 } 114 115 /** 116 * Scans all packages owned by the specified calling UID looking for contact 117 * directory providers. 118 */ 119 public void scanPackagesByUid(int callingUid) { 120 final String[] callerPackages = mPackageManager.getPackagesForUid(callingUid); 121 if (callerPackages != null) { 122 for (int i = 0; i < callerPackages.length; i++) { 123 onPackageChanged(callerPackages[i]); 124 } 125 } 126 } 127 128 /** 129 * Scans through existing directories to see if the cached resource IDs still 130 * match their original resource names. If not - plays it safe by refreshing all directories. 131 * 132 * @return true if all resource IDs were found valid 133 */ 134 private boolean areTypeResourceIdsValid() { 135 SQLiteDatabase db = getDbHelper().getReadableDatabase(); 136 137 Cursor cursor = db.query(Tables.DIRECTORIES, 138 new String[] { Directory.TYPE_RESOURCE_ID, Directory.PACKAGE_NAME, 139 DirectoryColumns.TYPE_RESOURCE_NAME }, null, null, null, null, null); 140 try { 141 while (cursor.moveToNext()) { 142 int resourceId = cursor.getInt(0); 143 if (resourceId != 0) { 144 String packageName = cursor.getString(1); 145 String storedResourceName = cursor.getString(2); 146 String resourceName = getResourceNameById(packageName, resourceId); 147 if (!TextUtils.equals(storedResourceName, resourceName)) { 148 return false; 149 } 150 } 151 } 152 } finally { 153 cursor.close(); 154 } 155 156 return true; 157 } 158 159 /** 160 * Given a resource ID, returns the corresponding resource name or null if the package name / 161 * resource ID combination is invalid. 162 */ 163 private String getResourceNameById(String packageName, int resourceId) { 164 try { 165 Resources resources = mPackageManager.getResourcesForApplication(packageName); 166 return resources.getResourceName(resourceId); 167 } catch (NameNotFoundException e) { 168 return null; 169 } catch (NotFoundException e) { 170 return null; 171 } 172 } 173 174 /** 175 * Scans all packages for directory content providers. 176 */ 177 public void scanAllPackages(boolean rescan) { 178 if (rescan || !areTypeResourceIdsValid()) { 179 getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0"); 180 } 181 182 scanAllPackagesIfNeeded(); 183 } 184 185 private void scanAllPackagesIfNeeded() { 186 String scanComplete = getDbHelper().getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0"); 187 if (!"0".equals(scanComplete)) { 188 return; 189 } 190 191 long start = SystemClock.currentThreadTimeMillis(); 192 int count = scanAllPackages(); 193 getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1"); 194 long end = SystemClock.currentThreadTimeMillis(); 195 Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms"); 196 197 // Announce the change to listeners of the contacts authority 198 mContactsProvider.notifyChange(false); 199 } 200 201 @VisibleForTesting 202 static boolean isDirectoryProvider(ProviderInfo provider) { 203 Bundle metaData = provider.metaData; 204 if (metaData == null) return false; 205 206 Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA); 207 return trueFalse != null && Boolean.TRUE.equals(trueFalse); 208 } 209 210 /** 211 * @return List of packages that contain a directory provider. 212 */ 213 @VisibleForTesting 214 static Set<String> getDirectoryProviderPackages(PackageManager pm) { 215 final Set<String> ret = Sets.newHashSet(); 216 217 // Note to 3rd party developers: 218 // queryContentProviders() is a public API but this method doesn't officially support 219 // the GET_META_DATA flag. Don't use it in your app. 220 final List<ProviderInfo> providers = pm.queryContentProviders(null, 0, 221 PackageManager.GET_META_DATA); 222 if (providers == null) { 223 return ret; 224 } 225 for (ProviderInfo provider : providers) { 226 if (isDirectoryProvider(provider)) { 227 ret.add(provider.packageName); 228 } 229 } 230 if (DEBUG) { 231 Log.d(TAG, "Found " + ret.size() + " directory provider packages"); 232 } 233 234 return ret; 235 } 236 237 @VisibleForTesting 238 int scanAllPackages() { 239 SQLiteDatabase db = getDbHelper().getWritableDatabase(); 240 insertDefaultDirectory(db); 241 insertLocalInvisibleDirectory(db); 242 243 int count = 0; 244 245 // Prepare query strings for removing stale rows which don't correspond to existing 246 // directories. 247 StringBuilder deleteWhereBuilder = new StringBuilder(); 248 ArrayList<String> deleteWhereArgs = new ArrayList<String>(); 249 deleteWhereBuilder.append("NOT (" + Directory._ID + "=? OR " + Directory._ID + "=?"); 250 deleteWhereArgs.add(String.valueOf(Directory.DEFAULT)); 251 deleteWhereArgs.add(String.valueOf(Directory.LOCAL_INVISIBLE)); 252 final String wherePart = "(" + Directory.PACKAGE_NAME + "=? AND " 253 + Directory.DIRECTORY_AUTHORITY + "=? AND " 254 + Directory.ACCOUNT_NAME + "=? AND " 255 + Directory.ACCOUNT_TYPE + "=?)"; 256 257 for (String packageName : getDirectoryProviderPackages(mPackageManager)) { 258 if (DEBUG) Log.d(TAG, "package=" + packageName); 259 260 final PackageInfo packageInfo; 261 try { 262 packageInfo = mPackageManager.getPackageInfo(packageName, 263 PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); 264 if (packageInfo == null) continue; // Just in case... 265 } catch (NameNotFoundException nnfe) { 266 continue; // Application just removed? 267 } 268 269 List<DirectoryInfo> directories = updateDirectoriesForPackage(packageInfo, true); 270 if (directories != null && !directories.isEmpty()) { 271 count += directories.size(); 272 273 // We shouldn't delete rows for existing directories. 274 for (DirectoryInfo info : directories) { 275 if (DEBUG) Log.d(TAG, " directory=" + info); 276 deleteWhereBuilder.append(" OR "); 277 deleteWhereBuilder.append(wherePart); 278 deleteWhereArgs.add(info.packageName); 279 deleteWhereArgs.add(info.authority); 280 deleteWhereArgs.add(info.accountName); 281 deleteWhereArgs.add(info.accountType); 282 } 283 } 284 } 285 286 deleteWhereBuilder.append(")"); // Close "NOT (" 287 288 int deletedRows = db.delete(Tables.DIRECTORIES, deleteWhereBuilder.toString(), 289 deleteWhereArgs.toArray(new String[0])); 290 Log.i(TAG, "deleted " + deletedRows 291 + " stale rows which don't have any relevant directory"); 292 return count; 293 } 294 295 private void insertDefaultDirectory(SQLiteDatabase db) { 296 ContentValues values = new ContentValues(); 297 values.put(Directory._ID, Directory.DEFAULT); 298 values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName); 299 values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY); 300 values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory); 301 values.put(DirectoryColumns.TYPE_RESOURCE_NAME, 302 mContext.getResources().getResourceName(R.string.default_directory)); 303 values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE); 304 values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL); 305 values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL); 306 db.replace(Tables.DIRECTORIES, null, values); 307 } 308 309 private void insertLocalInvisibleDirectory(SQLiteDatabase db) { 310 ContentValues values = new ContentValues(); 311 values.put(Directory._ID, Directory.LOCAL_INVISIBLE); 312 values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName); 313 values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY); 314 values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory); 315 values.put(DirectoryColumns.TYPE_RESOURCE_NAME, 316 mContext.getResources().getResourceName(R.string.local_invisible_directory)); 317 values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE); 318 values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL); 319 values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL); 320 db.replace(Tables.DIRECTORIES, null, values); 321 } 322 323 /** 324 * Scans the specified package for content directories. The package may have 325 * already been removed, so packageName does not necessarily correspond to 326 * an installed package. 327 */ 328 public void onPackageChanged(String packageName) { 329 PackageInfo packageInfo = null; 330 331 try { 332 packageInfo = mPackageManager.getPackageInfo(packageName, 333 PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); 334 } catch (NameNotFoundException e) { 335 // The package got removed 336 packageInfo = new PackageInfo(); 337 packageInfo.packageName = packageName; 338 } 339 340 updateDirectoriesForPackage(packageInfo, false); 341 } 342 343 344 /** 345 * Scans the specified package for content directories and updates the {@link Directory} 346 * table accordingly. 347 */ 348 private List<DirectoryInfo> updateDirectoriesForPackage( 349 PackageInfo packageInfo, boolean initialScan) { 350 ArrayList<DirectoryInfo> directories = Lists.newArrayList(); 351 352 ProviderInfo[] providers = packageInfo.providers; 353 if (providers != null) { 354 for (ProviderInfo provider : providers) { 355 if (isDirectoryProvider(provider)) { 356 queryDirectoriesForAuthority(directories, provider); 357 } 358 } 359 } 360 361 if (directories.size() == 0 && initialScan) { 362 return null; 363 } 364 365 SQLiteDatabase db = getDbHelper().getWritableDatabase(); 366 db.beginTransaction(); 367 try { 368 updateDirectories(db, directories); 369 // Clear out directories that are no longer present 370 StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?"); 371 if (!directories.isEmpty()) { 372 sb.append(" AND " + Directory._ID + " NOT IN("); 373 for (DirectoryInfo info: directories) { 374 sb.append(info.id).append(","); 375 } 376 sb.setLength(sb.length() - 1); // Remove the extra comma 377 sb.append(")"); 378 } 379 db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName }); 380 db.setTransactionSuccessful(); 381 } finally { 382 db.endTransaction(); 383 } 384 385 mContactsProvider.resetDirectoryCache(); 386 return directories; 387 } 388 389 /** 390 * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory 391 * provider and appends all discovered directories to the directoryInfo list. 392 */ 393 protected void queryDirectoriesForAuthority( 394 ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) { 395 Uri uri = new Uri.Builder().scheme("content") 396 .authority(provider.authority).appendPath("directories").build(); 397 Cursor cursor = null; 398 try { 399 cursor = mContext.getContentResolver().query( 400 uri, DirectoryQuery.PROJECTION, null, null, null); 401 if (cursor == null) { 402 Log.i(TAG, providerDescription(provider) + " returned a NULL cursor."); 403 } else { 404 while (cursor.moveToNext()) { 405 DirectoryInfo info = new DirectoryInfo(); 406 info.packageName = provider.packageName; 407 info.authority = provider.authority; 408 info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 409 info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 410 info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 411 if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) { 412 info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 413 } 414 if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) { 415 int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 416 switch (exportSupport) { 417 case Directory.EXPORT_SUPPORT_NONE: 418 case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: 419 case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: 420 info.exportSupport = exportSupport; 421 break; 422 default: 423 Log.e(TAG, providerDescription(provider) 424 + " - invalid export support flag: " + exportSupport); 425 } 426 } 427 if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) { 428 int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT); 429 switch (shortcutSupport) { 430 case Directory.SHORTCUT_SUPPORT_NONE: 431 case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY: 432 case Directory.SHORTCUT_SUPPORT_FULL: 433 info.shortcutSupport = shortcutSupport; 434 break; 435 default: 436 Log.e(TAG, providerDescription(provider) 437 + " - invalid shortcut support flag: " + shortcutSupport); 438 } 439 } 440 if (!cursor.isNull(DirectoryQuery.PHOTO_SUPPORT)) { 441 int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT); 442 switch (photoSupport) { 443 case Directory.PHOTO_SUPPORT_NONE: 444 case Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY: 445 case Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY: 446 case Directory.PHOTO_SUPPORT_FULL: 447 info.photoSupport = photoSupport; 448 break; 449 default: 450 Log.e(TAG, providerDescription(provider) 451 + " - invalid photo support flag: " + photoSupport); 452 } 453 } 454 directoryInfo.add(info); 455 } 456 } 457 } catch (Throwable t) { 458 Log.e(TAG, providerDescription(provider) + " exception", t); 459 } finally { 460 if (cursor != null) { 461 cursor.close(); 462 } 463 } 464 } 465 466 /** 467 * Updates the directories tables in the database to match the info received 468 * from directory providers. 469 */ 470 private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) { 471 // Insert or replace existing directories. 472 // This happens so infrequently that we can use a less-then-optimal one-a-time approach 473 for (DirectoryInfo info : directoryInfo) { 474 ContentValues values = new ContentValues(); 475 values.put(Directory.PACKAGE_NAME, info.packageName); 476 values.put(Directory.DIRECTORY_AUTHORITY, info.authority); 477 values.put(Directory.ACCOUNT_NAME, info.accountName); 478 values.put(Directory.ACCOUNT_TYPE, info.accountType); 479 values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId); 480 values.put(Directory.DISPLAY_NAME, info.displayName); 481 values.put(Directory.EXPORT_SUPPORT, info.exportSupport); 482 values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport); 483 values.put(Directory.PHOTO_SUPPORT, info.photoSupport); 484 485 if (info.typeResourceId != 0) { 486 String resourceName = getResourceNameById(info.packageName, info.typeResourceId); 487 values.put(DirectoryColumns.TYPE_RESOURCE_NAME, resourceName); 488 } 489 490 Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID }, 491 Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND " 492 + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?", 493 new String[] { 494 info.packageName, info.authority, info.accountName, info.accountType }, 495 null, null, null); 496 try { 497 long id; 498 if (cursor.moveToFirst()) { 499 id = cursor.getLong(0); 500 db.update(Tables.DIRECTORIES, values, Directory._ID + "=?", 501 new String[] { String.valueOf(id) }); 502 } else { 503 id = db.insert(Tables.DIRECTORIES, null, values); 504 } 505 info.id = id; 506 } finally { 507 cursor.close(); 508 } 509 } 510 } 511 512 protected String providerDescription(ProviderInfo provider) { 513 return "Directory provider " + provider.packageName + "(" + provider.authority + ")"; 514 } 515} 516