blob: 12dd0167717a3109715f8e6d42a97af77817d498 [file] [log] [blame]
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001/*
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
Joe Onoratoa5902522009-07-30 13:37:37 -070017package com.android.launcher2;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080018
19import android.content.ComponentName;
20import android.content.ContentResolver;
21import android.content.ContentValues;
22import android.content.Intent;
23import android.content.Context;
24import android.content.pm.ActivityInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.ResolveInfo;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.graphics.Bitmap;
30import android.graphics.BitmapFactory;
The Android Open Source Projectf96811c2009-03-18 17:39:48 -070031import android.graphics.drawable.Drawable;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080032import android.net.Uri;
The Android Open Source Projectf96811c2009-03-18 17:39:48 -070033import static android.util.Log.*;
Joe Onorato9c1289c2009-08-17 11:03:03 -040034import android.util.Log;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080035import android.os.Process;
Joe Onorato9c1289c2009-08-17 11:03:03 -040036import android.os.SystemClock;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080037
Joe Onorato9c1289c2009-08-17 11:03:03 -040038import java.lang.ref.WeakReference;
39import java.net.URISyntaxException;
40import java.text.Collator;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080041import java.util.ArrayList;
Joe Onorato9c1289c2009-08-17 11:03:03 -040042import java.util.Comparator;
43import java.util.Collections;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080044import java.util.HashMap;
45import java.util.List;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080046
47/**
48 * Maintains in-memory state of the Launcher. It is expected that there should be only one
49 * LauncherModel object held in a static. Also provide APIs for updating the database state
The Android Open Source Projectbc219c32009-03-09 11:52:14 -070050 * for the Launcher.
The Android Open Source Project31dd5032009-03-03 19:32:27 -080051 */
52public class LauncherModel {
Romain Guy829f56a2009-03-27 16:58:13 -070053 static final boolean DEBUG_LOADERS = true;
Joe Onorato9c1289c2009-08-17 11:03:03 -040054 static final String TAG = "Launcher.Model";
The Android Open Source Projectf96811c2009-03-18 17:39:48 -070055
Joe Onorato9c1289c2009-08-17 11:03:03 -040056 private final Object mLock = new Object();
57 private DeferredHandler mHandler = new DeferredHandler();
58 private Loader mLoader = new Loader();
The Android Open Source Project31dd5032009-03-03 19:32:27 -080059
Joe Onorato9c1289c2009-08-17 11:03:03 -040060 private WeakReference<Callbacks> mCallbacks;
The Android Open Source Project31dd5032009-03-03 19:32:27 -080061
Joe Onorato9c1289c2009-08-17 11:03:03 -040062 private AllAppsList mAllAppsList = new AllAppsList();
The Android Open Source Project31dd5032009-03-03 19:32:27 -080063
Joe Onorato9c1289c2009-08-17 11:03:03 -040064 public interface Callbacks {
65 public int getCurrentWorkspaceScreen();
66 public void startBinding();
67 public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end);
68 public void finishBindingItems();
69 public void bindAppWidget(LauncherAppWidgetInfo info);
70 public void bindAllApplications(ArrayList<ApplicationInfo> apps);
71 public void bindPackageAdded(ArrayList<ApplicationInfo> apps);
72 public void bindPackageUpdated(String packageName, ArrayList<ApplicationInfo> apps);
73 public void bindPackageRemoved(String packageName, ArrayList<ApplicationInfo> apps);
74 }
The Android Open Source Project31dd5032009-03-03 19:32:27 -080075
The Android Open Source Project31dd5032009-03-03 19:32:27 -080076
Joe Onorato9c1289c2009-08-17 11:03:03 -040077 /**
78 * Adds an item to the DB if it was not created previously, or move it to a new
79 * <container, screen, cellX, cellY>
80 */
81 static void addOrMoveItemInDatabase(Context context, ItemInfo item, long container,
82 int screen, int cellX, int cellY) {
83 if (item.container == ItemInfo.NO_ID) {
84 // From all apps
85 addItemToDatabase(context, item, container, screen, cellX, cellY, false);
86 } else {
87 // From somewhere else
88 moveItemInDatabase(context, item, container, screen, cellX, cellY);
The Android Open Source Project31dd5032009-03-03 19:32:27 -080089 }
90 }
91
92 /**
Joe Onorato9c1289c2009-08-17 11:03:03 -040093 * Move an item in the DB to a new <container, screen, cellX, cellY>
The Android Open Source Projectbc219c32009-03-09 11:52:14 -070094 */
Joe Onorato9c1289c2009-08-17 11:03:03 -040095 static void moveItemInDatabase(Context context, ItemInfo item, long container, int screen,
96 int cellX, int cellY) {
97 item.container = container;
98 item.screen = screen;
99 item.cellX = cellX;
100 item.cellY = cellY;
101
102 final ContentValues values = new ContentValues();
103 final ContentResolver cr = context.getContentResolver();
104
105 values.put(LauncherSettings.Favorites.CONTAINER, item.container);
106 values.put(LauncherSettings.Favorites.CELLX, item.cellX);
107 values.put(LauncherSettings.Favorites.CELLY, item.cellY);
108 values.put(LauncherSettings.Favorites.SCREEN, item.screen);
109
110 cr.update(LauncherSettings.Favorites.getContentUri(item.id, false), values, null, null);
The Android Open Source Projectbc219c32009-03-09 11:52:14 -0700111 }
112
113 /**
Joe Onorato9c1289c2009-08-17 11:03:03 -0400114 * Returns true if the shortcuts already exists in the database.
115 * we identify a shortcut by its title and intent.
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800116 */
Joe Onorato9c1289c2009-08-17 11:03:03 -0400117 static boolean shortcutExists(Context context, String title, Intent intent) {
118 final ContentResolver cr = context.getContentResolver();
119 Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI,
120 new String[] { "title", "intent" }, "title=? and intent=?",
121 new String[] { title, intent.toUri(0) }, null);
122 boolean result = false;
123 try {
124 result = c.moveToFirst();
125 } finally {
126 c.close();
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800127 }
Joe Onorato9c1289c2009-08-17 11:03:03 -0400128 return result;
The Android Open Source Projectca9475f2009-03-13 13:04:24 -0700129 }
130
Joe Onorato9c1289c2009-08-17 11:03:03 -0400131 /**
132 * Find a folder in the db, creating the FolderInfo if necessary, and adding it to folderList.
133 */
134 FolderInfo getFolderById(Context context, HashMap<Long,FolderInfo> folderList, long id) {
135 final ContentResolver cr = context.getContentResolver();
136 Cursor c = cr.query(LauncherSettings.Favorites.CONTENT_URI, null,
137 "_id=? and (itemType=? or itemType=?)",
138 new String[] { String.valueOf(id),
139 String.valueOf(LauncherSettings.Favorites.ITEM_TYPE_USER_FOLDER),
140 String.valueOf(LauncherSettings.Favorites.ITEM_TYPE_LIVE_FOLDER) }, null);
The Android Open Source Projectca9475f2009-03-13 13:04:24 -0700141
Joe Onorato9c1289c2009-08-17 11:03:03 -0400142 try {
143 if (c.moveToFirst()) {
144 final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
145 final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
146 final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
147 final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
148 final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
149 final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800150
Joe Onorato9c1289c2009-08-17 11:03:03 -0400151 FolderInfo folderInfo = null;
152 switch (c.getInt(itemTypeIndex)) {
153 case LauncherSettings.Favorites.ITEM_TYPE_USER_FOLDER:
154 folderInfo = findOrMakeUserFolder(folderList, id);
155 break;
156 case LauncherSettings.Favorites.ITEM_TYPE_LIVE_FOLDER:
157 folderInfo = findOrMakeLiveFolder(folderList, id);
158 break;
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700159 }
160
Joe Onorato9c1289c2009-08-17 11:03:03 -0400161 folderInfo.title = c.getString(titleIndex);
162 folderInfo.id = id;
163 folderInfo.container = c.getInt(containerIndex);
164 folderInfo.screen = c.getInt(screenIndex);
165 folderInfo.cellX = c.getInt(cellXIndex);
166 folderInfo.cellY = c.getInt(cellYIndex);
167
168 return folderInfo;
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700169 }
Joe Onorato9c1289c2009-08-17 11:03:03 -0400170 } finally {
171 c.close();
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700172 }
173
174 return null;
175 }
176
Joe Onorato9c1289c2009-08-17 11:03:03 -0400177 /**
178 * Add an item to the database in a specified container. Sets the container, screen, cellX and
179 * cellY fields of the item. Also assigns an ID to the item.
180 */
181 static void addItemToDatabase(Context context, ItemInfo item, long container,
182 int screen, int cellX, int cellY, boolean notify) {
183 item.container = container;
184 item.screen = screen;
185 item.cellX = cellX;
186 item.cellY = cellY;
187
188 final ContentValues values = new ContentValues();
189 final ContentResolver cr = context.getContentResolver();
190
191 item.onAddToDatabase(values);
192
193 Uri result = cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI :
194 LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values);
195
196 if (result != null) {
197 item.id = Integer.parseInt(result.getPathSegments().get(1));
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700198 }
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700199 }
200
Joe Onorato9c1289c2009-08-17 11:03:03 -0400201 /**
202 * Update an item to the database in a specified container.
203 */
204 static void updateItemInDatabase(Context context, ItemInfo item) {
205 final ContentValues values = new ContentValues();
206 final ContentResolver cr = context.getContentResolver();
207
208 item.onAddToDatabase(values);
209
210 cr.update(LauncherSettings.Favorites.getContentUri(item.id, false), values, null, null);
211 }
212
213 /**
214 * Removes the specified item from the database
215 * @param context
216 * @param item
217 */
218 static void deleteItemFromDatabase(Context context, ItemInfo item) {
219 final ContentResolver cr = context.getContentResolver();
220
221 cr.delete(LauncherSettings.Favorites.getContentUri(item.id, false), null, null);
222 }
223
224 /**
225 * Remove the contents of the specified folder from the database
226 */
227 static void deleteUserFolderContentsFromDatabase(Context context, UserFolderInfo info) {
228 final ContentResolver cr = context.getContentResolver();
229
230 cr.delete(LauncherSettings.Favorites.getContentUri(info.id, false), null, null);
231 cr.delete(LauncherSettings.Favorites.CONTENT_URI,
232 LauncherSettings.Favorites.CONTAINER + "=" + info.id, null);
233 }
234
235 /**
236 * Set this as the current Launcher activity object for the loader.
237 */
238 public void initialize(Callbacks callbacks) {
239 synchronized (mLock) {
240 mCallbacks = new WeakReference<Callbacks>(callbacks);
241 }
242 }
243
244 public void startLoader(Context context, boolean isLaunching) {
245 mLoader.startLoader(context, isLaunching);
246 }
247
248 public void stopLoader() {
249 mLoader.stopLoader();
250 }
251
Joe Onorato1d8e7bb2009-10-15 19:49:43 -0700252 /**
253 * We pick up most of the changes to all apps.
254 */
255 public void setAllAppsDirty() {
256 mLoader.setAllAppsDirty();
257 }
258
Joe Onorato9c1289c2009-08-17 11:03:03 -0400259 public void setWorkspaceDirty() {
260 mLoader.setWorkspaceDirty();
261 }
262
263 /**
264 * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
265 * ACTION_PACKAGE_CHANGED.
266 */
267 public void onReceiveIntent(Context context, Intent intent) {
268 final String packageName = intent.getData().getSchemeSpecificPart();
269
270 ArrayList<ApplicationInfo> added = null;
271 ArrayList<ApplicationInfo> removed = null;
272 ArrayList<ApplicationInfo> modified = null;
273 boolean update = false;
274 boolean remove = false;
275
276 synchronized (mLock) {
277 final String action = intent.getAction();
278 final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
279
280 if (packageName == null || packageName.length() == 0) {
281 // they sent us a bad intent
282 return;
283 }
284
285 if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
286 mAllAppsList.updatePackage(context, packageName);
287 update = true;
288 } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
289 if (!replacing) {
290 mAllAppsList.removePackage(packageName);
291 remove = true;
292 }
293 // else, we are replacing the package, so a PACKAGE_ADDED will be sent
294 // later, we will update the package at this time
295 } else {
296 if (!replacing) {
297 mAllAppsList.addPackage(context, packageName);
298 } else {
299 mAllAppsList.updatePackage(context, packageName);
300 update = true;
301 }
302 }
303
304 if (mAllAppsList.added.size() > 0) {
305 added = mAllAppsList.added;
Joe Onoratoefabe002009-08-28 09:38:18 -0700306 mAllAppsList.added = new ArrayList();
Joe Onorato9c1289c2009-08-17 11:03:03 -0400307 }
308 if (mAllAppsList.removed.size() > 0) {
309 removed = mAllAppsList.removed;
Joe Onoratoefabe002009-08-28 09:38:18 -0700310 mAllAppsList.removed = new ArrayList();
Joe Onorato9c1289c2009-08-17 11:03:03 -0400311 for (ApplicationInfo info: removed) {
312 AppInfoCache.remove(info.intent.getComponent());
313 }
314 }
315 if (mAllAppsList.modified.size() > 0) {
316 modified = mAllAppsList.modified;
Joe Onoratoefabe002009-08-28 09:38:18 -0700317 mAllAppsList.modified = new ArrayList();
Joe Onorato9c1289c2009-08-17 11:03:03 -0400318 }
319
Marco Nelissen3c8b90d2009-09-11 14:49:50 -0700320 final Callbacks callbacks = mCallbacks != null ? mCallbacks.get() : null;
Joe Onorato9c1289c2009-08-17 11:03:03 -0400321 if (callbacks == null) {
322 return;
323 }
324
325 if (added != null) {
326 final ArrayList<ApplicationInfo> addedFinal = added;
327 mHandler.post(new Runnable() {
328 public void run() {
329 callbacks.bindPackageAdded(addedFinal);
330 }
331 });
332 }
333 if (update || modified != null) {
334 final ArrayList<ApplicationInfo> modifiedFinal = modified;
335 mHandler.post(new Runnable() {
336 public void run() {
337 callbacks.bindPackageUpdated(packageName, modifiedFinal);
338 }
339 });
340 }
341 if (remove || removed != null) {
342 final ArrayList<ApplicationInfo> removedFinal = removed;
343 mHandler.post(new Runnable() {
344 public void run() {
345 callbacks.bindPackageRemoved(packageName, removedFinal);
346 }
347 });
348 }
349 }
350 }
351
352 public class Loader {
353 private static final int ITEMS_CHUNK = 6;
354
355 private LoaderThread mLoaderThread;
356
357 private int mLastWorkspaceSeq = 0;
358 private int mWorkspaceSeq = 1;
359
360 private int mLastAllAppsSeq = 0;
361 private int mAllAppsSeq = 1;
362
363 final ArrayList<ItemInfo> mItems = new ArrayList();
364 final ArrayList<LauncherAppWidgetInfo> mAppWidgets = new ArrayList();
365 final HashMap<Long, FolderInfo> folders = new HashMap();
366
367 /**
368 * Call this from the ui thread so the handler is initialized on the correct thread.
369 */
370 public Loader() {
371 }
372
373 public void startLoader(Context context, boolean isLaunching) {
374 synchronized (mLock) {
375 Log.d(TAG, "startLoader isLaunching=" + isLaunching);
376 // Don't bother to start the thread if we know it's not going to do anything
377 if (mCallbacks.get() != null) {
378 LoaderThread oldThread = mLoaderThread;
379 if (oldThread != null) {
380 if (oldThread.isLaunching()) {
381 // don't downgrade isLaunching if we're already running
382 isLaunching = true;
383 }
384 oldThread.stopLocked();
385 }
386 mLoaderThread = new LoaderThread(context, oldThread, isLaunching);
387 mLoaderThread.start();
388 }
389 }
390 }
391
392 public void stopLoader() {
393 synchronized (mLock) {
394 if (mLoaderThread != null) {
395 mLoaderThread.stopLocked();
396 }
397 }
398 }
399
400 public void setWorkspaceDirty() {
401 synchronized (mLock) {
402 mWorkspaceSeq++;
403 }
404 }
405
406 public void setAllAppsDirty() {
407 synchronized (mLock) {
408 mAllAppsSeq++;
409 }
410 }
411
412 /**
413 * Runnable for the thread that loads the contents of the launcher:
414 * - workspace icons
415 * - widgets
416 * - all apps icons
417 */
418 private class LoaderThread extends Thread {
419 private Context mContext;
420 private Thread mWaitThread;
421 private boolean mIsLaunching;
422 private boolean mStopped;
423 private boolean mWorkspaceDoneBinding;
424
425 LoaderThread(Context context, Thread waitThread, boolean isLaunching) {
426 mContext = context;
427 mWaitThread = waitThread;
428 mIsLaunching = isLaunching;
429 }
430
431 boolean isLaunching() {
432 return mIsLaunching;
433 }
434
435 /**
436 * If another LoaderThread was supplied, we need to wait for that to finish before
437 * we start our processing. This keeps the ordering of the setting and clearing
438 * of the dirty flags correct by making sure we don't start processing stuff until
439 * they've had a chance to re-set them. We do this waiting the worker thread, not
440 * the ui thread to avoid ANRs.
441 */
442 private void waitForOtherThread() {
443 if (mWaitThread != null) {
444 boolean done = false;
445 while (!done) {
446 try {
447 mWaitThread.join();
Joe Onoratoefabe002009-08-28 09:38:18 -0700448 done = true;
Joe Onorato9c1289c2009-08-17 11:03:03 -0400449 } catch (InterruptedException ex) {
450 }
451 }
452 mWaitThread = null;
453 }
454 }
455
456 public void run() {
457 waitForOtherThread();
458
459 // Elevate priority when Home launches for the first time to avoid
460 // starving at boot time. Staring at a blank home is not cool.
461 synchronized (mLock) {
462 android.os.Process.setThreadPriority(mIsLaunching
463 ? Process.THREAD_PRIORITY_DEFAULT : Process.THREAD_PRIORITY_BACKGROUND);
464 }
465
466 // Load the workspace only if it's dirty.
467 int workspaceSeq;
468 boolean workspaceDirty;
469 synchronized (mLock) {
470 workspaceSeq = mWorkspaceSeq;
471 workspaceDirty = mWorkspaceSeq != mLastWorkspaceSeq;
472 }
473 if (workspaceDirty) {
474 loadWorkspace();
475 }
476 synchronized (mLock) {
477 // If we're not stopped, and nobody has incremented mWorkspaceSeq.
478 if (mStopped) {
479 return;
480 }
481 if (workspaceSeq == mWorkspaceSeq) {
482 mLastWorkspaceSeq = mWorkspaceSeq;
483 }
484 }
485
486 // Bind the workspace
487 bindWorkspace();
488
489 // Wait until the either we're stopped or the other threads are done.
490 // This way we don't start loading all apps until the workspace has settled
491 // down.
492 synchronized (LoaderThread.this) {
493 mHandler.post(new Runnable() {
494 public void run() {
495 synchronized (LoaderThread.this) {
496 mWorkspaceDoneBinding = true;
497 Log.d(TAG, "done with workspace");
498 LoaderThread.this.notify();
499 }
500 }
501 });
502 Log.d(TAG, "waiting to be done with workspace");
503 while (!mStopped && !mWorkspaceDoneBinding) {
504 try {
505 this.wait();
506 } catch (InterruptedException ex) {
507 }
508 }
509 Log.d(TAG, "done waiting to be done with workspace");
510 }
511
512 // Load all apps if they're dirty
513 int allAppsSeq;
514 boolean allAppsDirty;
515 synchronized (mLock) {
516 allAppsSeq = mAllAppsSeq;
517 allAppsDirty = mAllAppsSeq != mLastAllAppsSeq;
518 }
519 if (allAppsDirty) {
520 loadAllApps();
521 }
522 synchronized (mLock) {
523 // If we're not stopped, and nobody has incremented mAllAppsSeq.
524 if (mStopped) {
525 return;
526 }
527 if (allAppsSeq == mAllAppsSeq) {
528 mLastAllAppsSeq = mAllAppsSeq;
529 }
530 }
531
532 // Bind all apps
Joe Onorato34b02492009-10-14 11:13:48 -0700533 if (allAppsDirty) {
534 bindAllApps();
535 }
Joe Onorato9c1289c2009-08-17 11:03:03 -0400536
537 // Clear out this reference, otherwise we end up holding it until all of the
538 // callback runnables are done.
539 mContext = null;
540
541 synchronized (mLock) {
542 // Setting the reference is atomic, but we can't do it inside the other critical
543 // sections.
544 mLoaderThread = null;
545 return;
546 }
547 }
548
549 public void stopLocked() {
550 synchronized (LoaderThread.this) {
551 mStopped = true;
552 this.notify();
553 }
554 }
555
556 /**
557 * Gets the callbacks object. If we've been stopped, or if the launcher object
558 * has somehow been garbage collected, return null instead.
559 */
560 Callbacks tryGetCallbacks() {
561 synchronized (mLock) {
562 if (mStopped) {
563 return null;
564 }
565
566 final Callbacks callbacks = mCallbacks.get();
567 if (callbacks == null) {
568 Log.w(TAG, "no mCallbacks");
569 return null;
570 }
571
572 return callbacks;
573 }
574 }
575
576 private void loadWorkspace() {
577 long t = SystemClock.uptimeMillis();
578
579 final Context context = mContext;
580 final ContentResolver contentResolver = context.getContentResolver();
581 final PackageManager manager = context.getPackageManager();
582
583 /* TODO
584 if (mLocaleChanged) {
585 updateShortcutLabels(contentResolver, manager);
586 }
587 */
588
589 final Cursor c = contentResolver.query(
590 LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
591
592 try {
593 final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
594 final int intentIndex = c.getColumnIndexOrThrow
595 (LauncherSettings.Favorites.INTENT);
596 final int titleIndex = c.getColumnIndexOrThrow
597 (LauncherSettings.Favorites.TITLE);
598 final int iconTypeIndex = c.getColumnIndexOrThrow(
599 LauncherSettings.Favorites.ICON_TYPE);
600 final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
601 final int iconPackageIndex = c.getColumnIndexOrThrow(
602 LauncherSettings.Favorites.ICON_PACKAGE);
603 final int iconResourceIndex = c.getColumnIndexOrThrow(
604 LauncherSettings.Favorites.ICON_RESOURCE);
605 final int containerIndex = c.getColumnIndexOrThrow(
606 LauncherSettings.Favorites.CONTAINER);
607 final int itemTypeIndex = c.getColumnIndexOrThrow(
608 LauncherSettings.Favorites.ITEM_TYPE);
609 final int appWidgetIdIndex = c.getColumnIndexOrThrow(
610 LauncherSettings.Favorites.APPWIDGET_ID);
611 final int screenIndex = c.getColumnIndexOrThrow(
612 LauncherSettings.Favorites.SCREEN);
613 final int cellXIndex = c.getColumnIndexOrThrow
614 (LauncherSettings.Favorites.CELLX);
615 final int cellYIndex = c.getColumnIndexOrThrow
616 (LauncherSettings.Favorites.CELLY);
617 final int spanXIndex = c.getColumnIndexOrThrow
618 (LauncherSettings.Favorites.SPANX);
619 final int spanYIndex = c.getColumnIndexOrThrow(
620 LauncherSettings.Favorites.SPANY);
621 final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
622 final int displayModeIndex = c.getColumnIndexOrThrow(
623 LauncherSettings.Favorites.DISPLAY_MODE);
624
625 ApplicationInfo info;
626 String intentDescription;
627 Widget widgetInfo;
628 LauncherAppWidgetInfo appWidgetInfo;
629 int container;
630 long id;
631 Intent intent;
632
633 while (!mStopped && c.moveToNext()) {
634 try {
635 int itemType = c.getInt(itemTypeIndex);
636
637 switch (itemType) {
638 case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
639 case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
640 intentDescription = c.getString(intentIndex);
641 try {
642 intent = Intent.parseUri(intentDescription, 0);
643 } catch (URISyntaxException e) {
644 continue;
645 }
646
647 if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
648 info = getApplicationInfo(manager, intent, context);
649 } else {
650 info = getApplicationInfoShortcut(c, context, iconTypeIndex,
651 iconPackageIndex, iconResourceIndex, iconIndex);
652 }
653
654 if (info == null) {
655 info = new ApplicationInfo();
656 info.icon = manager.getDefaultActivityIcon();
657 }
658
659 if (info != null) {
660 info.title = c.getString(titleIndex);
661 info.intent = intent;
662
663 info.id = c.getLong(idIndex);
664 container = c.getInt(containerIndex);
665 info.container = container;
666 info.screen = c.getInt(screenIndex);
667 info.cellX = c.getInt(cellXIndex);
668 info.cellY = c.getInt(cellYIndex);
669
670 switch (container) {
671 case LauncherSettings.Favorites.CONTAINER_DESKTOP:
672 mItems.add(info);
673 break;
674 default:
675 // Item is in a user folder
676 UserFolderInfo folderInfo =
677 findOrMakeUserFolder(folders, container);
678 folderInfo.add(info);
679 break;
680 }
681 }
682 break;
683 case LauncherSettings.Favorites.ITEM_TYPE_USER_FOLDER:
684
685 id = c.getLong(idIndex);
686 UserFolderInfo folderInfo = findOrMakeUserFolder(folders, id);
687
688 folderInfo.title = c.getString(titleIndex);
689
690 folderInfo.id = id;
691 container = c.getInt(containerIndex);
692 folderInfo.container = container;
693 folderInfo.screen = c.getInt(screenIndex);
694 folderInfo.cellX = c.getInt(cellXIndex);
695 folderInfo.cellY = c.getInt(cellYIndex);
696
697 switch (container) {
698 case LauncherSettings.Favorites.CONTAINER_DESKTOP:
699 mItems.add(folderInfo);
700 break;
701 }
702 break;
703 case LauncherSettings.Favorites.ITEM_TYPE_LIVE_FOLDER:
704
705 id = c.getLong(idIndex);
706 LiveFolderInfo liveFolderInfo = findOrMakeLiveFolder(folders, id);
707
708 intentDescription = c.getString(intentIndex);
709 intent = null;
710 if (intentDescription != null) {
711 try {
712 intent = Intent.parseUri(intentDescription, 0);
713 } catch (URISyntaxException e) {
714 // Ignore, a live folder might not have a base intent
715 }
716 }
717
718 liveFolderInfo.title = c.getString(titleIndex);
719 liveFolderInfo.id = id;
720 container = c.getInt(containerIndex);
721 liveFolderInfo.container = container;
722 liveFolderInfo.screen = c.getInt(screenIndex);
723 liveFolderInfo.cellX = c.getInt(cellXIndex);
724 liveFolderInfo.cellY = c.getInt(cellYIndex);
725 liveFolderInfo.uri = Uri.parse(c.getString(uriIndex));
726 liveFolderInfo.baseIntent = intent;
727 liveFolderInfo.displayMode = c.getInt(displayModeIndex);
728
729 loadLiveFolderIcon(context, c, iconTypeIndex, iconPackageIndex,
730 iconResourceIndex, liveFolderInfo);
731
732 switch (container) {
733 case LauncherSettings.Favorites.CONTAINER_DESKTOP:
734 mItems.add(liveFolderInfo);
735 break;
736 }
737 break;
738 case LauncherSettings.Favorites.ITEM_TYPE_WIDGET_SEARCH:
739 widgetInfo = Widget.makeSearch();
740
741 container = c.getInt(containerIndex);
742 if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
743 Log.e(TAG, "Widget found where container "
744 + "!= CONTAINER_DESKTOP ignoring!");
745 continue;
746 }
747
748 widgetInfo.id = c.getLong(idIndex);
749 widgetInfo.screen = c.getInt(screenIndex);
750 widgetInfo.container = container;
751 widgetInfo.cellX = c.getInt(cellXIndex);
752 widgetInfo.cellY = c.getInt(cellYIndex);
753
754 mItems.add(widgetInfo);
755 break;
756 case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
757 // Read all Launcher-specific widget details
758 int appWidgetId = c.getInt(appWidgetIdIndex);
759 appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId);
760 appWidgetInfo.id = c.getLong(idIndex);
761 appWidgetInfo.screen = c.getInt(screenIndex);
762 appWidgetInfo.cellX = c.getInt(cellXIndex);
763 appWidgetInfo.cellY = c.getInt(cellYIndex);
764 appWidgetInfo.spanX = c.getInt(spanXIndex);
765 appWidgetInfo.spanY = c.getInt(spanYIndex);
766
767 container = c.getInt(containerIndex);
768 if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
769 Log.e(TAG, "Widget found where container "
770 + "!= CONTAINER_DESKTOP -- ignoring!");
771 continue;
772 }
773 appWidgetInfo.container = c.getInt(containerIndex);
774
775 mAppWidgets.add(appWidgetInfo);
776 break;
777 }
778 } catch (Exception e) {
779 Log.w(TAG, "Desktop items loading interrupted:", e);
780 }
781 }
782 } finally {
783 c.close();
784 }
785 Log.d(TAG, "loaded workspace in " + (SystemClock.uptimeMillis()-t) + "ms");
786 }
787
788 /**
789 * Read everything out of our database.
790 */
791 private void bindWorkspace() {
792 final long t = SystemClock.uptimeMillis();
793
794 // Don't use these two variables in any of the callback runnables.
795 // Otherwise we hold a reference to them.
796 Callbacks callbacks = mCallbacks.get();
797 if (callbacks == null) {
798 // This launcher has exited and nobody bothered to tell us. Just bail.
799 Log.w(TAG, "LoaderThread running with no launcher");
800 return;
801 }
802
803 int N;
804 // Tell the workspace that we're about to start firing items at it
805 mHandler.post(new Runnable() {
806 public void run() {
807 Callbacks callbacks = tryGetCallbacks();
808 if (callbacks != null) {
809 callbacks.startBinding();
810 }
811 }
812 });
813 // Add the items to the workspace.
814 N = mItems.size();
815 for (int i=0; i<N; i+=ITEMS_CHUNK) {
816 final int start = i;
817 final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i);
818 mHandler.post(new Runnable() {
819 public void run() {
820 Callbacks callbacks = tryGetCallbacks();
821 if (callbacks != null) {
822 callbacks.bindItems(mItems, start, start+chunkSize);
823 }
824 }
825 });
826 }
827 // Wait until the queue goes empty.
828 mHandler.postIdle(new Runnable() {
829 public void run() {
830 Log.d(TAG, "Going to start binding widgets soon.");
831 }
832 });
833 // Bind the widgets, one at a time.
834 // WARNING: this is calling into the workspace from the background thread,
835 // but since getCurrentScreen() just returns the int, we should be okay. This
836 // is just a hint for the order, and if it's wrong, we'll be okay.
837 // TODO: instead, we should have that push the current screen into here.
838 final int currentScreen = callbacks.getCurrentWorkspaceScreen();
839 N = mAppWidgets.size();
840 // once for the current screen
841 for (int i=0; i<N; i++) {
842 final LauncherAppWidgetInfo widget = mAppWidgets.get(i);
843 if (widget.screen == currentScreen) {
844 mHandler.post(new Runnable() {
845 public void run() {
846 Callbacks callbacks = tryGetCallbacks();
847 if (callbacks != null) {
848 callbacks.bindAppWidget(widget);
849 }
850 }
851 });
852 }
853 }
854 // once for the other screens
855 for (int i=0; i<N; i++) {
856 final LauncherAppWidgetInfo widget = mAppWidgets.get(i);
857 if (widget.screen != currentScreen) {
858 mHandler.post(new Runnable() {
859 public void run() {
860 Callbacks callbacks = tryGetCallbacks();
861 if (callbacks != null) {
862 callbacks.bindAppWidget(widget);
863 }
864 }
865 });
866 }
867 }
868 // TODO: Bind the folders
869 // Tell the workspace that we're done.
870 mHandler.post(new Runnable() {
871 public void run() {
872 Callbacks callbacks = tryGetCallbacks();
873 if (callbacks != null) {
874 callbacks.finishBindingItems();
875 }
876 }
877 });
878 // If we're profiling, this is the last thing in the queue.
879 mHandler.post(new Runnable() {
880 public void run() {
881 Log.d(TAG, "bound workspace in " + (SystemClock.uptimeMillis()-t) + "ms");
882 if (Launcher.PROFILE_ROTATE) {
883 android.os.Debug.stopMethodTracing();
884 }
885 }
886 });
887 }
888
889 private void loadAllApps() {
890 final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
891 mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
892
893 final Callbacks callbacks = tryGetCallbacks();
894 if (callbacks == null) {
895 return;
896 }
897
898 final Context context = mContext;
899 final PackageManager packageManager = context.getPackageManager();
900
901 final List<ResolveInfo> apps = packageManager.queryIntentActivities(mainIntent, 0);
902
903 synchronized (mLock) {
904 mAllAppsList.clear();
905 if (apps != null) {
906 long t = SystemClock.uptimeMillis();
907
908 int N = apps.size();
909 Utilities.BubbleText bubble = new Utilities.BubbleText(context);
910 for (int i=0; i<N && !mStopped; i++) {
911 // This builds the icon bitmaps.
912 mAllAppsList.add(AppInfoCache.cache(apps.get(i), context, bubble));
913 }
914 Collections.sort(mAllAppsList.data, sComparator);
915 Collections.sort(mAllAppsList.added, sComparator);
916 Log.d(TAG, "cached app icons in " + (SystemClock.uptimeMillis()-t) + "ms");
917 }
918 }
919 }
920
921 private void bindAllApps() {
922 synchronized (mLock) {
923 final ArrayList<ApplicationInfo> results = mAllAppsList.added;
924 mAllAppsList.added = new ArrayList();
925 mHandler.post(new Runnable() {
926 public void run() {
Joe Onorato34b02492009-10-14 11:13:48 -0700927 final long t = SystemClock.uptimeMillis();
928 final int count = results.size();
Joe Onorato9c1289c2009-08-17 11:03:03 -0400929
930 Callbacks callbacks = tryGetCallbacks();
931 if (callbacks != null) {
932 callbacks.bindAllApplications(results);
933 }
934
Joe Onorato34b02492009-10-14 11:13:48 -0700935 Log.d(TAG, "bound app " + count + " icons in "
Joe Onorato9c1289c2009-08-17 11:03:03 -0400936 + (SystemClock.uptimeMillis()-t) + "ms");
937 }
938 });
939 }
940 }
941 }
942 }
943
944 /**
945 * Make an ApplicationInfo object for an application.
946 */
947 private static ApplicationInfo getApplicationInfo(PackageManager manager, Intent intent,
948 Context context) {
949 final ResolveInfo resolveInfo = manager.resolveActivity(intent, 0);
950
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700951 if (resolveInfo == null) {
952 return null;
953 }
954
Joe Onorato9c1289c2009-08-17 11:03:03 -0400955 final ApplicationInfo info = new ApplicationInfo();
956 final ActivityInfo activityInfo = resolveInfo.activityInfo;
Joe Onorato6665c0f2009-09-02 15:27:24 -0700957 info.icon = Utilities.createIconThumbnail(activityInfo.loadIcon(manager), context);
Joe Onorato9c1289c2009-08-17 11:03:03 -0400958 if (info.title == null || info.title.length() == 0) {
959 info.title = activityInfo.loadLabel(manager);
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700960 }
Joe Onorato9c1289c2009-08-17 11:03:03 -0400961 if (info.title == null) {
962 info.title = "";
The Android Open Source Projectf96811c2009-03-18 17:39:48 -0700963 }
Joe Onorato9c1289c2009-08-17 11:03:03 -0400964 info.itemType = LauncherSettings.Favorites.ITEM_TYPE_APPLICATION;
965 return info;
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800966 }
The Android Open Source Projectbc219c32009-03-09 11:52:14 -0700967
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800968 /**
Joe Onorato9c1289c2009-08-17 11:03:03 -0400969 * Make an ApplicationInfo object for a sortcut
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800970 */
Joe Onorato9c1289c2009-08-17 11:03:03 -0400971 private static ApplicationInfo getApplicationInfoShortcut(Cursor c, Context context,
972 int iconTypeIndex, int iconPackageIndex, int iconResourceIndex, int iconIndex) {
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800973
Joe Onorato9c1289c2009-08-17 11:03:03 -0400974 final ApplicationInfo info = new ApplicationInfo();
975 info.itemType = LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT;
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800976
Joe Onorato9c1289c2009-08-17 11:03:03 -0400977 int iconType = c.getInt(iconTypeIndex);
978 switch (iconType) {
979 case LauncherSettings.Favorites.ICON_TYPE_RESOURCE:
980 String packageName = c.getString(iconPackageIndex);
981 String resourceName = c.getString(iconResourceIndex);
982 PackageManager packageManager = context.getPackageManager();
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800983 try {
Joe Onorato9c1289c2009-08-17 11:03:03 -0400984 Resources resources = packageManager.getResourcesForApplication(packageName);
985 final int id = resources.getIdentifier(resourceName, null, null);
Joe Onorato6665c0f2009-09-02 15:27:24 -0700986 info.icon = Utilities.createIconThumbnail(resources.getDrawable(id), context);
Joe Onorato9c1289c2009-08-17 11:03:03 -0400987 } catch (Exception e) {
988 info.icon = packageManager.getDefaultActivityIcon();
The Android Open Source Project31dd5032009-03-03 19:32:27 -0800989 }
Joe Onorato9c1289c2009-08-17 11:03:03 -0400990 info.iconResource = new Intent.ShortcutIconResource();
991 info.iconResource.packageName = packageName;
992 info.iconResource.resourceName = resourceName;
993 info.customIcon = false;
994 break;
995 case LauncherSettings.Favorites.ICON_TYPE_BITMAP:
996 byte[] data = c.getBlob(iconIndex);
997 try {
998 Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
999 info.icon = new FastBitmapDrawable(
1000 Utilities.createBitmapThumbnail(bitmap, context));
1001 } catch (Exception e) {
1002 packageManager = context.getPackageManager();
1003 info.icon = packageManager.getDefaultActivityIcon();
1004 }
1005 info.filtered = true;
1006 info.customIcon = true;
1007 break;
1008 default:
1009 info.icon = context.getPackageManager().getDefaultActivityIcon();
1010 info.customIcon = false;
1011 break;
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001012 }
Joe Onorato9c1289c2009-08-17 11:03:03 -04001013 return info;
1014 }
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001015
Joe Onorato9c1289c2009-08-17 11:03:03 -04001016 private static void loadLiveFolderIcon(Context context, Cursor c, int iconTypeIndex,
1017 int iconPackageIndex, int iconResourceIndex, LiveFolderInfo liveFolderInfo) {
1018
1019 int iconType = c.getInt(iconTypeIndex);
1020 switch (iconType) {
1021 case LauncherSettings.Favorites.ICON_TYPE_RESOURCE:
1022 String packageName = c.getString(iconPackageIndex);
1023 String resourceName = c.getString(iconResourceIndex);
1024 PackageManager packageManager = context.getPackageManager();
1025 try {
1026 Resources resources = packageManager.getResourcesForApplication(packageName);
1027 final int id = resources.getIdentifier(resourceName, null, null);
1028 liveFolderInfo.icon = resources.getDrawable(id);
1029 } catch (Exception e) {
1030 liveFolderInfo.icon =
1031 context.getResources().getDrawable(R.drawable.ic_launcher_folder);
1032 }
1033 liveFolderInfo.iconResource = new Intent.ShortcutIconResource();
1034 liveFolderInfo.iconResource.packageName = packageName;
1035 liveFolderInfo.iconResource.resourceName = resourceName;
1036 break;
1037 default:
1038 liveFolderInfo.icon =
1039 context.getResources().getDrawable(R.drawable.ic_launcher_folder);
1040 }
1041 }
1042
1043 /**
1044 * Return an existing UserFolderInfo object if we have encountered this ID previously,
1045 * or make a new one.
1046 */
1047 private static UserFolderInfo findOrMakeUserFolder(HashMap<Long, FolderInfo> folders, long id) {
1048 // See if a placeholder was created for us already
1049 FolderInfo folderInfo = folders.get(id);
1050 if (folderInfo == null || !(folderInfo instanceof UserFolderInfo)) {
1051 // No placeholder -- create a new instance
1052 folderInfo = new UserFolderInfo();
1053 folders.put(id, folderInfo);
1054 }
1055 return (UserFolderInfo) folderInfo;
1056 }
1057
1058 /**
1059 * Return an existing UserFolderInfo object if we have encountered this ID previously, or make a
1060 * new one.
1061 */
1062 private static LiveFolderInfo findOrMakeLiveFolder(HashMap<Long, FolderInfo> folders, long id) {
1063 // See if a placeholder was created for us already
1064 FolderInfo folderInfo = folders.get(id);
1065 if (folderInfo == null || !(folderInfo instanceof LiveFolderInfo)) {
1066 // No placeholder -- create a new instance
1067 folderInfo = new LiveFolderInfo();
1068 folders.put(id, folderInfo);
1069 }
1070 return (LiveFolderInfo) folderInfo;
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001071 }
1072
1073 private static void updateShortcutLabels(ContentResolver resolver, PackageManager manager) {
1074 final Cursor c = resolver.query(LauncherSettings.Favorites.CONTENT_URI,
Romain Guy73b979d2009-06-09 12:57:21 -07001075 new String[] { LauncherSettings.Favorites._ID, LauncherSettings.Favorites.TITLE,
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001076 LauncherSettings.Favorites.INTENT, LauncherSettings.Favorites.ITEM_TYPE },
1077 null, null, null);
1078
Romain Guy73b979d2009-06-09 12:57:21 -07001079 final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001080 final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
1081 final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
1082 final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
1083
1084 // boolean changed = false;
1085
1086 try {
1087 while (c.moveToNext()) {
1088 try {
1089 if (c.getInt(itemTypeIndex) !=
1090 LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
1091 continue;
1092 }
1093
1094 final String intentUri = c.getString(intentIndex);
1095 if (intentUri != null) {
Romain Guy1ce1a242009-06-23 17:34:54 -07001096 final Intent shortcut = Intent.parseUri(intentUri, 0);
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001097 if (Intent.ACTION_MAIN.equals(shortcut.getAction())) {
1098 final ComponentName name = shortcut.getComponent();
1099 if (name != null) {
1100 final ActivityInfo activityInfo = manager.getActivityInfo(name, 0);
1101 final String title = c.getString(titleIndex);
1102 String label = getLabel(manager, activityInfo);
1103
1104 if (title == null || !title.equals(label)) {
1105 final ContentValues values = new ContentValues();
1106 values.put(LauncherSettings.Favorites.TITLE, label);
1107
Romain Guyfedc4fc2009-03-27 20:48:20 -07001108 resolver.update(
1109 LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION,
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001110 values, "_id=?",
1111 new String[] { String.valueOf(c.getLong(idIndex)) });
1112
1113 // changed = true;
1114 }
1115 }
1116 }
1117 }
1118 } catch (URISyntaxException e) {
1119 // Ignore
1120 } catch (PackageManager.NameNotFoundException e) {
1121 // Ignore
1122 }
1123 }
1124 } finally {
1125 c.close();
1126 }
1127
1128 // if (changed) resolver.notifyChange(Settings.Favorites.CONTENT_URI, null);
1129 }
1130
1131 private static String getLabel(PackageManager manager, ActivityInfo activityInfo) {
1132 String label = activityInfo.loadLabel(manager).toString();
1133 if (label == null) {
1134 label = manager.getApplicationLabel(activityInfo.applicationInfo).toString();
1135 if (label == null) {
1136 label = activityInfo.name;
1137 }
1138 }
1139 return label;
1140 }
1141
Joe Onorato9c1289c2009-08-17 11:03:03 -04001142 private static final Collator sCollator = Collator.getInstance();
1143 private static final Comparator<ApplicationInfo> sComparator
1144 = new Comparator<ApplicationInfo>() {
1145 public final int compare(ApplicationInfo a, ApplicationInfo b) {
1146 return sCollator.compare(a.title.toString(), b.title.toString());
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001147 }
Joe Onorato9c1289c2009-08-17 11:03:03 -04001148 };
The Android Open Source Project31dd5032009-03-03 19:32:27 -08001149}