ContactDirectoryManager.java revision 6522ca932caf4a4921115a2a3d13068c2357084c
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
17package com.android.providers.contacts;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.pm.PackageInfo;
22import android.content.pm.PackageManager;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.content.pm.ProviderInfo;
25import android.content.res.Resources;
26import android.content.res.Resources.NotFoundException;
27import android.database.Cursor;
28import android.database.sqlite.SQLiteDatabase;
29import android.net.Uri;
30import android.os.Bundle;
31import android.os.SystemClock;
32import android.provider.ContactsContract;
33import android.provider.ContactsContract.Directory;
34import android.text.TextUtils;
35import android.util.Log;
36
37import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
38import com.android.providers.contacts.ContactsDatabaseHelper.DirectoryColumns;
39import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
40import com.google.android.collect.Lists;
41import com.google.android.collect.Sets;
42import com.google.common.annotations.VisibleForTesting;
43
44import java.util.ArrayList;
45import java.util.List;
46import java.util.Set;
47
48/**
49 * Manages the contents of the {@link Directory} table.
50 */
51public 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