ContactDirectoryManager.java revision cf832869bcf91b8037d8b7f510a3a213b30764a3
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.Tables; 20import com.google.android.collect.Lists; 21 22import android.content.ContentValues; 23import android.content.Context; 24import android.content.pm.PackageInfo; 25import android.content.pm.PackageManager; 26import android.content.pm.PackageManager.NameNotFoundException; 27import android.content.pm.ProviderInfo; 28import android.database.Cursor; 29import android.database.sqlite.SQLiteDatabase; 30import android.net.Uri; 31import android.os.Binder; 32import android.os.Bundle; 33import android.os.Debug; 34import android.os.Handler; 35import android.os.HandlerThread; 36import android.os.Message; 37import android.os.Process; 38import android.os.SystemClock; 39import android.provider.ContactsContract.Directory; 40import android.util.Log; 41 42import java.util.ArrayList; 43import java.util.List; 44 45/** 46 * Manages the contents of the {@link Directory} table. 47 */ 48public class ContactDirectoryManager extends HandlerThread { 49 50 private static final String TAG = "ContactDirectoryManager"; 51 52 private static final int MESSAGE_SCAN_ALL_PROVIDERS = 0; 53 private static final int MESSAGE_SCAN_PACKAGES_BY_UID = 1; 54 55 private static final String PROPERTY_DIRECTORY_SCAN_COMPLETE = "directoryScanComplete"; 56 private static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory"; 57 58 public 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 } 69 70 private final static class DirectoryQuery { 71 public static final String[] PROJECTION = { 72 Directory.ACCOUNT_NAME, 73 Directory.ACCOUNT_TYPE, 74 Directory.DISPLAY_NAME, 75 Directory.TYPE_RESOURCE_ID, 76 Directory.EXPORT_SUPPORT, 77 Directory.SHORTCUT_SUPPORT, 78 }; 79 80 public static final int ACCOUNT_NAME = 0; 81 public static final int ACCOUNT_TYPE = 1; 82 public static final int DISPLAY_NAME = 2; 83 public static final int TYPE_RESOURCE_ID = 3; 84 public static final int EXPORT_SUPPORT = 4; 85 public static final int SHORTCUT_SUPPORT = 5; 86 } 87 88 private final ContactsProvider2 mContactsProvider; 89 private Context mContext; 90 private Handler mHandler; 91 92 public ContactDirectoryManager(ContactsProvider2 contactsProvider) { 93 super("DirectoryManager", Process.THREAD_PRIORITY_BACKGROUND); 94 this.mContactsProvider = contactsProvider; 95 this.mContext = contactsProvider.getContext(); 96 } 97 98 /** 99 * Launches an asynchronous scan of all packages. 100 */ 101 @Override 102 public void start() { 103 super.start(); 104 scheduleScanAllPackages(false); 105 } 106 107 /** 108 * Launches an asynchronous scan of all packages owned by the current calling UID. 109 */ 110 public void scheduleDirectoryUpdateForCaller() { 111 final int callingUid = Binder.getCallingUid(); 112 if (isAlive()) { 113 Handler handler = getHandler(); 114 handler.sendMessage(handler.obtainMessage(MESSAGE_SCAN_PACKAGES_BY_UID, callingUid, 0)); 115 } else { 116 scanPackagesByUid(callingUid); 117 } 118 } 119 120 protected Handler getHandler() { 121 if (mHandler == null) { 122 mHandler = new Handler(getLooper()) { 123 @Override 124 public void handleMessage(Message msg) { 125 ContactDirectoryManager.this.handleMessage(msg); 126 } 127 }; 128 } 129 return mHandler; 130 } 131 132 protected void handleMessage(Message msg) { 133 switch(msg.what) { 134 case MESSAGE_SCAN_ALL_PROVIDERS: 135 scanAllPackagesIfNeeded(); 136 break; 137 case MESSAGE_SCAN_PACKAGES_BY_UID: 138 scanPackagesByUid(msg.arg1); 139 break; 140 } 141 } 142 143 /** 144 * Scans all packages owned by the specified calling UID looking for contact 145 * directory providers. 146 */ 147 public void scanPackagesByUid(int callingUid) { 148 final PackageManager pm = mContext.getPackageManager(); 149 final String[] callerPackages = pm.getPackagesForUid(callingUid); 150 if (callerPackages != null) { 151 for (int i = 0; i < callerPackages.length; i++) { 152 onPackageChanged(callerPackages[i]); 153 } 154 } 155 } 156 157 /** 158 * Scans all packages for directory content providers. 159 */ 160 private void scanAllPackagesIfNeeded() { 161 ContactsDatabaseHelper dbHelper = 162 (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper(); 163 164 String scanComplete = dbHelper.getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0"); 165 if (!"0".equals(scanComplete)) { 166 return; 167 } 168 169 long start = SystemClock.currentThreadTimeMillis(); 170 int count = scanAllPackages(); 171 dbHelper.setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1"); 172 long end = SystemClock.currentThreadTimeMillis(); 173 Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms"); 174 175 // Announce the change to listeners of the contacts authority 176 mContactsProvider.notifyChange(false); 177 } 178 179 public void scheduleScanAllPackages(boolean rescan) { 180 if (rescan) { 181 ContactsDatabaseHelper dbHelper = 182 (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper(); 183 dbHelper.setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0"); 184 } 185 if (isAlive()) { 186 getHandler().sendEmptyMessage(MESSAGE_SCAN_ALL_PROVIDERS); 187 } else { 188 scanAllPackagesIfNeeded(); 189 } 190 } 191 192 /* Visible for testing */ 193 int scanAllPackages() { 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 for (PackageInfo packageInfo : packages) { 200 // Check all packages except the one containing ContactsProvider itself 201 if (!packageInfo.packageName.equals(mContext.getPackageName())) { 202 count += updateDirectoriesForPackage(packageInfo, true); 203 } 204 } 205 } 206 return count; 207 } 208 209 /** 210 * Scans the specified package for content directories. The package may have 211 * already been removed, so packageName does not necessarily correspond to 212 * an installed package. 213 */ 214 public void onPackageChanged(String packageName) { 215 PackageManager pm = mContext.getPackageManager(); 216 PackageInfo packageInfo = null; 217 218 try { 219 packageInfo = pm.getPackageInfo(packageName, 220 PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); 221 } catch (NameNotFoundException e) { 222 // The package got removed 223 packageInfo = new PackageInfo(); 224 packageInfo.packageName = packageName; 225 } 226 227 updateDirectoriesForPackage(packageInfo, false); 228 } 229 230 /** 231 * Scans the specified package for content directories and updates the {@link Directory} 232 * table accordingly. 233 */ 234 private int updateDirectoriesForPackage(PackageInfo packageInfo, boolean initialScan) { 235 ArrayList<DirectoryInfo> directories = Lists.newArrayList(); 236 237 ProviderInfo[] providers = packageInfo.providers; 238 if (providers != null) { 239 for (ProviderInfo provider : providers) { 240 Bundle metaData = provider.metaData; 241 if (metaData != null) { 242 Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA); 243 if (trueFalse != null && Boolean.TRUE.equals(trueFalse)) { 244 queryDirectoriesForAuthority(directories, provider); 245 } 246 } 247 } 248 } 249 250 if (directories.size() == 0 && initialScan) { 251 return 0; 252 } 253 254 SQLiteDatabase db = ((ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper()) 255 .getWritableDatabase(); 256 db.beginTransaction(); 257 try { 258 updateDirectories(db, directories); 259 // Clear out directories that are no longer present 260 StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?"); 261 if (!directories.isEmpty()) { 262 sb.append(" AND " + Directory._ID + " NOT IN("); 263 for (DirectoryInfo info: directories) { 264 sb.append(info.id).append(","); 265 } 266 sb.setLength(sb.length() - 1); // Remove the extra comma 267 sb.append(")"); 268 } 269 db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName }); 270 db.setTransactionSuccessful(); 271 } finally { 272 db.endTransaction(); 273 } 274 275 mContactsProvider.resetDirectoryCache(); 276 return directories.size(); 277 } 278 279 /** 280 * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory 281 * provider and appends all discovered directories to the directoryInfo list. 282 */ 283 protected void queryDirectoriesForAuthority( 284 ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) { 285 Uri uri = new Uri.Builder().scheme("content") 286 .authority(provider.authority).appendPath("directories").build(); 287 Cursor cursor = null; 288 try { 289 cursor = mContext.getContentResolver().query( 290 uri, DirectoryQuery.PROJECTION, null, null, null); 291 if (cursor == null) { 292 Log.i(TAG, providerDescription(provider) + " returned a NULL cursor."); 293 } else { 294 while (cursor.moveToNext()) { 295 DirectoryInfo info = new DirectoryInfo(); 296 info.packageName = provider.packageName; 297 info.authority = provider.authority; 298 info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 299 info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 300 info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 301 if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) { 302 info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 303 } 304 if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) { 305 int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 306 switch (exportSupport) { 307 case Directory.EXPORT_SUPPORT_NONE: 308 case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: 309 case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: 310 info.exportSupport = exportSupport; 311 break; 312 default: 313 Log.e(TAG, providerDescription(provider) 314 + " - invalid export support flag: " + exportSupport); 315 } 316 } 317 if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) { 318 int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT); 319 switch (shortcutSupport) { 320 case Directory.SHORTCUT_SUPPORT_NONE: 321 case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY: 322 case Directory.SHORTCUT_SUPPORT_FULL: 323 info.shortcutSupport = shortcutSupport; 324 break; 325 default: 326 Log.e(TAG, providerDescription(provider) 327 + " - invalid shortcut support flag: " + shortcutSupport); 328 } 329 } 330 directoryInfo.add(info); 331 } 332 } 333 } catch (Throwable t) { 334 Log.e(TAG, providerDescription(provider) + " exception", t); 335 } finally { 336 if (cursor != null) { 337 cursor.close(); 338 } 339 } 340 } 341 342 /** 343 * Updates the directories tables in the database to match the info received 344 * from directory providers. 345 */ 346 private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) { 347 348 // Insert or replace existing directories. 349 // This happens so infrequently that we can use a less-then-optimal one-a-time approach 350 for (DirectoryInfo info : directoryInfo) { 351 ContentValues values = new ContentValues(); 352 values.put(Directory.PACKAGE_NAME, info.packageName); 353 values.put(Directory.DIRECTORY_AUTHORITY, info.authority); 354 values.put(Directory.ACCOUNT_NAME, info.accountName); 355 values.put(Directory.ACCOUNT_TYPE, info.accountType); 356 values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId); 357 values.put(Directory.DISPLAY_NAME, info.displayName); 358 values.put(Directory.EXPORT_SUPPORT, info.exportSupport); 359 values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport); 360 361 Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID }, 362 Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND " 363 + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?", 364 new String[] { 365 info.packageName, info.authority, info.accountName, info.accountType }, 366 null, null, null); 367 try { 368 long id; 369 if (cursor.moveToFirst()) { 370 id = cursor.getLong(0); 371 db.update(Tables.DIRECTORIES, values, Directory._ID + "=?", 372 new String[] { String.valueOf(id) }); 373 } else { 374 id = db.insert(Tables.DIRECTORIES, null, values); 375 } 376 info.id = id; 377 } finally { 378 cursor.close(); 379 } 380 } 381 } 382 383 protected String providerDescription(ProviderInfo provider) { 384 return "Directory provider " + provider.packageName + "(" + provider.authority + ")"; 385 } 386} 387