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