ApplicationsProvider.java revision a0316493881ad09b647a4198952f2880003b8d37
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 17package com.android.providers.applications; 18 19import android.app.SearchManager; 20import android.content.BroadcastReceiver; 21import android.content.ComponentName; 22import android.content.ContentProvider; 23import android.content.ContentResolver; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.Intent; 27import android.content.IntentFilter; 28import android.content.UriMatcher; 29import android.content.pm.PackageManager; 30import android.content.pm.ResolveInfo; 31import android.database.Cursor; 32import android.database.DatabaseUtils; 33import android.database.sqlite.SQLiteDatabase; 34import android.database.sqlite.SQLiteQueryBuilder; 35import android.net.Uri; 36import android.provider.Applications; 37import android.text.TextUtils; 38import android.util.Log; 39 40import java.util.HashMap; 41import java.util.LinkedList; 42import java.util.List; 43import java.util.concurrent.Executor; 44import java.util.concurrent.LinkedBlockingQueue; 45import java.util.concurrent.ThreadFactory; 46import java.util.concurrent.ThreadPoolExecutor; 47import java.util.concurrent.TimeUnit; 48import java.util.concurrent.atomic.AtomicInteger; 49 50/** 51 * Fetches the list of applications installed on the phone to provide search suggestions. 52 * If the functionality of this provider changes, the documentation at 53 * {@link android.provider.Applications} should be updated. 54 * 55 * TODO: this provider should be moved to the Launcher, which contains similar logic to keep an up 56 * to date list of installed applications. Alternatively, Launcher could be updated to use this 57 * provider. 58 */ 59public class ApplicationsProvider extends ContentProvider implements ThreadFactory { 60 61 private static final boolean DBG = false; 62 63 private static final String TAG = "ApplicationsProvider"; 64 65 private static final int SEARCH_SUGGEST = 0; 66 private static final int SHORTCUT_REFRESH = 1; 67 private static final UriMatcher sURIMatcher = buildUriMatcher(); 68 69 // TODO: Move these to android.provider.Applications? 70 public static final String _ID = "_id"; 71 public static final String NAME = "name"; 72 public static final String DESCRIPTION = "description"; 73 public static final String PACKAGE = "package"; 74 public static final String CLASS = "class"; 75 public static final String ICON = "icon"; 76 77 private static final String APPLICATIONS_TABLE = "applications"; 78 79 private static final HashMap<String, String> sSearchSuggestionsProjectionMap = 80 buildSuggestionsProjectionMap(); 81 82 private SQLiteDatabase mDb; 83 private final AtomicInteger mThreadCount = new AtomicInteger(1); 84 private Executor mExecutor; 85 86 // mQLock protects access to the list of pending updates 87 private final Object mQLock = new Object(); 88 private final LinkedList<UpdateRunnable> mPending = new LinkedList<UpdateRunnable>(); 89 90 /** 91 * We delay application updates by this many millis to avoid doing more than one update to the 92 * applications list within this window. 93 */ 94 private static final long UPDATE_DELAY_MILLIS = 1000L; 95 96 private static UriMatcher buildUriMatcher() { 97 UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 98 matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY, 99 SEARCH_SUGGEST); 100 matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*", 101 SEARCH_SUGGEST); 102 matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT, 103 SHORTCUT_REFRESH); 104 matcher.addURI(Applications.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*", 105 SHORTCUT_REFRESH); 106 return matcher; 107 } 108 109 // Broadcast receiver for updating applications list. 110 private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { 111 @Override 112 public void onReceive(Context context, Intent intent) { 113 String action = intent.getAction(); 114 if (Intent.ACTION_PACKAGE_ADDED.equals(action) 115 || Intent.ACTION_PACKAGE_REMOVED.equals(action) 116 || Intent.ACTION_PACKAGE_CHANGED.equals(action)) { 117 // do this in a worker thread to avoid ANRs 118 if (DBG) Log.d(TAG, "package update: " + intent); 119 postAppsUpdate(); 120 } 121 } 122 }; 123 124 @Override 125 public boolean onCreate() { 126 createDatabase(); 127 registerBroadcastReceiver(); 128 mExecutor = new ThreadPoolExecutor(1, 1, 129 5, TimeUnit.SECONDS, 130 new LinkedBlockingQueue<Runnable>(), 131 this); 132 postAppsUpdate(); 133 return true; 134 } 135 136 // ---------- 137 // BEGIN ASYC UPDATE CODE 138 // - only one update at a time 139 // - cancel any outstanding updates when a new one comes in so they become no-ops 140 // ---------- 141 142 /** 143 * {@inheritDoc} 144 */ 145 public Thread newThread(Runnable r) { 146 return new WorkerThread(r, "ApplicationsProvider #" + mThreadCount.getAndIncrement()); 147 } 148 149 // a thread that runs with background priority 150 private static class WorkerThread extends Thread { 151 152 private WorkerThread(Runnable runnable, String threadName) { 153 super(runnable, threadName); 154 } 155 156 @Override 157 public void run() { 158 android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND); 159 super.run(); 160 } 161 } 162 163 /** 164 * Post an update, and add it to the pending queue. Cancel any other pending operatinos. 165 */ 166 private void postAppsUpdate() { 167 final UpdateRunnable r = new UpdateRunnable(); 168 synchronized (mQLock) { 169 for (UpdateRunnable updateRunnable : mPending) { 170 updateRunnable.cancel(); 171 } 172 mPending.add(r); 173 } 174 mExecutor.execute(r); 175 } 176 177 private void doneRunning(UpdateRunnable runnable) { 178 synchronized (mQLock) { 179 mPending.remove(runnable); 180 } 181 } 182 183 /** 184 * Updates the applications list, unless it was cancelled. When done, calls back to 185 * {@link ApplicationsProvider#doneRunning} do be removed from pending queue. 186 */ 187 class UpdateRunnable implements Runnable { 188 189 private volatile boolean mCancelled = false; 190 191 void cancel() { 192 mCancelled = true; 193 } 194 195 public void run() { 196 197 try { 198 Thread.sleep(UPDATE_DELAY_MILLIS); 199 } catch (InterruptedException e) { 200 // not expected, but meh 201 mCancelled = true; 202 } 203 204 try { 205 if (!mCancelled) { 206 updateApplicationsList(); 207 } else if (DBG) { 208 Log.d(TAG, "avoided applications update."); 209 210 } 211 } catch (Exception e) { 212 Log.e(TAG, "error updating applications list.", e); 213 } finally { 214 doneRunning(this); 215 } 216 } 217 } 218 219 // ---------- 220 // END ASYC UPDATE CODE 221 // ---------- 222 223 224 /** 225 * Creates an in-memory database for storing application info. 226 */ 227 private void createDatabase() { 228 mDb = SQLiteDatabase.create(null); 229 mDb.execSQL("CREATE TABLE IF NOT EXISTS " + APPLICATIONS_TABLE + " ("+ 230 _ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 231 NAME + " TEXT COLLATE LOCALIZED," + 232 DESCRIPTION + " description TEXT," + 233 PACKAGE + " TEXT," + 234 CLASS + " TEXT," + 235 ICON + " TEXT" + 236 ");"); 237 // Needed for efficient update and remove 238 mDb.execSQL("CREATE INDEX applicationsComponentIndex ON " + APPLICATIONS_TABLE + " (" 239 + PACKAGE + "," + CLASS + ");"); 240 // Maps token from the app name to records in the applications table 241 mDb.execSQL("CREATE TABLE applicationsLookup (" + 242 "token TEXT," + 243 "source INTEGER REFERENCES " + APPLICATIONS_TABLE + "(" + _ID + ")," + 244 "token_index INTEGER" + 245 ");"); 246 mDb.execSQL("CREATE INDEX applicationsLookupIndex ON applicationsLookup (" + 247 "token," + 248 "source" + 249 ");"); 250 // Triggers to keep the applicationsLookup table up to date 251 mDb.execSQL("CREATE TRIGGER applicationsLookup_update UPDATE OF " + NAME + " ON " + 252 APPLICATIONS_TABLE + " " + 253 "BEGIN " + 254 "DELETE FROM applicationsLookup WHERE source = new." + _ID + ";" + 255 "SELECT _TOKENIZE('applicationsLookup', new." + _ID + ", new." + NAME + ", ' ', 1);" + 256 "END"); 257 mDb.execSQL("CREATE TRIGGER applicationsLookup_insert AFTER INSERT ON " + 258 APPLICATIONS_TABLE + " " + 259 "BEGIN " + 260 "SELECT _TOKENIZE('applicationsLookup', new." + _ID + ", new." + NAME + ", ' ', 1);" + 261 "END"); 262 mDb.execSQL("CREATE TRIGGER applicationsLookup_delete DELETE ON " + 263 APPLICATIONS_TABLE + " " + 264 "BEGIN " + 265 "DELETE FROM applicationsLookup WHERE source = old." + _ID + ";" + 266 "END"); 267 } 268 269 /** 270 * Registers a receiver which will be notified when packages are added, removed, 271 * or changed. 272 */ 273 private void registerBroadcastReceiver() { 274 IntentFilter packageFilter = new IntentFilter(); 275 packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED); 276 packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED); 277 packageFilter.addAction(Intent.ACTION_PACKAGE_CHANGED); 278 packageFilter.addDataScheme("package"); 279 getContext().registerReceiver(mBroadcastReceiver, packageFilter); 280 } 281 282 /** 283 * This will always return {@link SearchManager#SUGGEST_MIME_TYPE} as this 284 * provider is purely to provide suggestions. 285 */ 286 @Override 287 public String getType(Uri uri) { 288 return SearchManager.SUGGEST_MIME_TYPE; 289 } 290 291 /** 292 * Queries for a given search term and returns a cursor containing 293 * suggestions ordered by best match. 294 */ 295 @Override 296 public Cursor query(Uri uri, String[] projectionIn, String selection, 297 String[] selectionArgs, String sortOrder) { 298 if (DBG) Log.d(TAG, "query(" + uri + ")"); 299 300 if (!TextUtils.isEmpty(selection)) { 301 throw new IllegalArgumentException("selection not allowed for " + uri); 302 } 303 if (selectionArgs != null && selectionArgs.length != 0) { 304 throw new IllegalArgumentException("selectionArgs not allowed for " + uri); 305 } 306 if (!TextUtils.isEmpty(sortOrder)) { 307 throw new IllegalArgumentException("sortOrder not allowed for " + uri); 308 } 309 310 switch (sURIMatcher.match(uri)) { 311 case SEARCH_SUGGEST: 312 String query = null; 313 if (uri.getPathSegments().size() > 1) { 314 query = uri.getLastPathSegment().toLowerCase(); 315 } 316 return getSuggestions(query, projectionIn); 317 case SHORTCUT_REFRESH: 318 String shortcutId = null; 319 if (uri.getPathSegments().size() > 1) { 320 shortcutId = uri.getLastPathSegment(); 321 } 322 return refreshShortcut(shortcutId, projectionIn); 323 default: 324 throw new IllegalArgumentException("Unknown URL " + uri); 325 } 326 } 327 328 private Cursor getSuggestions(String query, String[] projectionIn) { 329 // No zero-query suggestions 330 if (TextUtils.isEmpty(query)) { 331 return null; 332 } 333 334 // Build SQL query 335 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 336 qb.setTables("applicationsLookup JOIN " + APPLICATIONS_TABLE + " ON" 337 + " applicationsLookup.source = " + APPLICATIONS_TABLE + "." + _ID); 338 qb.setProjectionMap(sSearchSuggestionsProjectionMap); 339 qb.appendWhere(buildTokenFilter(query)); 340 // don't return duplicates when there are two matching tokens for an app 341 String groupBy = APPLICATIONS_TABLE + "." + _ID; 342 // order first by whether it a full prefix match, then by name 343 // MIN(token_index) != 0 is true for non-full prefix matches, 344 // and since false (0) < true(1), this expression makes sure 345 // that full prefix matches come first. 346 String order = "MIN(token_index) != 0, " + NAME; 347 Cursor cursor = qb.query(mDb, projectionIn, null, null, groupBy, null, order); 348 if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for " + query); 349 return cursor; 350 } 351 352 /** 353 * Refreshes the shortcut of an application. 354 * 355 * @param shortcutId Flattened component name of an activity. 356 */ 357 private Cursor refreshShortcut(String shortcutId, String[] projectionIn) { 358 ComponentName component = ComponentName.unflattenFromString(shortcutId); 359 if (component == null) { 360 Log.w(TAG, "Bad shortcut id: " + shortcutId); 361 return null; 362 } 363 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 364 qb.setTables(APPLICATIONS_TABLE); 365 qb.setProjectionMap(sSearchSuggestionsProjectionMap); 366 qb.appendWhere("package = ? AND class = ?"); 367 String[] selectionArgs = { component.getPackageName(), component.getClassName() }; 368 Cursor cursor = qb.query(mDb, projectionIn, null, selectionArgs, null, null, null); 369 if (DBG) Log.d(TAG, "Returning " + cursor.getCount() + " results for shortcut refresh."); 370 return cursor; 371 } 372 373 @SuppressWarnings("deprecation") 374 private String buildTokenFilter(String filterParam) { 375 StringBuilder filter = new StringBuilder("token GLOB "); 376 // NOTE: Query parameters won't work here since the SQL compiler 377 // needs to parse the actual string to know that it can use the 378 // index to do a prefix scan. 379 DatabaseUtils.appendEscapedSQLString(filter, 380 DatabaseUtils.getHexCollationKey(filterParam) + "*"); 381 return filter.toString(); 382 } 383 384 private static HashMap<String, String> buildSuggestionsProjectionMap() { 385 HashMap<String, String> map = new HashMap<String, String>(); 386 map.put(_ID, _ID); 387 map.put(SearchManager.SUGGEST_COLUMN_TEXT_1, 388 NAME + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_1); 389 map.put(SearchManager.SUGGEST_COLUMN_TEXT_2, 390 DESCRIPTION + " AS " + SearchManager.SUGGEST_COLUMN_TEXT_2); 391 map.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA, 392 "'content://" + Applications.AUTHORITY + "/applications/'" 393 + " || " + PACKAGE + " || '/' || " + CLASS 394 + " AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA); 395 map.put(SearchManager.SUGGEST_COLUMN_ICON_1, 396 ICON + " AS " + SearchManager.SUGGEST_COLUMN_ICON_1); 397 map.put(SearchManager.SUGGEST_COLUMN_ICON_2, 398 "NULL AS " + SearchManager.SUGGEST_COLUMN_ICON_2); 399 map.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, 400 PACKAGE + " || '/' || " + CLASS + " AS " 401 + SearchManager.SUGGEST_COLUMN_SHORTCUT_ID); 402 return map; 403 } 404 405 /** 406 * Updates the cached list of installed applications. 407 */ 408 private void updateApplicationsList() { 409 // TODO: Instead of rebuilding the whole list on every change, 410 // just add, remove or update the application that has changed. 411 // Adding and updating seem tricky, since I can't see an easy way to list the 412 // launchable activities in a given package. 413 if (DBG) Log.d(TAG, "Updating database..."); 414 415 DatabaseUtils.InsertHelper inserter = 416 new DatabaseUtils.InsertHelper(mDb, APPLICATIONS_TABLE); 417 int nameCol = inserter.getColumnIndex(NAME); 418 int descriptionCol = inserter.getColumnIndex(DESCRIPTION); 419 int packageCol = inserter.getColumnIndex(PACKAGE); 420 int classCol = inserter.getColumnIndex(CLASS); 421 int iconCol = inserter.getColumnIndex(ICON); 422 423 mDb.beginTransaction(); 424 try { 425 mDb.execSQL("DELETE FROM " + APPLICATIONS_TABLE); 426 String description = getContext().getString(R.string.application_desc); 427 // Iterate and find all the activities which have the LAUNCHER category set. 428 Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); 429 mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); 430 final PackageManager manager = getContext().getPackageManager(); 431 for (ResolveInfo info : manager.queryIntentActivities(mainIntent, 0)) { 432 String title = info.loadLabel(manager).toString(); 433 if (TextUtils.isEmpty(title)) { 434 title = info.activityInfo.name; 435 } 436 437 String icon; 438 if (info.activityInfo.getIconResource() != 0) { 439 // Use a resource Uri for the icon. 440 icon = new Uri.Builder() 441 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 442 .authority(info.activityInfo.applicationInfo.packageName) 443 .encodedPath(String.valueOf(info.activityInfo.getIconResource())) 444 .toString(); 445 } else { 446 // No icon for app, use default app icon. 447 icon = String.valueOf(com.android.internal.R.drawable.sym_def_app_icon); 448 } 449 inserter.prepareForInsert(); 450 inserter.bind(nameCol, title); 451 inserter.bind(descriptionCol, description); 452 inserter.bind(packageCol, info.activityInfo.applicationInfo.packageName); 453 inserter.bind(classCol, info.activityInfo.name); 454 inserter.bind(iconCol, icon); 455 inserter.execute(); 456 } 457 mDb.setTransactionSuccessful(); 458 } finally { 459 mDb.endTransaction(); 460 } 461 if (DBG) Log.d(TAG, "Finished updating database."); 462 } 463 464 465 @Override 466 public Uri insert(Uri uri, ContentValues values) { 467 throw new UnsupportedOperationException(); 468 } 469 470 @Override 471 public int update(Uri uri, ContentValues values, String selection, 472 String[] selectionArgs) { 473 throw new UnsupportedOperationException(); 474 } 475 476 @Override 477 public int delete(Uri uri, String selection, String[] selectionArgs) { 478 throw new UnsupportedOperationException(); 479 } 480 481 /** 482 * Gets the application component name from an application URI. 483 * TODO: Move this to android.provider.Applications? 484 * 485 * @param appUri A URI of the form 486 * "content://applications/applications/<packageName>/<className>". 487 * @return The component name for the application, or 488 * <code>null</null> if the given URI was <code>null</code> 489 * or malformed. 490 */ 491 public static ComponentName getComponentName(Uri appUri) { 492 if (appUri == null) { 493 return null; 494 } 495 List<String> pathSegments = appUri.getPathSegments(); 496 if (pathSegments.size() < 3) { 497 return null; 498 } 499 String packageName = pathSegments.get(1); 500 String name = pathSegments.get(2); 501 return new ComponentName(packageName, name); 502 } 503 504 /** 505 * Gets the URI for an application. 506 * TODO: Move this to android.provider.Applications? 507 * 508 * @param packageName The name of the application's package. 509 * @param className The class name of the application. 510 * @return A URI of the form 511 * "content://applications/applications/<packageName>/<className>". 512 */ 513 public static Uri getUri(String packageName, String className) { 514 return Applications.CONTENT_URI.buildUpon() 515 .appendEncodedPath("applications") 516 .appendPath(packageName) 517 .appendPath(className) 518 .build(); 519 } 520} 521