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