• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Handler;
46 import android.os.HandlerThread;
47 import android.os.Looper;
48 import android.os.Message;
49 import android.provider.Applications;
50 import android.text.TextUtils;
51 import android.util.Log;
52 
53 import java.lang.Runnable;
54 import java.util.HashMap;
55 import java.util.List;
56 import java.util.Map;
57 
58 import 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  */
69 public 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 
buildUriMatcher()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
onSomePackagesChanged()151         public void onSomePackagesChanged() {
152             postUpdateAll();
153         }
154 
155         @Override
onPackageModified(String packageName)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
onCreate()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
createHandler(Looper looper)191     Handler createHandler(Looper looper) {
192         return new UpdateHandler(looper);
193     }
194 
195     @VisibleForTesting
196     class UpdateHandler extends Handler {
197 
UpdateHandler(Looper looper)198         public UpdateHandler(Looper looper) {
199             super(looper);
200         }
201 
202         @Override
handleMessage(Message msg)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      */
postUpdateAll()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 
postUpdatePackage(String packageName)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      */
createDatabase()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
getType(Uri uri)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
query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sortOrder)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 
getSuggestions(String query, String[] projectionIn)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      */
refreshShortcut(String shortcutId, String[] projectionIn)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 
getSearchResults(String query, String[] projectionIn)389     private Cursor getSearchResults(String query, String[] projectionIn) {
390         return searchApplications(query, projectionIn, sSearchProjectionMap);
391     }
392 
searchApplications(String query, String[] projectionIn, Map<String, String> columnMap)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 
getOrderBy(boolean zeroQuery)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")
buildTokenFilter(String filterParam)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 
buildSuggestionsProjectionMap(boolean forGlobalSearch)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 
buildSearchProjectionMap()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 
addProjection(HashMap<String, String> map, String name, String value)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      */
updateApplicationsList(String packageName)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
updateUsageStats()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 
getActivityIconUri(ActivityInfo activityInfo)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 
removeApplications(String packageName)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
insert(Uri uri, ContentValues values)611     public Uri insert(Uri uri, ContentValues values) {
612         throw new UnsupportedOperationException();
613     }
614 
615     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)616     public int update(Uri uri, ContentValues values, String selection,
617             String[] selectionArgs) {
618         throw new UnsupportedOperationException();
619     }
620 
621     @Override
delete(Uri uri, String selection, String[] selectionArgs)622     public int delete(Uri uri, String selection, String[] selectionArgs) {
623         throw new UnsupportedOperationException();
624     }
625 
getResourceUri(ApplicationInfo appInfo, int res)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 
getResourceUri(Resources resources, String appPkg, int res)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 
makeResourceUri(String appPkg, String resPkg, String type, String name)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
fetchUsageStats()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
getPackageManager()682     protected PackageManager getPackageManager() {
683         return getContext().getPackageManager();
684     }
685 
686     @VisibleForTesting
hasGlobalSearchPermission()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