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