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