/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.providers.contacts; import android.content.ContentValues; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ProviderInfo; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.provider.ContactsContract; import android.provider.ContactsContract.Directory; import android.text.TextUtils; import android.util.Log; import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties; import com.android.providers.contacts.ContactsDatabaseHelper.DirectoryColumns; import com.android.providers.contacts.ContactsDatabaseHelper.Tables; import com.google.android.collect.Lists; import com.google.android.collect.Sets; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Manages the contents of the {@link Directory} table. */ public class ContactDirectoryManager { private static final String TAG = "ContactDirectoryManager"; private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE public static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory"; public static class DirectoryInfo { long id; String packageName; String authority; String accountName; String accountType; String displayName; int typeResourceId; int exportSupport = Directory.EXPORT_SUPPORT_NONE; int shortcutSupport = Directory.SHORTCUT_SUPPORT_NONE; int photoSupport = Directory.PHOTO_SUPPORT_NONE; @Override public String toString() { return "DirectoryInfo:" + "id=" + id + " packageName=" + accountType + " authority=" + authority + " accountName=***" + " accountType=" + accountType; } } private final static class DirectoryQuery { public static final String[] PROJECTION = { Directory.ACCOUNT_NAME, Directory.ACCOUNT_TYPE, Directory.DISPLAY_NAME, Directory.TYPE_RESOURCE_ID, Directory.EXPORT_SUPPORT, Directory.SHORTCUT_SUPPORT, Directory.PHOTO_SUPPORT, }; public static final int ACCOUNT_NAME = 0; public static final int ACCOUNT_TYPE = 1; public static final int DISPLAY_NAME = 2; public static final int TYPE_RESOURCE_ID = 3; public static final int EXPORT_SUPPORT = 4; public static final int SHORTCUT_SUPPORT = 5; public static final int PHOTO_SUPPORT = 6; } private final ContactsProvider2 mContactsProvider; private final Context mContext; private final PackageManager mPackageManager; public ContactDirectoryManager(ContactsProvider2 contactsProvider) { mContactsProvider = contactsProvider; mContext = contactsProvider.getContext(); mPackageManager = mContext.getPackageManager(); } public ContactsDatabaseHelper getDbHelper() { return (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper(); } /** * Scans all packages owned by the specified calling UID looking for contact * directory providers. */ public void scanPackagesByUid(int callingUid) { final String[] callerPackages = mPackageManager.getPackagesForUid(callingUid); if (callerPackages != null) { for (int i = 0; i < callerPackages.length; i++) { onPackageChanged(callerPackages[i]); } } } /** * Scans through existing directories to see if the cached resource IDs still * match their original resource names. If not - plays it safe by refreshing all directories. * * @return true if all resource IDs were found valid */ private boolean areTypeResourceIdsValid() { SQLiteDatabase db = getDbHelper().getReadableDatabase(); Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory.TYPE_RESOURCE_ID, Directory.PACKAGE_NAME, DirectoryColumns.TYPE_RESOURCE_NAME }, null, null, null, null, null); try { while (cursor.moveToNext()) { int resourceId = cursor.getInt(0); if (resourceId != 0) { String packageName = cursor.getString(1); String storedResourceName = cursor.getString(2); String resourceName = getResourceNameById(packageName, resourceId); if (!TextUtils.equals(storedResourceName, resourceName)) { return false; } } } } finally { cursor.close(); } return true; } /** * Given a resource ID, returns the corresponding resource name or null if the package name / * resource ID combination is invalid. */ private String getResourceNameById(String packageName, int resourceId) { try { Resources resources = mPackageManager.getResourcesForApplication(packageName); return resources.getResourceName(resourceId); } catch (NameNotFoundException e) { return null; } catch (NotFoundException e) { return null; } } /** * Scans all packages for directory content providers. */ public void scanAllPackages(boolean rescan) { if (rescan || !areTypeResourceIdsValid()) { getDbHelper().setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0"); } scanAllPackagesIfNeeded(); } private void scanAllPackagesIfNeeded() { String scanComplete = getDbHelper().getProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0"); if (!"0".equals(scanComplete)) { return; } final long start = SystemClock.elapsedRealtime(); int count = scanAllPackages(); getDbHelper().setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "1"); final long end = SystemClock.elapsedRealtime(); Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms"); // Announce the change to listeners of the contacts authority mContactsProvider.notifyChange(false); } @VisibleForTesting static boolean isDirectoryProvider(ProviderInfo provider) { if (provider == null) return false; Bundle metaData = provider.metaData; if (metaData == null) return false; Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA); return trueFalse != null && Boolean.TRUE.equals(trueFalse); } /** * @return List of packages that contain a directory provider. */ @VisibleForTesting static Set getDirectoryProviderPackages(PackageManager pm) { final Set ret = Sets.newHashSet(); final List packages = pm.getInstalledPackages(PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); if (packages == null) { return ret; } for (PackageInfo packageInfo : packages) { if (DEBUG) { Log.d(TAG, "package=" + packageInfo.packageName); } if (packageInfo.providers == null) { continue; } for (ProviderInfo provider : packageInfo.providers) { if (DEBUG) { Log.d(TAG, "provider=" + provider.authority); } if (isDirectoryProvider(provider)) { Log.d(TAG, "Found " + provider.authority); ret.add(provider.packageName); } } } if (DEBUG) { Log.d(TAG, "Found " + ret.size() + " directory provider packages"); } return ret; } @VisibleForTesting int scanAllPackages() { SQLiteDatabase db = getDbHelper().getWritableDatabase(); insertDefaultDirectory(db); insertLocalInvisibleDirectory(db); int count = 0; // Prepare query strings for removing stale rows which don't correspond to existing // directories. StringBuilder deleteWhereBuilder = new StringBuilder(); ArrayList deleteWhereArgs = new ArrayList(); deleteWhereBuilder.append("NOT (" + Directory._ID + "=? OR " + Directory._ID + "=?"); deleteWhereArgs.add(String.valueOf(Directory.DEFAULT)); deleteWhereArgs.add(String.valueOf(Directory.LOCAL_INVISIBLE)); final String wherePart = "(" + Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND " + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?)"; for (String packageName : getDirectoryProviderPackages(mPackageManager)) { if (DEBUG) Log.d(TAG, "package=" + packageName); // getDirectoryProviderPackages() shouldn't return the contacts provider package // because it doesn't have CONTACT_DIRECTORY_META_DATA, but just to make sure... if (mContext.getPackageName().equals(packageName)) { Log.w(TAG, " skipping self"); continue; } final PackageInfo packageInfo; try { packageInfo = mPackageManager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); if (packageInfo == null) continue; // Just in case... } catch (NameNotFoundException nnfe) { continue; // Application just removed? } List directories = updateDirectoriesForPackage(packageInfo, true); if (directories != null && !directories.isEmpty()) { count += directories.size(); // We shouldn't delete rows for existing directories. for (DirectoryInfo info : directories) { if (DEBUG) Log.d(TAG, " directory=" + info); deleteWhereBuilder.append(" OR "); deleteWhereBuilder.append(wherePart); deleteWhereArgs.add(info.packageName); deleteWhereArgs.add(info.authority); deleteWhereArgs.add(info.accountName); deleteWhereArgs.add(info.accountType); } } } deleteWhereBuilder.append(")"); // Close "NOT (" int deletedRows = db.delete(Tables.DIRECTORIES, deleteWhereBuilder.toString(), deleteWhereArgs.toArray(new String[0])); Log.i(TAG, "deleted " + deletedRows + " stale rows which don't have any relevant directory"); return count; } private void insertDefaultDirectory(SQLiteDatabase db) { ContentValues values = new ContentValues(); values.put(Directory._ID, Directory.DEFAULT); values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName); values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY); values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory); values.put(DirectoryColumns.TYPE_RESOURCE_NAME, mContext.getResources().getResourceName(R.string.default_directory)); values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE); values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL); values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL); db.replace(Tables.DIRECTORIES, null, values); } private void insertLocalInvisibleDirectory(SQLiteDatabase db) { ContentValues values = new ContentValues(); values.put(Directory._ID, Directory.LOCAL_INVISIBLE); values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName); values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY); values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory); values.put(DirectoryColumns.TYPE_RESOURCE_NAME, mContext.getResources().getResourceName(R.string.local_invisible_directory)); values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE); values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL); values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL); db.replace(Tables.DIRECTORIES, null, values); } /** * Scans the specified package for content directories. The package may have * already been removed, so packageName does not necessarily correspond to * an installed package. */ public void onPackageChanged(String packageName) { PackageInfo packageInfo = null; try { packageInfo = mPackageManager.getPackageInfo(packageName, PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA); } catch (NameNotFoundException e) { // The package got removed packageInfo = new PackageInfo(); packageInfo.packageName = packageName; } if (mContext.getPackageName().equals(packageInfo.packageName)) { if (DEBUG) Log.d(TAG, "Ignoring onPackageChanged for self"); return; } updateDirectoriesForPackage(packageInfo, false); } /** * Scans the specified package for content directories and updates the {@link Directory} * table accordingly. */ private List updateDirectoriesForPackage( PackageInfo packageInfo, boolean initialScan) { if (DEBUG) { Log.d(TAG, "updateDirectoriesForPackage packageName=" + packageInfo.packageName + " initialScan=" + initialScan); } ArrayList directories = Lists.newArrayList(); ProviderInfo[] providers = packageInfo.providers; if (providers != null) { for (ProviderInfo provider : providers) { if (isDirectoryProvider(provider)) { queryDirectoriesForAuthority(directories, provider); } } } if (directories.size() == 0 && initialScan) { return null; } SQLiteDatabase db = getDbHelper().getWritableDatabase(); db.beginTransaction(); try { updateDirectories(db, directories); // Clear out directories that are no longer present StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?"); if (!directories.isEmpty()) { sb.append(" AND " + Directory._ID + " NOT IN("); for (DirectoryInfo info: directories) { sb.append(info.id).append(","); } sb.setLength(sb.length() - 1); // Remove the extra comma sb.append(")"); } final int numDeleted = db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName }); if (DEBUG) { Log.d(TAG, " deleted " + numDeleted + " stale rows"); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } mContactsProvider.resetDirectoryCache(); return directories; } /** * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory * provider and appends all discovered directories to the directoryInfo list. */ protected void queryDirectoriesForAuthority( ArrayList directoryInfo, ProviderInfo provider) { Uri uri = new Uri.Builder().scheme("content") .authority(provider.authority).appendPath("directories").build(); Cursor cursor = null; try { cursor = mContext.getContentResolver().query( uri, DirectoryQuery.PROJECTION, null, null, null); if (cursor == null) { Log.i(TAG, providerDescription(provider) + " returned a NULL cursor."); } else { while (cursor.moveToNext()) { DirectoryInfo info = new DirectoryInfo(); info.packageName = provider.packageName; info.authority = provider.authority; info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME); info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE); info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) { info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); } if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) { int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT); switch (exportSupport) { case Directory.EXPORT_SUPPORT_NONE: case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY: case Directory.EXPORT_SUPPORT_ANY_ACCOUNT: info.exportSupport = exportSupport; break; default: Log.e(TAG, providerDescription(provider) + " - invalid export support flag: " + exportSupport); } } if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) { int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT); switch (shortcutSupport) { case Directory.SHORTCUT_SUPPORT_NONE: case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY: case Directory.SHORTCUT_SUPPORT_FULL: info.shortcutSupport = shortcutSupport; break; default: Log.e(TAG, providerDescription(provider) + " - invalid shortcut support flag: " + shortcutSupport); } } if (!cursor.isNull(DirectoryQuery.PHOTO_SUPPORT)) { int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT); switch (photoSupport) { case Directory.PHOTO_SUPPORT_NONE: case Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY: case Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY: case Directory.PHOTO_SUPPORT_FULL: info.photoSupport = photoSupport; break; default: Log.e(TAG, providerDescription(provider) + " - invalid photo support flag: " + photoSupport); } } directoryInfo.add(info); } } } catch (Throwable t) { Log.e(TAG, providerDescription(provider) + " exception", t); } finally { if (cursor != null) { cursor.close(); } } } /** * Updates the directories tables in the database to match the info received * from directory providers. */ private void updateDirectories(SQLiteDatabase db, ArrayList directoryInfo) { // Insert or replace existing directories. // This happens so infrequently that we can use a less-then-optimal one-a-time approach for (DirectoryInfo info : directoryInfo) { ContentValues values = new ContentValues(); values.put(Directory.PACKAGE_NAME, info.packageName); values.put(Directory.DIRECTORY_AUTHORITY, info.authority); values.put(Directory.ACCOUNT_NAME, info.accountName); values.put(Directory.ACCOUNT_TYPE, info.accountType); values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId); values.put(Directory.DISPLAY_NAME, info.displayName); values.put(Directory.EXPORT_SUPPORT, info.exportSupport); values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport); values.put(Directory.PHOTO_SUPPORT, info.photoSupport); if (info.typeResourceId != 0) { String resourceName = getResourceNameById(info.packageName, info.typeResourceId); values.put(DirectoryColumns.TYPE_RESOURCE_NAME, resourceName); } Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID }, Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND " + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?", new String[] { info.packageName, info.authority, info.accountName, info.accountType }, null, null, null); try { long id; if (cursor.moveToFirst()) { id = cursor.getLong(0); db.update(Tables.DIRECTORIES, values, Directory._ID + "=?", new String[] { String.valueOf(id) }); } else { id = db.insert(Tables.DIRECTORIES, null, values); } info.id = id; } finally { cursor.close(); } } } protected String providerDescription(ProviderInfo provider) { return "Directory provider " + provider.packageName + "(" + provider.authority + ")"; } }