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 all packages owned by the specified calling UID looking for contact
116      * directory providers.
117      */
scanPackagesByUid(int callingUid)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      */
areTypeResourceIdsValid()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      */
getResourceNameById(String packageName, int resourceId)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      */
scanAllPackages(boolean rescan)176     public void scanAllPackages(boolean rescan) {
177         if (rescan || !areTypeResourceIdsValid()) {
178             getDbHelper().setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
179         }
180 
181         scanAllPackagesIfNeeded();
182     }
183 
scanAllPackagesIfNeeded()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
isDirectoryProvider(ProviderInfo provider)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
getDirectoryProviderPackages(PackageManager pm)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
scanAllPackages()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 
insertDefaultDirectory(SQLiteDatabase db)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 
insertLocalInvisibleDirectory(SQLiteDatabase db)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      */
onPackageChanged(String packageName)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      */
updateDirectoriesForPackage( PackageInfo packageInfo, boolean initialScan)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      */
queryDirectoriesForAuthority( ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider)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      */
updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo)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 
providerDescription(ProviderInfo provider)541     protected String providerDescription(ProviderInfo provider) {
542         return "Directory provider " + provider.packageName + "(" + provider.authority + ")";
543     }
544 }
545