LauncherModel.java revision cb1a4778686a46c46d8dc88b6c83674f6fac6592
1/* 2 * Copyright (C) 2008 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.launcher2; 18 19import android.app.SearchManager; 20import android.appwidget.AppWidgetManager; 21import android.appwidget.AppWidgetProviderInfo; 22import android.content.BroadcastReceiver; 23import android.content.ComponentName; 24import android.content.ContentProviderClient; 25import android.content.ContentResolver; 26import android.content.ContentValues; 27import android.content.Context; 28import android.content.Intent; 29import android.content.Intent.ShortcutIconResource; 30import android.content.pm.ActivityInfo; 31import android.content.pm.PackageManager; 32import android.content.pm.ResolveInfo; 33import android.content.res.Resources; 34import android.database.Cursor; 35import android.graphics.Bitmap; 36import android.graphics.BitmapFactory; 37import android.net.Uri; 38import android.os.Environment; 39import android.os.Handler; 40import android.os.HandlerThread; 41import android.os.Parcelable; 42import android.os.Process; 43import android.os.RemoteException; 44import android.os.SystemClock; 45import android.util.Log; 46 47import com.android.launcher.R; 48import com.android.launcher2.InstallWidgetReceiver.WidgetMimeTypeHandlerData; 49 50import java.lang.ref.WeakReference; 51import java.net.URISyntaxException; 52import java.text.Collator; 53import java.util.ArrayList; 54import java.util.Collections; 55import java.util.Comparator; 56import java.util.HashMap; 57import java.util.List; 58import java.util.Locale; 59 60/** 61 * Maintains in-memory state of the Launcher. It is expected that there should be only one 62 * LauncherModel object held in a static. Also provide APIs for updating the database state 63 * for the Launcher. 64 */ 65public class LauncherModel extends BroadcastReceiver { 66 static final boolean DEBUG_LOADERS = false; 67 static final String TAG = "Launcher.Model"; 68 69 private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons 70 private final boolean mAppsCanBeOnExternalStorage; 71 private int mBatchSize; // 0 is all apps at once 72 private int mAllAppsLoadDelay; // milliseconds between batches 73 74 private final LauncherApplication mApp; 75 private final Object mLock = new Object(); 76 private DeferredHandler mHandler = new DeferredHandler(); 77 private LoaderTask mLoaderTask; 78 79 private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); 80 static { 81 sWorkerThread.start(); 82 } 83 private static final Handler sWorker = new Handler(sWorkerThread.getLooper()); 84 85 // We start off with everything not loaded. After that, we assume that 86 // our monitoring of the package manager provides all updates and we never 87 // need to do a requery. These are only ever touched from the loader thread. 88 private boolean mWorkspaceLoaded; 89 private boolean mAllAppsLoaded; 90 91 private WeakReference<Callbacks> mCallbacks; 92 93 // < only access in worker thread > 94 private AllAppsList mAllAppsList; 95 96 // sItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by 97 // LauncherModel to their ids 98 static final HashMap<Long, ItemInfo> sItemsIdMap = new HashMap<Long, ItemInfo>(); 99 100 // sItems is passed to bindItems, which expects a list of all folders and shortcuts created by 101 // LauncherModel that are directly on the home screen (however, no widgets or shortcuts 102 // within folders). 103 static final ArrayList<ItemInfo> sWorkspaceItems = new ArrayList<ItemInfo>(); 104 105 // sAppWidgets is all LauncherAppWidgetInfo created by LauncherModel. Passed to bindAppWidget() 106 static final ArrayList<LauncherAppWidgetInfo> sAppWidgets = 107 new ArrayList<LauncherAppWidgetInfo>(); 108 109 // sFolders is all FolderInfos created by LauncherModel. Passed to bindFolders() 110 static final HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>(); 111 // </ only access in worker thread > 112 113 private IconCache mIconCache; 114 private Bitmap mDefaultIcon; 115 116 private static int mCellCountX; 117 private static int mCellCountY; 118 119 public interface Callbacks { 120 public boolean setLoadOnResume(); 121 public int getCurrentWorkspaceScreen(); 122 public void startBinding(); 123 public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end); 124 public void bindFolders(HashMap<Long,FolderInfo> folders); 125 public void finishBindingItems(); 126 public void bindAppWidget(LauncherAppWidgetInfo info); 127 public void bindAllApplications(ArrayList<ApplicationInfo> apps); 128 public void bindAppsAdded(ArrayList<ApplicationInfo> apps); 129 public void bindAppsUpdated(ArrayList<ApplicationInfo> apps); 130 public void bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent); 131 public void bindPackagesUpdated(); 132 public boolean isAllAppsVisible(); 133 public void bindSearchablesChanged(); 134 } 135 136 LauncherModel(LauncherApplication app, IconCache iconCache) { 137 mAppsCanBeOnExternalStorage = !Environment.isExternalStorageEmulated(); 138 mApp = app; 139 mAllAppsList = new AllAppsList(iconCache); 140 mIconCache = iconCache; 141 142 mDefaultIcon = Utilities.createIconBitmap( 143 mIconCache.getFullResDefaultActivityIcon(), app); 144 145 mAllAppsLoadDelay = app.getResources().getInteger(R.integer.config_allAppsBatchLoadDelay); 146 147 mBatchSize = app.getResources().getInteger(R.integer.config_allAppsBatchSize); 148 } 149 150 public Bitmap getFallbackIcon() { 151 return Bitmap.createBitmap(mDefaultIcon); 152 } 153 154 public static void unbindWorkspaceItems() { 155 for (ItemInfo item: sWorkspaceItems) { 156 item.unbind(); 157 } 158 } 159 160 /** 161 * Adds an item to the DB if it was not created previously, or move it to a new 162 * <container, screen, cellX, cellY> 163 */ 164 static void addOrMoveItemInDatabase(Context context, ItemInfo item, long container, 165 int screen, int cellX, int cellY) { 166 if (item.container == ItemInfo.NO_ID) { 167 // From all apps 168 addItemToDatabase(context, item, container, screen, cellX, cellY, false); 169 } else { 170 // From somewhere else 171 moveItemInDatabase(context, item, container, screen, cellX, cellY); 172 } 173 } 174 175 /** 176 * Move an item in the DB to a new <container, screen, cellX, cellY> 177 */ 178 static void moveItemInDatabase(Context context, final ItemInfo item, final long container, 179 final int screen, final int cellX, final int cellY) { 180 item.container = container; 181 item.screen = screen; 182 item.cellX = cellX; 183 item.cellY = cellY; 184 185 final Uri uri = LauncherSettings.Favorites.getContentUri(item.id, false); 186 final ContentValues values = new ContentValues(); 187 final ContentResolver cr = context.getContentResolver(); 188 189 values.put(LauncherSettings.Favorites.CONTAINER, item.container); 190 values.put(LauncherSettings.Favorites.CELLX, cellX); 191 values.put(LauncherSettings.Favorites.CELLY, cellY); 192 values.put(LauncherSettings.Favorites.SCREEN, item.screen); 193 194 sWorker.post(new Runnable() { 195 public void run() { 196 cr.update(uri, values, null, null); 197 ItemInfo modelItem = sItemsIdMap.get(item.id); 198 if (item != modelItem) { 199 // the modelItem needs to match up perfectly with item if our model is to be 200 // consistent with the database-- for now, just require modelItem == item 201 throw new RuntimeException("Error: ItemInfo passed to moveItemInDatabase " + 202 "doesn't match original"); 203 } 204 205 // Items are added/removed from the corresponding FolderInfo elsewhere, such 206 // as in Workspace.onDrop. Here, we just add/remove them from the list of items 207 // that are on the desktop, as appropriate 208 if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 209 if (!sWorkspaceItems.contains(modelItem)) { 210 sWorkspaceItems.add(modelItem); 211 } 212 } else { 213 sWorkspaceItems.remove(modelItem); 214 } 215 } 216 }); 217 } 218 219 /** 220 * Resize an item in the DB to a new <spanX, spanY, cellX, cellY> 221 */ 222 static void resizeItemInDatabase(Context context, final ItemInfo item, final int cellX, 223 final int cellY, final int spanX, final int spanY) { 224 item.spanX = spanX; 225 item.spanY = spanY; 226 item.cellX = cellX; 227 item.cellY = cellY; 228 229 final Uri uri = LauncherSettings.Favorites.getContentUri(item.id, false); 230 final ContentValues values = new ContentValues(); 231 final ContentResolver cr = context.getContentResolver(); 232 233 values.put(LauncherSettings.Favorites.CONTAINER, item.container); 234 values.put(LauncherSettings.Favorites.SPANX, spanX); 235 values.put(LauncherSettings.Favorites.SPANY, spanY); 236 values.put(LauncherSettings.Favorites.CELLX, cellX); 237 values.put(LauncherSettings.Favorites.CELLY, cellY); 238 239 sWorker.post(new Runnable() { 240 public void run() { 241 cr.update(uri, values, null, null); 242 ItemInfo modelItem = sItemsIdMap.get(item.id); 243 if (item != modelItem) { 244 // the modelItem needs to match up perfectly with item if our model is to be 245 // consistent with the database-- for now, just require modelItem == item 246 throw new RuntimeException("Error: ItemInfo passed to moveItemInDatabase " + 247 "doesn't match original"); 248 } 249 } 250 }); 251 } 252 253 /** 254 * Returns true if the shortcuts already exists in the database. 255 * we identify a shortcut by its title and intent. 256 */ 257 static boolean shortcutExists(Context context, String title, Intent intent) { 258 final ContentResolver cr = context.getContentResolver(); 259 Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, 260 new String[] { "title", "intent" }, "title=? and intent=?", 261 new String[] { title, intent.toUri(0) }, null); 262 boolean result = false; 263 try { 264 result = c.moveToFirst(); 265 } finally { 266 c.close(); 267 } 268 return result; 269 } 270 271 /** 272 * Returns an ItemInfo array containing all the items in the LauncherModel. 273 * The ItemInfo.id is not set through this function. 274 */ 275 static ArrayList<ItemInfo> getItemsInLocalCoordinates(Context context) { 276 ArrayList<ItemInfo> items = new ArrayList<ItemInfo>(); 277 final ContentResolver cr = context.getContentResolver(); 278 Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, new String[] { 279 LauncherSettings.Favorites.ITEM_TYPE, LauncherSettings.Favorites.CONTAINER, 280 LauncherSettings.Favorites.SCREEN, LauncherSettings.Favorites.CELLX, LauncherSettings.Favorites.CELLY, 281 LauncherSettings.Favorites.SPANX, LauncherSettings.Favorites.SPANY }, null, null, null); 282 283 final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); 284 final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); 285 final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); 286 final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); 287 final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); 288 final int spanXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANX); 289 final int spanYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SPANY); 290 291 try { 292 while (c.moveToNext()) { 293 ItemInfo item = new ItemInfo(); 294 item.cellX = c.getInt(cellXIndex); 295 item.cellY = c.getInt(cellYIndex); 296 item.spanX = c.getInt(spanXIndex); 297 item.spanY = c.getInt(spanYIndex); 298 item.container = c.getInt(containerIndex); 299 item.itemType = c.getInt(itemTypeIndex); 300 item.screen = c.getInt(screenIndex); 301 302 items.add(item); 303 } 304 } catch (Exception e) { 305 items.clear(); 306 } finally { 307 c.close(); 308 } 309 310 return items; 311 } 312 313 /** 314 * Find a folder in the db, creating the FolderInfo if necessary, and adding it to folderList. 315 */ 316 FolderInfo getFolderById(Context context, HashMap<Long,FolderInfo> folderList, long id) { 317 final ContentResolver cr = context.getContentResolver(); 318 Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, null, 319 "_id=? and (itemType=? or itemType=?)", 320 new String[] { String.valueOf(id), 321 String.valueOf(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)}, null); 322 323 try { 324 if (c.moveToFirst()) { 325 final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); 326 final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); 327 final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); 328 final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); 329 final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); 330 final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); 331 332 FolderInfo folderInfo = null; 333 switch (c.getInt(itemTypeIndex)) { 334 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: 335 folderInfo = findOrMakeFolder(folderList, id); 336 break; 337 } 338 339 folderInfo.title = c.getString(titleIndex); 340 folderInfo.id = id; 341 folderInfo.container = c.getInt(containerIndex); 342 folderInfo.screen = c.getInt(screenIndex); 343 folderInfo.cellX = c.getInt(cellXIndex); 344 folderInfo.cellY = c.getInt(cellYIndex); 345 346 return folderInfo; 347 } 348 } finally { 349 c.close(); 350 } 351 352 return null; 353 } 354 355 /** 356 * Add an item to the database in a specified container. Sets the container, screen, cellX and 357 * cellY fields of the item. Also assigns an ID to the item. 358 */ 359 static void addItemToDatabase(Context context, final ItemInfo item, long container, 360 int screen, int cellX, int cellY, final boolean notify) { 361 item.container = container; 362 item.screen = screen; 363 item.cellX = cellX; 364 item.cellY = cellY; 365 366 final ContentValues values = new ContentValues(); 367 final ContentResolver cr = context.getContentResolver(); 368 item.onAddToDatabase(values); 369 370 Launcher l = (Launcher) context; 371 LauncherApplication app = (LauncherApplication) l.getApplication(); 372 item.id = app.getLauncherProvider().generateNewId(); 373 values.put(LauncherSettings.Favorites._ID, item.id); 374 item.updateValuesWithCoordinates(values, cellX, cellY); 375 376 sWorker.post(new Runnable() { 377 public void run() { 378 cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI : 379 LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values); 380 381 sItemsIdMap.put(item.id, item); 382 switch (item.itemType) { 383 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: 384 sFolders.put(item.id, (FolderInfo) item); 385 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 386 sWorkspaceItems.add(item); 387 } 388 break; 389 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: 390 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: 391 if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) { 392 sWorkspaceItems.add(item); 393 } 394 break; 395 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: 396 sAppWidgets.add((LauncherAppWidgetInfo) item); 397 break; 398 } 399 } 400 }); 401 } 402 403 /** 404 * Creates a new unique child id, for a given cell span across all layouts. 405 */ 406 static int getCellLayoutChildId( 407 int cellId, int screen, int localCellX, int localCellY, int spanX, int spanY) { 408 return ((cellId & 0xFF) << 24) 409 | (screen & 0xFF) << 16 | (localCellX & 0xFF) << 8 | (localCellY & 0xFF); 410 } 411 412 static int getCellCountX() { 413 return mCellCountX; 414 } 415 416 static int getCellCountY() { 417 return mCellCountY; 418 } 419 420 /** 421 * Updates the model orientation helper to take into account the current layout dimensions 422 * when performing local/canonical coordinate transformations. 423 */ 424 static void updateWorkspaceLayoutCells(int shortAxisCellCount, int longAxisCellCount) { 425 mCellCountX = shortAxisCellCount; 426 mCellCountY = longAxisCellCount; 427 } 428 429 /** 430 * Update an item to the database in a specified container. 431 */ 432 static void updateItemInDatabase(Context context, final ItemInfo item) { 433 final ContentValues values = new ContentValues(); 434 final ContentResolver cr = context.getContentResolver(); 435 436 item.onAddToDatabase(values); 437 item.updateValuesWithCoordinates(values, item.cellX, item.cellY); 438 439 sWorker.post(new Runnable() { 440 public void run() { 441 cr.update(LauncherSettings.Favorites.getContentUri(item.id, false), 442 values, null, null); 443 final ItemInfo modelItem = sItemsIdMap.get(item.id); 444 if (item != modelItem) { 445 // the modelItem needs to match up perfectly with item if our model is to be 446 // consistent with the database-- for now, just require modelItem == item 447 throw new RuntimeException("Error: ItemInfo passed to moveItemInDatabase " + 448 "doesn't match original"); 449 } 450 } 451 }); 452 } 453 454 /** 455 * Removes the specified item from the database 456 * @param context 457 * @param item 458 */ 459 static void deleteItemFromDatabase(Context context, final ItemInfo item) { 460 final ContentResolver cr = context.getContentResolver(); 461 final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false); 462 sWorker.post(new Runnable() { 463 public void run() { 464 cr.delete(uriToDelete, null, null); 465 switch (item.itemType) { 466 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: 467 sFolders.remove(item.id); 468 sWorkspaceItems.remove(item); 469 break; 470 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: 471 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: 472 sWorkspaceItems.remove(item); 473 break; 474 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: 475 sAppWidgets.remove((LauncherAppWidgetInfo) item); 476 break; 477 } 478 sItemsIdMap.remove(item.id); 479 } 480 }); 481 } 482 483 /** 484 * Remove the contents of the specified folder from the database 485 */ 486 static void deleteFolderContentsFromDatabase(Context context, final FolderInfo info) { 487 final ContentResolver cr = context.getContentResolver(); 488 489 sWorker.post(new Runnable() { 490 public void run() { 491 cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null); 492 sItemsIdMap.remove(info.id); 493 sFolders.remove(info.id); 494 sWorkspaceItems.remove(info); 495 496 cr.delete(LauncherSettings.Favorites.CONTENT_URI, 497 LauncherSettings.Favorites.CONTAINER + "=" + info.id, null); 498 for (ItemInfo childInfo : info.contents) { 499 sItemsIdMap.remove(childInfo.id); 500 } 501 } 502 }); 503 } 504 505 /** 506 * Set this as the current Launcher activity object for the loader. 507 */ 508 public void initialize(Callbacks callbacks) { 509 synchronized (mLock) { 510 mCallbacks = new WeakReference<Callbacks>(callbacks); 511 } 512 } 513 514 /** 515 * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and 516 * ACTION_PACKAGE_CHANGED. 517 */ 518 @Override 519 public void onReceive(Context context, Intent intent) { 520 if (DEBUG_LOADERS) Log.d(TAG, "onReceive intent=" + intent); 521 522 final String action = intent.getAction(); 523 524 if (Intent.ACTION_PACKAGE_CHANGED.equals(action) 525 || Intent.ACTION_PACKAGE_REMOVED.equals(action) 526 || Intent.ACTION_PACKAGE_ADDED.equals(action)) { 527 final String packageName = intent.getData().getSchemeSpecificPart(); 528 final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false); 529 530 int op = PackageUpdatedTask.OP_NONE; 531 532 if (packageName == null || packageName.length() == 0) { 533 // they sent us a bad intent 534 return; 535 } 536 537 if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) { 538 op = PackageUpdatedTask.OP_UPDATE; 539 } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) { 540 if (!replacing) { 541 op = PackageUpdatedTask.OP_REMOVE; 542 } 543 // else, we are replacing the package, so a PACKAGE_ADDED will be sent 544 // later, we will update the package at this time 545 } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) { 546 if (!replacing) { 547 op = PackageUpdatedTask.OP_ADD; 548 } else { 549 op = PackageUpdatedTask.OP_UPDATE; 550 } 551 } 552 553 if (op != PackageUpdatedTask.OP_NONE) { 554 enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName })); 555 } 556 557 } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) { 558 // First, schedule to add these apps back in. 559 String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); 560 enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages)); 561 // Then, rebind everything. 562 startLoaderFromBackground(); 563 } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { 564 String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST); 565 enqueuePackageUpdated(new PackageUpdatedTask( 566 PackageUpdatedTask.OP_UNAVAILABLE, packages)); 567 } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { 568 // If we have changed locale we need to clear out the labels in all apps. 569 // Do this here because if the launcher activity is running it will be restarted. 570 // If it's not running startLoaderFromBackground will merely tell it that it needs 571 // to reload. Either way, mAllAppsLoaded will be cleared so it re-reads everything 572 // next time. 573 mAllAppsLoaded = false; 574 mWorkspaceLoaded = false; 575 startLoaderFromBackground(); 576 } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action)) { 577 Callbacks callbacks = mCallbacks.get(); 578 callbacks.bindSearchablesChanged(); 579 } 580 } 581 582 /** 583 * When the launcher is in the background, it's possible for it to miss paired 584 * configuration changes. So whenever we trigger the loader from the background 585 * tell the launcher that it needs to re-run the loader when it comes back instead 586 * of doing it now. 587 */ 588 public void startLoaderFromBackground() { 589 boolean runLoader = false; 590 if (mCallbacks != null) { 591 Callbacks callbacks = mCallbacks.get(); 592 if (callbacks != null) { 593 // Only actually run the loader if they're not paused. 594 if (!callbacks.setLoadOnResume()) { 595 runLoader = true; 596 } 597 } 598 } 599 if (runLoader) { 600 startLoader(mApp, false); 601 } 602 } 603 604 public void startLoader(Context context, boolean isLaunching) { 605 synchronized (mLock) { 606 if (DEBUG_LOADERS) { 607 Log.d(TAG, "startLoader isLaunching=" + isLaunching); 608 } 609 610 // Don't bother to start the thread if we know it's not going to do anything 611 if (mCallbacks != null && mCallbacks.get() != null) { 612 // If there is already one running, tell it to stop. 613 LoaderTask oldTask = mLoaderTask; 614 if (oldTask != null) { 615 if (oldTask.isLaunching()) { 616 // don't downgrade isLaunching if we're already running 617 isLaunching = true; 618 } 619 oldTask.stopLocked(); 620 } 621 mLoaderTask = new LoaderTask(context, isLaunching); 622 sWorker.post(mLoaderTask); 623 } 624 } 625 } 626 627 public void stopLoader() { 628 synchronized (mLock) { 629 if (mLoaderTask != null) { 630 mLoaderTask.stopLocked(); 631 } 632 } 633 } 634 635 /** 636 * Runnable for the thread that loads the contents of the launcher: 637 * - workspace icons 638 * - widgets 639 * - all apps icons 640 */ 641 private class LoaderTask implements Runnable { 642 private Context mContext; 643 private Thread mWaitThread; 644 private boolean mIsLaunching; 645 private boolean mStopped; 646 private boolean mLoadAndBindStepFinished; 647 private HashMap<Object, CharSequence> mLabelCache; 648 649 LoaderTask(Context context, boolean isLaunching) { 650 mContext = context; 651 mIsLaunching = isLaunching; 652 mLabelCache = new HashMap<Object, CharSequence>(); 653 } 654 655 boolean isLaunching() { 656 return mIsLaunching; 657 } 658 659 private void loadAndBindWorkspace() { 660 // Load the workspace 661 if (DEBUG_LOADERS) { 662 Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded); 663 } 664 665 if (!mWorkspaceLoaded) { 666 loadWorkspace(); 667 if (mStopped) { 668 return; 669 } 670 mWorkspaceLoaded = true; 671 } 672 673 // Bind the workspace 674 bindWorkspace(); 675 } 676 677 private void waitForIdle() { 678 // Wait until the either we're stopped or the other threads are done. 679 // This way we don't start loading all apps until the workspace has settled 680 // down. 681 synchronized (LoaderTask.this) { 682 final long workspaceWaitTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; 683 684 mHandler.postIdle(new Runnable() { 685 public void run() { 686 synchronized (LoaderTask.this) { 687 mLoadAndBindStepFinished = true; 688 if (DEBUG_LOADERS) { 689 Log.d(TAG, "done with previous binding step"); 690 } 691 LoaderTask.this.notify(); 692 } 693 } 694 }); 695 696 while (!mStopped && !mLoadAndBindStepFinished) { 697 try { 698 this.wait(); 699 } catch (InterruptedException ex) { 700 // Ignore 701 } 702 } 703 if (DEBUG_LOADERS) { 704 Log.d(TAG, "waited " 705 + (SystemClock.uptimeMillis()-workspaceWaitTime) 706 + "ms for previous step to finish binding"); 707 } 708 } 709 } 710 711 public void run() { 712 // Optimize for end-user experience: if the Launcher is up and // running with the 713 // All Apps interface in the foreground, load All Apps first. Otherwise, load the 714 // workspace first (default). 715 final Callbacks cbk = mCallbacks.get(); 716 final boolean loadWorkspaceFirst = cbk != null ? (!cbk.isAllAppsVisible()) : true; 717 718 keep_running: { 719 // Elevate priority when Home launches for the first time to avoid 720 // starving at boot time. Staring at a blank home is not cool. 721 synchronized (mLock) { 722 android.os.Process.setThreadPriority(mIsLaunching 723 ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND); 724 } 725 if (loadWorkspaceFirst) { 726 if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace"); 727 loadAndBindWorkspace(); 728 } else { 729 if (DEBUG_LOADERS) Log.d(TAG, "step 1: special: loading all apps"); 730 loadAndBindAllApps(); 731 } 732 733 if (mStopped) { 734 break keep_running; 735 } 736 737 // Whew! Hard work done. Slow us down, and wait until the UI thread has 738 // settled down. 739 synchronized (mLock) { 740 if (mIsLaunching) { 741 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 742 } 743 } 744 waitForIdle(); 745 746 // second step 747 if (loadWorkspaceFirst) { 748 if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps"); 749 loadAndBindAllApps(); 750 } else { 751 if (DEBUG_LOADERS) Log.d(TAG, "step 2: special: loading workspace"); 752 loadAndBindWorkspace(); 753 } 754 } 755 756 // Clear out this reference, otherwise we end up holding it until all of the 757 // callback runnables are done. 758 mContext = null; 759 760 synchronized (mLock) { 761 // If we are still the last one to be scheduled, remove ourselves. 762 if (mLoaderTask == this) { 763 mLoaderTask = null; 764 } 765 } 766 } 767 768 public void stopLocked() { 769 synchronized (LoaderTask.this) { 770 mStopped = true; 771 this.notify(); 772 } 773 } 774 775 /** 776 * Gets the callbacks object. If we've been stopped, or if the launcher object 777 * has somehow been garbage collected, return null instead. Pass in the Callbacks 778 * object that was around when the deferred message was scheduled, and if there's 779 * a new Callbacks object around then also return null. This will save us from 780 * calling onto it with data that will be ignored. 781 */ 782 Callbacks tryGetCallbacks(Callbacks oldCallbacks) { 783 synchronized (mLock) { 784 if (mStopped) { 785 return null; 786 } 787 788 if (mCallbacks == null) { 789 return null; 790 } 791 792 final Callbacks callbacks = mCallbacks.get(); 793 if (callbacks != oldCallbacks) { 794 return null; 795 } 796 if (callbacks == null) { 797 Log.w(TAG, "no mCallbacks"); 798 return null; 799 } 800 801 return callbacks; 802 } 803 } 804 805 // check & update map of what's occupied; used to discard overlapping/invalid items 806 private boolean checkItemPlacement(ItemInfo occupied[][][], ItemInfo item) { 807 if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { 808 return true; 809 } 810 for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { 811 for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { 812 if (occupied[item.screen][x][y] != null) { 813 Log.e(TAG, "Error loading shortcut " + item 814 + " into cell (" + item.screen + ":" 815 + x + "," + y 816 + ") occupied by " 817 + occupied[item.screen][x][y]); 818 return false; 819 } 820 } 821 } 822 for (int x = item.cellX; x < (item.cellX+item.spanX); x++) { 823 for (int y = item.cellY; y < (item.cellY+item.spanY); y++) { 824 occupied[item.screen][x][y] = item; 825 } 826 } 827 return true; 828 } 829 830 private void loadWorkspace() { 831 final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; 832 833 final Context context = mContext; 834 final ContentResolver contentResolver = context.getContentResolver(); 835 final PackageManager manager = context.getPackageManager(); 836 final AppWidgetManager widgets = AppWidgetManager.getInstance(context); 837 final boolean isSafeMode = manager.isSafeMode(); 838 839 sWorkspaceItems.clear(); 840 sAppWidgets.clear(); 841 sFolders.clear(); 842 sItemsIdMap.clear(); 843 844 final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); 845 846 final Cursor c = contentResolver.query( 847 LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); 848 849 final ItemInfo occupied[][][] = 850 new ItemInfo[Launcher.SCREEN_COUNT][mCellCountX][mCellCountY]; 851 852 try { 853 final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); 854 final int intentIndex = c.getColumnIndexOrThrow 855 (LauncherSettings.Favorites.INTENT); 856 final int titleIndex = c.getColumnIndexOrThrow 857 (LauncherSettings.Favorites.TITLE); 858 final int iconTypeIndex = c.getColumnIndexOrThrow( 859 LauncherSettings.Favorites.ICON_TYPE); 860 final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); 861 final int iconPackageIndex = c.getColumnIndexOrThrow( 862 LauncherSettings.Favorites.ICON_PACKAGE); 863 final int iconResourceIndex = c.getColumnIndexOrThrow( 864 LauncherSettings.Favorites.ICON_RESOURCE); 865 final int containerIndex = c.getColumnIndexOrThrow( 866 LauncherSettings.Favorites.CONTAINER); 867 final int itemTypeIndex = c.getColumnIndexOrThrow( 868 LauncherSettings.Favorites.ITEM_TYPE); 869 final int appWidgetIdIndex = c.getColumnIndexOrThrow( 870 LauncherSettings.Favorites.APPWIDGET_ID); 871 final int screenIndex = c.getColumnIndexOrThrow( 872 LauncherSettings.Favorites.SCREEN); 873 final int cellXIndex = c.getColumnIndexOrThrow 874 (LauncherSettings.Favorites.CELLX); 875 final int cellYIndex = c.getColumnIndexOrThrow 876 (LauncherSettings.Favorites.CELLY); 877 final int spanXIndex = c.getColumnIndexOrThrow 878 (LauncherSettings.Favorites.SPANX); 879 final int spanYIndex = c.getColumnIndexOrThrow( 880 LauncherSettings.Favorites.SPANY); 881 final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); 882 final int displayModeIndex = c.getColumnIndexOrThrow( 883 LauncherSettings.Favorites.DISPLAY_MODE); 884 885 ShortcutInfo info; 886 String intentDescription; 887 LauncherAppWidgetInfo appWidgetInfo; 888 int container; 889 long id; 890 Intent intent; 891 892 while (!mStopped && c.moveToNext()) { 893 try { 894 int itemType = c.getInt(itemTypeIndex); 895 896 switch (itemType) { 897 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: 898 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: 899 intentDescription = c.getString(intentIndex); 900 try { 901 intent = Intent.parseUri(intentDescription, 0); 902 } catch (URISyntaxException e) { 903 continue; 904 } 905 906 if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { 907 info = getShortcutInfo(manager, intent, context, c, iconIndex, 908 titleIndex, mLabelCache); 909 } else { 910 info = getShortcutInfo(c, context, iconTypeIndex, 911 iconPackageIndex, iconResourceIndex, iconIndex, 912 titleIndex); 913 } 914 915 if (info != null) { 916 info.intent = intent; 917 info.id = c.getLong(idIndex); 918 container = c.getInt(containerIndex); 919 info.container = container; 920 info.screen = c.getInt(screenIndex); 921 info.cellX = c.getInt(cellXIndex); 922 info.cellY = c.getInt(cellYIndex); 923 924 // check & update map of what's occupied 925 if (!checkItemPlacement(occupied, info)) { 926 break; 927 } 928 929 switch (container) { 930 case LauncherSettings.Favorites.CONTAINER_DESKTOP: 931 sWorkspaceItems.add(info); 932 break; 933 default: 934 // Item is in a user folder 935 FolderInfo folderInfo = 936 findOrMakeFolder(sFolders, container); 937 folderInfo.add(info); 938 break; 939 } 940 sItemsIdMap.put(info.id, info); 941 942 // now that we've loaded everthing re-save it with the 943 // icon in case it disappears somehow. 944 updateSavedIcon(context, info, c, iconIndex); 945 } else { 946 // Failed to load the shortcut, probably because the 947 // activity manager couldn't resolve it (maybe the app 948 // was uninstalled), or the db row was somehow screwed up. 949 // Delete it. 950 id = c.getLong(idIndex); 951 Log.e(TAG, "Error loading shortcut " + id + ", removing it"); 952 contentResolver.delete(LauncherSettings.Favorites.getContentUri( 953 id, false), null, null); 954 } 955 break; 956 957 case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: 958 id = c.getLong(idIndex); 959 FolderInfo folderInfo = findOrMakeFolder(sFolders, id); 960 961 folderInfo.title = c.getString(titleIndex); 962 folderInfo.id = id; 963 container = c.getInt(containerIndex); 964 folderInfo.container = container; 965 folderInfo.screen = c.getInt(screenIndex); 966 folderInfo.cellX = c.getInt(cellXIndex); 967 folderInfo.cellY = c.getInt(cellYIndex); 968 969 // check & update map of what's occupied 970 if (!checkItemPlacement(occupied, folderInfo)) { 971 break; 972 } 973 switch (container) { 974 case LauncherSettings.Favorites.CONTAINER_DESKTOP: 975 sWorkspaceItems.add(folderInfo); 976 break; 977 } 978 979 sItemsIdMap.put(folderInfo.id, folderInfo); 980 sFolders.put(folderInfo.id, folderInfo); 981 break; 982 983 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: 984 // Read all Launcher-specific widget details 985 int appWidgetId = c.getInt(appWidgetIdIndex); 986 id = c.getLong(idIndex); 987 988 final AppWidgetProviderInfo provider = 989 widgets.getAppWidgetInfo(appWidgetId); 990 991 if (!isSafeMode && (provider == null || provider.provider == null || 992 provider.provider.getPackageName() == null)) { 993 Log.e(TAG, "Deleting widget that isn't installed anymore: id=" 994 + id + " appWidgetId=" + appWidgetId); 995 itemsToRemove.add(id); 996 } else { 997 appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId); 998 appWidgetInfo.id = id; 999 appWidgetInfo.screen = c.getInt(screenIndex); 1000 appWidgetInfo.cellX = c.getInt(cellXIndex); 1001 appWidgetInfo.cellY = c.getInt(cellYIndex); 1002 appWidgetInfo.spanX = c.getInt(spanXIndex); 1003 appWidgetInfo.spanY = c.getInt(spanYIndex); 1004 1005 container = c.getInt(containerIndex); 1006 if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) { 1007 Log.e(TAG, "Widget found where container " 1008 + "!= CONTAINER_DESKTOP -- ignoring!"); 1009 continue; 1010 } 1011 appWidgetInfo.container = c.getInt(containerIndex); 1012 1013 // check & update map of what's occupied 1014 if (!checkItemPlacement(occupied, appWidgetInfo)) { 1015 break; 1016 } 1017 sItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); 1018 sAppWidgets.add(appWidgetInfo); 1019 } 1020 break; 1021 } 1022 } catch (Exception e) { 1023 Log.w(TAG, "Desktop items loading interrupted:", e); 1024 } 1025 } 1026 } finally { 1027 c.close(); 1028 } 1029 1030 if (itemsToRemove.size() > 0) { 1031 ContentProviderClient client = contentResolver.acquireContentProviderClient( 1032 LauncherSettings.Favorites.CONTENT_URI); 1033 // Remove dead items 1034 for (long id : itemsToRemove) { 1035 if (DEBUG_LOADERS) { 1036 Log.d(TAG, "Removed id = " + id); 1037 } 1038 // Don't notify content observers 1039 try { 1040 client.delete(LauncherSettings.Favorites.getContentUri(id, false), 1041 null, null); 1042 } catch (RemoteException e) { 1043 Log.w(TAG, "Could not remove id = " + id); 1044 } 1045 } 1046 } 1047 1048 if (DEBUG_LOADERS) { 1049 Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms"); 1050 Log.d(TAG, "workspace layout: "); 1051 for (int y = 0; y < mCellCountY; y++) { 1052 String line = ""; 1053 for (int s = 0; s < Launcher.SCREEN_COUNT; s++) { 1054 if (s > 0) { 1055 line += " | "; 1056 } 1057 for (int x = 0; x < mCellCountX; x++) { 1058 line += ((occupied[s][x][y] != null) ? "#" : "."); 1059 } 1060 } 1061 Log.d(TAG, "[ " + line + " ]"); 1062 } 1063 } 1064 } 1065 1066 /** 1067 * Read everything out of our database. 1068 */ 1069 private void bindWorkspace() { 1070 final long t = SystemClock.uptimeMillis(); 1071 1072 // Don't use these two variables in any of the callback runnables. 1073 // Otherwise we hold a reference to them. 1074 final Callbacks oldCallbacks = mCallbacks.get(); 1075 if (oldCallbacks == null) { 1076 // This launcher has exited and nobody bothered to tell us. Just bail. 1077 Log.w(TAG, "LoaderTask running with no launcher"); 1078 return; 1079 } 1080 1081 int N; 1082 // Tell the workspace that we're about to start firing items at it 1083 mHandler.post(new Runnable() { 1084 public void run() { 1085 Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1086 if (callbacks != null) { 1087 callbacks.startBinding(); 1088 } 1089 } 1090 }); 1091 // Add the items to the workspace. 1092 N = sWorkspaceItems.size(); 1093 for (int i=0; i<N; i+=ITEMS_CHUNK) { 1094 final int start = i; 1095 final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i); 1096 mHandler.post(new Runnable() { 1097 public void run() { 1098 Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1099 if (callbacks != null) { 1100 callbacks.bindItems(sWorkspaceItems, start, start+chunkSize); 1101 } 1102 } 1103 }); 1104 } 1105 mHandler.post(new Runnable() { 1106 public void run() { 1107 Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1108 if (callbacks != null) { 1109 callbacks.bindFolders(sFolders); 1110 } 1111 } 1112 }); 1113 // Wait until the queue goes empty. 1114 mHandler.post(new Runnable() { 1115 public void run() { 1116 if (DEBUG_LOADERS) { 1117 Log.d(TAG, "Going to start binding widgets soon."); 1118 } 1119 } 1120 }); 1121 // Bind the widgets, one at a time. 1122 // WARNING: this is calling into the workspace from the background thread, 1123 // but since getCurrentScreen() just returns the int, we should be okay. This 1124 // is just a hint for the order, and if it's wrong, we'll be okay. 1125 // TODO: instead, we should have that push the current screen into here. 1126 final int currentScreen = oldCallbacks.getCurrentWorkspaceScreen(); 1127 N = sAppWidgets.size(); 1128 // once for the current screen 1129 for (int i=0; i<N; i++) { 1130 final LauncherAppWidgetInfo widget = sAppWidgets.get(i); 1131 if (widget.screen == currentScreen) { 1132 mHandler.post(new Runnable() { 1133 public void run() { 1134 Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1135 if (callbacks != null) { 1136 callbacks.bindAppWidget(widget); 1137 } 1138 } 1139 }); 1140 } 1141 } 1142 // once for the other screens 1143 for (int i=0; i<N; i++) { 1144 final LauncherAppWidgetInfo widget = sAppWidgets.get(i); 1145 if (widget.screen != currentScreen) { 1146 mHandler.post(new Runnable() { 1147 public void run() { 1148 Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1149 if (callbacks != null) { 1150 callbacks.bindAppWidget(widget); 1151 } 1152 } 1153 }); 1154 } 1155 } 1156 // Tell the workspace that we're done. 1157 mHandler.post(new Runnable() { 1158 public void run() { 1159 Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1160 if (callbacks != null) { 1161 callbacks.finishBindingItems(); 1162 } 1163 } 1164 }); 1165 // If we're profiling, this is the last thing in the queue. 1166 mHandler.post(new Runnable() { 1167 public void run() { 1168 if (DEBUG_LOADERS) { 1169 Log.d(TAG, "bound workspace in " 1170 + (SystemClock.uptimeMillis()-t) + "ms"); 1171 } 1172 } 1173 }); 1174 } 1175 1176 private void loadAndBindAllApps() { 1177 if (DEBUG_LOADERS) { 1178 Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded); 1179 } 1180 if (!mAllAppsLoaded) { 1181 loadAllAppsByBatch(); 1182 if (mStopped) { 1183 return; 1184 } 1185 mAllAppsLoaded = true; 1186 } else { 1187 onlyBindAllApps(); 1188 } 1189 } 1190 1191 private void onlyBindAllApps() { 1192 final Callbacks oldCallbacks = mCallbacks.get(); 1193 if (oldCallbacks == null) { 1194 // This launcher has exited and nobody bothered to tell us. Just bail. 1195 Log.w(TAG, "LoaderTask running with no launcher (onlyBindAllApps)"); 1196 return; 1197 } 1198 1199 // shallow copy 1200 final ArrayList<ApplicationInfo> list 1201 = (ArrayList<ApplicationInfo>)mAllAppsList.data.clone(); 1202 mHandler.post(new Runnable() { 1203 public void run() { 1204 final long t = SystemClock.uptimeMillis(); 1205 final Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1206 if (callbacks != null) { 1207 callbacks.bindAllApplications(list); 1208 } 1209 if (DEBUG_LOADERS) { 1210 Log.d(TAG, "bound all " + list.size() + " apps from cache in " 1211 + (SystemClock.uptimeMillis()-t) + "ms"); 1212 } 1213 } 1214 }); 1215 1216 } 1217 1218 private void loadAllAppsByBatch() { 1219 final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; 1220 1221 // Don't use these two variables in any of the callback runnables. 1222 // Otherwise we hold a reference to them. 1223 final Callbacks oldCallbacks = mCallbacks.get(); 1224 if (oldCallbacks == null) { 1225 // This launcher has exited and nobody bothered to tell us. Just bail. 1226 Log.w(TAG, "LoaderTask running with no launcher (loadAllAppsByBatch)"); 1227 return; 1228 } 1229 1230 final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); 1231 mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); 1232 1233 final PackageManager packageManager = mContext.getPackageManager(); 1234 List<ResolveInfo> apps = null; 1235 1236 int N = Integer.MAX_VALUE; 1237 1238 int startIndex; 1239 int i=0; 1240 int batchSize = -1; 1241 while (i < N && !mStopped) { 1242 if (i == 0) { 1243 mAllAppsList.clear(); 1244 final long qiaTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; 1245 apps = packageManager.queryIntentActivities(mainIntent, 0); 1246 if (DEBUG_LOADERS) { 1247 Log.d(TAG, "queryIntentActivities took " 1248 + (SystemClock.uptimeMillis()-qiaTime) + "ms"); 1249 } 1250 if (apps == null) { 1251 return; 1252 } 1253 N = apps.size(); 1254 if (DEBUG_LOADERS) { 1255 Log.d(TAG, "queryIntentActivities got " + N + " apps"); 1256 } 1257 if (N == 0) { 1258 // There are no apps?!? 1259 return; 1260 } 1261 if (mBatchSize == 0) { 1262 batchSize = N; 1263 } else { 1264 batchSize = mBatchSize; 1265 } 1266 1267 final long sortTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; 1268 Collections.sort(apps, 1269 new LauncherModel.ShortcutNameComparator(packageManager, mLabelCache)); 1270 if (DEBUG_LOADERS) { 1271 Log.d(TAG, "sort took " 1272 + (SystemClock.uptimeMillis()-sortTime) + "ms"); 1273 } 1274 } 1275 1276 final long t2 = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0; 1277 1278 startIndex = i; 1279 for (int j=0; i<N && j<batchSize; j++) { 1280 // This builds the icon bitmaps. 1281 mAllAppsList.add(new ApplicationInfo(packageManager, apps.get(i), 1282 mIconCache, mLabelCache)); 1283 i++; 1284 } 1285 1286 final boolean first = i <= batchSize; 1287 final Callbacks callbacks = tryGetCallbacks(oldCallbacks); 1288 final ArrayList<ApplicationInfo> added = mAllAppsList.added; 1289 mAllAppsList.added = new ArrayList<ApplicationInfo>(); 1290 1291 mHandler.post(new Runnable() { 1292 public void run() { 1293 final long t = SystemClock.uptimeMillis(); 1294 if (callbacks != null) { 1295 if (first) { 1296 callbacks.bindAllApplications(added); 1297 } else { 1298 callbacks.bindAppsAdded(added); 1299 } 1300 if (DEBUG_LOADERS) { 1301 Log.d(TAG, "bound " + added.size() + " apps in " 1302 + (SystemClock.uptimeMillis() - t) + "ms"); 1303 } 1304 } else { 1305 Log.i(TAG, "not binding apps: no Launcher activity"); 1306 } 1307 } 1308 }); 1309 1310 if (DEBUG_LOADERS) { 1311 Log.d(TAG, "batch of " + (i-startIndex) + " icons processed in " 1312 + (SystemClock.uptimeMillis()-t2) + "ms"); 1313 } 1314 1315 if (mAllAppsLoadDelay > 0 && i < N) { 1316 try { 1317 if (DEBUG_LOADERS) { 1318 Log.d(TAG, "sleeping for " + mAllAppsLoadDelay + "ms"); 1319 } 1320 Thread.sleep(mAllAppsLoadDelay); 1321 } catch (InterruptedException exc) { } 1322 } 1323 } 1324 1325 if (DEBUG_LOADERS) { 1326 Log.d(TAG, "cached all " + N + " apps in " 1327 + (SystemClock.uptimeMillis()-t) + "ms" 1328 + (mAllAppsLoadDelay > 0 ? " (including delay)" : "")); 1329 } 1330 } 1331 1332 public void dumpState() { 1333 Log.d(TAG, "mLoaderTask.mContext=" + mContext); 1334 Log.d(TAG, "mLoaderTask.mWaitThread=" + mWaitThread); 1335 Log.d(TAG, "mLoaderTask.mIsLaunching=" + mIsLaunching); 1336 Log.d(TAG, "mLoaderTask.mStopped=" + mStopped); 1337 Log.d(TAG, "mLoaderTask.mLoadAndBindStepFinished=" + mLoadAndBindStepFinished); 1338 Log.d(TAG, "mItems size=" + sWorkspaceItems.size()); 1339 } 1340 } 1341 1342 void enqueuePackageUpdated(PackageUpdatedTask task) { 1343 sWorker.post(task); 1344 } 1345 1346 private class PackageUpdatedTask implements Runnable { 1347 int mOp; 1348 String[] mPackages; 1349 1350 public static final int OP_NONE = 0; 1351 public static final int OP_ADD = 1; 1352 public static final int OP_UPDATE = 2; 1353 public static final int OP_REMOVE = 3; // uninstlled 1354 public static final int OP_UNAVAILABLE = 4; // external media unmounted 1355 1356 1357 public PackageUpdatedTask(int op, String[] packages) { 1358 mOp = op; 1359 mPackages = packages; 1360 } 1361 1362 public void run() { 1363 final Context context = mApp; 1364 1365 final String[] packages = mPackages; 1366 final int N = packages.length; 1367 switch (mOp) { 1368 case OP_ADD: 1369 for (int i=0; i<N; i++) { 1370 if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]); 1371 mAllAppsList.addPackage(context, packages[i]); 1372 } 1373 break; 1374 case OP_UPDATE: 1375 for (int i=0; i<N; i++) { 1376 if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]); 1377 mAllAppsList.updatePackage(context, packages[i]); 1378 } 1379 break; 1380 case OP_REMOVE: 1381 case OP_UNAVAILABLE: 1382 for (int i=0; i<N; i++) { 1383 if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]); 1384 mAllAppsList.removePackage(packages[i]); 1385 } 1386 break; 1387 } 1388 1389 ArrayList<ApplicationInfo> added = null; 1390 ArrayList<ApplicationInfo> removed = null; 1391 ArrayList<ApplicationInfo> modified = null; 1392 1393 if (mAllAppsList.added.size() > 0) { 1394 added = mAllAppsList.added; 1395 mAllAppsList.added = new ArrayList<ApplicationInfo>(); 1396 } 1397 if (mAllAppsList.removed.size() > 0) { 1398 removed = mAllAppsList.removed; 1399 mAllAppsList.removed = new ArrayList<ApplicationInfo>(); 1400 for (ApplicationInfo info: removed) { 1401 mIconCache.remove(info.intent.getComponent()); 1402 } 1403 } 1404 if (mAllAppsList.modified.size() > 0) { 1405 modified = mAllAppsList.modified; 1406 mAllAppsList.modified = new ArrayList<ApplicationInfo>(); 1407 } 1408 1409 final Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null; 1410 if (callbacks == null) { 1411 Log.w(TAG, "Nobody to tell about the new app. Launcher is probably loading."); 1412 return; 1413 } 1414 1415 if (added != null) { 1416 final ArrayList<ApplicationInfo> addedFinal = added; 1417 mHandler.post(new Runnable() { 1418 public void run() { 1419 Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; 1420 if (callbacks == cb && cb != null) { 1421 callbacks.bindAppsAdded(addedFinal); 1422 } 1423 } 1424 }); 1425 } 1426 if (modified != null) { 1427 final ArrayList<ApplicationInfo> modifiedFinal = modified; 1428 mHandler.post(new Runnable() { 1429 public void run() { 1430 Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; 1431 if (callbacks == cb && cb != null) { 1432 callbacks.bindAppsUpdated(modifiedFinal); 1433 } 1434 } 1435 }); 1436 } 1437 if (removed != null) { 1438 final boolean permanent = mOp != OP_UNAVAILABLE; 1439 final ArrayList<ApplicationInfo> removedFinal = removed; 1440 mHandler.post(new Runnable() { 1441 public void run() { 1442 Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; 1443 if (callbacks == cb && cb != null) { 1444 callbacks.bindAppsRemoved(removedFinal, permanent); 1445 } 1446 } 1447 }); 1448 } 1449 1450 mHandler.post(new Runnable() { 1451 @Override 1452 public void run() { 1453 Callbacks cb = mCallbacks != null ? mCallbacks.get() : null; 1454 if (callbacks == cb && cb != null) { 1455 callbacks.bindPackagesUpdated(); 1456 } 1457 } 1458 }); 1459 } 1460 } 1461 1462 /** 1463 * This is called from the code that adds shortcuts from the intent receiver. This 1464 * doesn't have a Cursor, but 1465 */ 1466 public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, Context context) { 1467 return getShortcutInfo(manager, intent, context, null, -1, -1, null); 1468 } 1469 1470 /** 1471 * Make an ShortcutInfo object for a shortcut that is an application. 1472 * 1473 * If c is not null, then it will be used to fill in missing data like the title and icon. 1474 */ 1475 public ShortcutInfo getShortcutInfo(PackageManager manager, Intent intent, Context context, 1476 Cursor c, int iconIndex, int titleIndex, HashMap<Object, CharSequence> labelCache) { 1477 Bitmap icon = null; 1478 final ShortcutInfo info = new ShortcutInfo(); 1479 1480 ComponentName componentName = intent.getComponent(); 1481 if (componentName == null) { 1482 return null; 1483 } 1484 1485 // TODO: See if the PackageManager knows about this case. If it doesn't 1486 // then return null & delete this. 1487 1488 // the resource -- This may implicitly give us back the fallback icon, 1489 // but don't worry about that. All we're doing with usingFallbackIcon is 1490 // to avoid saving lots of copies of that in the database, and most apps 1491 // have icons anyway. 1492 final ResolveInfo resolveInfo = manager.resolveActivity(intent, 0); 1493 if (resolveInfo != null) { 1494 icon = mIconCache.getIcon(componentName, resolveInfo); 1495 } 1496 // the db 1497 if (icon == null) { 1498 if (c != null) { 1499 icon = getIconFromCursor(c, iconIndex); 1500 } 1501 } 1502 // the fallback icon 1503 if (icon == null) { 1504 icon = getFallbackIcon(); 1505 info.usingFallbackIcon = true; 1506 } 1507 info.setIcon(icon); 1508 1509 // from the resource 1510 if (resolveInfo != null) { 1511 if (labelCache != null && labelCache.containsKey(resolveInfo)) { 1512 info.title = labelCache.get(resolveInfo); 1513 } else { 1514 info.title = resolveInfo.activityInfo.loadLabel(manager); 1515 if (labelCache != null) { 1516 labelCache.put(resolveInfo, info.title); 1517 } 1518 } 1519 } 1520 // from the db 1521 if (info.title == null) { 1522 if (c != null) { 1523 info.title = c.getString(titleIndex); 1524 } 1525 } 1526 // fall back to the class name of the activity 1527 if (info.title == null) { 1528 info.title = componentName.getClassName(); 1529 } 1530 info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION; 1531 return info; 1532 } 1533 1534 /** 1535 * Make an ShortcutInfo object for a shortcut that isn't an application. 1536 */ 1537 private ShortcutInfo getShortcutInfo(Cursor c, Context context, 1538 int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex, 1539 int titleIndex) { 1540 1541 Bitmap icon = null; 1542 final ShortcutInfo info = new ShortcutInfo(); 1543 info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT; 1544 1545 // TODO: If there's an explicit component and we can't install that, delete it. 1546 1547 info.title = c.getString(titleIndex); 1548 1549 int iconType = c.getInt(iconTypeIndex); 1550 switch (iconType) { 1551 case LauncherSettings.Favorites.ICON_TYPE_RESOURCE: 1552 String packageName = c.getString(iconPackageIndex); 1553 String resourceName = c.getString(iconResourceIndex); 1554 PackageManager packageManager = context.getPackageManager(); 1555 info.customIcon = false; 1556 // the resource 1557 try { 1558 Resources resources = packageManager.getResourcesForApplication(packageName); 1559 if (resources != null) { 1560 final int id = resources.getIdentifier(resourceName, null, null); 1561 icon = Utilities.createIconBitmap( 1562 mIconCache.getFullResIcon(resources, id), context); 1563 } 1564 } catch (Exception e) { 1565 // drop this. we have other places to look for icons 1566 } 1567 // the db 1568 if (icon == null) { 1569 icon = getIconFromCursor(c, iconIndex); 1570 } 1571 // the fallback icon 1572 if (icon == null) { 1573 icon = getFallbackIcon(); 1574 info.usingFallbackIcon = true; 1575 } 1576 break; 1577 case LauncherSettings.Favorites.ICON_TYPE_BITMAP: 1578 icon = getIconFromCursor(c, iconIndex); 1579 if (icon == null) { 1580 icon = getFallbackIcon(); 1581 info.customIcon = false; 1582 info.usingFallbackIcon = true; 1583 } else { 1584 info.customIcon = true; 1585 } 1586 break; 1587 default: 1588 icon = getFallbackIcon(); 1589 info.usingFallbackIcon = true; 1590 info.customIcon = false; 1591 break; 1592 } 1593 info.setIcon(icon); 1594 return info; 1595 } 1596 1597 Bitmap getIconFromCursor(Cursor c, int iconIndex) { 1598 if (false) { 1599 Log.d(TAG, "getIconFromCursor app=" 1600 + c.getString(c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE))); 1601 } 1602 byte[] data = c.getBlob(iconIndex); 1603 try { 1604 return BitmapFactory.decodeByteArray(data, 0, data.length); 1605 } catch (Exception e) { 1606 return null; 1607 } 1608 } 1609 1610 ShortcutInfo addShortcut(Context context, Intent data, 1611 int screen, int cellX, int cellY, boolean notify) { 1612 1613 final ShortcutInfo info = infoFromShortcutIntent(context, data, null); 1614 addItemToDatabase(context, info, LauncherSettings.Favorites.CONTAINER_DESKTOP, 1615 screen, cellX, cellY, notify); 1616 1617 return info; 1618 } 1619 1620 /** 1621 * Attempts to find an AppWidgetProviderInfo that matches the given component. 1622 */ 1623 AppWidgetProviderInfo findAppWidgetProviderInfoWithComponent(Context context, 1624 ComponentName component) { 1625 List<AppWidgetProviderInfo> widgets = 1626 AppWidgetManager.getInstance(context).getInstalledProviders(); 1627 for (AppWidgetProviderInfo info : widgets) { 1628 if (info.provider.equals(component)) { 1629 return info; 1630 } 1631 } 1632 return null; 1633 } 1634 1635 /** 1636 * Returns a list of all the widgets that can handle configuration with a particular mimeType. 1637 */ 1638 List<WidgetMimeTypeHandlerData> resolveWidgetsForMimeType(Context context, String mimeType) { 1639 final PackageManager packageManager = context.getPackageManager(); 1640 final List<WidgetMimeTypeHandlerData> supportedConfigurationActivities = 1641 new ArrayList<WidgetMimeTypeHandlerData>(); 1642 1643 final Intent supportsIntent = 1644 new Intent(InstallWidgetReceiver.ACTION_SUPPORTS_CLIPDATA_MIMETYPE); 1645 supportsIntent.setType(mimeType); 1646 1647 // Create a set of widget configuration components that we can test against 1648 final List<AppWidgetProviderInfo> widgets = 1649 AppWidgetManager.getInstance(context).getInstalledProviders(); 1650 final HashMap<ComponentName, AppWidgetProviderInfo> configurationComponentToWidget = 1651 new HashMap<ComponentName, AppWidgetProviderInfo>(); 1652 for (AppWidgetProviderInfo info : widgets) { 1653 configurationComponentToWidget.put(info.configure, info); 1654 } 1655 1656 // Run through each of the intents that can handle this type of clip data, and cross 1657 // reference them with the components that are actual configuration components 1658 final List<ResolveInfo> activities = packageManager.queryIntentActivities(supportsIntent, 1659 PackageManager.MATCH_DEFAULT_ONLY); 1660 for (ResolveInfo info : activities) { 1661 final ActivityInfo activityInfo = info.activityInfo; 1662 final ComponentName infoComponent = new ComponentName(activityInfo.packageName, 1663 activityInfo.name); 1664 if (configurationComponentToWidget.containsKey(infoComponent)) { 1665 supportedConfigurationActivities.add( 1666 new InstallWidgetReceiver.WidgetMimeTypeHandlerData(info, 1667 configurationComponentToWidget.get(infoComponent))); 1668 } 1669 } 1670 return supportedConfigurationActivities; 1671 } 1672 1673 ShortcutInfo infoFromShortcutIntent(Context context, Intent data, Bitmap fallbackIcon) { 1674 Intent intent = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_INTENT); 1675 String name = data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME); 1676 Parcelable bitmap = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON); 1677 1678 Bitmap icon = null; 1679 boolean customIcon = false; 1680 ShortcutIconResource iconResource = null; 1681 1682 if (bitmap != null && bitmap instanceof Bitmap) { 1683 icon = Utilities.createIconBitmap(new FastBitmapDrawable((Bitmap)bitmap), context); 1684 customIcon = true; 1685 } else { 1686 Parcelable extra = data.getParcelableExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE); 1687 if (extra != null && extra instanceof ShortcutIconResource) { 1688 try { 1689 iconResource = (ShortcutIconResource) extra; 1690 final PackageManager packageManager = context.getPackageManager(); 1691 Resources resources = packageManager.getResourcesForApplication( 1692 iconResource.packageName); 1693 final int id = resources.getIdentifier(iconResource.resourceName, null, null); 1694 icon = Utilities.createIconBitmap( 1695 mIconCache.getFullResIcon(resources, id), context); 1696 } catch (Exception e) { 1697 Log.w(TAG, "Could not load shortcut icon: " + extra); 1698 } 1699 } 1700 } 1701 1702 final ShortcutInfo info = new ShortcutInfo(); 1703 1704 if (icon == null) { 1705 if (fallbackIcon != null) { 1706 icon = fallbackIcon; 1707 } else { 1708 icon = getFallbackIcon(); 1709 info.usingFallbackIcon = true; 1710 } 1711 } 1712 info.setIcon(icon); 1713 1714 info.title = name; 1715 info.intent = intent; 1716 info.customIcon = customIcon; 1717 info.iconResource = iconResource; 1718 1719 return info; 1720 } 1721 1722 void updateSavedIcon(Context context, ShortcutInfo info, Cursor c, int iconIndex) { 1723 // If apps can't be on SD, don't even bother. 1724 if (!mAppsCanBeOnExternalStorage) { 1725 return; 1726 } 1727 // If this icon doesn't have a custom icon, check to see 1728 // what's stored in the DB, and if it doesn't match what 1729 // we're going to show, store what we are going to show back 1730 // into the DB. We do this so when we're loading, if the 1731 // package manager can't find an icon (for example because 1732 // the app is on SD) then we can use that instead. 1733 if (!info.customIcon && !info.usingFallbackIcon) { 1734 boolean needSave; 1735 byte[] data = c.getBlob(iconIndex); 1736 try { 1737 if (data != null) { 1738 Bitmap saved = BitmapFactory.decodeByteArray(data, 0, data.length); 1739 Bitmap loaded = info.getIcon(mIconCache); 1740 needSave = !saved.sameAs(loaded); 1741 } else { 1742 needSave = true; 1743 } 1744 } catch (Exception e) { 1745 needSave = true; 1746 } 1747 if (needSave) { 1748 Log.d(TAG, "going to save icon bitmap for info=" + info); 1749 // This is slower than is ideal, but this only happens once 1750 // or when the app is updated with a new icon. 1751 updateItemInDatabase(context, info); 1752 } 1753 } 1754 } 1755 1756 /** 1757 * Return an existing FolderInfo object if we have encountered this ID previously, 1758 * or make a new one. 1759 */ 1760 private static FolderInfo findOrMakeFolder(HashMap<Long, FolderInfo> folders, long id) { 1761 // See if a placeholder was created for us already 1762 FolderInfo folderInfo = folders.get(id); 1763 if (folderInfo == null) { 1764 // No placeholder -- create a new instance 1765 folderInfo = new FolderInfo(); 1766 folders.put(id, folderInfo); 1767 } 1768 return folderInfo; 1769 } 1770 1771 private static final Collator sCollator = Collator.getInstance(); 1772 public static final Comparator<ApplicationInfo> APP_NAME_COMPARATOR 1773 = new Comparator<ApplicationInfo>() { 1774 public final int compare(ApplicationInfo a, ApplicationInfo b) { 1775 int result = sCollator.compare(a.title.toString(), b.title.toString()); 1776 if (result == 0) { 1777 result = a.componentName.compareTo(b.componentName); 1778 } 1779 return result; 1780 } 1781 }; 1782 public static final Comparator<ApplicationInfo> APP_INSTALL_TIME_COMPARATOR 1783 = new Comparator<ApplicationInfo>() { 1784 public final int compare(ApplicationInfo a, ApplicationInfo b) { 1785 if (a.firstInstallTime < b.firstInstallTime) return 1; 1786 if (a.firstInstallTime > b.firstInstallTime) return -1; 1787 return 0; 1788 } 1789 }; 1790 public static final Comparator<AppWidgetProviderInfo> WIDGET_NAME_COMPARATOR 1791 = new Comparator<AppWidgetProviderInfo>() { 1792 public final int compare(AppWidgetProviderInfo a, AppWidgetProviderInfo b) { 1793 return sCollator.compare(a.label.toString(), b.label.toString()); 1794 } 1795 }; 1796 public static class ShortcutNameComparator implements Comparator<ResolveInfo> { 1797 private PackageManager mPackageManager; 1798 private HashMap<Object, CharSequence> mLabelCache; 1799 ShortcutNameComparator(PackageManager pm) { 1800 mPackageManager = pm; 1801 mLabelCache = new HashMap<Object, CharSequence>(); 1802 } 1803 ShortcutNameComparator(PackageManager pm, HashMap<Object, CharSequence> labelCache) { 1804 mPackageManager = pm; 1805 mLabelCache = labelCache; 1806 } 1807 public final int compare(ResolveInfo a, ResolveInfo b) { 1808 CharSequence labelA, labelB; 1809 if (mLabelCache.containsKey(a)) { 1810 labelA = mLabelCache.get(a); 1811 } else { 1812 labelA = a.loadLabel(mPackageManager).toString(); 1813 1814 mLabelCache.put(a, labelA); 1815 } 1816 if (mLabelCache.containsKey(b)) { 1817 labelB = mLabelCache.get(b); 1818 } else { 1819 labelB = b.loadLabel(mPackageManager).toString(); 1820 1821 mLabelCache.put(b, labelB); 1822 } 1823 return sCollator.compare(labelA, labelB); 1824 } 1825 }; 1826 public static class WidgetAndShortcutNameComparator implements Comparator<Object> { 1827 private PackageManager mPackageManager; 1828 private HashMap<Object, String> mLabelCache; 1829 WidgetAndShortcutNameComparator(PackageManager pm) { 1830 mPackageManager = pm; 1831 mLabelCache = new HashMap<Object, String>(); 1832 } 1833 public final int compare(Object a, Object b) { 1834 String labelA, labelB; 1835 if (mLabelCache.containsKey(a)) { 1836 labelA = mLabelCache.get(a); 1837 } else { 1838 labelA = (a instanceof AppWidgetProviderInfo) ? 1839 ((AppWidgetProviderInfo) a).label : 1840 ((ResolveInfo) a).loadLabel(mPackageManager).toString(); 1841 mLabelCache.put(a, labelA); 1842 } 1843 if (mLabelCache.containsKey(b)) { 1844 labelB = mLabelCache.get(b); 1845 } else { 1846 labelB = (b instanceof AppWidgetProviderInfo) ? 1847 ((AppWidgetProviderInfo) b).label : 1848 ((ResolveInfo) b).loadLabel(mPackageManager).toString(); 1849 mLabelCache.put(b, labelB); 1850 } 1851 return sCollator.compare(labelA, labelB); 1852 } 1853 }; 1854 1855 public void dumpState() { 1856 Log.d(TAG, "mCallbacks=" + mCallbacks); 1857 ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.data", mAllAppsList.data); 1858 ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.added", mAllAppsList.added); 1859 ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.removed", mAllAppsList.removed); 1860 ApplicationInfo.dumpApplicationInfoList(TAG, "mAllAppsList.modified", mAllAppsList.modified); 1861 if (mLoaderTask != null) { 1862 mLoaderTask.dumpState(); 1863 } else { 1864 Log.d(TAG, "mLoaderTask=null"); 1865 } 1866 } 1867} 1868