ContactDirectoryManager.java revision cf832869bcf91b8037d8b7f510a3a213b30764a3
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(false);
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.
159     */
160    private void scanAllPackagesIfNeeded() {
161        ContactsDatabaseHelper dbHelper =
162                (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper();
163
164        String scanComplete = dbHelper.getProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
165        if (!"0".equals(scanComplete)) {
166            return;
167        }
168
169        long start = SystemClock.currentThreadTimeMillis();
170        int count = scanAllPackages();
171        dbHelper.setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "1");
172        long end = SystemClock.currentThreadTimeMillis();
173        Log.i(TAG, "Discovered " + count + " contact directories in " + (end - start) + "ms");
174
175        // Announce the change to listeners of the contacts authority
176        mContactsProvider.notifyChange(false);
177    }
178
179    public void scheduleScanAllPackages(boolean rescan) {
180        if (rescan) {
181            ContactsDatabaseHelper dbHelper =
182                    (ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper();
183            dbHelper.setProperty(PROPERTY_DIRECTORY_SCAN_COMPLETE, "0");
184        }
185        if (isAlive()) {
186            getHandler().sendEmptyMessage(MESSAGE_SCAN_ALL_PROVIDERS);
187        } else {
188            scanAllPackagesIfNeeded();
189        }
190    }
191
192    /* Visible for testing */
193    int scanAllPackages() {
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            for (PackageInfo packageInfo : packages) {
200                // Check all packages except the one containing ContactsProvider itself
201                if (!packageInfo.packageName.equals(mContext.getPackageName())) {
202                    count += updateDirectoriesForPackage(packageInfo, true);
203                }
204            }
205        }
206        return count;
207    }
208
209    /**
210     * Scans the specified package for content directories.  The package may have
211     * already been removed, so packageName does not necessarily correspond to
212     * an installed package.
213     */
214    public void onPackageChanged(String packageName) {
215        PackageManager pm = mContext.getPackageManager();
216        PackageInfo packageInfo = null;
217
218        try {
219            packageInfo = pm.getPackageInfo(packageName,
220                    PackageManager.GET_PROVIDERS | PackageManager.GET_META_DATA);
221        } catch (NameNotFoundException e) {
222            // The package got removed
223            packageInfo = new PackageInfo();
224            packageInfo.packageName = packageName;
225        }
226
227        updateDirectoriesForPackage(packageInfo, false);
228  }
229
230    /**
231     * Scans the specified package for content directories and updates the {@link Directory}
232     * table accordingly.
233     */
234    private int updateDirectoriesForPackage(PackageInfo packageInfo, boolean initialScan) {
235        ArrayList<DirectoryInfo> directories = Lists.newArrayList();
236
237        ProviderInfo[] providers = packageInfo.providers;
238        if (providers != null) {
239            for (ProviderInfo provider : providers) {
240                Bundle metaData = provider.metaData;
241                if (metaData != null) {
242                    Object trueFalse = metaData.get(CONTACT_DIRECTORY_META_DATA);
243                    if (trueFalse != null && Boolean.TRUE.equals(trueFalse)) {
244                        queryDirectoriesForAuthority(directories, provider);
245                    }
246                }
247            }
248        }
249
250        if (directories.size() == 0 && initialScan) {
251            return 0;
252        }
253
254        SQLiteDatabase db = ((ContactsDatabaseHelper) mContactsProvider.getDatabaseHelper())
255                .getWritableDatabase();
256        db.beginTransaction();
257        try {
258            updateDirectories(db, directories);
259            // Clear out directories that are no longer present
260            StringBuilder sb = new StringBuilder(Directory.PACKAGE_NAME + "=?");
261            if (!directories.isEmpty()) {
262                sb.append(" AND " + Directory._ID + " NOT IN(");
263                for (DirectoryInfo info: directories) {
264                    sb.append(info.id).append(",");
265                }
266                sb.setLength(sb.length() - 1);  // Remove the extra comma
267                sb.append(")");
268            }
269            db.delete(Tables.DIRECTORIES, sb.toString(), new String[] { packageInfo.packageName });
270            db.setTransactionSuccessful();
271        } finally {
272            db.endTransaction();
273        }
274
275        mContactsProvider.resetDirectoryCache();
276        return directories.size();
277    }
278
279    /**
280     * Sends a {@link Directory#CONTENT_URI} request to a specific contact directory
281     * provider and appends all discovered directories to the directoryInfo list.
282     */
283    protected void queryDirectoriesForAuthority(
284            ArrayList<DirectoryInfo> directoryInfo, ProviderInfo provider) {
285        Uri uri = new Uri.Builder().scheme("content")
286                .authority(provider.authority).appendPath("directories").build();
287        Cursor cursor = null;
288        try {
289            cursor = mContext.getContentResolver().query(
290                    uri, DirectoryQuery.PROJECTION, null, null, null);
291            if (cursor == null) {
292                Log.i(TAG, providerDescription(provider) + " returned a NULL cursor.");
293            } else {
294                while (cursor.moveToNext()) {
295                    DirectoryInfo info = new DirectoryInfo();
296                    info.packageName = provider.packageName;
297                    info.authority = provider.authority;
298                    info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
299                    info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
300                    info.displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME);
301                    if (!cursor.isNull(DirectoryQuery.TYPE_RESOURCE_ID)) {
302                        info.typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID);
303                    }
304                    if (!cursor.isNull(DirectoryQuery.EXPORT_SUPPORT)) {
305                        int exportSupport = cursor.getInt(DirectoryQuery.EXPORT_SUPPORT);
306                        switch (exportSupport) {
307                            case Directory.EXPORT_SUPPORT_NONE:
308                            case Directory.EXPORT_SUPPORT_SAME_ACCOUNT_ONLY:
309                            case Directory.EXPORT_SUPPORT_ANY_ACCOUNT:
310                                info.exportSupport = exportSupport;
311                                break;
312                            default:
313                                Log.e(TAG, providerDescription(provider)
314                                        + " - invalid export support flag: " + exportSupport);
315                        }
316                    }
317                    if (!cursor.isNull(DirectoryQuery.SHORTCUT_SUPPORT)) {
318                        int shortcutSupport = cursor.getInt(DirectoryQuery.SHORTCUT_SUPPORT);
319                        switch (shortcutSupport) {
320                            case Directory.SHORTCUT_SUPPORT_NONE:
321                            case Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY:
322                            case Directory.SHORTCUT_SUPPORT_FULL:
323                                info.shortcutSupport = shortcutSupport;
324                                break;
325                            default:
326                                Log.e(TAG, providerDescription(provider)
327                                        + " - invalid shortcut support flag: " + shortcutSupport);
328                        }
329                    }
330                    directoryInfo.add(info);
331                }
332            }
333        } catch (Throwable t) {
334            Log.e(TAG, providerDescription(provider) + " exception", t);
335        } finally {
336            if (cursor != null) {
337                cursor.close();
338            }
339        }
340    }
341
342    /**
343     * Updates the directories tables in the database to match the info received
344     * from directory providers.
345     */
346    private void updateDirectories(SQLiteDatabase db, ArrayList<DirectoryInfo> directoryInfo) {
347
348        // Insert or replace existing directories.
349        // This happens so infrequently that we can use a less-then-optimal one-a-time approach
350        for (DirectoryInfo info : directoryInfo) {
351            ContentValues values = new ContentValues();
352            values.put(Directory.PACKAGE_NAME, info.packageName);
353            values.put(Directory.DIRECTORY_AUTHORITY, info.authority);
354            values.put(Directory.ACCOUNT_NAME, info.accountName);
355            values.put(Directory.ACCOUNT_TYPE, info.accountType);
356            values.put(Directory.TYPE_RESOURCE_ID, info.typeResourceId);
357            values.put(Directory.DISPLAY_NAME, info.displayName);
358            values.put(Directory.EXPORT_SUPPORT, info.exportSupport);
359            values.put(Directory.SHORTCUT_SUPPORT, info.shortcutSupport);
360
361            Cursor cursor = db.query(Tables.DIRECTORIES, new String[] { Directory._ID },
362                    Directory.PACKAGE_NAME + "=? AND " + Directory.DIRECTORY_AUTHORITY + "=? AND "
363                            + Directory.ACCOUNT_NAME + "=? AND " + Directory.ACCOUNT_TYPE + "=?",
364                    new String[] {
365                            info.packageName, info.authority, info.accountName, info.accountType },
366                    null, null, null);
367            try {
368                long id;
369                if (cursor.moveToFirst()) {
370                    id = cursor.getLong(0);
371                    db.update(Tables.DIRECTORIES, values, Directory._ID + "=?",
372                            new String[] { String.valueOf(id) });
373                } else {
374                    id = db.insert(Tables.DIRECTORIES, null, values);
375                }
376                info.id = id;
377            } finally {
378                cursor.close();
379            }
380        }
381    }
382
383    protected String providerDescription(ProviderInfo provider) {
384        return "Directory provider " + provider.packageName + "(" + provider.authority + ")";
385    }
386}
387