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