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