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