ApplicationsProvider.java revision a0316493881ad09b647a4198952f2880003b8d37
1/*
2 * Copyright (C) 2009 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.applications;
18
19import android.app.SearchManager;
20import android.content.BroadcastReceiver;
21import android.content.ComponentName;
22import android.content.ContentProvider;
23import android.content.ContentResolver;
24import android.content.ContentValues;
25import android.content.Context;
26import android.content.Intent;
27import android.content.IntentFilter;
28import android.content.UriMatcher;
29import android.content.pm.PackageManager;
30import android.content.pm.ResolveInfo;
31import android.database.Cursor;
32import android.database.DatabaseUtils;
33import android.database.sqlite.SQLiteDatabase;
34import android.database.sqlite.SQLiteQueryBuilder;
35import android.net.Uri;
36import android.provider.Applications;
37import android.text.TextUtils;
38import android.util.Log;
39
40import java.util.HashMap;
41import java.util.LinkedList;
42import java.util.List;
43import java.util.concurrent.Executor;
44import java.util.concurrent.LinkedBlockingQueue;
45import java.util.concurrent.ThreadFactory;
46import java.util.concurrent.ThreadPoolExecutor;
47import java.util.concurrent.TimeUnit;
48import java.util.concurrent.atomic.AtomicInteger;
49
50/**
51 * Fetches the list of applications installed on the phone to provide search suggestions.
52 * If the functionality of this provider changes, the documentation at
53 * {@link android.provider.Applications} should be updated.
54 *
55 * TODO: this provider should be moved to the Launcher, which contains similar logic to keep an up
56 * to date list of installed applications.  Alternatively, Launcher could be updated to use this
57 * provider.
58 */
59public class ApplicationsProvider extends ContentProvider implements ThreadFactory {
60
61    private static final boolean DBG = false;
62
63    private static final String TAG = "ApplicationsProvider";
64
65    private static final int SEARCH_SUGGEST = 0;
66    private static final int SHORTCUT_REFRESH = 1;
67    private static final UriMatcher sURIMatcher = buildUriMatcher();
68
69    // TODO: Move these to android.provider.Applications?
70    public static final String _ID = "_id";
71    public static final String NAME = "name";
72    public static final String DESCRIPTION = "description";
73    public static final String PACKAGE = "package";
74    public static final String CLASS = "class";
75    public static final String ICON = "icon";
76
77    private static final String APPLICATIONS_TABLE = "applications";
78
79    private static final HashMap<String, String> sSearchSuggestionsProjectionMap =
80            buildSuggestionsProjectionMap();
81
82    private SQLiteDatabase mDb;
83    private final AtomicInteger mThreadCount = new AtomicInteger(1);
84    private Executor mExecutor;
85
86    // mQLock protects access to the list of pending updates
87    private final Object mQLock = new Object();
88    private final LinkedList<UpdateRunnable> mPending = new LinkedList<UpdateRunnable>();
89
90    /**
91     * We delay application updates by this many millis to avoid doing more than one update to the
92     * applications list within this window.
93     */
94    private static final long UPDATE_DELAY_MILLIS = 1000L;
95
96    private static UriMatcher buildUriMatcher() {
97        UriMatcher matcher =  new UriMatcher(UriMatcher.NO_MATCH);
98        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
99                SEARCH_SUGGEST);
100        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
101                SEARCH_SUGGEST);
102        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT,
103                SHORTCUT_REFRESH);
104        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
105                SHORTCUT_REFRESH);
106        return matcher;
107    }
108
109    // Broadcast receiver for updating applications list.
110    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
111        @Override
112        public void onReceive(Context context, Intent intent) {
113            String action = intent.getAction();
114            if (Intent.ACTION_PACKAGE_ADDED.equals(action)
115                    || Intent.ACTION_PACKAGE_REMOVED.equals(action)
116                    || Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
117                // do this in a worker thread to avoid ANRs
118                if (DBG) Log.d(TAG, "package update: " + intent);
119                postAppsUpdate();
120            }
121        }
122    };
123
124    @Override
125    public boolean onCreate() {
126        createDatabase();
127        registerBroadcastReceiver();
128        mExecutor = new ThreadPoolExecutor(1, 1,
129                5, TimeUnit.SECONDS,
130                new LinkedBlockingQueue<Runnable>(),
131                this);
132        postAppsUpdate();
133        return true;
134    }
135
136    // ----------
137    // BEGIN ASYC UPDATE CODE
138    // - only one update at a time
139    // - cancel any outstanding updates when a new one comes in so they become no-ops
140    // ----------
141
142    /**
143     * {@inheritDoc}
144     */
145    public Thread newThread(Runnable r) {
146        return new WorkerThread(r, "ApplicationsProvider #" + mThreadCount.getAndIncrement());
147    }
148
149    // a thread that runs with background priority
150    private static class WorkerThread extends Thread {
151
152        private WorkerThread(Runnable runnable, String threadName) {
153            super(runnable, threadName);
154        }
155
156        @Override
157        public void run() {
158            android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
159            super.run();
160        }
161    }
162
163    /**
164     * Post an update, and add it to the pending queue.  Cancel any other pending operatinos.
165     */
166    private void postAppsUpdate() {
167        final UpdateRunnable r = new UpdateRunnable();
168        synchronized (mQLock) {
169            for (UpdateRunnable updateRunnable : mPending) {
170                updateRunnable.cancel();
171            }
172            mPending.add(r);
173        }
174        mExecutor.execute(r);
175    }
176
177    private void doneRunning(UpdateRunnable runnable) {
178        synchronized (mQLock) {
179            mPending.remove(runnable);
180        }
181    }
182
183    /**
184     * Updates the applications list, unless it was cancelled.  When done, calls back to
185     * {@link ApplicationsProvider#doneRunning} do be removed from pending queue.
186     */
187    class UpdateRunnable implements Runnable {
188
189        private volatile boolean mCancelled = false;
190
191        void cancel() {
192            mCancelled = true;
193        }
194
195        public void run() {
196
197            try {
198                Thread.sleep(UPDATE_DELAY_MILLIS);
199            } catch (InterruptedException e) {
200                // not expected, but meh
201                mCancelled = true;
202            }
203
204            try {
205                if (!mCancelled) {
206                    updateApplicationsList();
207                } else if (DBG) {
208                    Log.d(TAG, "avoided applications update.");
209
210                }
211            } catch (Exception e) {
212                Log.e(TAG, "error updating applications list.", e);
213            } finally {
214                doneRunning(this);
215            }
216        }
217    }
218
219    // ----------
220    // END ASYC UPDATE CODE
221    // ----------
222
223
224    /**
225     * Creates an in-memory database for storing application info.
226     */
227    private void createDatabase() {
228        mDb = SQLiteDatabase.create(null);
229        mDb.execSQL("CREATE TABLE IF NOT EXISTS " + APPLICATIONS_TABLE + " ("+
230                _ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
231                NAME + " TEXT COLLATE LOCALIZED," +
232                DESCRIPTION + " description TEXT," +
233                PACKAGE + " TEXT," +
234                CLASS + " TEXT," +
235                ICON + " TEXT" +
236                ");");
237        // Needed for efficient update and remove
238        mDb.execSQL("CREATE INDEX applicationsComponentIndex ON " + APPLICATIONS_TABLE + " ("
239                + PACKAGE + "," + CLASS + ");");
240        // Maps token from the app name to records in the applications table
241        mDb.execSQL("CREATE TABLE applicationsLookup (" +
242                "token TEXT," +
243                "source INTEGER REFERENCES " + APPLICATIONS_TABLE + "(" + _ID + ")," +
244                "token_index INTEGER" +
245                ");");
246        mDb.execSQL("CREATE INDEX applicationsLookupIndex ON applicationsLookup (" +
247                "token," +
248                "source" +
249                ");");
250        // Triggers to keep the applicationsLookup table up to date
251        mDb.execSQL("CREATE TRIGGER applicationsLookup_update UPDATE OF " + NAME + " ON " +
252                APPLICATIONS_TABLE + " " +
253                "BEGIN " +
254                "DELETE FROM applicationsLookup WHERE source = new." + _ID + ";" +
255                "SELECT _TOKENIZE('applicationsLookup', new." + _ID + ", new." + NAME + ", ' ', 1);" +
256                "END");
257        mDb.execSQL("CREATE TRIGGER applicationsLookup_insert AFTER INSERT ON " +
258                APPLICATIONS_TABLE + " " +
259                "BEGIN " +
260                "SELECT _TOKENIZE('applicationsLookup', new." + _ID + ", new." + NAME + ", ' ', 1);" +
261                "END");
262        mDb.execSQL("CREATE TRIGGER applicationsLookup_delete DELETE ON " +
263                APPLICATIONS_TABLE + " " +
264                "BEGIN " +
265                "DELETE FROM applicationsLookup WHERE source = old." + _ID + ";" +
266                "END");
267    }
268
269    /**
270     * Registers a receiver which will be notified when packages are added, removed,
271     * or changed.
272     */
273    private void registerBroadcastReceiver() {
274        IntentFilter packageFilter = new IntentFilter();
275        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
276        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
277        packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED);
278        packageFilter.addDataScheme("package");
279        getContext().registerReceiver(mBroadcastReceiver, packageFilter);
280    }
281
282    /**
283     * This will always return {@link SearchManager#SUGGEST_MIME_TYPE} as this
284     * provider is purely to provide suggestions.
285     */
286    @Override
287    public String getType(Uri uri) {
288        return SearchManager.SUGGEST_MIME_TYPE;
289    }
290
291    /**
292     * Queries for a given search term and returns a cursor containing
293     * suggestions ordered by best match.
294     */
295    @Override
296    public Cursor query(Uri uri, String[] projectionIn, String selection,
297            String[] selectionArgs, String sortOrder) {
298        if (DBG) Log.d(TAG, "query(" + uri + ")");
299
300        if (!TextUtils.isEmpty(selection)) {
301            throw new IllegalArgumentException("selection not allowed for " + uri);
302        }
303        if (selectionArgs != null && selectionArgs.length != 0) {
304            throw new IllegalArgumentException("selectionArgs not allowed for " + uri);
305        }
306        if (!TextUtils.isEmpty(sortOrder)) {
307            throw new IllegalArgumentException("sortOrder not allowed for " + uri);
308        }
309
310        switch (sURIMatcher.match(uri)) {
311            case SEARCH_SUGGEST:
312                String query = null;
313                if (uri.getPathSegments().size() > 1) {
314                    query = uri.getLastPathSegment().toLowerCase();
315                }
316                return getSuggestions(query, projectionIn);
317            case SHORTCUT_REFRESH:
318                String shortcutId = null;
319                if (uri.getPathSegments().size() > 1) {
320                    shortcutId = uri.getLastPathSegment();
321                }
322                return refreshShortcut(shortcutId, projectionIn);
323            default:
324                throw new IllegalArgumentException("Unknown URL " + uri);
325        }
326    }
327
328    private Cursor getSuggestions(String query, String[] projectionIn) {
329        // No zero-query suggestions
330        if (TextUtils.isEmpty(query)) {
331            return null;
332        }
333
334        // Build SQL query
335        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
336        qb.setTables("applicationsLookup JOIN " + APPLICATIONS_TABLE + " ON"
337                + " applicationsLookup.source = " + APPLICATIONS_TABLE + "." + _ID);
338        qb.setProjectionMap(sSearchSuggestionsProjectionMap);
339        qb.appendWhere(buildTokenFilter(query));
340        // don't return duplicates when there are two matching tokens for an app
341        String groupBy = APPLICATIONS_TABLE + "." + _ID;
342        // order first by whether it a full prefix match, then by name
343        // MIN(token_index) != 0 is true for non-full prefix matches,
344        // and since false (0) < true(1), this expression makes sure
345        // that full prefix matches come first.
346        String order = "MIN(token_index) != 0, " + NAME;
347        Cursor cursor = qb.query(mDb, projectionIn, null, null, groupBy, null, order);
348        if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for " + query);
349        return cursor;
350    }
351
352    /**
353     * Refreshes the shortcut of an application.
354     *
355     * @param shortcutId Flattened component name of an activity.
356     */
357    private Cursor refreshShortcut(String shortcutId, String[] projectionIn) {
358        ComponentName component = ComponentName.unflattenFromString(shortcutId);
359        if (component == null) {
360            Log.w(TAG, "Bad shortcut id: " + shortcutId);
361            return null;
362        }
363        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
364        qb.setTables(APPLICATIONS_TABLE);
365        qb.setProjectionMap(sSearchSuggestionsProjectionMap);
366        qb.appendWhere("package = ? AND class = ?");
367        String[] selectionArgs = { component.getPackageName(), component.getClassName() };
368        Cursor cursor = qb.query(mDb, projectionIn, null, selectionArgs, null, null, null);
369        if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for shortcut refresh.");
370        return cursor;
371    }
372
373    @SuppressWarnings("deprecation")
374    private String buildTokenFilter(String filterParam) {
375        StringBuilder filter = new StringBuilder("token GLOB ");
376        // NOTE: Query parameters won't work here since the SQL compiler
377        // needs to parse the actual string to know that it can use the
378        // index to do a prefix scan.
379        DatabaseUtils.appendEscapedSQLString(filter,
380                DatabaseUtils.getHexCollationKey(filterParam) + "*");
381        return filter.toString();
382    }
383
384    private static HashMap<String, String> buildSuggestionsProjectionMap() {
385        HashMap<String, String> map = new HashMap<String, String>();
386        map.put(_ID, _ID);
387        map.put(SearchManager.SUGGEST_COLUMN_TEXT_1,
388                NAME + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1);
389        map.put(SearchManager.SUGGEST_COLUMN_TEXT_2,
390                DESCRIPTION + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2);
391        map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA,
392                "'content://" + Applications.AUTHORITY + "/applications/'"
393                + " || " + PACKAGE + " || '/' || " + CLASS
394                + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA);
395        map.put(SearchManager.SUGGEST_COLUMN_ICON_1,
396                ICON + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1);
397        map.put(SearchManager.SUGGEST_COLUMN_ICON_2,
398                "NULL AS " + SearchManager.SUGGEST_COLUMN_ICON_2);
399        map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
400                PACKAGE + " || '/' || " + CLASS + " AS "
401                + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
402        return map;
403    }
404
405    /**
406     * Updates the cached list of installed applications.
407     */
408    private void updateApplicationsList() {
409        // TODO: Instead of rebuilding the whole list on every change,
410        // just add, remove or update the application that has changed.
411        // Adding and updating seem tricky, since I can't see an easy way to list the
412        // launchable activities in a given package.
413        if (DBG) Log.d(TAG, "Updating database...");
414
415        DatabaseUtils.InsertHelper inserter =
416                new DatabaseUtils.InsertHelper(mDb, APPLICATIONS_TABLE);
417        int nameCol = inserter.getColumnIndex(NAME);
418        int descriptionCol = inserter.getColumnIndex(DESCRIPTION);
419        int packageCol = inserter.getColumnIndex(PACKAGE);
420        int classCol = inserter.getColumnIndex(CLASS);
421        int iconCol = inserter.getColumnIndex(ICON);
422
423        mDb.beginTransaction();
424        try {
425            mDb.execSQL("DELETE FROM " + APPLICATIONS_TABLE);
426            String description = getContext().getString(R.string.application_desc);
427            // Iterate and find all the activities which have the LAUNCHER category set.
428            Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
429            mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
430            final PackageManager manager = getContext().getPackageManager();
431            for (ResolveInfo info : manager.queryIntentActivities(mainIntent, 0)) {
432                String title = info.loadLabel(manager).toString();
433                if (TextUtils.isEmpty(title)) {
434                    title = info.activityInfo.name;
435                }
436
437                String icon;
438                if (info.activityInfo.getIconResource() != 0) {
439                    // Use a resource Uri for the icon.
440                    icon = new Uri.Builder()
441                            .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
442                            .authority(info.activityInfo.applicationInfo.packageName)
443                            .encodedPath(String.valueOf(info.activityInfo.getIconResource()))
444                            .toString();
445                } else {
446                    // No icon for app, use default app icon.
447                    icon = String.valueOf(com.android.internal.R.drawable.sym_def_app_icon);
448                }
449                inserter.prepareForInsert();
450                inserter.bind(nameCol, title);
451                inserter.bind(descriptionCol, description);
452                inserter.bind(packageCol, info.activityInfo.applicationInfo.packageName);
453                inserter.bind(classCol, info.activityInfo.name);
454                inserter.bind(iconCol, icon);
455                inserter.execute();
456            }
457            mDb.setTransactionSuccessful();
458        } finally {
459            mDb.endTransaction();
460        }
461        if (DBG) Log.d(TAG, "Finished updating database.");
462    }
463
464
465    @Override
466    public Uri insert(Uri uri, ContentValues values) {
467        throw new UnsupportedOperationException();
468    }
469
470    @Override
471    public int update(Uri uri, ContentValues values, String selection,
472            String[] selectionArgs) {
473        throw new UnsupportedOperationException();
474    }
475
476    @Override
477    public int delete(Uri uri, String selection, String[] selectionArgs) {
478        throw new UnsupportedOperationException();
479    }
480
481    /**
482     * Gets the application component name from an application URI.
483     * TODO: Move this to android.provider.Applications?
484     *
485     * @param appUri A URI of the form
486     * "content://applications/applications/&lt;packageName&gt;/&lt;className&gt;".
487     * @return The component name for the application, or
488     * <code>null</null> if the given URI was <code>null</code>
489     * or malformed.
490     */
491    public static ComponentName getComponentName(Uri appUri) {
492        if (appUri == null) {
493            return null;
494        }
495        List<String> pathSegments = appUri.getPathSegments();
496        if (pathSegments.size() < 3) {
497            return null;
498        }
499        String packageName = pathSegments.get(1);
500        String name = pathSegments.get(2);
501        return new ComponentName(packageName, name);
502    }
503
504    /**
505     * Gets the URI for an application.
506     * TODO: Move this to android.provider.Applications?
507     *
508     * @param packageName The name of the application's package.
509     * @param className The class name of the application.
510     * @return A URI of the form
511     * "content://applications/applications/&lt;packageName&gt;/&lt;className&gt;".
512     */
513    public static Uri getUri(String packageName, String className) {
514        return Applications.CONTENT_URI.buildUpon()
515                .appendEncodedPath("applications")
516                .appendPath(packageName)
517                .appendPath(className)
518                .build();
519    }
520}
521