• 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.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 
buildUriMatcher()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
onSomePackagesChanged()152         public void onSomePackagesChanged() {
153             postUpdateAll();
154         }
155 
156         @Override
onPackageModified(String packageName)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
onCreate()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
createHandler(Looper looper)192     Handler createHandler(Looper looper) {
193         return new UpdateHandler(looper);
194     }
195 
196     @VisibleForTesting
197     class UpdateHandler extends Handler {
198 
UpdateHandler(Looper looper)199         public UpdateHandler(Looper looper) {
200             super(looper);
201         }
202 
203         @Override
handleMessage(Message msg)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      */
postUpdateAll()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 
postUpdatePackage(String packageName)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      */
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     @Override
query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sortOrder)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
query(Uri uri, String[] projectionIn, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal)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 
getSuggestions(String query, String[] projectionIn, CancellationSignal cancellationSignal)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      */
refreshShortcut(String shortcutId, String[] projectionIn)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 
getSearchResults(String query, String[] projectionIn, CancellationSignal cancellationSignal)396     private Cursor getSearchResults(String query, String[] projectionIn,
397             CancellationSignal cancellationSignal) {
398         return searchApplications(query, projectionIn, sSearchProjectionMap, cancellationSignal);
399     }
400 
searchApplications(String query, String[] projectionIn, Map<String, String> columnMap, CancellationSignal cancelationSignal)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 
getOrderBy(boolean zeroQuery)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")
buildTokenFilter(String filterParam)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 
buildSuggestionsProjectionMap(boolean forGlobalSearch)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 
buildSearchProjectionMap()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 
addProjection(HashMap<String, String> map, String name, String value)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      */
updateApplicationsList(String packageName)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
updateUsageStats()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 
getActivityIconUri(ActivityInfo activityInfo)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 
removeApplications(String packageName)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
insert(Uri uri, ContentValues values)619     public Uri insert(Uri uri, ContentValues values) {
620         throw new UnsupportedOperationException();
621     }
622 
623     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)624     public int update(Uri uri, ContentValues values, String selection,
625             String[] selectionArgs) {
626         throw new UnsupportedOperationException();
627     }
628 
629     @Override
delete(Uri uri, String selection, String[] selectionArgs)630     public int delete(Uri uri, String selection, String[] selectionArgs) {
631         throw new UnsupportedOperationException();
632     }
633 
getResourceUri(ApplicationInfo appInfo, int res)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 
getResourceUri(Resources resources, String appPkg, int res)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 
makeResourceUri(String appPkg, String resPkg, String type, String name)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
fetchUsageStats()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
getPackageManager()690     protected PackageManager getPackageManager() {
691         return getContext().getPackageManager();
692     }
693 
694     @VisibleForTesting
hasGlobalSearchPermission()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