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