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