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