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