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