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