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 com.android.internal.content.PackageMonitor;
20import com.android.internal.os.PkgUsageStats;
21
22import android.app.ActivityManager;
23import android.app.AlarmManager;
24import android.app.PendingIntent;
25import android.app.SearchManager;
26import android.content.BroadcastReceiver;
27import android.content.ComponentName;
28import android.content.ContentProvider;
29import android.content.ContentResolver;
30import android.content.ContentValues;
31import android.content.Context;
32import android.content.Intent;
33import android.content.IntentFilter;
34import android.content.UriMatcher;
35import android.content.pm.ActivityInfo;
36import android.content.pm.ApplicationInfo;
37import android.content.pm.PackageManager;
38import android.content.pm.ResolveInfo;
39import android.content.res.Resources;
40import android.database.Cursor;
41import android.database.DatabaseUtils;
42import android.database.sqlite.SQLiteDatabase;
43import android.database.sqlite.SQLiteQueryBuilder;
44import android.net.Uri;
45import android.os.Handler;
46import android.os.HandlerThread;
47import android.os.Looper;
48import android.os.Message;
49import android.provider.Applications;
50import android.text.TextUtils;
51import android.util.Log;
52
53import java.lang.Runnable;
54import java.util.HashMap;
55import java.util.List;
56import java.util.Map;
57
58import com.google.common.annotations.VisibleForTesting;
59
60/**
61 * Fetches the list of applications installed on the phone to provide search suggestions.
62 * If the functionality of this provider changes, the documentation at
63 * {@link android.provider.Applications} should be updated.
64 *
65 * TODO: this provider should be moved to the Launcher, which contains similar logic to keep an up
66 * to date list of installed applications.  Alternatively, Launcher could be updated to use this
67 * provider.
68 */
69public class ApplicationsProvider extends ContentProvider {
70
71    private static final boolean DBG = false;
72
73    private static final String TAG = "ApplicationsProvider";
74
75    private static final int SEARCH_SUGGEST = 0;
76    private static final int SHORTCUT_REFRESH = 1;
77    private static final int SEARCH = 2;
78
79    private static final UriMatcher sURIMatcher = buildUriMatcher();
80
81    private static final int THREAD_PRIORITY = android.os.Process.THREAD_PRIORITY_BACKGROUND;
82
83    // Messages for mHandler
84    private static final int MSG_UPDATE_ALL = 0;
85    private static final int MSG_UPDATE_PACKAGE = 1;
86
87    public static final String _ID = "_id";
88    public static final String NAME = "name";
89    public static final String DESCRIPTION = "description";
90    public static final String PACKAGE = "package";
91    public static final String CLASS = "class";
92    public static final String ICON = "icon";
93    public static final String LAUNCH_COUNT = "launch_count";
94    public static final String LAST_RESUME_TIME = "last_resume_time";
95
96    // A query parameter to refresh application statistics. Used by QSB.
97    public static final String REFRESH_STATS = "refresh";
98
99    private static final String APPLICATIONS_TABLE = "applications";
100
101    private static final String APPLICATIONS_LOOKUP_JOIN =
102            "applicationsLookup JOIN " + APPLICATIONS_TABLE + " ON"
103            + " applicationsLookup.source = " + APPLICATIONS_TABLE + "." + _ID;
104
105    private static final HashMap<String, String> sSearchSuggestionsProjectionMap =
106            buildSuggestionsProjectionMap(false);
107    private static final HashMap<String, String> sGlobalSearchSuggestionsProjectionMap =
108            buildSuggestionsProjectionMap(true);
109    private static final HashMap<String, String> sSearchProjectionMap =
110            buildSearchProjectionMap();
111
112    /**
113     * An in-memory database storing the details of applications installed on
114     * the device. Populated when the ApplicationsProvider is launched.
115     */
116    private SQLiteDatabase mDb;
117
118    // Handler that runs DB updates.
119    private Handler mHandler;
120
121    /**
122     * We delay application updates by this many millis to avoid doing more than one update to the
123     * applications list within this window.
124     */
125    private static final long UPDATE_DELAY_MILLIS = 1000L;
126
127    private static UriMatcher buildUriMatcher() {
128        UriMatcher matcher =  new UriMatcher(UriMatcher.NO_MATCH);
129        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
130                SEARCH_SUGGEST);
131        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
132                SEARCH_SUGGEST);
133        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT,
134                SHORTCUT_REFRESH);
135        matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
136                SHORTCUT_REFRESH);
137        matcher.addURI(Applications.AUTHORITY, Applications.SEARCH_PATH,
138                SEARCH);
139        matcher.addURI(Applications.AUTHORITY, Applications.SEARCH_PATH + "/*",
140                SEARCH);
141        return matcher;
142    }
143
144    /**
145     * Updates applications list when packages are added/removed.
146     *
147     * TODO: Maybe this should listen for changes to individual apps instead.
148     */
149    private class MyPackageMonitor extends PackageMonitor {
150        @Override
151        public void onSomePackagesChanged() {
152            postUpdateAll();
153        }
154
155        @Override
156        public void onPackageModified(String packageName) {
157            postUpdatePackage(packageName);
158        }
159    }
160
161    // Broadcast receiver for updating applications list when the locale changes.
162    private BroadcastReceiver mLocaleChangeReceiver = new BroadcastReceiver() {
163        @Override
164        public void onReceive(Context context, Intent intent) {
165            String action = intent.getAction();
166            if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
167                if (DBG) Log.d(TAG, "locale changed");
168                postUpdateAll();
169            }
170        }
171    };
172
173    @Override
174    public boolean onCreate() {
175        createDatabase();
176        // Start thread that runs app updates
177        HandlerThread thread = new HandlerThread("ApplicationsProviderUpdater", THREAD_PRIORITY);
178        thread.start();
179        mHandler = createHandler(thread.getLooper());
180        // Kick off first apps update
181        postUpdateAll();
182        // Listen for package changes
183        new MyPackageMonitor().register(getContext(), null, true);
184        // Listen for locale changes
185        IntentFilter localeFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
186        getContext().registerReceiver(mLocaleChangeReceiver, localeFilter);
187        return true;
188    }
189
190    @VisibleForTesting
191    Handler createHandler(Looper looper) {
192        return new UpdateHandler(looper);
193    }
194
195    @VisibleForTesting
196    class UpdateHandler extends Handler {
197
198        public UpdateHandler(Looper looper) {
199            super(looper);
200        }
201
202        @Override
203        public void handleMessage(Message msg) {
204            switch (msg.what) {
205                case MSG_UPDATE_ALL:
206                    updateApplicationsList(null);
207                    break;
208                case MSG_UPDATE_PACKAGE:
209                    updateApplicationsList((String) msg.obj);
210                    break;
211                default:
212                    Log.e(TAG, "Unknown message: " + msg.what);
213                    break;
214            }
215        }
216    }
217
218    /**
219     * Posts an update to run on the DB update thread.
220     */
221    private void postUpdateAll() {
222        // Clear pending updates
223        mHandler.removeMessages(MSG_UPDATE_ALL);
224        // Post a new update
225        Message msg = Message.obtain();
226        msg.what = MSG_UPDATE_ALL;
227        mHandler.sendMessageDelayed(msg, UPDATE_DELAY_MILLIS);
228    }
229
230    private void postUpdatePackage(String packageName) {
231        Message msg = Message.obtain();
232        msg.what = MSG_UPDATE_PACKAGE;
233        msg.obj = packageName;
234        mHandler.sendMessageDelayed(msg, UPDATE_DELAY_MILLIS);
235    }
236
237    // ----------
238    // END ASYC UPDATE CODE
239    // ----------
240
241
242    /**
243     * Creates an in-memory database for storing application info.
244     */
245    private void createDatabase() {
246        mDb = SQLiteDatabase.create(null);
247        mDb.execSQL("CREATE TABLE IF NOT EXISTS " + APPLICATIONS_TABLE + " ("+
248                _ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
249                NAME + " TEXT COLLATE LOCALIZED," +
250                DESCRIPTION + " description TEXT," +
251                PACKAGE + " TEXT," +
252                CLASS + " TEXT," +
253                ICON + " TEXT," +
254                LAUNCH_COUNT + " INTEGER DEFAULT 0," +
255                LAST_RESUME_TIME + " INTEGER DEFAULT 0" +
256                ");");
257        // Needed for efficient update and remove
258        mDb.execSQL("CREATE INDEX applicationsComponentIndex ON " + APPLICATIONS_TABLE + " ("
259                + PACKAGE + "," + CLASS + ");");
260        // Maps token from the app name to records in the applications table
261        mDb.execSQL("CREATE TABLE applicationsLookup (" +
262                "token TEXT," +
263                "source INTEGER REFERENCES " + APPLICATIONS_TABLE + "(" + _ID + ")," +
264                "token_index INTEGER" +
265                ");");
266        mDb.execSQL("CREATE INDEX applicationsLookupIndex ON applicationsLookup (" +
267                "token," +
268                "source" +
269                ");");
270        // Triggers to keep the applicationsLookup table up to date
271        mDb.execSQL("CREATE TRIGGER applicationsLookup_update UPDATE OF " + NAME + " ON " +
272                APPLICATIONS_TABLE + " " +
273                "BEGIN " +
274                "DELETE FROM applicationsLookup WHERE source = new." + _ID + ";" +
275                "SELECT _TOKENIZE('applicationsLookup', new." + _ID + ", new." + NAME + ", ' ', 1);" +
276                "END");
277        mDb.execSQL("CREATE TRIGGER applicationsLookup_insert AFTER INSERT ON " +
278                APPLICATIONS_TABLE + " " +
279                "BEGIN " +
280                "SELECT _TOKENIZE('applicationsLookup', new." + _ID + ", new." + NAME + ", ' ', 1);" +
281                "END");
282        mDb.execSQL("CREATE TRIGGER applicationsLookup_delete DELETE ON " +
283                APPLICATIONS_TABLE + " " +
284                "BEGIN " +
285                "DELETE FROM applicationsLookup WHERE source = old." + _ID + ";" +
286                "END");
287    }
288
289    /**
290     * This will always return {@link SearchManager#SUGGEST_MIME_TYPE} as this
291     * provider is purely to provide suggestions.
292     */
293    @Override
294    public String getType(Uri uri) {
295        switch (sURIMatcher.match(uri)) {
296            case SEARCH_SUGGEST:
297                return SearchManager.SUGGEST_MIME_TYPE;
298            case SHORTCUT_REFRESH:
299                return SearchManager.SHORTCUT_MIME_TYPE;
300            case SEARCH:
301                return Applications.APPLICATION_DIR_TYPE;
302            default:
303                throw new IllegalArgumentException("URL " + uri + " doesn't support querying.");
304        }
305    }
306
307    /**
308     * Queries for a given search term and returns a cursor containing
309     * suggestions ordered by best match.
310     */
311    @Override
312    public Cursor query(Uri uri, String[] projectionIn, String selection,
313            String[] selectionArgs, String sortOrder) {
314        if (DBG) Log.d(TAG, "query(" + uri + ")");
315
316        if (!TextUtils.isEmpty(selection)) {
317            throw new IllegalArgumentException("selection not allowed for " + uri);
318        }
319        if (selectionArgs != null && selectionArgs.length != 0) {
320            throw new IllegalArgumentException("selectionArgs not allowed for " + uri);
321        }
322        if (!TextUtils.isEmpty(sortOrder)) {
323            throw new IllegalArgumentException("sortOrder not allowed for " + uri);
324        }
325
326        switch (sURIMatcher.match(uri)) {
327            case SEARCH_SUGGEST: {
328                String query = null;
329                if (uri.getPathSegments().size() > 1) {
330                    query = uri.getLastPathSegment().toLowerCase();
331                }
332                if (uri.getQueryParameter(REFRESH_STATS) != null) {
333                    updateUsageStats();
334                }
335                return getSuggestions(query, projectionIn);
336            }
337            case SHORTCUT_REFRESH: {
338                String shortcutId = null;
339                if (uri.getPathSegments().size() > 1) {
340                    shortcutId = uri.getLastPathSegment();
341                }
342                return refreshShortcut(shortcutId, projectionIn);
343            }
344            case SEARCH: {
345                String query = null;
346                if (uri.getPathSegments().size() > 1) {
347                    query = uri.getLastPathSegment().toLowerCase();
348                }
349                return getSearchResults(query, projectionIn);
350            }
351            default:
352                throw new IllegalArgumentException("URL " + uri + " doesn't support querying.");
353        }
354    }
355
356    private Cursor getSuggestions(String query, String[] projectionIn) {
357        Map<String, String> projectionMap = sSearchSuggestionsProjectionMap;
358        // No zero-query suggestions or launch times except for global search,
359        // to avoid leaking info about apps that have been used.
360        if (hasGlobalSearchPermission()) {
361            projectionMap = sGlobalSearchSuggestionsProjectionMap;
362        } else if (TextUtils.isEmpty(query)) {
363            return null;
364        }
365        return searchApplications(query, projectionIn, projectionMap);
366    }
367
368    /**
369     * Refreshes the shortcut of an application.
370     *
371     * @param shortcutId Flattened component name of an activity.
372     */
373    private Cursor refreshShortcut(String shortcutId, String[] projectionIn) {
374        ComponentName component = ComponentName.unflattenFromString(shortcutId);
375        if (component == null) {
376            Log.w(TAG, "Bad shortcut id: " + shortcutId);
377            return null;
378        }
379        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
380        qb.setTables(APPLICATIONS_TABLE);
381        qb.setProjectionMap(sSearchSuggestionsProjectionMap);
382        qb.appendWhere("package = ? AND class = ?");
383        String[] selectionArgs = { component.getPackageName(), component.getClassName() };
384        Cursor cursor = qb.query(mDb, projectionIn, null, selectionArgs, null, null, null);
385        if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for shortcut refresh.");
386        return cursor;
387    }
388
389    private Cursor getSearchResults(String query, String[] projectionIn) {
390        return searchApplications(query, projectionIn, sSearchProjectionMap);
391    }
392
393    private Cursor searchApplications(String query, String[] projectionIn,
394            Map<String, String> columnMap) {
395        final boolean zeroQuery = TextUtils.isEmpty(query);
396        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
397        qb.setTables(APPLICATIONS_LOOKUP_JOIN);
398        qb.setProjectionMap(columnMap);
399        String orderBy = null;
400        if (!zeroQuery) {
401            qb.appendWhere(buildTokenFilter(query));
402        } else {
403            if (hasGlobalSearchPermission()) {
404                qb.appendWhere(LAST_RESUME_TIME + " > 0");
405            }
406        }
407        if (!hasGlobalSearchPermission()) {
408            orderBy = getOrderBy(zeroQuery);
409        }
410        // don't return duplicates when there are two matching tokens for an app
411        String groupBy = APPLICATIONS_TABLE + "." + _ID;
412        Cursor cursor = qb.query(mDb, projectionIn, null, null, groupBy, null, orderBy);
413        if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for " + query);
414        return cursor;
415    }
416
417    private String getOrderBy(boolean zeroQuery) {
418        // order first by whether it a full prefix match, then by launch
419        // count (if allowed, frequently used apps rank higher), then name
420        // MIN(token_index) != 0 is true for non-full prefix matches,
421        // and since false (0) < true(1), this expression makes sure
422        // that full prefix matches come first.
423        StringBuilder orderBy = new StringBuilder();
424        if (!zeroQuery) {
425            orderBy.append("MIN(token_index) != 0, ");
426        }
427
428        if (hasGlobalSearchPermission()) {
429            orderBy.append(LAST_RESUME_TIME + " DESC, ");
430        }
431
432        orderBy.append(NAME);
433
434        return orderBy.toString();
435    }
436
437    @SuppressWarnings("deprecation")
438    private String buildTokenFilter(String filterParam) {
439        StringBuilder filter = new StringBuilder("token GLOB ");
440        // NOTE: Query parameters won't work here since the SQL compiler
441        // needs to parse the actual string to know that it can use the
442        // index to do a prefix scan.
443        DatabaseUtils.appendEscapedSQLString(filter,
444                DatabaseUtils.getHexCollationKey(filterParam) + "*");
445        return filter.toString();
446    }
447
448    private static HashMap<String, String> buildSuggestionsProjectionMap(boolean forGlobalSearch) {
449        HashMap<String, String> map = new HashMap<String, String>();
450        addProjection(map, Applications.ApplicationColumns._ID, _ID);
451        addProjection(map, SearchManager.SUGGEST_COLUMN_TEXT_1, NAME);
452        addProjection(map, SearchManager.SUGGEST_COLUMN_TEXT_2, DESCRIPTION);
453        addProjection(map, SearchManager.SUGGEST_COLUMN_INTENT_DATA,
454                "'content://" + Applications.AUTHORITY + "/applications/'"
455                + " || " + PACKAGE + " || '/' || " + CLASS);
456        addProjection(map, SearchManager.SUGGEST_COLUMN_ICON_1, ICON);
457        addProjection(map, SearchManager.SUGGEST_COLUMN_ICON_2, "NULL");
458        addProjection(map, SearchManager.SUGGEST_COLUMN_SHORTCUT_ID,
459                PACKAGE + " || '/' || " + CLASS);
460        if (forGlobalSearch) {
461            addProjection(map, SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT,
462                    LAST_RESUME_TIME);
463        }
464        return map;
465    }
466
467    private static HashMap<String, String> buildSearchProjectionMap() {
468        HashMap<String, String> map = new HashMap<String, String>();
469        addProjection(map, Applications.ApplicationColumns._ID, _ID);
470        addProjection(map, Applications.ApplicationColumns.NAME, NAME);
471        addProjection(map, Applications.ApplicationColumns.ICON, ICON);
472        addProjection(map, Applications.ApplicationColumns.URI,
473                "'content://" + Applications.AUTHORITY + "/applications/'"
474                + " || " + PACKAGE + " || '/' || " + CLASS);
475        return map;
476    }
477
478    private static void addProjection(HashMap<String, String> map, String name, String value) {
479        if (!value.equals(name)) {
480            value = value + " AS " + name;
481        }
482        map.put(name, value);
483    }
484
485    /**
486     * Updates the cached list of installed applications.
487     *
488     * @param packageName Name of package whose activities to update.
489     *        If {@code null}, all packages are updated.
490     */
491    private synchronized void updateApplicationsList(String packageName) {
492        if (DBG) Log.d(TAG, "Updating database (packageName = " + packageName + ")...");
493
494        DatabaseUtils.InsertHelper inserter =
495                new DatabaseUtils.InsertHelper(mDb, APPLICATIONS_TABLE);
496        int nameCol = inserter.getColumnIndex(NAME);
497        int descriptionCol = inserter.getColumnIndex(DESCRIPTION);
498        int packageCol = inserter.getColumnIndex(PACKAGE);
499        int classCol = inserter.getColumnIndex(CLASS);
500        int iconCol = inserter.getColumnIndex(ICON);
501        int launchCountCol = inserter.getColumnIndex(LAUNCH_COUNT);
502        int lastResumeTimeCol = inserter.getColumnIndex(LAST_RESUME_TIME);
503
504        Map<String, PkgUsageStats> usageStats = fetchUsageStats();
505
506        mDb.beginTransaction();
507        try {
508            removeApplications(packageName);
509            String description = getContext().getString(R.string.application_desc);
510            // Iterate and find all the activities which have the LAUNCHER category set.
511            Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
512            mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
513            if (packageName != null) {
514                // Limit to activities in the package, if given
515                mainIntent.setPackage(packageName);
516            }
517            final PackageManager manager = getPackageManager();
518            List<ResolveInfo> activities = manager.queryIntentActivities(mainIntent, 0);
519            int activityCount = activities == null ? 0 : activities.size();
520            for (int i = 0; i < activityCount; i++) {
521                ResolveInfo info = activities.get(i);
522                String title = info.loadLabel(manager).toString();
523                String activityClassName = info.activityInfo.name;
524                if (TextUtils.isEmpty(title)) {
525                    title = activityClassName;
526                }
527
528                String activityPackageName = info.activityInfo.applicationInfo.packageName;
529                if (DBG) Log.d(TAG, "activity " + activityPackageName + "/" + activityClassName);
530                PkgUsageStats stats = usageStats.get(activityPackageName);
531                int launchCount = 0;
532                long lastResumeTime = 0;
533                if (stats != null) {
534                    launchCount = stats.launchCount;
535                    if (stats.componentResumeTimes.containsKey(activityClassName)) {
536                        lastResumeTime = stats.componentResumeTimes.get(activityClassName);
537                    }
538                }
539
540                String icon = getActivityIconUri(info.activityInfo);
541                inserter.prepareForInsert();
542                inserter.bind(nameCol, title);
543                inserter.bind(descriptionCol, description);
544                inserter.bind(packageCol, activityPackageName);
545                inserter.bind(classCol, activityClassName);
546                inserter.bind(iconCol, icon);
547                inserter.bind(launchCountCol, launchCount);
548                inserter.bind(lastResumeTimeCol, lastResumeTime);
549                inserter.execute();
550            }
551            mDb.setTransactionSuccessful();
552        } finally {
553            mDb.endTransaction();
554            inserter.close();
555        }
556
557        if (DBG) Log.d(TAG, "Finished updating database.");
558    }
559
560    @VisibleForTesting
561    protected synchronized void updateUsageStats() {
562        if (DBG) Log.d(TAG, "Update application usage stats.");
563        Map<String, PkgUsageStats> usageStats = fetchUsageStats();
564
565        mDb.beginTransaction();
566        try {
567            for (Map.Entry<String, PkgUsageStats> statsEntry : usageStats.entrySet()) {
568                ContentValues updatedLaunchCount = new ContentValues();
569                String packageName = statsEntry.getKey();
570                PkgUsageStats stats = statsEntry.getValue();
571                updatedLaunchCount.put(LAUNCH_COUNT, stats.launchCount);
572
573                mDb.update(APPLICATIONS_TABLE, updatedLaunchCount,
574                        PACKAGE + " = ?", new String[] { packageName });
575
576                for (Map.Entry<String, Long> crtEntry: stats.componentResumeTimes.entrySet()) {
577                    ContentValues updatedLastResumeTime = new ContentValues();
578                    String componentName = crtEntry.getKey();
579                    updatedLastResumeTime.put(LAST_RESUME_TIME, crtEntry.getValue());
580
581                    mDb.update(APPLICATIONS_TABLE, updatedLastResumeTime,
582                            PACKAGE + " = ? AND " + CLASS + " = ?",
583                            new String[] { packageName, componentName });
584                }
585            }
586            mDb.setTransactionSuccessful();
587        } finally {
588            mDb.endTransaction();
589        }
590
591        if (DBG) Log.d(TAG, "Finished updating application usage stats in database.");
592    }
593
594    private String getActivityIconUri(ActivityInfo activityInfo) {
595        int icon = activityInfo.getIconResource();
596        if (icon == 0) return null;
597        Uri uri = getResourceUri(activityInfo.applicationInfo, icon);
598        return uri == null ? null : uri.toString();
599    }
600
601    private void removeApplications(String packageName) {
602        if (packageName == null) {
603            mDb.delete(APPLICATIONS_TABLE, null, null);
604        } else {
605            mDb.delete(APPLICATIONS_TABLE, PACKAGE + " = ?", new String[] { packageName });
606        }
607
608    }
609
610    @Override
611    public Uri insert(Uri uri, ContentValues values) {
612        throw new UnsupportedOperationException();
613    }
614
615    @Override
616    public int update(Uri uri, ContentValues values, String selection,
617            String[] selectionArgs) {
618        throw new UnsupportedOperationException();
619    }
620
621    @Override
622    public int delete(Uri uri, String selection, String[] selectionArgs) {
623        throw new UnsupportedOperationException();
624    }
625
626    private Uri getResourceUri(ApplicationInfo appInfo, int res) {
627        try {
628            Resources resources = getPackageManager().getResourcesForApplication(appInfo);
629            return getResourceUri(resources, appInfo.packageName, res);
630        } catch (PackageManager.NameNotFoundException e) {
631            return null;
632        } catch (Resources.NotFoundException e) {
633            return null;
634        }
635    }
636
637    private static Uri getResourceUri(Resources resources, String appPkg, int res)
638            throws Resources.NotFoundException {
639        String resPkg = resources.getResourcePackageName(res);
640        String type = resources.getResourceTypeName(res);
641        String name = resources.getResourceEntryName(res);
642        return makeResourceUri(appPkg, resPkg, type, name);
643    }
644
645    private static Uri makeResourceUri(String appPkg, String resPkg, String type, String name)
646            throws Resources.NotFoundException {
647        Uri.Builder uriBuilder = new Uri.Builder();
648        uriBuilder.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE);
649        uriBuilder.encodedAuthority(appPkg);
650        uriBuilder.appendEncodedPath(type);
651        if (!appPkg.equals(resPkg)) {
652            uriBuilder.appendEncodedPath(resPkg + ":" + name);
653        } else {
654            uriBuilder.appendEncodedPath(name);
655        }
656        return uriBuilder.build();
657    }
658
659    @VisibleForTesting
660    protected Map<String, PkgUsageStats> fetchUsageStats() {
661        try {
662            ActivityManager activityManager = (ActivityManager)
663                    getContext().getSystemService(Context.ACTIVITY_SERVICE);
664
665            if (activityManager != null) {
666                Map<String, PkgUsageStats> stats = new HashMap<String, PkgUsageStats>();
667                PkgUsageStats[] pkgUsageStats = activityManager.getAllPackageUsageStats();
668                if (pkgUsageStats != null) {
669                    for (PkgUsageStats pus : pkgUsageStats) {
670                        stats.put(pus.packageName, pus);
671                    }
672                }
673                return stats;
674            }
675        } catch (Exception e) {
676            Log.w(TAG, "Could not fetch usage stats", e);
677        }
678        return new HashMap<String, PkgUsageStats>();
679    }
680
681    @VisibleForTesting
682    protected PackageManager getPackageManager() {
683        return getContext().getPackageManager();
684    }
685
686    @VisibleForTesting
687    protected boolean hasGlobalSearchPermission() {
688        // Only the global-search system is allowed to see the usage stats of
689        // applications. Without this restriction the ApplicationsProvider
690        // could leak information about the user's behavior to applications.
691        return (PackageManager.PERMISSION_GRANTED ==
692                getContext().checkCallingPermission(android.Manifest.permission.GLOBAL_SEARCH));
693    }
694
695}
696