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