ContactDirectoryManager.java revision 43368a3f9e05a979e454e278d6a0e8475f08923d
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 com.android.providers.contacts.ContactsDatabaseHelper.DirectoryColumns;
20import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
21import com.google.android.collect.Lists;
22
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.pm.PackageInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.pm.ProviderInfo;
29import android.content.res.Resources;
30import android.content.res.Resources.NotFoundException;
31import android.database.Cursor;
32import android.database.sqlite.SQLiteDatabase;
33import android.net.Uri;
34import android.os.Bundle;
35import android.os.SystemClock;
36import android.provider.ContactsContract;
37import android.provider.ContactsContract.Directory;
38import android.text.TextUtils;
39import android.util.Log;
40
41import java.util.ArrayList;
42import java.util.List;
43
44/**
45 * Manages the contents of the {@link Directory} table.
46 */
47// TODO: Determine whether directories need to be aware of data sets under the account.
48public class ContactDirectoryManager {
49
50    private static final String TAG = "ContactDirectoryManager";
51
52    public static final String PROPERTY_DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
53    public static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory";
54
55    public class DirectoryInfo {
56        long id;
57        String packageName;
58        String authority;
59        String accountName;
60        String accountType;
61        String displayName;
62        int typeResourceId;
63        int exportSupport = Directory.EXPORT_SUPPORT_NONE;
64        int shortcutSupport = Directory.SHORTCUT_SUPPORT_NONE;
65        int photoSupport = Directory.PHOTO_SUPPORT_NONE;
66    }
67
68    private final static class DirectoryQuery {
69        public static final String[] PROJECTION = {
70            Directory.ACCOUNT_NAME,
71            Directory.ACCOUNT_TYPE,
72            Directory.DISPLAY_NAME,
73            Directory.TYPE_RESOURCE_ID,
74            Directory.EXPORT_SUPPORT,
75            Directory.SHORTCUT_SUPPORT,
76            Directory.PHOTO_SUPPORT,
77        };
78
79        public static final int ACCOUNT_NAME = 0;
80        public static final int ACCOUNT_TYPE = 1;
81        public static final int DISPLAY_NAME = 2;
82        public static final int TYPE_RESOURCE_ID = 3;
83        public static final int EXPORT_SUPPORT = 4;
84        public static final int SHORTCUT_SUPPORT = 5;
85        public static final int PHOTO_SUPPORT = 6;
86    }
87
88    private final ContactsProvider2 mContactsProvider;
89    private Context mContext;
90
91    public ContactDirectoryManager(ContactsProvider2 contactsProvider) {
92        this.mContactsProvider = contactsProvider;
93        this.mContext = contactsProvider.getContext();
94    }
95
96    public ContactsDatabaseHelper getDbHelper() {
97        return (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper();
98    }
99
100    /**
101     * Scans all packages owned by the specified calling UID looking for contact
102     * directory providers.
103     */
104    public void scanPackagesByUid(int callingUid) {
105        final PackageManager pm = mContext.getPackageManager();
106        final String[] callerPackages = pm.getPackagesForUid(callingUid);
107        if (callerPackages != null) {
108            for (int i = 0; i < callerPackages.length; i++) {
109                onPackageChanged(callerPackages[i]);
110            }
111        }
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     */
120    private boolean areTypeResourceIdsValid() {
121        final PackageManager pm = mContext.getPackageManager();
122        SQLiteDatabase db = getDbHelper().getReadableDatabase();
123
124        Cursor cursor = db.query(Tables.DIRECTORIES,
125                new String[] { Directory.TYPE_RESOURCE_ID, Directory.PACKAGE_NAME,
126                        DirectoryColumns.TYPE_RESOURCE_NAME }, null, null, null, null, null);
127        try {
128            while (cursor.moveToNext()) {
129                int resourceId = cursor.getInt(0);
130                if (resourceId != 0) {
131                    String packageName = cursor.getString(1);
132                    String storedResourceName = cursor.getString(2);
133                    String resourceName = getResourceNameById(pm, packageName, resourceId);
134                    if (!TextUtils.equals(storedResourceName, resourceName)) {
135                        return false;
136                    }
137                }
138            }
139        } finally {
140            cursor.close();
141        }
142
143        return true;
144    }
145
146    /**
147     * Given a resource ID, returns the corresponding resource name or null if the package name /
148     * resource ID combination is invalid.
149     */
150    private String getResourceNameById(PackageManager pm, String packageName, int resourceId) {
151        try {
152            Resources resources = pm.getResourcesForApplication(packageName);
153            return resources.getResourceName(resourceId);
154        } catch (NameNotFoundException e) {
155            return null;
156        } catch (NotFoundException e) {
157            return null;
158        }
159    }
160
161    /**
162     * Scans all packages for directory content providers.
163     */
164    public void scanAllPackages(boolean rescan) {
165        if (rescan || !areTypeResourceIdsValid()) {
166            getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
167        }
168
169        scanAllPackagesIfNeeded();
170    }
171
172    private void scanAllPackagesIfNeeded() {
173        String scanComplete = getDbHelper().getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
174        if (!"0".equals(scanComplete)) {
175            return;
176        }
177
178        long start = SystemClock.currentThreadTimeMillis();
179        int count = scanAllPackages();
180        getDbHelper().setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1");
181        long end = SystemClock.currentThreadTimeMillis();
182        Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
183
184        // Announce the change to listeners of the contacts authority
185        mContactsProvider.notifyChange(false);
186    }
187
188    /* Visible for testing */
189    int scanAllPackages() {
190        SQLiteDatabase db = getDbHelper().getWritableDatabase();
191        insertDefaultDirectory(db);
192        insertLocalInvisibleDirectory(db);
193
194        int count = 0;
195        PackageManager pm = mContext.getPackageManager();
196        List<PackageInfo> packages = pm.getInstalledPackages(
197                PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
198        if (packages != null) {
199            // Prepare query strings for removing stale rows which don't correspond to existing
200            // directories.
201            StringBuilder deleteWhereBuilder = new StringBuilder();
202            ArrayList<String> deleteWhereArgs = new ArrayList<String>();
203            deleteWhereBuilder.append("NOT (" + Directory._ID + "=? OR " + Directory._ID + "=?");
204            deleteWhereArgs.add(String.valueOf(Directory.DEFAULT));
205            deleteWhereArgs.add(String.valueOf(Directory.LOCAL_INVISIBLE));
206            final String wherePart = "(" + Directory.PACKAGE_NAME + "=? AND "
207                    + Directory.DIRECTORY_AUTHORITY + "=? AND "
208                    + Directory.ACCOUNT_NAME + "=? AND "
209                    + Directory.ACCOUNT_TYPE + "=?)";
210
211            for (PackageInfo packageInfo : packages) {
212                // Check all packages except the one containing ContactsProvider itself
213                if (!packageInfo.packageName.equals(mContext.getPackageName())) {
214                    List<DirectoryInfo> directories =
215                            updateDirectoriesForPackage(packageInfo, true);
216                    if (directories != null && !directories.isEmpty()) {
217                        count += directories.size();
218
219                        // We shouldn't delete rows for existing directories.
220                        for (DirectoryInfo info : directories) {
221                            deleteWhereBuilder.append(" OR ");
222                            deleteWhereBuilder.append(wherePart);
223                            deleteWhereArgs.add(info.packageName);
224                            deleteWhereArgs.add(info.authority);
225                            deleteWhereArgs.add(info.accountName);
226                            deleteWhereArgs.add(info.accountType);
227                        }
228                    }
229                }
230            }
231
232            deleteWhereBuilder.append(")");  // Close "NOT ("
233            int deletedRows = db.delete(Tables.DIRECTORIES, deleteWhereBuilder.toString(),
234                    deleteWhereArgs.toArray(new String[0]));
235            Log.i(TAG, "deleted " + deletedRows
236                    + " stale rows which don't have any relevant directory");
237        }
238        return count;
239    }
240
241    private void insertDefaultDirectory(SQLiteDatabase db) {
242        ContentValues values = new ContentValues();
243        values.put(Directory._ID, Directory.DEFAULT);
244        values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
245        values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
246        values.put(Directory.TYPE_RESOURCE_ID, R.string.default_directory);
247        values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
248                mContext.getResources().getResourceName(R.string.default_directory));
249        values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
250        values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL);
251        values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL);
252        db.replace(Tables.DIRECTORIES, null, values);
253    }
254
255    private void insertLocalInvisibleDirectory(SQLiteDatabase db) {
256        ContentValues values = new ContentValues();
257        values.put(Directory._ID, Directory.LOCAL_INVISIBLE);
258        values.put(Directory.PACKAGE_NAME, mContext.getApplicationInfo().packageName);
259        values.put(Directory.DIRECTORY_AUTHORITY, ContactsContract.AUTHORITY);
260        values.put(Directory.TYPE_RESOURCE_ID, R.string.local_invisible_directory);
261        values.put(DirectoryColumns.TYPE_RESOURCE_NAME,
262                mContext.getResources().getResourceName(R.string.local_invisible_directory));
263        values.put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_NONE);
264        values.put(Directory.SHORTCUT_SUPPORT, Directory.SHORTCUT_SUPPORT_FULL);
265        values.put(Directory.PHOTO_SUPPORT, Directory.PHOTO_SUPPORT_FULL);
266        db.replace(Tables.DIRECTORIES, null, values);
267    }
268
269    /**
270     * Scans the specified package for content directories.  The package may have
271     * already been removed, so packageName does not necessarily correspond to
272     * an installed package.
273     */
274    public void onPackageChanged(String packageName) {
275        PackageManager pm = mContext.getPackageManager();
276        PackageInfo packageInfo = null;
277
278        try {
279            packageInfo = pm.getPackageInfo(packageName,
280                    PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
281        } catch (NameNotFoundException e) {
282            // The package got removed
283            packageInfo = new PackageInfo();
284            packageInfo.packageName = packageName;
285        }
286
287        updateDirectoriesForPackage(packageInfo, false);
288    }
289
290    /**
291     * Scans the specified package for content directories and updates the {@link Directory}
292     * table accordingly.
293     */
294    private List<DirectoryInfo> updateDirectoriesForPackage(
295            PackageInfo packageInfo, boolean initialScan) {
296        ArrayList<DirectoryInfo> directories = Lists.newArrayList();
297
298        ProviderInfo[] providers = packageInfo.providers;
299        if (providers != null) {
300            for (ProviderInfo provider : providers) {
301                Bundle metaData = provider.metaData;
302                if (metaData != null) {
303                    Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA);
304                    if (trueFalse != null && Boolean.TRUE.equals(trueFalse)) {
305                        queryDirectoriesForAuthority(directories, provider);
306                    }
307                }
308            }
309        }
310
311        if (directories.size() == 0 && initialScan) {
312            return null;
313        }
314
315        SQLiteDatabase db = getDbHelper().getWritableDatabase();
316        db.beginTransaction();
317        try {
318            updateDirectories(db, directories);
319            // Clear out directories that are no longer present
320            StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?");
321            if (!directories.isEmpty()) {
322                sb.append(" AND " + Directory._ID + " NOT IN(");
323                for (DirectoryInfo info: directories) {
324                    sb.append(info.id).append(",");
325                }
326                sb.setLength(sb.length() - 1);  // Remove the extra comma
327                sb.append(")");
328            }
329            db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName });
330            db.setTransactionSuccessful();
331        } finally {
332            db.endTransaction();
333        }
334
335        mContactsProvider.resetDirectoryCache();
336        return directories;
337    }
338
339    /**
340     * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory
341     * provider and appends all discovered directories to the directoryInfo list.
342     */
343    protected void queryDirectoriesForAuthority(
344            ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) {
345        Uri uri = new Uri.Builder().scheme("content")
346                .authority(provider.authority).appendPath("directories").build();
347        Cursor cursor = null;
348        try {
349            cursor = mContext.getContentResolver().query(
350                    uri, DirectoryQuery.PROJECTION, null, null, null);
351            if (cursor == null) {
352                Log.i(TAG, providerDescription(provider) + " returned a NULL cursor.");
353            } else {
354                while (cursor.moveToNext()) {
355                    DirectoryInfo info = new DirectoryInfo();
356                    info.packageName = provider.packageName;
357                    info.authority = provider.authority;
358                    info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
359                    info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
360                    info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
361                    if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) {
362                        info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
363                    }
364                    if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) {
365                        int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
366                        switch (exportSupport) {
367                            case Directory.EXPORT_SUPPORT_NONE:
368                            case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY:
369                            case Directory.EXPORT_SUPPORT_ANY_ACCOUNT:
370                                info.exportSupport = exportSupport;
371                                break;
372                            default:
373                                Log.e(TAG, providerDescription(provider)
374                                        + " - invalid export support flag: " + exportSupport);
375                        }
376                    }
377                    if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) {
378                        int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT);
379                        switch (shortcutSupport) {
380                            case Directory.SHORTCUT_SUPPORT_NONE:
381                            case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY:
382                            case Directory.SHORTCUT_SUPPORT_FULL:
383                                info.shortcutSupport = shortcutSupport;
384                                break;
385                            default:
386                                Log.e(TAG, providerDescription(provider)
387                                        + " - invalid shortcut support flag: " + shortcutSupport);
388                        }
389                    }
390                    if (!cursor.isNull(DirectoryQuery.PHOTO_SUPPORT)) {
391                        int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT);
392                        switch (photoSupport) {
393                            case Directory.PHOTO_SUPPORT_NONE:
394                            case Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY:
395                            case Directory.PHOTO_SUPPORT_FULL_SIZE_ONLY:
396                            case Directory.PHOTO_SUPPORT_FULL:
397                                info.photoSupport = photoSupport;
398                                break;
399                            default:
400                                Log.e(TAG, providerDescription(provider)
401                                        + " - invalid photo support flag: " + photoSupport);
402                        }
403                    }
404                    directoryInfo.add(info);
405                }
406            }
407        } catch (Throwable t) {
408            Log.e(TAG, providerDescription(provider) + " exception", t);
409        } finally {
410            if (cursor != null) {
411                cursor.close();
412            }
413        }
414    }
415
416    /**
417     * Updates the directories tables in the database to match the info received
418     * from directory providers.
419     */
420    private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) {
421        PackageManager pm = mContext.getPackageManager();
422
423        // Insert or replace existing directories.
424        // This happens so infrequently that we can use a less-then-optimal one-a-time approach
425        for (DirectoryInfo info : directoryInfo) {
426            ContentValues values = new ContentValues();
427            values.put(Directory.PACKAGE_NAME, info.packageName);
428            values.put(Directory.DIRECTORY_AUTHORITY, info.authority);
429            values.put(Directory.ACCOUNT_NAME, info.accountName);
430            values.put(Directory.ACCOUNT_TYPE, info.accountType);
431            values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId);
432            values.put(Directory.DISPLAY_NAME, info.displayName);
433            values.put(Directory.EXPORT_SUPPORT, info.exportSupport);
434            values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport);
435            values.put(Directory.PHOTO_SUPPORT, info.photoSupport);
436
437            if (info.typeResourceId != 0) {
438                String resourceName = getResourceNameById(
439                        pm, info.packageName, info.typeResourceId);
440                values.put(DirectoryColumns.TYPE_RESOURCE_NAME, resourceName);
441            }
442
443            Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID },
444                    Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND "
445                            + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?",
446                    new String[] {
447                            info.packageName, info.authority, info.accountName, info.accountType },
448                    null, null, null);
449            try {
450                long id;
451                if (cursor.moveToFirst()) {
452                    id = cursor.getLong(0);
453                    db.update(Tables.DIRECTORIES, values, Directory._ID + "=?",
454                            new String[] { String.valueOf(id) });
455                } else {
456                    id = db.insert(Tables.DIRECTORIES, null, values);
457                }
458                info.id = id;
459            } finally {
460                cursor.close();
461            }
462        }
463    }
464
465    protected String providerDescription(ProviderInfo provider) {
466        return "Directory provider " + provider.packageName + "(" + provider.authority + ")";
467    }
468}
469