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