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