LauncherProvider.java revision b85f8a44b51258f22938773ca30dd85845345010
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 com.android.internal.util.XmlUtils; 20import com.android.launcher.R; 21import com.android.launcher2.LauncherSettings.Favorites; 22 23import org.xmlpull.v1.XmlPullParser; 24import org.xmlpull.v1.XmlPullParserException; 25 26import android.app.SearchManager; 27import android.appwidget.AppWidgetHost; 28import android.appwidget.AppWidgetManager; 29import android.appwidget.AppWidgetProviderInfo; 30import android.content.ComponentName; 31import android.content.ContentProvider; 32import android.content.ContentResolver; 33import android.content.ContentUris; 34import android.content.ContentValues; 35import android.content.Context; 36import android.content.Intent; 37import android.content.SharedPreferences; 38import android.content.pm.ActivityInfo; 39import android.content.pm.PackageManager; 40import android.content.res.Resources; 41import android.content.res.TypedArray; 42import android.content.res.XmlResourceParser; 43import android.database.Cursor; 44import android.database.SQLException; 45import android.database.sqlite.SQLiteDatabase; 46import android.database.sqlite.SQLiteOpenHelper; 47import android.database.sqlite.SQLiteQueryBuilder; 48import android.database.sqlite.SQLiteStatement; 49import android.graphics.Bitmap; 50import android.graphics.BitmapFactory; 51import android.net.Uri; 52import android.provider.Settings; 53import android.text.TextUtils; 54import android.util.AttributeSet; 55import android.util.Log; 56import android.util.Xml; 57 58import java.io.IOException; 59import java.net.URISyntaxException; 60import java.util.ArrayList; 61import java.util.List; 62 63public class LauncherProvider extends ContentProvider { 64 private static final String TAG = "Launcher.LauncherProvider"; 65 private static final boolean LOGD = false; 66 67 private static final String DATABASE_NAME = "launcher.db"; 68 69 private static final int DATABASE_VERSION = 10; 70 71 static final String AUTHORITY = "com.android.launcher2.settings"; 72 73 static final String TABLE_FAVORITES = "favorites"; 74 static final String PARAMETER_NOTIFY = "notify"; 75 static final String DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED = 76 "DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED"; 77 78 /** 79 * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when 80 * {@link AppWidgetHost#deleteHost()} is called during database creation. 81 * Use this to recall {@link AppWidgetHost#startListening()} if needed. 82 */ 83 static final Uri CONTENT_APPWIDGET_RESET_URI = 84 Uri.parse("content://" + AUTHORITY + "/appWidgetReset"); 85 86 private DatabaseHelper mOpenHelper; 87 88 @Override 89 public boolean onCreate() { 90 mOpenHelper = new DatabaseHelper(getContext()); 91 ((LauncherApplication) getContext()).setLauncherProvider(this); 92 return true; 93 } 94 95 @Override 96 public String getType(Uri uri) { 97 SqlArguments args = new SqlArguments(uri, null, null); 98 if (TextUtils.isEmpty(args.where)) { 99 return "vnd.android.cursor.dir/" + args.table; 100 } else { 101 return "vnd.android.cursor.item/" + args.table; 102 } 103 } 104 105 @Override 106 public Cursor query(Uri uri, String[] projection, String selection, 107 String[] selectionArgs, String sortOrder) { 108 109 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 110 SQLiteQueryBuilder qb = new SQLiteQueryBuilder(); 111 qb.setTables(args.table); 112 113 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 114 Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder); 115 result.setNotificationUri(getContext().getContentResolver(), uri); 116 117 return result; 118 } 119 120 private static long dbInsertAndCheck(DatabaseHelper helper, 121 SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) { 122 if (!values.containsKey(LauncherSettings.Favorites._ID)) { 123 throw new RuntimeException("Error: attempting to add item without specifying an id"); 124 } 125 return db.insert(table, nullColumnHack, values); 126 } 127 128 private static void deleteId(SQLiteDatabase db, long id) { 129 Uri uri = LauncherSettings.Favorites.getContentUri(id, false); 130 SqlArguments args = new SqlArguments(uri, null, null); 131 db.delete(args.table, args.where, args.args); 132 } 133 134 @Override 135 public Uri insert(Uri uri, ContentValues initialValues) { 136 SqlArguments args = new SqlArguments(uri); 137 138 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 139 final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues); 140 if (rowId <= 0) return null; 141 142 uri = ContentUris.withAppendedId(uri, rowId); 143 sendNotify(uri); 144 145 return uri; 146 } 147 148 @Override 149 public int bulkInsert(Uri uri, ContentValues[] values) { 150 SqlArguments args = new SqlArguments(uri); 151 152 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 153 db.beginTransaction(); 154 try { 155 int numValues = values.length; 156 for (int i = 0; i < numValues; i++) { 157 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) { 158 return 0; 159 } 160 } 161 db.setTransactionSuccessful(); 162 } finally { 163 db.endTransaction(); 164 } 165 166 sendNotify(uri); 167 return values.length; 168 } 169 170 @Override 171 public int delete(Uri uri, String selection, String[] selectionArgs) { 172 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 173 174 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 175 int count = db.delete(args.table, args.where, args.args); 176 if (count > 0) sendNotify(uri); 177 178 return count; 179 } 180 181 @Override 182 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 183 SqlArguments args = new SqlArguments(uri, selection, selectionArgs); 184 185 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 186 int count = db.update(args.table, values, args.where, args.args); 187 if (count > 0) sendNotify(uri); 188 189 return count; 190 } 191 192 private void sendNotify(Uri uri) { 193 String notify = uri.getQueryParameter(PARAMETER_NOTIFY); 194 if (notify == null || "true".equals(notify)) { 195 getContext().getContentResolver().notifyChange(uri, null); 196 } 197 } 198 199 public long generateNewId() { 200 return mOpenHelper.generateNewId(); 201 } 202 203 public void loadDefaultFavoritesIfNecessary() { 204 String spKey = LauncherApplication.getSharedPreferencesKey(); 205 SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE); 206 if (sp.getBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, false)) { 207 // Populate favorites table with initial favorites 208 SharedPreferences.Editor editor = sp.edit(); 209 editor.remove(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED); 210 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), R.xml.default_workspace); 211 editor.commit(); 212 } 213 } 214 215 private static class DatabaseHelper extends SQLiteOpenHelper { 216 private static final String TAG_FAVORITES = "favorites"; 217 private static final String TAG_FAVORITE = "favorite"; 218 private static final String TAG_CLOCK = "clock"; 219 private static final String TAG_SEARCH = "search"; 220 private static final String TAG_APPWIDGET = "appwidget"; 221 private static final String TAG_SHORTCUT = "shortcut"; 222 private static final String TAG_FOLDER = "folder"; 223 224 private final Context mContext; 225 private final AppWidgetHost mAppWidgetHost; 226 private long mMaxId = -1; 227 228 DatabaseHelper(Context context) { 229 super(context, DATABASE_NAME, null, DATABASE_VERSION); 230 mContext = context; 231 mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID); 232 233 // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from 234 // the DB here 235 if (mMaxId == -1) { 236 mMaxId = initializeMaxId(getWritableDatabase()); 237 } 238 } 239 240 /** 241 * Send notification that we've deleted the {@link AppWidgetHost}, 242 * probably as part of the initial database creation. The receiver may 243 * want to re-call {@link AppWidgetHost#startListening()} to ensure 244 * callbacks are correctly set. 245 */ 246 private void sendAppWidgetResetNotify() { 247 final ContentResolver resolver = mContext.getContentResolver(); 248 resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null); 249 } 250 251 @Override 252 public void onCreate(SQLiteDatabase db) { 253 if (LOGD) Log.d(TAG, "creating new launcher database"); 254 255 mMaxId = 1; 256 257 db.execSQL("CREATE TABLE favorites (" + 258 "_id INTEGER PRIMARY KEY," + 259 "title TEXT," + 260 "intent TEXT," + 261 "container INTEGER," + 262 "screen INTEGER," + 263 "cellX INTEGER," + 264 "cellY INTEGER," + 265 "spanX INTEGER," + 266 "spanY INTEGER," + 267 "itemType INTEGER," + 268 "appWidgetId INTEGER NOT NULL DEFAULT -1," + 269 "isShortcut INTEGER," + 270 "iconType INTEGER," + 271 "iconPackage TEXT," + 272 "iconResource TEXT," + 273 "icon BLOB," + 274 "uri TEXT," + 275 "displayMode INTEGER" + 276 ");"); 277 278 // Database was just created, so wipe any previous widgets 279 if (mAppWidgetHost != null) { 280 mAppWidgetHost.deleteHost(); 281 sendAppWidgetResetNotify(); 282 } 283 284 if (!convertDatabase(db)) { 285 // Set a shared pref so that we know we need to load the default workspace later 286 setFlagToLoadDefaultWorkspaceLater(); 287 } 288 } 289 290 private void setFlagToLoadDefaultWorkspaceLater() { 291 String spKey = LauncherApplication.getSharedPreferencesKey(); 292 SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE); 293 SharedPreferences.Editor editor = sp.edit(); 294 editor.putBoolean(DB_CREATED_BUT_DEFAULT_WORKSPACE_NOT_LOADED, true); 295 editor.commit(); 296 } 297 298 private boolean convertDatabase(SQLiteDatabase db) { 299 if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade"); 300 boolean converted = false; 301 302 final Uri uri = Uri.parse("content://" + Settings.AUTHORITY + 303 "/old_favorites?notify=true"); 304 final ContentResolver resolver = mContext.getContentResolver(); 305 Cursor cursor = null; 306 307 try { 308 cursor = resolver.query(uri, null, null, null, null); 309 } catch (Exception e) { 310 // Ignore 311 } 312 313 // We already have a favorites database in the old provider 314 if (cursor != null && cursor.getCount() > 0) { 315 try { 316 converted = copyFromCursor(db, cursor) > 0; 317 } finally { 318 cursor.close(); 319 } 320 321 if (converted) { 322 resolver.delete(uri, null, null); 323 } 324 } 325 326 if (converted) { 327 // Convert widgets from this import into widgets 328 if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade"); 329 convertWidgets(db); 330 } 331 332 return converted; 333 } 334 335 private int copyFromCursor(SQLiteDatabase db, Cursor c) { 336 final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID); 337 final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); 338 final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); 339 final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE); 340 final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); 341 final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); 342 final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); 343 final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); 344 final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); 345 final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); 346 final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); 347 final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); 348 final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI); 349 final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE); 350 351 ContentValues[] rows = new ContentValues[c.getCount()]; 352 int i = 0; 353 while (c.moveToNext()) { 354 ContentValues values = new ContentValues(c.getColumnCount()); 355 values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex)); 356 values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex)); 357 values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex)); 358 values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex)); 359 values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex)); 360 values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex)); 361 values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex)); 362 values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex)); 363 values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex)); 364 values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1); 365 values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex)); 366 values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex)); 367 values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex)); 368 values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex)); 369 values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex)); 370 rows[i++] = values; 371 } 372 373 db.beginTransaction(); 374 int total = 0; 375 try { 376 int numValues = rows.length; 377 for (i = 0; i < numValues; i++) { 378 if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) { 379 return 0; 380 } else { 381 total++; 382 } 383 } 384 db.setTransactionSuccessful(); 385 } finally { 386 db.endTransaction(); 387 } 388 389 return total; 390 } 391 392 @Override 393 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 394 if (LOGD) Log.d(TAG, "onUpgrade triggered"); 395 396 int version = oldVersion; 397 if (version < 3) { 398 // upgrade 1,2 -> 3 added appWidgetId column 399 db.beginTransaction(); 400 try { 401 // Insert new column for holding appWidgetIds 402 db.execSQL("ALTER TABLE favorites " + 403 "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;"); 404 db.setTransactionSuccessful(); 405 version = 3; 406 } catch (SQLException ex) { 407 // Old version remains, which means we wipe old data 408 Log.e(TAG, ex.getMessage(), ex); 409 } finally { 410 db.endTransaction(); 411 } 412 413 // Convert existing widgets only if table upgrade was successful 414 if (version == 3) { 415 convertWidgets(db); 416 } 417 } 418 419 if (version < 4) { 420 version = 4; 421 } 422 423 // Where's version 5? 424 // - Donut and sholes on 2.0 shipped with version 4 of launcher1. 425 // - Passion shipped on 2.1 with version 6 of launcher2 426 // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1 427 // but version 5 on there was the updateContactsShortcuts change 428 // which was version 6 in launcher 2 (first shipped on passion 2.1r1). 429 // The updateContactsShortcuts change is idempotent, so running it twice 430 // is okay so we'll do that when upgrading the devices that shipped with it. 431 if (version < 6) { 432 // We went from 3 to 5 screens. Move everything 1 to the right 433 db.beginTransaction(); 434 try { 435 db.execSQL("UPDATE favorites SET screen=(screen + 1);"); 436 db.setTransactionSuccessful(); 437 } catch (SQLException ex) { 438 // Old version remains, which means we wipe old data 439 Log.e(TAG, ex.getMessage(), ex); 440 } finally { 441 db.endTransaction(); 442 } 443 444 // We added the fast track. 445 if (updateContactsShortcuts(db)) { 446 version = 6; 447 } 448 } 449 450 if (version < 7) { 451 // Version 7 gets rid of the special search widget. 452 convertWidgets(db); 453 version = 7; 454 } 455 456 if (version < 8) { 457 // Version 8 (froyo) has the icons all normalized. This should 458 // already be the case in practice, but we now rely on it and don't 459 // resample the images each time. 460 normalizeIcons(db); 461 version = 8; 462 } 463 464 if (version < 9) { 465 // The max id is not yet set at this point (onUpgrade is triggered in the ctor 466 // before it gets a change to get set, so we need to read it here when we use it) 467 if (mMaxId == -1) { 468 mMaxId = initializeMaxId(db); 469 } 470 471 // Add default hotseat icons 472 loadFavorites(db, R.xml.update_workspace); 473 version = 9; 474 } 475 476 if (version < 10) { 477 // Contact shortcuts need a different set of flags to be launched now 478 // The updateContactsShortcuts change is idempotent, so we can keep using it like 479 // back in the Donut days 480 updateContactsShortcuts(db); 481 version = 10; 482 } 483 484 if (version != DATABASE_VERSION) { 485 Log.w(TAG, "Destroying all old data."); 486 db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES); 487 onCreate(db); 488 } 489 } 490 491 private boolean updateContactsShortcuts(SQLiteDatabase db) { 492 final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, 493 new int[] { Favorites.ITEM_TYPE_SHORTCUT }); 494 495 Cursor c = null; 496 final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT"; 497 db.beginTransaction(); 498 try { 499 // Select and iterate through each matching widget 500 c = db.query(TABLE_FAVORITES, 501 new String[] { Favorites._ID, Favorites.INTENT }, 502 selectWhere, null, null, null, null); 503 if (c == null) return false; 504 505 if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); 506 507 final int idIndex = c.getColumnIndex(Favorites._ID); 508 final int intentIndex = c.getColumnIndex(Favorites.INTENT); 509 510 while (c.moveToNext()) { 511 long favoriteId = c.getLong(idIndex); 512 final String intentUri = c.getString(intentIndex); 513 if (intentUri != null) { 514 try { 515 final Intent intent = Intent.parseUri(intentUri, 0); 516 android.util.Log.d("Home", intent.toString()); 517 final Uri uri = intent.getData(); 518 if (uri != null) { 519 final String data = uri.toString(); 520 if ((Intent.ACTION_VIEW.equals(intent.getAction()) || 521 actionQuickContact.equals(intent.getAction())) && 522 (data.startsWith("content://contacts/people/") || 523 data.startsWith("content://com.android.contacts/" + 524 "contacts/lookup/"))) { 525 526 final Intent newIntent = new Intent(actionQuickContact); 527 // When starting from the launcher, start in a new, cleared task 528 // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we 529 // clear the whole thing preemptively here since 530 // QuickContactActivity will finish itself when launching other 531 // detail activities. 532 newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 533 Intent.FLAG_ACTIVITY_CLEAR_TASK); 534 535 newIntent.setData(uri); 536 537 final ContentValues values = new ContentValues(); 538 values.put(LauncherSettings.Favorites.INTENT, 539 newIntent.toUri(0)); 540 541 String updateWhere = Favorites._ID + "=" + favoriteId; 542 db.update(TABLE_FAVORITES, values, updateWhere, null); 543 } 544 } 545 } catch (RuntimeException ex) { 546 Log.e(TAG, "Problem upgrading shortcut", ex); 547 } catch (URISyntaxException e) { 548 Log.e(TAG, "Problem upgrading shortcut", e); 549 } 550 } 551 } 552 553 db.setTransactionSuccessful(); 554 } catch (SQLException ex) { 555 Log.w(TAG, "Problem while upgrading contacts", ex); 556 return false; 557 } finally { 558 db.endTransaction(); 559 if (c != null) { 560 c.close(); 561 } 562 } 563 564 return true; 565 } 566 567 private void normalizeIcons(SQLiteDatabase db) { 568 Log.d(TAG, "normalizing icons"); 569 570 db.beginTransaction(); 571 Cursor c = null; 572 SQLiteStatement update = null; 573 try { 574 boolean logged = false; 575 update = db.compileStatement("UPDATE favorites " 576 + "SET icon=? WHERE _id=?"); 577 578 c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" + 579 Favorites.ICON_TYPE_BITMAP, null); 580 581 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID); 582 final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON); 583 584 while (c.moveToNext()) { 585 long id = c.getLong(idIndex); 586 byte[] data = c.getBlob(iconIndex); 587 try { 588 Bitmap bitmap = Utilities.resampleIconBitmap( 589 BitmapFactory.decodeByteArray(data, 0, data.length), 590 mContext); 591 if (bitmap != null) { 592 update.bindLong(1, id); 593 data = ItemInfo.flattenBitmap(bitmap); 594 if (data != null) { 595 update.bindBlob(2, data); 596 update.execute(); 597 } 598 bitmap.recycle(); 599 } 600 } catch (Exception e) { 601 if (!logged) { 602 Log.e(TAG, "Failed normalizing icon " + id, e); 603 } else { 604 Log.e(TAG, "Also failed normalizing icon " + id); 605 } 606 logged = true; 607 } 608 } 609 db.setTransactionSuccessful(); 610 } catch (SQLException ex) { 611 Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); 612 } finally { 613 db.endTransaction(); 614 if (update != null) { 615 update.close(); 616 } 617 if (c != null) { 618 c.close(); 619 } 620 } 621 } 622 623 // Generates a new ID to use for an object in your database. This method should be only 624 // called from the main UI thread. As an exception, we do call it when we call the 625 // constructor from the worker thread; however, this doesn't extend until after the 626 // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp 627 // after that point 628 public long generateNewId() { 629 if (mMaxId < 0) { 630 throw new RuntimeException("Error: max id was not initialized"); 631 } 632 mMaxId += 1; 633 return mMaxId; 634 } 635 636 private long initializeMaxId(SQLiteDatabase db) { 637 Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null); 638 639 // get the result 640 final int maxIdIndex = 0; 641 long id = -1; 642 if (c != null && c.moveToNext()) { 643 id = c.getLong(maxIdIndex); 644 } 645 if (c != null) { 646 c.close(); 647 } 648 649 if (id == -1) { 650 throw new RuntimeException("Error: could not query max id"); 651 } 652 653 return id; 654 } 655 656 /** 657 * Upgrade existing clock and photo frame widgets into their new widget 658 * equivalents. 659 */ 660 private void convertWidgets(SQLiteDatabase db) { 661 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); 662 final int[] bindSources = new int[] { 663 Favorites.ITEM_TYPE_WIDGET_CLOCK, 664 Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME, 665 Favorites.ITEM_TYPE_WIDGET_SEARCH, 666 }; 667 668 final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources); 669 670 Cursor c = null; 671 672 db.beginTransaction(); 673 try { 674 // Select and iterate through each matching widget 675 c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE }, 676 selectWhere, null, null, null, null); 677 678 if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount()); 679 680 final ContentValues values = new ContentValues(); 681 while (c != null && c.moveToNext()) { 682 long favoriteId = c.getLong(0); 683 int favoriteType = c.getInt(1); 684 685 // Allocate and update database with new appWidgetId 686 try { 687 int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); 688 689 if (LOGD) { 690 Log.d(TAG, "allocated appWidgetId=" + appWidgetId 691 + " for favoriteId=" + favoriteId); 692 } 693 values.clear(); 694 values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); 695 values.put(Favorites.APPWIDGET_ID, appWidgetId); 696 697 // Original widgets might not have valid spans when upgrading 698 if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { 699 values.put(LauncherSettings.Favorites.SPANX, 4); 700 values.put(LauncherSettings.Favorites.SPANY, 1); 701 } else { 702 values.put(LauncherSettings.Favorites.SPANX, 2); 703 values.put(LauncherSettings.Favorites.SPANY, 2); 704 } 705 706 String updateWhere = Favorites._ID + "=" + favoriteId; 707 db.update(TABLE_FAVORITES, values, updateWhere, null); 708 709 if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) { 710 appWidgetManager.bindAppWidgetId(appWidgetId, 711 new ComponentName("com.android.alarmclock", 712 "com.android.alarmclock.AnalogAppWidgetProvider")); 713 } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) { 714 appWidgetManager.bindAppWidgetId(appWidgetId, 715 new ComponentName("com.android.camera", 716 "com.android.camera.PhotoAppWidgetProvider")); 717 } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) { 718 appWidgetManager.bindAppWidgetId(appWidgetId, 719 getSearchWidgetProvider()); 720 } 721 } catch (RuntimeException ex) { 722 Log.e(TAG, "Problem allocating appWidgetId", ex); 723 } 724 } 725 726 db.setTransactionSuccessful(); 727 } catch (SQLException ex) { 728 Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex); 729 } finally { 730 db.endTransaction(); 731 if (c != null) { 732 c.close(); 733 } 734 } 735 } 736 737 /** 738 * Loads the default set of favorite packages from an xml file. 739 * 740 * @param db The database to write the values into 741 * @param filterContainerId The specific container id of items to load 742 */ 743 private int loadFavorites(SQLiteDatabase db, int workspaceResourceId) { 744 Intent intent = new Intent(Intent.ACTION_MAIN, null); 745 intent.addCategory(Intent.CATEGORY_LAUNCHER); 746 ContentValues values = new ContentValues(); 747 748 PackageManager packageManager = mContext.getPackageManager(); 749 int allAppsButtonRank = 750 mContext.getResources().getInteger(R.integer.hotseat_all_apps_index); 751 int i = 0; 752 try { 753 XmlResourceParser parser = mContext.getResources().getXml(workspaceResourceId); 754 AttributeSet attrs = Xml.asAttributeSet(parser); 755 XmlUtils.beginDocument(parser, TAG_FAVORITES); 756 757 final int depth = parser.getDepth(); 758 759 int type; 760 while (((type = parser.next()) != XmlPullParser.END_TAG || 761 parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { 762 763 if (type != XmlPullParser.START_TAG) { 764 continue; 765 } 766 767 boolean added = false; 768 final String name = parser.getName(); 769 770 TypedArray a = mContext.obtainStyledAttributes(attrs, R.styleable.Favorite); 771 772 long container = LauncherSettings.Favorites.CONTAINER_DESKTOP; 773 if (a.hasValue(R.styleable.Favorite_container)) { 774 container = Long.valueOf(a.getString(R.styleable.Favorite_container)); 775 } 776 777 String screen = a.getString(R.styleable.Favorite_screen); 778 String x = a.getString(R.styleable.Favorite_x); 779 String y = a.getString(R.styleable.Favorite_y); 780 781 // If we are adding to the hotseat, the screen is used as the position in the 782 // hotseat. This screen can't be at position 0 because AllApps is in the 783 // zeroth position. 784 if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT 785 && Integer.valueOf(screen) == allAppsButtonRank) { 786 throw new RuntimeException("Invalid screen position for hotseat item"); 787 } 788 789 values.clear(); 790 values.put(LauncherSettings.Favorites.CONTAINER, container); 791 values.put(LauncherSettings.Favorites.SCREEN, screen); 792 values.put(LauncherSettings.Favorites.CELLX, x); 793 values.put(LauncherSettings.Favorites.CELLY, y); 794 795 if (TAG_FAVORITE.equals(name)) { 796 long id = addAppShortcut(db, values, a, packageManager, intent); 797 added = id >= 0; 798 } else if (TAG_SEARCH.equals(name)) { 799 added = addSearchWidget(db, values); 800 } else if (TAG_CLOCK.equals(name)) { 801 added = addClockWidget(db, values); 802 } else if (TAG_APPWIDGET.equals(name)) { 803 added = addAppWidget(db, values, a, packageManager); 804 } else if (TAG_SHORTCUT.equals(name)) { 805 long id = addUriShortcut(db, values, a); 806 added = id >= 0; 807 } else if (TAG_FOLDER.equals(name)) { 808 String title; 809 int titleResId = a.getResourceId(R.styleable.Favorite_title, -1); 810 if (titleResId != -1) { 811 title = mContext.getResources().getString(titleResId); 812 } else { 813 title = mContext.getResources().getString(R.string.folder_name); 814 } 815 values.put(LauncherSettings.Favorites.TITLE, title); 816 long folderId = addFolder(db, values); 817 added = folderId >= 0; 818 819 ArrayList<Long> folderItems = new ArrayList<Long>(); 820 821 int folderDepth = parser.getDepth(); 822 while ((type = parser.next()) != XmlPullParser.END_TAG || 823 parser.getDepth() > folderDepth) { 824 if (type != XmlPullParser.START_TAG) { 825 continue; 826 } 827 final String folder_item_name = parser.getName(); 828 829 TypedArray ar = mContext.obtainStyledAttributes(attrs, 830 R.styleable.Favorite); 831 values.clear(); 832 values.put(LauncherSettings.Favorites.CONTAINER, folderId); 833 834 if (TAG_FAVORITE.equals(folder_item_name) && folderId >= 0) { 835 long id = 836 addAppShortcut(db, values, ar, packageManager, intent); 837 if (id >= 0) { 838 folderItems.add(id); 839 } 840 } else if (TAG_SHORTCUT.equals(folder_item_name) && folderId >= 0) { 841 long id = addUriShortcut(db, values, ar); 842 if (id >= 0) { 843 folderItems.add(id); 844 } 845 } else { 846 throw new RuntimeException("Folders can " + 847 "contain only shortcuts"); 848 } 849 ar.recycle(); 850 } 851 // We can only have folders with >= 2 items, so we need to remove the 852 // folder and clean up if less than 2 items were included, or some 853 // failed to add, and less than 2 were actually added 854 if (folderItems.size() < 2 && folderId >= 0) { 855 // We just delete the folder and any items that made it 856 deleteId(db, folderId); 857 if (folderItems.size() > 0) { 858 deleteId(db, folderItems.get(0)); 859 } 860 added = false; 861 } 862 } 863 if (added) i++; 864 a.recycle(); 865 } 866 } catch (XmlPullParserException e) { 867 Log.w(TAG, "Got exception parsing favorites.", e); 868 } catch (IOException e) { 869 Log.w(TAG, "Got exception parsing favorites.", e); 870 } catch (RuntimeException e) { 871 Log.w(TAG, "Got exception parsing favorites.", e); 872 } 873 874 return i; 875 } 876 877 private long addAppShortcut(SQLiteDatabase db, ContentValues values, TypedArray a, 878 PackageManager packageManager, Intent intent) { 879 long id = -1; 880 ActivityInfo info; 881 String packageName = a.getString(R.styleable.Favorite_packageName); 882 String className = a.getString(R.styleable.Favorite_className); 883 try { 884 ComponentName cn; 885 try { 886 cn = new ComponentName(packageName, className); 887 info = packageManager.getActivityInfo(cn, 0); 888 } catch (PackageManager.NameNotFoundException nnfe) { 889 String[] packages = packageManager.currentToCanonicalPackageNames( 890 new String[] { packageName }); 891 cn = new ComponentName(packages[0], className); 892 info = packageManager.getActivityInfo(cn, 0); 893 } 894 id = generateNewId(); 895 intent.setComponent(cn); 896 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 897 Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 898 values.put(Favorites.INTENT, intent.toUri(0)); 899 values.put(Favorites.TITLE, info.loadLabel(packageManager).toString()); 900 values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPLICATION); 901 values.put(Favorites.SPANX, 1); 902 values.put(Favorites.SPANY, 1); 903 values.put(Favorites._ID, generateNewId()); 904 if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { 905 return -1; 906 } 907 } catch (PackageManager.NameNotFoundException e) { 908 Log.w(TAG, "Unable to add favorite: " + packageName + 909 "/" + className, e); 910 } 911 return id; 912 } 913 914 private long addFolder(SQLiteDatabase db, ContentValues values) { 915 values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_FOLDER); 916 values.put(Favorites.SPANX, 1); 917 values.put(Favorites.SPANY, 1); 918 long id = generateNewId(); 919 values.put(Favorites._ID, id); 920 if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) <= 0) { 921 return -1; 922 } else { 923 return id; 924 } 925 } 926 927 private ComponentName getSearchWidgetProvider() { 928 SearchManager searchManager = 929 (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); 930 ComponentName searchComponent = searchManager.getGlobalSearchActivity(); 931 if (searchComponent == null) return null; 932 return getProviderInPackage(searchComponent.getPackageName()); 933 } 934 935 /** 936 * Gets an appwidget provider from the given package. If the package contains more than 937 * one appwidget provider, an arbitrary one is returned. 938 */ 939 private ComponentName getProviderInPackage(String packageName) { 940 AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); 941 List<AppWidgetProviderInfo> providers = appWidgetManager.getInstalledProviders(); 942 if (providers == null) return null; 943 final int providerCount = providers.size(); 944 for (int i = 0; i < providerCount; i++) { 945 ComponentName provider = providers.get(i).provider; 946 if (provider != null && provider.getPackageName().equals(packageName)) { 947 return provider; 948 } 949 } 950 return null; 951 } 952 953 private boolean addSearchWidget(SQLiteDatabase db, ContentValues values) { 954 ComponentName cn = getSearchWidgetProvider(); 955 return addAppWidget(db, values, cn, 4, 1); 956 } 957 958 private boolean addClockWidget(SQLiteDatabase db, ContentValues values) { 959 ComponentName cn = new ComponentName("com.android.alarmclock", 960 "com.android.alarmclock.AnalogAppWidgetProvider"); 961 return addAppWidget(db, values, cn, 2, 2); 962 } 963 964 private boolean addAppWidget(SQLiteDatabase db, ContentValues values, TypedArray a, 965 PackageManager packageManager) { 966 967 String packageName = a.getString(R.styleable.Favorite_packageName); 968 String className = a.getString(R.styleable.Favorite_className); 969 970 if (packageName == null || className == null) { 971 return false; 972 } 973 974 boolean hasPackage = true; 975 ComponentName cn = new ComponentName(packageName, className); 976 try { 977 packageManager.getReceiverInfo(cn, 0); 978 } catch (Exception e) { 979 String[] packages = packageManager.currentToCanonicalPackageNames( 980 new String[] { packageName }); 981 cn = new ComponentName(packages[0], className); 982 try { 983 packageManager.getReceiverInfo(cn, 0); 984 } catch (Exception e1) { 985 hasPackage = false; 986 } 987 } 988 989 if (hasPackage) { 990 int spanX = a.getInt(R.styleable.Favorite_spanX, 0); 991 int spanY = a.getInt(R.styleable.Favorite_spanY, 0); 992 return addAppWidget(db, values, cn, spanX, spanY); 993 } 994 995 return false; 996 } 997 998 private boolean addAppWidget(SQLiteDatabase db, ContentValues values, ComponentName cn, 999 int spanX, int spanY) { 1000 boolean allocatedAppWidgets = false; 1001 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext); 1002 1003 try { 1004 int appWidgetId = mAppWidgetHost.allocateAppWidgetId(); 1005 1006 values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET); 1007 values.put(Favorites.SPANX, spanX); 1008 values.put(Favorites.SPANY, spanY); 1009 values.put(Favorites.APPWIDGET_ID, appWidgetId); 1010 values.put(Favorites._ID, generateNewId()); 1011 dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values); 1012 1013 allocatedAppWidgets = true; 1014 1015 appWidgetManager.bindAppWidgetId(appWidgetId, cn); 1016 } catch (RuntimeException ex) { 1017 Log.e(TAG, "Problem allocating appWidgetId", ex); 1018 } 1019 1020 return allocatedAppWidgets; 1021 } 1022 1023 private long addUriShortcut(SQLiteDatabase db, ContentValues values, 1024 TypedArray a) { 1025 Resources r = mContext.getResources(); 1026 1027 final int iconResId = a.getResourceId(R.styleable.Favorite_icon, 0); 1028 final int titleResId = a.getResourceId(R.styleable.Favorite_title, 0); 1029 1030 Intent intent; 1031 String uri = null; 1032 try { 1033 uri = a.getString(R.styleable.Favorite_uri); 1034 intent = Intent.parseUri(uri, 0); 1035 } catch (URISyntaxException e) { 1036 Log.w(TAG, "Shortcut has malformed uri: " + uri); 1037 return -1; // Oh well 1038 } 1039 1040 if (iconResId == 0 || titleResId == 0) { 1041 Log.w(TAG, "Shortcut is missing title or icon resource ID"); 1042 return -1; 1043 } 1044 1045 long id = generateNewId(); 1046 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 1047 values.put(Favorites.INTENT, intent.toUri(0)); 1048 values.put(Favorites.TITLE, r.getString(titleResId)); 1049 values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_SHORTCUT); 1050 values.put(Favorites.SPANX, 1); 1051 values.put(Favorites.SPANY, 1); 1052 values.put(Favorites.ICON_TYPE, Favorites.ICON_TYPE_RESOURCE); 1053 values.put(Favorites.ICON_PACKAGE, mContext.getPackageName()); 1054 values.put(Favorites.ICON_RESOURCE, r.getResourceName(iconResId)); 1055 values.put(Favorites._ID, id); 1056 1057 if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values) < 0) { 1058 return -1; 1059 } 1060 return id; 1061 } 1062 } 1063 1064 /** 1065 * Build a query string that will match any row where the column matches 1066 * anything in the values list. 1067 */ 1068 static String buildOrWhereString(String column, int[] values) { 1069 StringBuilder selectWhere = new StringBuilder(); 1070 for (int i = values.length - 1; i >= 0; i--) { 1071 selectWhere.append(column).append("=").append(values[i]); 1072 if (i > 0) { 1073 selectWhere.append(" OR "); 1074 } 1075 } 1076 return selectWhere.toString(); 1077 } 1078 1079 static class SqlArguments { 1080 public final String table; 1081 public final String where; 1082 public final String[] args; 1083 1084 SqlArguments(Uri url, String where, String[] args) { 1085 if (url.getPathSegments().size() == 1) { 1086 this.table = url.getPathSegments().get(0); 1087 this.where = where; 1088 this.args = args; 1089 } else if (url.getPathSegments().size() != 2) { 1090 throw new IllegalArgumentException("Invalid URI: " + url); 1091 } else if (!TextUtils.isEmpty(where)) { 1092 throw new UnsupportedOperationException("WHERE clause not supported: " + url); 1093 } else { 1094 this.table = url.getPathSegments().get(0); 1095 this.where = "_id=" + ContentUris.parseId(url); 1096 this.args = null; 1097 } 1098 } 1099 1100 SqlArguments(Uri url) { 1101 if (url.getPathSegments().size() == 1) { 1102 table = url.getPathSegments().get(0); 1103 where = null; 1104 args = null; 1105 } else { 1106 throw new IllegalArgumentException("Invalid URI: " + url); 1107 } 1108 } 1109 } 1110} 1111