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         if (provider == null) return false;
    203         Bundle metaData = provider.metaData;
    204         if (metaData == null) return false;
    205 
    206         Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA);
    207         return trueFalse != null && Boolean.TRUE.equals(trueFalse);
    208     }
    209 
    210     /**
    211      * @return List of packages that contain a directory provider.
    212      */
    213     @VisibleForTesting
    214     static Set<String> getDirectoryProviderPackages(PackageManager pm) {
    215         final Set<String> ret = Sets.newHashSet();
    216 
    217         final List<PackageInfo> packages = pm.getInstalledPackages(PackageManager.GET_PROVIDERS
    218                 | PackageManager.GET_META_DATA);
    219         if (packages == null) {
    220             return ret;
    221         }
    222         for (PackageInfo packageInfo : packages) {
    223             if (DEBUG) {
    224                 Log.d(TAG, "package=" + packageInfo.packageName);
    225             }
    226             if (packageInfo.providers == null) {
    227                 continue;
    228             }
    229             for (ProviderInfo provider : packageInfo.providers) {
    230                 if (DEBUG) {
    231                     Log.d(TAG, "provider=" + provider.authority);
    232                 }
    233                 if (isDirectoryProvider(provider)) {
    234                     Log.d(TAG, "Found " + provider.authority);
    235                     ret.add(provider.packageName);
    236                 }
    237             }
    238         }
    239         if (DEBUG) {
    240             Log.d(TAG, "Found " + ret.size() + " directory provider packages");
    241         }
    242 
    243         return ret;
    244     }
    245 
    246     @VisibleForTesting
    247     int scanAllPackages() {
    248         SQLiteDatabase db = getDbHelper().getWritableDatabase();
    249         insertDefaultDirectory(db);
    250         insertLocalInvisibleDirectory(db);
    251 
    252         int count = 0;
    253 
    254         // Prepare query strings for removing stale rows which don't correspond to existing
    255         // directories.
    256         StringBuilder deleteWhereBuilder = new StringBuilder();
    257         ArrayList<String> deleteWhereArgs = new ArrayList<String>();
    258         deleteWhereBuilder.append("NOT (" + Directory._ID + "=? OR " + Directory._ID + "=?");
    259         deleteWhereArgs.add(String.valueOf(Directory.DEFAULT));
    260         deleteWhereArgs.add(String.valueOf(Directory.LOCAL_INVISIBLE));
    261         final String wherePart = "(" + Directory.PACKAGE_NAME + "=? AND "
    262                 + Directory.DIRECTORY_AUTHORITY + "=? AND "
    263                 + Directory.ACCOUNT_NAME + "=? AND "
    264                 + Directory.ACCOUNT_TYPE + "=?)";
    265 
    266         for (String packageName : getDirectoryProviderPackages(mPackageManager)) {
    267             if (DEBUG) Log.d(TAG, "package=" + packageName);
    268 
    269             // getDirectoryProviderPackages() shouldn't return the contacts provider package
    270             // because it doesn't have CONTACT_DIRECTORY_META_DATA, but just to make sure...
    271             if (mContext.getPackageName().equals(packageName)) {
    272                 Log.w(TAG, "  skipping self");
    273                 continue;
    274             }
    275 
    276             final PackageInfo packageInfo;
    277             try {
    278                 packageInfo = mPackageManager.getPackageInfo(packageName,
    279                         PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
    280                 if (packageInfo == null) continue;  // Just in case...
    281             } catch (NameNotFoundException nnfe) {
    282                 continue; // Application just removed?
    283             }
    284 
    285             List<DirectoryInfo> directories = updateDirectoriesForPackage(packageInfo, true);
    286             if (directories != null && !directories.isEmpty()) {
    287                 count += directories.size();
    288 
    289                 // We shouldn't delete rows for existing directories.
    290                 for (DirectoryInfo info : directories) {
    291                     if (DEBUG) Log.d(TAG, "  directory=" + info);
    292                     deleteWhereBuilder.append(" OR ");
    293                     deleteWhereBuilder.append(wherePart);
    294                     deleteWhereArgs.add(info.packageName);
    295                     deleteWhereArgs.add(info.authority);
    296                     deleteWhereArgs.add(info.accountName);
    297                     deleteWhereArgs.add(info.accountType);
    298                 }
    299             }
    300         }
    301 
    302         deleteWhereBuilder.append(")");  // Close "NOT ("
    303 
    304         int deletedRows = db.delete(Tables.DIRECTORIES, deleteWhereBuilder.toString(),
    305                 deleteWhereArgs.toArray(new String[0]));
    306         Log.i(TAG, "deleted " + deletedRows
    307                 + " stale rows which don't have any relevant directory");
    308         return count;
    309     }
    310 
    311     private void insertDefaultDirectory(SQLiteDatabase db) {
    312         ContentValues values = new ContentValues();
    313         values.put(Directory._ID, Directory.DEFAULT);
    314         values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
    315         values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
    316         values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
    317         values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
    318                 mContext.getResources().getResourceName(R.string.default_directory));
    319         values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
    320         values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL);
    321         values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL);
    322         db.replace(Tables.DIRECTORIES, null, values);
    323     }
    324 
    325     private void insertLocalInvisibleDirectory(SQLiteDatabase db) {
    326         ContentValues values = new ContentValues();
    327         values.put(Directory._ID, Directory.LOCAL_INVISIBLE);
    328         values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
    329         values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
    330         values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
    331         values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
    332                 mContext.getResources().getResourceName(R.string.local_invisible_directory));
    333         values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
    334         values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL);
    335         values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL);
    336         db.replace(Tables.DIRECTORIES, null, values);
    337     }
    338 
    339     /**
    340      * Scans the specified package for content directories.  The package may have
    341      * already been removed, so packageName does not necessarily correspond to
    342      * an installed package.
    343      */
    344     public void onPackageChanged(String packageName) {
    345         PackageInfo packageInfo = null;
    346 
    347         try {
    348             packageInfo = mPackageManager.getPackageInfo(packageName,
    349                     PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
    350         } catch (NameNotFoundException e) {
    351             // The package got removed
    352             packageInfo = new PackageInfo();
    353             packageInfo.packageName = packageName;
    354         }
    355 
    356         if (mContext.getPackageName().equals(packageInfo.packageName)) {
    357             if (DEBUG) Log.d(TAG, "Ignoring onPackageChanged for self");
    358             return;
    359         }
    360         updateDirectoriesForPackage(packageInfo, false);
    361     }
    362 
    363 
    364     /**
    365      * Scans the specified package for content directories and updates the {@link Directory}
    366      * table accordingly.
    367      */
    368     private List<DirectoryInfo> updateDirectoriesForPackage(
    369             PackageInfo packageInfo, boolean initialScan) {
    370         if (DEBUG) {
    371             Log.d(TAG, "updateDirectoriesForPackage  packageName=" + packageInfo.packageName
    372                     + " initialScan=" + initialScan);
    373         }
    374 
    375         ArrayList<DirectoryInfo> directories = Lists.newArrayList();
    376 
    377         ProviderInfo[] providers = packageInfo.providers;
    378         if (providers != null) {
    379             for (ProviderInfo provider : providers) {
    380                 if (isDirectoryProvider(provider)) {
    381                     queryDirectoriesForAuthority(directories, provider);
    382                 }
    383             }
    384         }
    385 
    386         if (directories.size() == 0 && initialScan) {
    387             return null;
    388         }
    389 
    390         SQLiteDatabase db = getDbHelper().getWritableDatabase();
    391         db.beginTransaction();
    392         try {
    393             updateDirectories(db, directories);
    394             // Clear out directories that are no longer present
    395             StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?");
    396             if (!directories.isEmpty()) {
    397                 sb.append(" AND " + Directory._ID + " NOT IN(");
    398                 for (DirectoryInfo info: directories) {
    399                     sb.append(info.id).append(",");
    400                 }
    401                 sb.setLength(sb.length() - 1);  // Remove the extra comma
    402                 sb.append(")");
    403             }
    404             final int numDeleted = db.delete(Tables.DIRECTORIES, sb.toString(),
    405                     new String[] { packageInfo.packageName });
    406             if (DEBUG) {
    407                 Log.d(TAG, "  deleted " + numDeleted + " stale rows");
    408             }
    409             db.setTransactionSuccessful();
    410         } finally {
    411             db.endTransaction();
    412         }
    413 
    414         mContactsProvider.resetDirectoryCache();
    415         return directories;
    416     }
    417 
    418     /**
    419      * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory
    420      * provider and appends all discovered directories to the directoryInfo list.
    421      */
    422     protected void queryDirectoriesForAuthority(
    423             ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) {
    424         Uri uri = new Uri.Builder().scheme("content")
    425                 .authority(provider.authority).appendPath("directories").build();
    426         Cursor cursor = null;
    427         try {
    428             cursor = mContext.getContentResolver().query(
    429                     uri, DirectoryQuery.PROJECTION, null, null, null);
    430             if (cursor == null) {
    431                 Log.i(TAG, providerDescription(provider) + " returned a NULL cursor.");
    432             } else {
    433                 while (cursor.moveToNext()) {
    434                     DirectoryInfo info = new DirectoryInfo();
    435                     info.packageName = provider.packageName;
    436                     info.authority = provider.authority;
    437                     info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
    438                     info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
    439                     info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
    440                     if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) {
    441                         info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
    442                     }
    443                     if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) {
    444                         int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
    445                         switch (exportSupport) {
    446                             case Directory.EXPORT_SUPPORT_NONE:
    447                             case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY:
    448                             case Directory.EXPORT_SUPPORT_ANY_ACCOUNT:
    449                                 info.exportSupport = exportSupport;
    450                                 break;
    451                             default:
    452                                 Log.e(TAG, providerDescription(provider)
    453                                         + " - invalid export support flag: " + exportSupport);
    454                         }
    455                     }
    456                     if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) {
    457                         int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT);
    458                         switch (shortcutSupport) {
    459                             case Directory.SHORTCUT_SUPPORT_NONE:
    460                             case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY:
    461                             case Directory.SHORTCUT_SUPPORT_FULL:
    462                                 info.shortcutSupport = shortcutSupport;
    463                                 break;
    464                             default:
    465                                 Log.e(TAG, providerDescription(provider)
    466                                         + " - invalid shortcut support flag: " + shortcutSupport);
    467                         }
    468                     }
    469                     if (!cursor.isNull(DirectoryQuery.PHOTO_SUPPORT)) {
    470                         int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT);
    471                         switch (photoSupport) {
    472                             case Directory.PHOTO_SUPPORT_NONE:
    473                             case Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY:
    474                             case Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY:
    475                             case Directory.PHOTO_SUPPORT_FULL:
    476                                 info.photoSupport = photoSupport;
    477                                 break;
    478                             default:
    479                                 Log.e(TAG, providerDescription(provider)
    480                                         + " - invalid photo support flag: " + photoSupport);
    481                         }
    482                     }
    483                     directoryInfo.add(info);
    484                 }
    485             }
    486         } catch (Throwable t) {
    487             Log.e(TAG, providerDescription(provider) + " exception", t);
    488         } finally {
    489             if (cursor != null) {
    490                 cursor.close();
    491             }
    492         }
    493     }
    494 
    495     /**
    496      * Updates the directories tables in the database to match the info received
    497      * from directory providers.
    498      */
    499     private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) {
    500         // Insert or replace existing directories.
    501         // This happens so infrequently that we can use a less-then-optimal one-a-time approach
    502         for (DirectoryInfo info : directoryInfo) {
    503             ContentValues values = new ContentValues();
    504             values.put(Directory.PACKAGE_NAME, info.packageName);
    505             values.put(Directory.DIRECTORY_AUTHORITY, info.authority);
    506             values.put(Directory.ACCOUNT_NAME, info.accountName);
    507             values.put(Directory.ACCOUNT_TYPE, info.accountType);
    508             values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId);
    509             values.put(Directory.DISPLAY_NAME, info.displayName);
    510             values.put(Directory.EXPORT_SUPPORT, info.exportSupport);
    511             values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport);
    512             values.put(Directory.PHOTO_SUPPORT, info.photoSupport);
    513 
    514             if (info.typeResourceId != 0) {
    515                 String resourceName = getResourceNameById(info.packageName, info.typeResourceId);
    516                 values.put(DirectoryColumns.TYPE_RESOURCE_NAME, resourceName);
    517             }
    518 
    519             Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID },
    520                     Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND "
    521                             + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?",
    522                     new String[] {
    523                             info.packageName, info.authority, info.accountName, info.accountType },
    524                     null, null, null);
    525             try {
    526                 long id;
    527                 if (cursor.moveToFirst()) {
    528                     id = cursor.getLong(0);
    529                     db.update(Tables.DIRECTORIES, values, Directory._ID + "=?",
    530                             new String[] { String.valueOf(id) });
    531                 } else {
    532                     id = db.insert(Tables.DIRECTORIES, null, values);
    533                 }
    534                 info.id = id;
    535             } finally {
    536                 cursor.close();
    537             }
    538         }
    539     }
    540 
    541     protected String providerDescription(ProviderInfo provider) {
    542         return "Directory provider " + provider.packageName + "(" + provider.authority + ")";
    543     }
    544 }
    545