ContactDirectoryManager.java revision 6255d756615cfa89fb3411d1840dbe08e1375ffe
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.Tables;
20import com.google.android.collect.Lists;
21
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.pm.PackageInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.content.pm.ProviderInfo;
28import android.database.Cursor;
29import android.database.sqlite.SQLiteDatabase;
30import android.net.Uri;
31import android.os.Binder;
32import android.os.Bundle;
33import android.os.Debug;
34import android.os.Handler;
35import android.os.HandlerThread;
36import android.os.Message;
37import android.os.Process;
38import android.os.SystemClock;
39import android.provider.ContactsContract.Directory;
40import android.util.Log;
41
42import java.util.ArrayList;
43import java.util.List;
44
45/**
46 * Manages the contents of the {@link Directory} table.
47 */
48public class ContactDirectoryManager extends HandlerThread {
49
50    private static final String TAG = "ContactDirectoryManager";
51
52    private static final int MESSAGE_SCAN_ALL_PROVIDERS = 0;
53    private static final int MESSAGE_SCAN_PACKAGES_BY_UID = 1;
54
55    private static final String PROPERTY_DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
56    private static final String CONTACT_DIRECTORY_META_DATA = "android.content.ContactDirectory";
57
58    public 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    }
69
70    private final static class DirectoryQuery {
71        public static final String[] PROJECTION = {
72            Directory.ACCOUNT_NAME,
73            Directory.ACCOUNT_TYPE,
74            Directory.DISPLAY_NAME,
75            Directory.TYPE_RESOURCE_ID,
76            Directory.EXPORT_SUPPORT,
77            Directory.SHORTCUT_SUPPORT,
78        };
79
80        public static final int ACCOUNT_NAME = 0;
81        public static final int ACCOUNT_TYPE = 1;
82        public static final int DISPLAY_NAME = 2;
83        public static final int TYPE_RESOURCE_ID = 3;
84        public static final int EXPORT_SUPPORT = 4;
85        public static final int SHORTCUT_SUPPORT = 5;
86    }
87
88    private final ContactsProvider2 mContactsProvider;
89    private Context mContext;
90    private Handler mHandler;
91
92    public ContactDirectoryManager(ContactsProvider2 contactsProvider) {
93        super("DirectoryManager", Process.THREAD_PRIORITY_BACKGROUND);
94        this.mContactsProvider = contactsProvider;
95        this.mContext = contactsProvider.getContext();
96    }
97
98    /**
99     * Launches an asynchronous scan of all packages.
100     */
101    @Override
102    public void start() {
103        super.start();
104        scheduleScanAllPackages();
105    }
106
107    /**
108     * Launches an asynchronous scan of all packages owned by the current calling UID.
109     */
110    public void scheduleDirectoryUpdateForCaller() {
111        final int callingUid = Binder.getCallingUid();
112        if (isAlive()) {
113            Handler handler = getHandler();
114            handler.sendMessage(handler.obtainMessage(MESSAGE_SCAN_PACKAGES_BY_UID, callingUid, 0));
115        } else {
116            scanPackagesByUid(callingUid);
117        }
118    }
119
120    protected Handler getHandler() {
121        if (mHandler == null) {
122            mHandler = new Handler(getLooper()) {
123                @Override
124                public void handleMessage(Message msg) {
125                    ContactDirectoryManager.this.handleMessage(msg);
126                }
127            };
128        }
129        return mHandler;
130    }
131
132    protected void handleMessage(Message msg) {
133        switch(msg.what) {
134            case MESSAGE_SCAN_ALL_PROVIDERS:
135                scanAllPackagesIfNeeded();
136                break;
137            case MESSAGE_SCAN_PACKAGES_BY_UID:
138                scanPackagesByUid(msg.arg1);
139                break;
140        }
141    }
142
143    /**
144     * Scans all packages owned by the specified calling UID looking for contact
145     * directory providers.
146     */
147    public void scanPackagesByUid(int callingUid) {
148        final PackageManager pm = mContext.getPackageManager();
149        final String[] callerPackages = pm.getPackagesForUid(callingUid);
150        if (callerPackages != null) {
151            for (int i = 0; i < callerPackages.length; i++) {
152                onPackageChanged(callerPackages[i]);
153            }
154        }
155    }
156
157    /**
158     * Scans all packages for directory content providers. This is only done once
159     * in the lifetime of a contacts DB.
160     */
161    private void scanAllPackagesIfNeeded() {
162        ContactsDatabaseHelper dbHelper =
163                (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper();
164        SQLiteDatabase db = dbHelper.getWritableDatabase();
165
166        String scanComplete = dbHelper.getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
167        if (!"0".equals(scanComplete)) {
168            return;
169        }
170
171        long start = SystemClock.currentThreadTimeMillis();
172        int count = scanAllPackages();
173        dbHelper.setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1");
174        long end = SystemClock.currentThreadTimeMillis();
175        Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
176
177        // Announce the change to listeners of the contacts authority
178        mContactsProvider.notifyChange(false);
179    }
180
181    public void scheduleScanAllPackages() {
182        getHandler().sendEmptyMessage(MESSAGE_SCAN_ALL_PROVIDERS);
183    }
184
185    /* Visible for testing */
186    int scanAllPackages() {
187        int count = 0;
188        PackageManager pm = mContext.getPackageManager();
189        List<PackageInfo> packages = pm.getInstalledPackages(
190                PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
191        for (PackageInfo packageInfo : packages) {
192            // Check all packages except the one containing ContactsProvider itself
193            if (!packageInfo.packageName.equals(mContext.getPackageName())) {
194                count += updateDirectoriesForPackage(packageInfo, true);
195            }
196        }
197        return count;
198    }
199
200    /**
201     * Scans the specified package for content directories.  The package may have
202     * already been removed, so packageName does not necessarily correspond to
203     * an installed package.
204     */
205    public void onPackageChanged(String packageName) {
206        PackageManager pm = mContext.getPackageManager();
207        PackageInfo packageInfo = null;
208
209        try {
210            packageInfo = pm.getPackageInfo(packageName,
211                    PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
212        } catch (NameNotFoundException e) {
213            // The package got removed
214            packageInfo = new PackageInfo();
215            packageInfo.packageName = packageName;
216        }
217
218        updateDirectoriesForPackage(packageInfo, false);
219  }
220
221    /**
222     * Scans the specified package for content directories and updates the {@link Directory}
223     * table accordingly.
224     */
225    private int updateDirectoriesForPackage(PackageInfo packageInfo, boolean initialScan) {
226        ArrayList<DirectoryInfo> directories = Lists.newArrayList();
227
228        ProviderInfo[] providers = packageInfo.providers;
229        if (providers != null) {
230            for (ProviderInfo provider : providers) {
231                Bundle metaData = provider.metaData;
232                if (metaData != null) {
233                    Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA);
234                    if (trueFalse != null && Boolean.TRUE.equals(trueFalse)) {
235                        queryDirectoriesForAuthority(directories, provider);
236                    }
237                }
238            }
239        }
240
241        if (directories.size() == 0 && initialScan) {
242            return 0;
243        }
244
245        SQLiteDatabase db = ((ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper())
246                .getWritableDatabase();
247        db.beginTransaction();
248        try {
249            updateDirectories(db, directories);
250            // Clear out directories that are no longer present
251            StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?");
252            if (!directories.isEmpty()) {
253                sb.append(" AND " + Directory._ID + " NOT IN(");
254                for (DirectoryInfo info: directories) {
255                    sb.append(info.id).append(",");
256                }
257                sb.setLength(sb.length() - 1);  // Remove the extra comma
258                sb.append(")");
259            }
260            db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName });
261            db.setTransactionSuccessful();
262        } finally {
263            db.endTransaction();
264        }
265
266        mContactsProvider.resetDirectoryCache();
267        return directories.size();
268    }
269
270    /**
271     * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory
272     * provider and appends all discovered directories to the directoryInfo list.
273     */
274    protected void queryDirectoriesForAuthority(
275            ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) {
276        Uri uri = new Uri.Builder().scheme("content")
277                .authority(provider.authority).appendPath("directories").build();
278        Cursor cursor = null;
279        try {
280            cursor = mContext.getContentResolver().query(
281                    uri, DirectoryQuery.PROJECTION, null, null, null);
282            if (cursor == null) {
283                Log.i(TAG, providerDescription(provider) + " returned a NULL cursor.");
284            } else {
285                while (cursor.moveToNext()) {
286                    DirectoryInfo info = new DirectoryInfo();
287                    info.packageName = provider.packageName;
288                    info.authority = provider.authority;
289                    info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
290                    info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
291                    info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
292                    if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) {
293                        info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
294                    }
295                    if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) {
296                        int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
297                        switch (exportSupport) {
298                            case Directory.EXPORT_SUPPORT_NONE:
299                            case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY:
300                            case Directory.EXPORT_SUPPORT_ANY_ACCOUNT:
301                                info.exportSupport = exportSupport;
302                                break;
303                            default:
304                                Log.e(TAG, providerDescription(provider)
305                                        + " - invalid export support flag: " + exportSupport);
306                        }
307                    }
308                    if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) {
309                        int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT);
310                        switch (shortcutSupport) {
311                            case Directory.SHORTCUT_SUPPORT_NONE:
312                            case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY:
313                            case Directory.SHORTCUT_SUPPORT_FULL:
314                                info.shortcutSupport = shortcutSupport;
315                                break;
316                            default:
317                                Log.e(TAG, providerDescription(provider)
318                                        + " - invalid shortcut support flag: " + shortcutSupport);
319                        }
320                    }
321                    directoryInfo.add(info);
322                }
323            }
324        } catch (Throwable t) {
325            Log.e(TAG, providerDescription(provider) + " exception", t);
326        } finally {
327            if (cursor != null) {
328                cursor.close();
329            }
330        }
331    }
332
333    /**
334     * Updates the directories tables in the database to match the info received
335     * from directory providers.
336     */
337    private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) {
338
339        // Insert or replace existing directories.
340        // This happens so infrequently that we can use a less-then-optimal one-a-time approach
341        for (DirectoryInfo info : directoryInfo) {
342            ContentValues values = new ContentValues();
343            values.put(Directory.PACKAGE_NAME, info.packageName);
344            values.put(Directory.DIRECTORY_AUTHORITY, info.authority);
345            values.put(Directory.ACCOUNT_NAME, info.accountName);
346            values.put(Directory.ACCOUNT_TYPE, info.accountType);
347            values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId);
348            values.put(Directory.DISPLAY_NAME, info.displayName);
349            values.put(Directory.EXPORT_SUPPORT, info.exportSupport);
350            values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport);
351
352            Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID },
353                    Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND "
354                            + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?",
355                    new String[] {
356                            info.packageName, info.authority, info.accountName, info.accountType },
357                    null, null, null);
358            try {
359                long id;
360                if (cursor.moveToFirst()) {
361                    id = cursor.getLong(0);
362                    db.update(Tables.DIRECTORIES, values, Directory._ID + "=?",
363                            new String[] { String.valueOf(id) });
364                } else {
365                    id = db.insert(Tables.DIRECTORIES, null, values);
366                }
367                info.id = id;
368            } finally {
369                cursor.close();
370            }
371        }
372    }
373
374    protected String providerDescription(ProviderInfo provider) {
375        return "Directory provider " + provider.packageName + "(" + provider.authority + ")";
376    }
377}
378