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