ContactDirectoryManager.java revision 6255d756615cfa89fb3411d1840dbe08e1375ffe
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(); 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. This is only done once 159 * in the lifetime of a contacts DB. 160 */ 161 private void scanAllPackagesIfNeeded() { 162 ContactsDatabaseHelper dbHelper = 163 (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper(); 164 SQLiteDatabase db = dbHelper.getWritableDatabase(); 165 166 String scanComplete = dbHelper.getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0"); 167 if (!"0".equals(scanComplete)) { 168 return; 169 } 170 171 long start = SystemClock.currentThreadTimeMillis(); 172 int count = scanAllPackages(); 173 dbHelper.setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1"); 174 long end = SystemClock.currentThreadTimeMillis(); 175 Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms"); 176 177 // Announce the change to listeners of the contacts authority 178 mContactsProvider.notifyChange(false); 179 } 180 181 public void scheduleScanAllPackages() { 182 getHandler().sendEmptyMessage(MESSAGE_SCAN_ALL_PROVIDERS); 183 } 184 185 /* Visible for testing */ 186 int scanAllPackages() { 187 int count = 0; 188 PackageManager pm = mContext.getPackageManager(); 189 List<PackageInfo> packages = pm.getInstalledPackages( 190 PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); 191 for (PackageInfo packageInfo : packages) { 192 // Check all packages except the one containing ContactsProvider itself 193 if (!packageInfo.packageName.equals(mContext.getPackageName())) { 194 count += updateDirectoriesForPackage(packageInfo, true); 195 } 196 } 197 return count; 198 } 199 200 /** 201 * Scans the specified package for content directories. The package may have 202 * already been removed, so packageName does not necessarily correspond to 203 * an installed package. 204 */ 205 public void onPackageChanged(String packageName) { 206 PackageManager pm = mContext.getPackageManager(); 207 PackageInfo packageInfo = null; 208 209 try { 210 packageInfo = pm.getPackageInfo(packageName, 211 PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); 212 } catch (NameNotFoundException e) { 213 // The package got removed 214 packageInfo = new PackageInfo(); 215 packageInfo.packageName = packageName; 216 } 217 218 updateDirectoriesForPackage(packageInfo, false); 219 } 220 221 /** 222 * Scans the specified package for content directories and updates the {@link Directory} 223 * table accordingly. 224 */ 225 private int updateDirectoriesForPackage(PackageInfo packageInfo, boolean initialScan) { 226 ArrayList<DirectoryInfo> directories = Lists.newArrayList(); 227 228 ProviderInfo[] providers = packageInfo.providers; 229 if (providers != null) { 230 for (ProviderInfo provider : providers) { 231 Bundle metaData = provider.metaData; 232 if (metaData != null) { 233 Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA); 234 if (trueFalse != null && Boolean.TRUE.equals(trueFalse)) { 235 queryDirectoriesForAuthority(directories, provider); 236 } 237 } 238 } 239 } 240 241 if (directories.size() == 0 && initialScan) { 242 return 0; 243 } 244 245 SQLiteDatabase db = ((ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper()) 246 .getWritableDatabase(); 247 db.beginTransaction(); 248 try { 249 updateDirectories(db, directories); 250 // Clear out directories that are no longer present 251 StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?"); 252 if (!directories.isEmpty()) { 253 sb.append(" AND " + Directory._ID + " NOT IN("); 254 for (DirectoryInfo info: directories) { 255 sb.append(info.id).append(","); 256 } 257 sb.setLength(sb.length() - 1); // Remove the extra comma 258 sb.append(")"); 259 } 260 db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName }); 261 db.setTransactionSuccessful(); 262 } finally { 263 db.endTransaction(); 264 } 265 266 mContactsProvider.resetDirectoryCache(); 267 return directories.size(); 268 } 269 270 /** 271 * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory 272 * provider and appends all discovered directories to the directoryInfo list. 273 */ 274 protected void queryDirectoriesForAuthority( 275 ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) { 276 Uri uri = new Uri.Builder().scheme("content") 277 .authority(provider.authority).appendPath("directories").build(); 278 Cursor cursor = null; 279 try { 280 cursor = mContext.getContentResolver().query( 281 uri, DirectoryQuery.PROJECTION, null, null, null); 282 if (cursor == null) { 283 Log.i(TAG, providerDescription(provider) + " returned a NULL cursor."); 284 } else { 285 while (cursor.moveToNext()) { 286 DirectoryInfo info = new DirectoryInfo(); 287 info.packageName = provider.packageName; 288 info.authority = provider.authority; 289 info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); 290 info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); 291 info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); 292 if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) { 293 info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); 294 } 295 if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) { 296 int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); 297 switch (exportSupport) { 298 case Directory.EXPORT_SUPPORT_NONE: 299 case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: 300 case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: 301 info.exportSupport = exportSupport; 302 break; 303 default: 304 Log.e(TAG, providerDescription(provider) 305 + " - invalid export support flag: " + exportSupport); 306 } 307 } 308 if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) { 309 int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT); 310 switch (shortcutSupport) { 311 case Directory.SHORTCUT_SUPPORT_NONE: 312 case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY: 313 case Directory.SHORTCUT_SUPPORT_FULL: 314 info.shortcutSupport = shortcutSupport; 315 break; 316 default: 317 Log.e(TAG, providerDescription(provider) 318 + " - invalid shortcut support flag: " + shortcutSupport); 319 } 320 } 321 directoryInfo.add(info); 322 } 323 } 324 } catch (Throwable t) { 325 Log.e(TAG, providerDescription(provider) + " exception", t); 326 } finally { 327 if (cursor != null) { 328 cursor.close(); 329 } 330 } 331 } 332 333 /** 334 * Updates the directories tables in the database to match the info received 335 * from directory providers. 336 */ 337 private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) { 338 339 // Insert or replace existing directories. 340 // This happens so infrequently that we can use a less-then-optimal one-a-time approach 341 for (DirectoryInfo info : directoryInfo) { 342 ContentValues values = new ContentValues(); 343 values.put(Directory.PACKAGE_NAME, info.packageName); 344 values.put(Directory.DIRECTORY_AUTHORITY, info.authority); 345 values.put(Directory.ACCOUNT_NAME, info.accountName); 346 values.put(Directory.ACCOUNT_TYPE, info.accountType); 347 values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId); 348 values.put(Directory.DISPLAY_NAME, info.displayName); 349 values.put(Directory.EXPORT_SUPPORT, info.exportSupport); 350 values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport); 351 352 Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID }, 353 Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND " 354 + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?", 355 new String[] { 356 info.packageName, info.authority, info.accountName, info.accountType }, 357 null, null, null); 358 try { 359 long id; 360 if (cursor.moveToFirst()) { 361 id = cursor.getLong(0); 362 db.update(Tables.DIRECTORIES, values, Directory._ID + "=?", 363 new String[] { String.valueOf(id) }); 364 } else { 365 id = db.insert(Tables.DIRECTORIES, null, values); 366 } 367 info.id = id; 368 } finally { 369 cursor.close(); 370 } 371 } 372 } 373 374 protected String providerDescription(ProviderInfo provider) { 375 return "Directory provider " + provider.packageName + "(" + provider.authority + ")"; 376 } 377} 378