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.appwidget.AppWidgetHost;
20 import android.appwidget.AppWidgetManager;
21 import android.appwidget.AppWidgetProviderInfo;
22 import android.content.ComponentName;
23 import android.content.ContentProvider;
24 import android.content.ContentProviderOperation;
25 import android.content.ContentProviderResult;
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.OperationApplicationException;
32 import android.content.SharedPreferences;
33 import android.content.res.Resources;
34 import android.database.Cursor;
35 import android.database.SQLException;
36 import android.database.sqlite.SQLiteDatabase;
37 import android.database.sqlite.SQLiteOpenHelper;
38 import android.database.sqlite.SQLiteQueryBuilder;
39 import android.database.sqlite.SQLiteStatement;
40 import android.graphics.Bitmap;
41 import android.graphics.BitmapFactory;
42 import android.net.Uri;
43 import android.provider.Settings;
44 import android.text.TextUtils;
45 import android.util.Log;
46 import android.util.SparseArray;
47 
48 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
49 import com.android.launcher3.LauncherSettings.Favorites;
50 import com.android.launcher3.compat.UserHandleCompat;
51 import com.android.launcher3.compat.UserManagerCompat;
52 import com.android.launcher3.config.ProviderConfig;
53 
54 import java.io.File;
55 import java.net.URISyntaxException;
56 import java.util.ArrayList;
57 import java.util.Collections;
58 import java.util.HashSet;
59 
60 public class LauncherProvider extends ContentProvider {
61     private static final String TAG = "Launcher.LauncherProvider";
62     private static final boolean LOGD = false;
63 
64     private static final int DATABASE_VERSION = 20;
65 
66     static final String OLD_AUTHORITY = "com.android.launcher2.settings";
67     static final String AUTHORITY = ProviderConfig.AUTHORITY;
68 
69     // Should we attempt to load anything from the com.android.launcher2 provider?
70     static final boolean IMPORT_LAUNCHER2_DATABASE = false;
71 
72     static final String TABLE_FAVORITES = "favorites";
73     static final String TABLE_WORKSPACE_SCREENS = "workspaceScreens";
74     static final String PARAMETER_NOTIFY = "notify";
75     static final String UPGRADED_FROM_OLD_DATABASE =
76             "UPGRADED_FROM_OLD_DATABASE";
77     static final String EMPTY_DATABASE_CREATED =
78             "EMPTY_DATABASE_CREATED";
79 
80     private static final String URI_PARAM_IS_EXTERNAL_ADD = "isExternalAdd";
81 
82     private LauncherProviderChangeListener mListener;
83 
84     /**
85      * {@link Uri} triggered at any registered {@link android.database.ContentObserver} when
86      * {@link AppWidgetHost#deleteHost()} is called during database creation.
87      * Use this to recall {@link AppWidgetHost#startListening()} if needed.
88      */
89     static final Uri CONTENT_APPWIDGET_RESET_URI =
90             Uri.parse("content://" + AUTHORITY + "/appWidgetReset");
91 
92     private DatabaseHelper mOpenHelper;
93     private static boolean sJustLoadedFromOldDb;
94 
95     @Override
onCreate()96     public boolean onCreate() {
97         final Context context = getContext();
98         mOpenHelper = new DatabaseHelper(context);
99         LauncherAppState.setLauncherProvider(this);
100         return true;
101     }
102 
wasNewDbCreated()103     public boolean wasNewDbCreated() {
104         return mOpenHelper.wasNewDbCreated();
105     }
106 
setLauncherProviderChangeListener(LauncherProviderChangeListener listener)107     public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
108         mListener = listener;
109     }
110 
111     @Override
getType(Uri uri)112     public String getType(Uri uri) {
113         SqlArguments args = new SqlArguments(uri, null, null);
114         if (TextUtils.isEmpty(args.where)) {
115             return "vnd.android.cursor.dir/" + args.table;
116         } else {
117             return "vnd.android.cursor.item/" + args.table;
118         }
119     }
120 
121     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)122     public Cursor query(Uri uri, String[] projection, String selection,
123             String[] selectionArgs, String sortOrder) {
124 
125         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
126         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
127         qb.setTables(args.table);
128 
129         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
130         Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
131         result.setNotificationUri(getContext().getContentResolver(), uri);
132 
133         return result;
134     }
135 
dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values)136     private static long dbInsertAndCheck(DatabaseHelper helper,
137             SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
138         if (values == null) {
139             throw new RuntimeException("Error: attempting to insert null values");
140         }
141         if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
142             throw new RuntimeException("Error: attempting to add item without specifying an id");
143         }
144         helper.checkId(table, values);
145         return db.insert(table, nullColumnHack, values);
146     }
147 
148     @Override
insert(Uri uri, ContentValues initialValues)149     public Uri insert(Uri uri, ContentValues initialValues) {
150         SqlArguments args = new SqlArguments(uri);
151 
152         // In very limited cases, we support system|signature permission apps to add to the db
153         String externalAdd = uri.getQueryParameter(URI_PARAM_IS_EXTERNAL_ADD);
154         if (externalAdd != null && "true".equals(externalAdd)) {
155             if (!mOpenHelper.initializeExternalAdd(initialValues)) {
156                 return null;
157             }
158         }
159 
160         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
161         addModifiedTime(initialValues);
162         final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
163         if (rowId <= 0) return null;
164 
165         uri = ContentUris.withAppendedId(uri, rowId);
166         sendNotify(uri);
167 
168         return uri;
169     }
170 
171 
172     @Override
bulkInsert(Uri uri, ContentValues[] values)173     public int bulkInsert(Uri uri, ContentValues[] values) {
174         SqlArguments args = new SqlArguments(uri);
175 
176         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
177         db.beginTransaction();
178         try {
179             int numValues = values.length;
180             for (int i = 0; i < numValues; i++) {
181                 addModifiedTime(values[i]);
182                 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
183                     return 0;
184                 }
185             }
186             db.setTransactionSuccessful();
187         } finally {
188             db.endTransaction();
189         }
190 
191         sendNotify(uri);
192         return values.length;
193     }
194 
195     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)196     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
197             throws OperationApplicationException {
198         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
199         db.beginTransaction();
200         try {
201             ContentProviderResult[] result =  super.applyBatch(operations);
202             db.setTransactionSuccessful();
203             return result;
204         } finally {
205             db.endTransaction();
206         }
207     }
208 
209     @Override
delete(Uri uri, String selection, String[] selectionArgs)210     public int delete(Uri uri, String selection, String[] selectionArgs) {
211         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
212 
213         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
214         int count = db.delete(args.table, args.where, args.args);
215         if (count > 0) sendNotify(uri);
216 
217         return count;
218     }
219 
220     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)221     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
222         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
223 
224         addModifiedTime(values);
225         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
226         int count = db.update(args.table, values, args.where, args.args);
227         if (count > 0) sendNotify(uri);
228 
229         return count;
230     }
231 
sendNotify(Uri uri)232     private void sendNotify(Uri uri) {
233         String notify = uri.getQueryParameter(PARAMETER_NOTIFY);
234         if (notify == null || "true".equals(notify)) {
235             getContext().getContentResolver().notifyChange(uri, null);
236         }
237 
238         // always notify the backup agent
239         LauncherBackupAgentHelper.dataChanged(getContext());
240         if (mListener != null) {
241             mListener.onLauncherProviderChange();
242         }
243     }
244 
addModifiedTime(ContentValues values)245     private void addModifiedTime(ContentValues values) {
246         values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
247     }
248 
generateNewItemId()249     public long generateNewItemId() {
250         return mOpenHelper.generateNewItemId();
251     }
252 
updateMaxItemId(long id)253     public void updateMaxItemId(long id) {
254         mOpenHelper.updateMaxItemId(id);
255     }
256 
generateNewScreenId()257     public long generateNewScreenId() {
258         return mOpenHelper.generateNewScreenId();
259     }
260 
261     // This is only required one time while loading the workspace during the
262     // upgrade path, and should never be called from anywhere else.
updateMaxScreenId(long maxScreenId)263     public void updateMaxScreenId(long maxScreenId) {
264         mOpenHelper.updateMaxScreenId(maxScreenId);
265     }
266 
267     /**
268      * @param Should we load the old db for upgrade? first run only.
269      */
justLoadedOldDb()270     synchronized public boolean justLoadedOldDb() {
271         String spKey = LauncherAppState.getSharedPreferencesKey();
272         SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
273 
274         boolean loadedOldDb = false || sJustLoadedFromOldDb;
275 
276         sJustLoadedFromOldDb = false;
277         if (sp.getBoolean(UPGRADED_FROM_OLD_DATABASE, false)) {
278 
279             SharedPreferences.Editor editor = sp.edit();
280             editor.remove(UPGRADED_FROM_OLD_DATABASE);
281             editor.commit();
282             loadedOldDb = true;
283         }
284         return loadedOldDb;
285     }
286 
287     /**
288      * Clears all the data for a fresh start.
289      */
createEmptyDB()290     synchronized public void createEmptyDB() {
291         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
292     }
293 
clearFlagEmptyDbCreated()294     public void clearFlagEmptyDbCreated() {
295         String spKey = LauncherAppState.getSharedPreferencesKey();
296         getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE)
297             .edit()
298             .remove(EMPTY_DATABASE_CREATED)
299             .commit();
300     }
301 
302     /**
303      * Loads the default workspace based on the following priority scheme:
304      *   1) From a package provided by play store
305      *   2) From a partner configuration APK, already in the system image
306      *   3) The default configuration for the particular device
307      */
loadDefaultFavoritesIfNecessary()308     synchronized public void loadDefaultFavoritesIfNecessary() {
309         String spKey = LauncherAppState.getSharedPreferencesKey();
310         SharedPreferences sp = getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE);
311 
312         if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
313             Log.d(TAG, "loading default workspace");
314 
315             AutoInstallsLayout loader = AutoInstallsLayout.get(getContext(),
316                     mOpenHelper.mAppWidgetHost, mOpenHelper);
317 
318             if (loader == null) {
319                 final Partner partner = Partner.get(getContext().getPackageManager());
320                 if (partner != null && partner.hasDefaultLayout()) {
321                     final Resources partnerRes = partner.getResources();
322                     int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
323                             "xml", partner.getPackageName());
324                     if (workspaceResId != 0) {
325                         loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
326                                 mOpenHelper, partnerRes, workspaceResId);
327                     }
328                 }
329             }
330 
331             final boolean usingExternallyProvidedLayout = loader != null;
332             if (loader == null) {
333                 loader = getDefaultLayoutParser();
334             }
335             // Populate favorites table with initial favorites
336             if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
337                     && usingExternallyProvidedLayout) {
338                 // Unable to load external layout. Cleanup and load the internal layout.
339                 createEmptyDB();
340                 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
341                         getDefaultLayoutParser());
342             }
343             clearFlagEmptyDbCreated();
344         }
345     }
346 
getDefaultLayoutParser()347     private DefaultLayoutParser getDefaultLayoutParser() {
348         int defaultLayout = LauncherAppState.getInstance()
349                 .getDynamicGrid().getDeviceProfile().defaultLayoutId;
350         return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
351                 mOpenHelper, getContext().getResources(), defaultLayout);
352     }
353 
migrateLauncher2Shortcuts()354     public void migrateLauncher2Shortcuts() {
355         mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
356                 Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
357     }
358 
359     private static interface ContentValuesCallback {
onRow(ContentValues values)360         public void onRow(ContentValues values);
361     }
362 
shouldImportLauncher2Database(Context context)363     private static boolean shouldImportLauncher2Database(Context context) {
364         boolean isTablet = context.getResources().getBoolean(R.bool.is_tablet);
365 
366         // We don't import the old databse for tablets, as the grid size has changed.
367         return !isTablet && IMPORT_LAUNCHER2_DATABASE;
368     }
369 
deleteDatabase()370     public void deleteDatabase() {
371         // Are you sure? (y/n)
372         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
373         final File dbFile = new File(db.getPath());
374         mOpenHelper.close();
375         if (dbFile.exists()) {
376             SQLiteDatabase.deleteDatabase(dbFile);
377         }
378         mOpenHelper = new DatabaseHelper(getContext());
379     }
380 
381     private static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
382         private final Context mContext;
383         private final AppWidgetHost mAppWidgetHost;
384         private long mMaxItemId = -1;
385         private long mMaxScreenId = -1;
386 
387         private boolean mNewDbCreated = false;
388 
DatabaseHelper(Context context)389         DatabaseHelper(Context context) {
390             super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
391             mContext = context;
392             mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
393 
394             // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
395             // the DB here
396             if (mMaxItemId == -1) {
397                 mMaxItemId = initializeMaxItemId(getWritableDatabase());
398             }
399             if (mMaxScreenId == -1) {
400                 mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
401             }
402         }
403 
wasNewDbCreated()404         public boolean wasNewDbCreated() {
405             return mNewDbCreated;
406         }
407 
408         /**
409          * Send notification that we've deleted the {@link AppWidgetHost},
410          * probably as part of the initial database creation. The receiver may
411          * want to re-call {@link AppWidgetHost#startListening()} to ensure
412          * callbacks are correctly set.
413          */
sendAppWidgetResetNotify()414         private void sendAppWidgetResetNotify() {
415             final ContentResolver resolver = mContext.getContentResolver();
416             resolver.notifyChange(CONTENT_APPWIDGET_RESET_URI, null);
417         }
418 
419         @Override
onCreate(SQLiteDatabase db)420         public void onCreate(SQLiteDatabase db) {
421             if (LOGD) Log.d(TAG, "creating new launcher database");
422 
423             mMaxItemId = 1;
424             mMaxScreenId = 0;
425             mNewDbCreated = true;
426 
427             UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
428             long userSerialNumber = userManager.getSerialNumberForUser(
429                     UserHandleCompat.myUserHandle());
430 
431             db.execSQL("CREATE TABLE favorites (" +
432                     "_id INTEGER PRIMARY KEY," +
433                     "title TEXT," +
434                     "intent TEXT," +
435                     "container INTEGER," +
436                     "screen INTEGER," +
437                     "cellX INTEGER," +
438                     "cellY INTEGER," +
439                     "spanX INTEGER," +
440                     "spanY INTEGER," +
441                     "itemType INTEGER," +
442                     "appWidgetId INTEGER NOT NULL DEFAULT -1," +
443                     "isShortcut INTEGER," +
444                     "iconType INTEGER," +
445                     "iconPackage TEXT," +
446                     "iconResource TEXT," +
447                     "icon BLOB," +
448                     "uri TEXT," +
449                     "displayMode INTEGER," +
450                     "appWidgetProvider TEXT," +
451                     "modified INTEGER NOT NULL DEFAULT 0," +
452                     "restored INTEGER NOT NULL DEFAULT 0," +
453                     "profileId INTEGER DEFAULT " + userSerialNumber +
454                     ");");
455             addWorkspacesTable(db);
456 
457             // Database was just created, so wipe any previous widgets
458             if (mAppWidgetHost != null) {
459                 mAppWidgetHost.deleteHost();
460                 sendAppWidgetResetNotify();
461             }
462 
463             if (shouldImportLauncher2Database(mContext)) {
464                 // Try converting the old database
465                 ContentValuesCallback permuteScreensCb = new ContentValuesCallback() {
466                     public void onRow(ContentValues values) {
467                         int container = values.getAsInteger(LauncherSettings.Favorites.CONTAINER);
468                         if (container == Favorites.CONTAINER_DESKTOP) {
469                             int screen = values.getAsInteger(LauncherSettings.Favorites.SCREEN);
470                             screen = (int) upgradeLauncherDb_permuteScreens(screen);
471                             values.put(LauncherSettings.Favorites.SCREEN, screen);
472                         }
473                     }
474                 };
475                 Uri uri = Uri.parse("content://" + Settings.AUTHORITY +
476                         "/old_favorites?notify=true");
477                 if (!convertDatabase(db, uri, permuteScreensCb, true)) {
478                     // Try and upgrade from the Launcher2 db
479                     uri = Uri.parse(mContext.getString(R.string.old_launcher_provider_uri));
480                     if (!convertDatabase(db, uri, permuteScreensCb, false)) {
481                         // If we fail, then set a flag to load the default workspace
482                         setFlagEmptyDbCreated();
483                         return;
484                     }
485                 }
486                 // Right now, in non-default workspace cases, we want to run the final
487                 // upgrade code (ie. to fix workspace screen indices -> ids, etc.), so
488                 // set that flag too.
489                 setFlagJustLoadedOldDb();
490             } else {
491                 // Fresh and clean launcher DB.
492                 mMaxItemId = initializeMaxItemId(db);
493                 setFlagEmptyDbCreated();
494             }
495         }
496 
addWorkspacesTable(SQLiteDatabase db)497         private void addWorkspacesTable(SQLiteDatabase db) {
498             db.execSQL("CREATE TABLE " + TABLE_WORKSPACE_SCREENS + " (" +
499                     LauncherSettings.WorkspaceScreens._ID + " INTEGER," +
500                     LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
501                     LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
502                     ");");
503         }
504 
removeOrphanedItems(SQLiteDatabase db)505         private void removeOrphanedItems(SQLiteDatabase db) {
506             // Delete items directly on the workspace who's screen id doesn't exist
507             //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
508             //   AND container = -100"
509             String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
510                     " WHERE " +
511                     LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
512                     LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
513                     " AND " +
514                     LauncherSettings.Favorites.CONTAINER + " = " +
515                     LauncherSettings.Favorites.CONTAINER_DESKTOP;
516             db.execSQL(removeOrphanedDesktopItems);
517 
518             // Delete items contained in folders which no longer exist (after above statement)
519             //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
520             //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
521             String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
522                     " WHERE " +
523                     LauncherSettings.Favorites.CONTAINER + " <> " +
524                     LauncherSettings.Favorites.CONTAINER_DESKTOP +
525                     " AND "
526                     + LauncherSettings.Favorites.CONTAINER + " <> " +
527                     LauncherSettings.Favorites.CONTAINER_HOTSEAT +
528                     " AND "
529                     + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
530                     LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
531                     " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
532                     LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
533             db.execSQL(removeOrphanedFolderItems);
534         }
535 
setFlagJustLoadedOldDb()536         private void setFlagJustLoadedOldDb() {
537             String spKey = LauncherAppState.getSharedPreferencesKey();
538             SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
539             SharedPreferences.Editor editor = sp.edit();
540             editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, true);
541             editor.putBoolean(EMPTY_DATABASE_CREATED, false);
542             editor.commit();
543         }
544 
setFlagEmptyDbCreated()545         private void setFlagEmptyDbCreated() {
546             String spKey = LauncherAppState.getSharedPreferencesKey();
547             SharedPreferences sp = mContext.getSharedPreferences(spKey, Context.MODE_PRIVATE);
548             SharedPreferences.Editor editor = sp.edit();
549             editor.putBoolean(EMPTY_DATABASE_CREATED, true);
550             editor.putBoolean(UPGRADED_FROM_OLD_DATABASE, false);
551             editor.commit();
552         }
553 
554         // We rearrange the screens from the old launcher
555         // 12345 -> 34512
upgradeLauncherDb_permuteScreens(long screen)556         private long upgradeLauncherDb_permuteScreens(long screen) {
557             if (screen >= 2) {
558                 return screen - 2;
559             } else {
560                 return screen + 3;
561             }
562         }
563 
convertDatabase(SQLiteDatabase db, Uri uri, ContentValuesCallback cb, boolean deleteRows)564         private boolean convertDatabase(SQLiteDatabase db, Uri uri,
565                                         ContentValuesCallback cb, boolean deleteRows) {
566             if (LOGD) Log.d(TAG, "converting database from an older format, but not onUpgrade");
567             boolean converted = false;
568 
569             final ContentResolver resolver = mContext.getContentResolver();
570             Cursor cursor = null;
571 
572             try {
573                 cursor = resolver.query(uri, null, null, null, null);
574             } catch (Exception e) {
575                 // Ignore
576             }
577 
578             // We already have a favorites database in the old provider
579             if (cursor != null) {
580                 try {
581                      if (cursor.getCount() > 0) {
582                         converted = copyFromCursor(db, cursor, cb) > 0;
583                         if (converted && deleteRows) {
584                             resolver.delete(uri, null, null);
585                         }
586                     }
587                 } finally {
588                     cursor.close();
589                 }
590             }
591 
592             if (converted) {
593                 // Convert widgets from this import into widgets
594                 if (LOGD) Log.d(TAG, "converted and now triggering widget upgrade");
595                 convertWidgets(db);
596 
597                 // Update max item id
598                 mMaxItemId = initializeMaxItemId(db);
599                 if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
600             }
601 
602             return converted;
603         }
604 
copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb)605         private int copyFromCursor(SQLiteDatabase db, Cursor c, ContentValuesCallback cb) {
606             final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
607             final int intentIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
608             final int titleIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
609             final int iconTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
610             final int iconIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
611             final int iconPackageIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
612             final int iconResourceIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
613             final int containerIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
614             final int itemTypeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
615             final int screenIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
616             final int cellXIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
617             final int cellYIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
618             final int uriIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
619             final int displayModeIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
620 
621             ContentValues[] rows = new ContentValues[c.getCount()];
622             int i = 0;
623             while (c.moveToNext()) {
624                 ContentValues values = new ContentValues(c.getColumnCount());
625                 values.put(LauncherSettings.Favorites._ID, c.getLong(idIndex));
626                 values.put(LauncherSettings.Favorites.INTENT, c.getString(intentIndex));
627                 values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
628                 values.put(LauncherSettings.Favorites.ICON_TYPE, c.getInt(iconTypeIndex));
629                 values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
630                 values.put(LauncherSettings.Favorites.ICON_PACKAGE, c.getString(iconPackageIndex));
631                 values.put(LauncherSettings.Favorites.ICON_RESOURCE, c.getString(iconResourceIndex));
632                 values.put(LauncherSettings.Favorites.CONTAINER, c.getInt(containerIndex));
633                 values.put(LauncherSettings.Favorites.ITEM_TYPE, c.getInt(itemTypeIndex));
634                 values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
635                 values.put(LauncherSettings.Favorites.SCREEN, c.getInt(screenIndex));
636                 values.put(LauncherSettings.Favorites.CELLX, c.getInt(cellXIndex));
637                 values.put(LauncherSettings.Favorites.CELLY, c.getInt(cellYIndex));
638                 values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
639                 values.put(LauncherSettings.Favorites.DISPLAY_MODE, c.getInt(displayModeIndex));
640                 if (cb != null) {
641                     cb.onRow(values);
642                 }
643                 rows[i++] = values;
644             }
645 
646             int total = 0;
647             if (i > 0) {
648                 db.beginTransaction();
649                 try {
650                     int numValues = rows.length;
651                     for (i = 0; i < numValues; i++) {
652                         if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, rows[i]) < 0) {
653                             return 0;
654                         } else {
655                             total++;
656                         }
657                     }
658                     db.setTransactionSuccessful();
659                 } finally {
660                     db.endTransaction();
661                 }
662             }
663 
664             return total;
665         }
666 
667         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)668         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
669             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
670 
671             int version = oldVersion;
672             if (version < 3) {
673                 // upgrade 1,2 -> 3 added appWidgetId column
674                 db.beginTransaction();
675                 try {
676                     // Insert new column for holding appWidgetIds
677                     db.execSQL("ALTER TABLE favorites " +
678                         "ADD COLUMN appWidgetId INTEGER NOT NULL DEFAULT -1;");
679                     db.setTransactionSuccessful();
680                     version = 3;
681                 } catch (SQLException ex) {
682                     // Old version remains, which means we wipe old data
683                     Log.e(TAG, ex.getMessage(), ex);
684                 } finally {
685                     db.endTransaction();
686                 }
687 
688                 // Convert existing widgets only if table upgrade was successful
689                 if (version == 3) {
690                     convertWidgets(db);
691                 }
692             }
693 
694             if (version < 4) {
695                 version = 4;
696             }
697 
698             // Where's version 5?
699             // - Donut and sholes on 2.0 shipped with version 4 of launcher1.
700             // - Passion shipped on 2.1 with version 6 of launcher3
701             // - Sholes shipped on 2.1r1 (aka Mr. 3) with version 5 of launcher 1
702             //   but version 5 on there was the updateContactsShortcuts change
703             //   which was version 6 in launcher 2 (first shipped on passion 2.1r1).
704             // The updateContactsShortcuts change is idempotent, so running it twice
705             // is okay so we'll do that when upgrading the devices that shipped with it.
706             if (version < 6) {
707                 // We went from 3 to 5 screens. Move everything 1 to the right
708                 db.beginTransaction();
709                 try {
710                     db.execSQL("UPDATE favorites SET screen=(screen + 1);");
711                     db.setTransactionSuccessful();
712                 } catch (SQLException ex) {
713                     // Old version remains, which means we wipe old data
714                     Log.e(TAG, ex.getMessage(), ex);
715                 } finally {
716                     db.endTransaction();
717                 }
718 
719                // We added the fast track.
720                 if (updateContactsShortcuts(db)) {
721                     version = 6;
722                 }
723             }
724 
725             if (version < 7) {
726                 // Version 7 gets rid of the special search widget.
727                 convertWidgets(db);
728                 version = 7;
729             }
730 
731             if (version < 8) {
732                 // Version 8 (froyo) has the icons all normalized.  This should
733                 // already be the case in practice, but we now rely on it and don't
734                 // resample the images each time.
735                 normalizeIcons(db);
736                 version = 8;
737             }
738 
739             if (version < 9) {
740                 // The max id is not yet set at this point (onUpgrade is triggered in the ctor
741                 // before it gets a change to get set, so we need to read it here when we use it)
742                 if (mMaxItemId == -1) {
743                     mMaxItemId = initializeMaxItemId(db);
744                 }
745 
746                 // Add default hotseat icons
747                 loadFavorites(db, new DefaultLayoutParser(mContext, mAppWidgetHost, this,
748                         mContext.getResources(), R.xml.update_workspace));
749                 version = 9;
750             }
751 
752             // We bumped the version three time during JB, once to update the launch flags, once to
753             // update the override for the default launch animation and once to set the mimetype
754             // to improve startup performance
755             if (version < 12) {
756                 // Contact shortcuts need a different set of flags to be launched now
757                 // The updateContactsShortcuts change is idempotent, so we can keep using it like
758                 // back in the Donut days
759                 updateContactsShortcuts(db);
760                 version = 12;
761             }
762 
763             if (version < 13) {
764                 // With the new shrink-wrapped and re-orderable workspaces, it makes sense
765                 // to persist workspace screens and their relative order.
766                 mMaxScreenId = 0;
767 
768                 // This will never happen in the wild, but when we switch to using workspace
769                 // screen ids, redo the import from old launcher.
770                 sJustLoadedFromOldDb = true;
771 
772                 addWorkspacesTable(db);
773                 version = 13;
774             }
775 
776             if (version < 14) {
777                 db.beginTransaction();
778                 try {
779                     // Insert new column for holding widget provider name
780                     db.execSQL("ALTER TABLE favorites " +
781                             "ADD COLUMN appWidgetProvider TEXT;");
782                     db.setTransactionSuccessful();
783                     version = 14;
784                 } catch (SQLException ex) {
785                     // Old version remains, which means we wipe old data
786                     Log.e(TAG, ex.getMessage(), ex);
787                 } finally {
788                     db.endTransaction();
789                 }
790             }
791 
792             if (version < 15) {
793                 db.beginTransaction();
794                 try {
795                     // Insert new column for holding update timestamp
796                     db.execSQL("ALTER TABLE favorites " +
797                             "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
798                     db.execSQL("ALTER TABLE workspaceScreens " +
799                             "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
800                     db.setTransactionSuccessful();
801                     version = 15;
802                 } catch (SQLException ex) {
803                     // Old version remains, which means we wipe old data
804                     Log.e(TAG, ex.getMessage(), ex);
805                 } finally {
806                     db.endTransaction();
807                 }
808             }
809 
810 
811             if (version < 16) {
812                 db.beginTransaction();
813                 try {
814                     // Insert new column for holding restore status
815                     db.execSQL("ALTER TABLE favorites " +
816                             "ADD COLUMN restored INTEGER NOT NULL DEFAULT 0;");
817                     db.setTransactionSuccessful();
818                     version = 16;
819                 } catch (SQLException ex) {
820                     // Old version remains, which means we wipe old data
821                     Log.e(TAG, ex.getMessage(), ex);
822                 } finally {
823                     db.endTransaction();
824                 }
825             }
826 
827             if (version < 17) {
828                 // We use the db version upgrade here to identify users who may not have seen
829                 // clings yet (because they weren't available), but for whom the clings are now
830                 // available (tablet users). Because one of the possible cling flows (migration)
831                 // is very destructive (wipes out workspaces), we want to prevent this from showing
832                 // until clear data. We do so by marking that the clings have been shown.
833                 LauncherClings.synchonouslyMarkFirstRunClingDismissed(mContext);
834                 version = 17;
835             }
836 
837             if (version < 18) {
838                 // No-op
839                 version = 18;
840             }
841 
842             if (version < 19) {
843                 // Due to a data loss bug, some users may have items associated with screen ids
844                 // which no longer exist. Since this can cause other problems, and since the user
845                 // will never see these items anyway, we use database upgrade as an opportunity to
846                 // clean things up.
847                 removeOrphanedItems(db);
848                 version = 19;
849             }
850 
851             if (version < 20) {
852                 // Add userId column
853                 if (addProfileColumn(db)) {
854                     version = 20;
855                 }
856                 // else old version remains, which means we wipe old data
857             }
858 
859             if (version != DATABASE_VERSION) {
860                 Log.w(TAG, "Destroying all old data.");
861                 db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
862                 db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
863 
864                 onCreate(db);
865             }
866         }
867 
868         @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)869         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
870             // This shouldn't happen -- throw our hands up in the air and start over.
871             Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
872                     ". Wiping databse.");
873             createEmptyDB(db);
874         }
875 
876 
877         /**
878          * Clears all the data for a fresh start.
879          */
createEmptyDB(SQLiteDatabase db)880         public void createEmptyDB(SQLiteDatabase db) {
881             db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
882             db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
883             onCreate(db);
884         }
885 
addProfileColumn(SQLiteDatabase db)886         private boolean addProfileColumn(SQLiteDatabase db) {
887             db.beginTransaction();
888             try {
889                 UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
890                 // Default to the serial number of this user, for older
891                 // shortcuts.
892                 long userSerialNumber = userManager.getSerialNumberForUser(
893                         UserHandleCompat.myUserHandle());
894                 // Insert new column for holding user serial number
895                 db.execSQL("ALTER TABLE favorites " +
896                         "ADD COLUMN profileId INTEGER DEFAULT "
897                                         + userSerialNumber + ";");
898                 db.setTransactionSuccessful();
899             } catch (SQLException ex) {
900                 // Old version remains, which means we wipe old data
901                 Log.e(TAG, ex.getMessage(), ex);
902                 return false;
903             } finally {
904                 db.endTransaction();
905             }
906             return true;
907         }
908 
updateContactsShortcuts(SQLiteDatabase db)909         private boolean updateContactsShortcuts(SQLiteDatabase db) {
910             final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE,
911                     new int[] { Favorites.ITEM_TYPE_SHORTCUT });
912 
913             Cursor c = null;
914             final String actionQuickContact = "com.android.contacts.action.QUICK_CONTACT";
915             db.beginTransaction();
916             try {
917                 // Select and iterate through each matching widget
918                 c = db.query(TABLE_FAVORITES,
919                         new String[] { Favorites._ID, Favorites.INTENT },
920                         selectWhere, null, null, null, null);
921                 if (c == null) return false;
922 
923                 if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
924 
925                 final int idIndex = c.getColumnIndex(Favorites._ID);
926                 final int intentIndex = c.getColumnIndex(Favorites.INTENT);
927 
928                 while (c.moveToNext()) {
929                     long favoriteId = c.getLong(idIndex);
930                     final String intentUri = c.getString(intentIndex);
931                     if (intentUri != null) {
932                         try {
933                             final Intent intent = Intent.parseUri(intentUri, 0);
934                             android.util.Log.d("Home", intent.toString());
935                             final Uri uri = intent.getData();
936                             if (uri != null) {
937                                 final String data = uri.toString();
938                                 if ((Intent.ACTION_VIEW.equals(intent.getAction()) ||
939                                         actionQuickContact.equals(intent.getAction())) &&
940                                         (data.startsWith("content://contacts/people/") ||
941                                         data.startsWith("content://com.android.contacts/" +
942                                                 "contacts/lookup/"))) {
943 
944                                     final Intent newIntent = new Intent(actionQuickContact);
945                                     // When starting from the launcher, start in a new, cleared task
946                                     // CLEAR_WHEN_TASK_RESET cannot reset the root of a task, so we
947                                     // clear the whole thing preemptively here since
948                                     // QuickContactActivity will finish itself when launching other
949                                     // detail activities.
950                                     newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
951                                             Intent.FLAG_ACTIVITY_CLEAR_TASK);
952                                     newIntent.putExtra(
953                                             Launcher.INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION, true);
954                                     newIntent.setData(uri);
955                                     // Determine the type and also put that in the shortcut
956                                     // (that can speed up launch a bit)
957                                     newIntent.setDataAndType(uri, newIntent.resolveType(mContext));
958 
959                                     final ContentValues values = new ContentValues();
960                                     values.put(LauncherSettings.Favorites.INTENT,
961                                             newIntent.toUri(0));
962 
963                                     String updateWhere = Favorites._ID + "=" + favoriteId;
964                                     db.update(TABLE_FAVORITES, values, updateWhere, null);
965                                 }
966                             }
967                         } catch (RuntimeException ex) {
968                             Log.e(TAG, "Problem upgrading shortcut", ex);
969                         } catch (URISyntaxException e) {
970                             Log.e(TAG, "Problem upgrading shortcut", e);
971                         }
972                     }
973                 }
974 
975                 db.setTransactionSuccessful();
976             } catch (SQLException ex) {
977                 Log.w(TAG, "Problem while upgrading contacts", ex);
978                 return false;
979             } finally {
980                 db.endTransaction();
981                 if (c != null) {
982                     c.close();
983                 }
984             }
985 
986             return true;
987         }
988 
normalizeIcons(SQLiteDatabase db)989         private void normalizeIcons(SQLiteDatabase db) {
990             Log.d(TAG, "normalizing icons");
991 
992             db.beginTransaction();
993             Cursor c = null;
994             SQLiteStatement update = null;
995             try {
996                 boolean logged = false;
997                 update = db.compileStatement("UPDATE favorites "
998                         + "SET icon=? WHERE _id=?");
999 
1000                 c = db.rawQuery("SELECT _id, icon FROM favorites WHERE iconType=" +
1001                         Favorites.ICON_TYPE_BITMAP, null);
1002 
1003                 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
1004                 final int iconIndex = c.getColumnIndexOrThrow(Favorites.ICON);
1005 
1006                 while (c.moveToNext()) {
1007                     long id = c.getLong(idIndex);
1008                     byte[] data = c.getBlob(iconIndex);
1009                     try {
1010                         Bitmap bitmap = Utilities.createIconBitmap(
1011                                 BitmapFactory.decodeByteArray(data, 0, data.length),
1012                                 mContext);
1013                         if (bitmap != null) {
1014                             update.bindLong(1, id);
1015                             data = ItemInfo.flattenBitmap(bitmap);
1016                             if (data != null) {
1017                                 update.bindBlob(2, data);
1018                                 update.execute();
1019                             }
1020                             bitmap.recycle();
1021                         }
1022                     } catch (Exception e) {
1023                         if (!logged) {
1024                             Log.e(TAG, "Failed normalizing icon " + id, e);
1025                         } else {
1026                             Log.e(TAG, "Also failed normalizing icon " + id);
1027                         }
1028                         logged = true;
1029                     }
1030                 }
1031                 db.setTransactionSuccessful();
1032             } catch (SQLException ex) {
1033                 Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
1034             } finally {
1035                 db.endTransaction();
1036                 if (update != null) {
1037                     update.close();
1038                 }
1039                 if (c != null) {
1040                     c.close();
1041                 }
1042             }
1043         }
1044 
1045         // Generates a new ID to use for an object in your database. This method should be only
1046         // called from the main UI thread. As an exception, we do call it when we call the
1047         // constructor from the worker thread; however, this doesn't extend until after the
1048         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1049         // after that point
1050         @Override
generateNewItemId()1051         public long generateNewItemId() {
1052             if (mMaxItemId < 0) {
1053                 throw new RuntimeException("Error: max item id was not initialized");
1054             }
1055             mMaxItemId += 1;
1056             return mMaxItemId;
1057         }
1058 
1059         @Override
insertAndCheck(SQLiteDatabase db, ContentValues values)1060         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
1061             return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
1062         }
1063 
updateMaxItemId(long id)1064         public void updateMaxItemId(long id) {
1065             mMaxItemId = id + 1;
1066         }
1067 
checkId(String table, ContentValues values)1068         public void checkId(String table, ContentValues values) {
1069             long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
1070             if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
1071                 mMaxScreenId = Math.max(id, mMaxScreenId);
1072             }  else {
1073                 mMaxItemId = Math.max(id, mMaxItemId);
1074             }
1075         }
1076 
initializeMaxItemId(SQLiteDatabase db)1077         private long initializeMaxItemId(SQLiteDatabase db) {
1078             Cursor c = db.rawQuery("SELECT MAX(_id) FROM favorites", null);
1079 
1080             // get the result
1081             final int maxIdIndex = 0;
1082             long id = -1;
1083             if (c != null && c.moveToNext()) {
1084                 id = c.getLong(maxIdIndex);
1085             }
1086             if (c != null) {
1087                 c.close();
1088             }
1089 
1090             if (id == -1) {
1091                 throw new RuntimeException("Error: could not query max item id");
1092             }
1093 
1094             return id;
1095         }
1096 
1097         // Generates a new ID to use for an workspace screen in your database. This method
1098         // should be only called from the main UI thread. As an exception, we do call it when we
1099         // call the constructor from the worker thread; however, this doesn't extend until after the
1100         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1101         // after that point
generateNewScreenId()1102         public long generateNewScreenId() {
1103             if (mMaxScreenId < 0) {
1104                 throw new RuntimeException("Error: max screen id was not initialized");
1105             }
1106             mMaxScreenId += 1;
1107             // Log to disk
1108             Launcher.addDumpLog(TAG, "11683562 - generateNewScreenId(): " + mMaxScreenId, true);
1109             return mMaxScreenId;
1110         }
1111 
updateMaxScreenId(long maxScreenId)1112         public void updateMaxScreenId(long maxScreenId) {
1113             // Log to disk
1114             Launcher.addDumpLog(TAG, "11683562 - updateMaxScreenId(): " + maxScreenId, true);
1115             mMaxScreenId = maxScreenId;
1116         }
1117 
initializeMaxScreenId(SQLiteDatabase db)1118         private long initializeMaxScreenId(SQLiteDatabase db) {
1119             Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens._ID + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
1120 
1121             // get the result
1122             final int maxIdIndex = 0;
1123             long id = -1;
1124             if (c != null && c.moveToNext()) {
1125                 id = c.getLong(maxIdIndex);
1126             }
1127             if (c != null) {
1128                 c.close();
1129             }
1130 
1131             if (id == -1) {
1132                 throw new RuntimeException("Error: could not query max screen id");
1133             }
1134 
1135             // Log to disk
1136             Launcher.addDumpLog(TAG, "11683562 - initializeMaxScreenId(): " + id, true);
1137             return id;
1138         }
1139 
1140         /**
1141          * Upgrade existing clock and photo frame widgets into their new widget
1142          * equivalents.
1143          */
convertWidgets(SQLiteDatabase db)1144         private void convertWidgets(SQLiteDatabase db) {
1145             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1146             final int[] bindSources = new int[] {
1147                     Favorites.ITEM_TYPE_WIDGET_CLOCK,
1148                     Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME,
1149                     Favorites.ITEM_TYPE_WIDGET_SEARCH,
1150             };
1151 
1152             final String selectWhere = buildOrWhereString(Favorites.ITEM_TYPE, bindSources);
1153 
1154             Cursor c = null;
1155 
1156             db.beginTransaction();
1157             try {
1158                 // Select and iterate through each matching widget
1159                 c = db.query(TABLE_FAVORITES, new String[] { Favorites._ID, Favorites.ITEM_TYPE },
1160                         selectWhere, null, null, null, null);
1161 
1162                 if (LOGD) Log.d(TAG, "found upgrade cursor count=" + c.getCount());
1163 
1164                 final ContentValues values = new ContentValues();
1165                 while (c != null && c.moveToNext()) {
1166                     long favoriteId = c.getLong(0);
1167                     int favoriteType = c.getInt(1);
1168 
1169                     // Allocate and update database with new appWidgetId
1170                     try {
1171                         int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1172 
1173                         if (LOGD) {
1174                             Log.d(TAG, "allocated appWidgetId=" + appWidgetId
1175                                     + " for favoriteId=" + favoriteId);
1176                         }
1177                         values.clear();
1178                         values.put(Favorites.ITEM_TYPE, Favorites.ITEM_TYPE_APPWIDGET);
1179                         values.put(Favorites.APPWIDGET_ID, appWidgetId);
1180 
1181                         // Original widgets might not have valid spans when upgrading
1182                         if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
1183                             values.put(LauncherSettings.Favorites.SPANX, 4);
1184                             values.put(LauncherSettings.Favorites.SPANY, 1);
1185                         } else {
1186                             values.put(LauncherSettings.Favorites.SPANX, 2);
1187                             values.put(LauncherSettings.Favorites.SPANY, 2);
1188                         }
1189 
1190                         String updateWhere = Favorites._ID + "=" + favoriteId;
1191                         db.update(TABLE_FAVORITES, values, updateWhere, null);
1192 
1193                         if (favoriteType == Favorites.ITEM_TYPE_WIDGET_CLOCK) {
1194                             // TODO: check return value
1195                             appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1196                                     new ComponentName("com.android.alarmclock",
1197                                     "com.android.alarmclock.AnalogAppWidgetProvider"));
1198                         } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_PHOTO_FRAME) {
1199                             // TODO: check return value
1200                             appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1201                                     new ComponentName("com.android.camera",
1202                                     "com.android.camera.PhotoAppWidgetProvider"));
1203                         } else if (favoriteType == Favorites.ITEM_TYPE_WIDGET_SEARCH) {
1204                             // TODO: check return value
1205                             appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,
1206                                     getSearchWidgetProvider());
1207                         }
1208                     } catch (RuntimeException ex) {
1209                         Log.e(TAG, "Problem allocating appWidgetId", ex);
1210                     }
1211                 }
1212 
1213                 db.setTransactionSuccessful();
1214             } catch (SQLException ex) {
1215                 Log.w(TAG, "Problem while allocating appWidgetIds for existing widgets", ex);
1216             } finally {
1217                 db.endTransaction();
1218                 if (c != null) {
1219                     c.close();
1220                 }
1221             }
1222 
1223             // Update max item id
1224             mMaxItemId = initializeMaxItemId(db);
1225             if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId);
1226         }
1227 
initializeExternalAdd(ContentValues values)1228         private boolean initializeExternalAdd(ContentValues values) {
1229             // 1. Ensure that externally added items have a valid item id
1230             long id = generateNewItemId();
1231             values.put(LauncherSettings.Favorites._ID, id);
1232 
1233             // 2. In the case of an app widget, and if no app widget id is specified, we
1234             // attempt allocate and bind the widget.
1235             Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
1236             if (itemType != null &&
1237                     itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
1238                     !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
1239 
1240                 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
1241                 ComponentName cn = ComponentName.unflattenFromString(
1242                         values.getAsString(Favorites.APPWIDGET_PROVIDER));
1243 
1244                 if (cn != null) {
1245                     try {
1246                         int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
1247                         values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
1248                         if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
1249                             return false;
1250                         }
1251                     } catch (RuntimeException e) {
1252                         Log.e(TAG, "Failed to initialize external widget", e);
1253                         return false;
1254                     }
1255                 } else {
1256                     return false;
1257                 }
1258             }
1259 
1260             // Add screen id if not present
1261             long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
1262             if (!addScreenIdIfNecessary(screenId)) {
1263                 return false;
1264             }
1265             return true;
1266         }
1267 
1268         // Returns true of screen id exists, or if successfully added
addScreenIdIfNecessary(long screenId)1269         private boolean addScreenIdIfNecessary(long screenId) {
1270             if (!hasScreenId(screenId)) {
1271                 int rank = getMaxScreenRank() + 1;
1272 
1273                 ContentValues v = new ContentValues();
1274                 v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
1275                 v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1276                 if (dbInsertAndCheck(this, getWritableDatabase(),
1277                         TABLE_WORKSPACE_SCREENS, null, v) < 0) {
1278                     return false;
1279                 }
1280             }
1281             return true;
1282         }
1283 
hasScreenId(long screenId)1284         private boolean hasScreenId(long screenId) {
1285             SQLiteDatabase db = getWritableDatabase();
1286             Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
1287                     + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
1288             if (c != null) {
1289                 int count = c.getCount();
1290                 c.close();
1291                 return count > 0;
1292             } else {
1293                 return false;
1294             }
1295         }
1296 
getMaxScreenRank()1297         private int getMaxScreenRank() {
1298             SQLiteDatabase db = getWritableDatabase();
1299             Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
1300                     + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
1301 
1302             // get the result
1303             final int maxRankIndex = 0;
1304             int rank = -1;
1305             if (c != null && c.moveToNext()) {
1306                 rank = c.getInt(maxRankIndex);
1307             }
1308             if (c != null) {
1309                 c.close();
1310             }
1311 
1312             return rank;
1313         }
1314 
loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader)1315         private int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
1316             ArrayList<Long> screenIds = new ArrayList<Long>();
1317             // TODO: Use multiple loaders with fall-back and transaction.
1318             int count = loader.loadLayout(db, screenIds);
1319 
1320             // Add the screens specified by the items above
1321             Collections.sort(screenIds);
1322             int rank = 0;
1323             ContentValues values = new ContentValues();
1324             for (Long id : screenIds) {
1325                 values.clear();
1326                 values.put(LauncherSettings.WorkspaceScreens._ID, id);
1327                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1328                 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
1329                     throw new RuntimeException("Failed initialize screen table"
1330                             + "from default layout");
1331                 }
1332                 rank++;
1333             }
1334 
1335             // Ensure that the max ids are initialized
1336             mMaxItemId = initializeMaxItemId(db);
1337             mMaxScreenId = initializeMaxScreenId(db);
1338 
1339             return count;
1340         }
1341 
getSearchWidgetProvider()1342         private ComponentName getSearchWidgetProvider() {
1343             AppWidgetProviderInfo searchProvider = Utilities.getSearchWidgetProvider(mContext);
1344             return (searchProvider == null) ? null : searchProvider.provider;
1345         }
1346 
migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri)1347         private void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
1348             final ContentResolver resolver = mContext.getContentResolver();
1349             Cursor c = null;
1350             int count = 0;
1351             int curScreen = 0;
1352 
1353             try {
1354                 c = resolver.query(uri, null, null, null, "title ASC");
1355             } catch (Exception e) {
1356                 // Ignore
1357             }
1358 
1359             // We already have a favorites database in the old provider
1360             if (c != null) {
1361                 try {
1362                     if (c.getCount() > 0) {
1363                         final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
1364                         final int intentIndex
1365                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
1366                         final int titleIndex
1367                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
1368                         final int iconTypeIndex
1369                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
1370                         final int iconIndex
1371                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
1372                         final int iconPackageIndex
1373                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
1374                         final int iconResourceIndex
1375                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
1376                         final int containerIndex
1377                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
1378                         final int itemTypeIndex
1379                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
1380                         final int screenIndex
1381                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
1382                         final int cellXIndex
1383                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
1384                         final int cellYIndex
1385                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
1386                         final int uriIndex
1387                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
1388                         final int displayModeIndex
1389                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
1390                         final int profileIndex
1391                                 = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID);
1392 
1393                         int i = 0;
1394                         int curX = 0;
1395                         int curY = 0;
1396 
1397                         final LauncherAppState app = LauncherAppState.getInstance();
1398                         final DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
1399                         final int width = (int) grid.numColumns;
1400                         final int height = (int) grid.numRows;
1401                         final int hotseatWidth = (int) grid.numHotseatIcons;
1402 
1403                         final HashSet<String> seenIntents = new HashSet<String>(c.getCount());
1404 
1405                         final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>();
1406                         final ArrayList<ContentValues> folders = new ArrayList<ContentValues>();
1407                         final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>();
1408 
1409                         while (c.moveToNext()) {
1410                             final int itemType = c.getInt(itemTypeIndex);
1411                             if (itemType != Favorites.ITEM_TYPE_APPLICATION
1412                                     && itemType != Favorites.ITEM_TYPE_SHORTCUT
1413                                     && itemType != Favorites.ITEM_TYPE_FOLDER) {
1414                                 continue;
1415                             }
1416 
1417                             final int cellX = c.getInt(cellXIndex);
1418                             final int cellY = c.getInt(cellYIndex);
1419                             final int screen = c.getInt(screenIndex);
1420                             int container = c.getInt(containerIndex);
1421                             final String intentStr = c.getString(intentIndex);
1422 
1423                             UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
1424                             UserHandleCompat userHandle;
1425                             final long userSerialNumber;
1426                             if (profileIndex != -1 && !c.isNull(profileIndex)) {
1427                                 userSerialNumber = c.getInt(profileIndex);
1428                                 userHandle = userManager.getUserForSerialNumber(userSerialNumber);
1429                             } else {
1430                                 // Default to the serial number of this user, for older
1431                                 // shortcuts.
1432                                 userHandle = UserHandleCompat.myUserHandle();
1433                                 userSerialNumber = userManager.getSerialNumberForUser(userHandle);
1434                             }
1435 
1436                             if (userHandle == null) {
1437                                 Launcher.addDumpLog(TAG, "skipping deleted user", true);
1438                                 continue;
1439                             }
1440 
1441                             Launcher.addDumpLog(TAG, "migrating \""
1442                                 + c.getString(titleIndex) + "\" ("
1443                                 + cellX + "," + cellY + "@"
1444                                 + LauncherSettings.Favorites.containerToString(container)
1445                                 + "/" + screen
1446                                 + "): " + intentStr, true);
1447 
1448                             if (itemType != Favorites.ITEM_TYPE_FOLDER) {
1449 
1450                                 final Intent intent;
1451                                 final ComponentName cn;
1452                                 try {
1453                                     intent = Intent.parseUri(intentStr, 0);
1454                                 } catch (URISyntaxException e) {
1455                                     // bogus intent?
1456                                     Launcher.addDumpLog(TAG,
1457                                             "skipping invalid intent uri", true);
1458                                     continue;
1459                                 }
1460 
1461                                 cn = intent.getComponent();
1462                                 if (TextUtils.isEmpty(intentStr)) {
1463                                     // no intent? no icon
1464                                     Launcher.addDumpLog(TAG, "skipping empty intent", true);
1465                                     continue;
1466                                 } else if (cn != null &&
1467                                         !LauncherModel.isValidPackageActivity(mContext, cn,
1468                                                 userHandle)) {
1469                                     // component no longer exists.
1470                                     Launcher.addDumpLog(TAG, "skipping item whose component " +
1471                                             "no longer exists.", true);
1472                                     continue;
1473                                 } else if (container ==
1474                                         LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1475                                     // Dedupe icons directly on the workspace
1476 
1477                                     // Canonicalize
1478                                     // the Play Store sets the package parameter, but Launcher
1479                                     // does not, so we clear that out to keep them the same.
1480                                     // Also ignore intent flags for the purposes of deduping.
1481                                     intent.setPackage(null);
1482                                     int flags = intent.getFlags();
1483                                     intent.setFlags(0);
1484                                     final String key = intent.toUri(0);
1485                                     intent.setFlags(flags);
1486                                     if (seenIntents.contains(key)) {
1487                                         Launcher.addDumpLog(TAG, "skipping duplicate", true);
1488                                         continue;
1489                                     } else {
1490                                         seenIntents.add(key);
1491                                     }
1492                                 }
1493                             }
1494 
1495                             ContentValues values = new ContentValues(c.getColumnCount());
1496                             values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex));
1497                             values.put(LauncherSettings.Favorites.INTENT, intentStr);
1498                             values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
1499                             values.put(LauncherSettings.Favorites.ICON_TYPE,
1500                                     c.getInt(iconTypeIndex));
1501                             values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
1502                             values.put(LauncherSettings.Favorites.ICON_PACKAGE,
1503                                     c.getString(iconPackageIndex));
1504                             values.put(LauncherSettings.Favorites.ICON_RESOURCE,
1505                                     c.getString(iconResourceIndex));
1506                             values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
1507                             values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
1508                             values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
1509                             values.put(LauncherSettings.Favorites.DISPLAY_MODE,
1510                                     c.getInt(displayModeIndex));
1511                             values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber);
1512 
1513                             if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
1514                                 hotseat.put(screen, values);
1515                             }
1516 
1517                             if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1518                                 // In a folder or in the hotseat, preserve position
1519                                 values.put(LauncherSettings.Favorites.SCREEN, screen);
1520                                 values.put(LauncherSettings.Favorites.CELLX, cellX);
1521                                 values.put(LauncherSettings.Favorites.CELLY, cellY);
1522                             } else {
1523                                 // For items contained directly on one of the workspace screen,
1524                                 // we'll determine their location (screen, x, y) in a second pass.
1525                             }
1526 
1527                             values.put(LauncherSettings.Favorites.CONTAINER, container);
1528 
1529                             if (itemType != Favorites.ITEM_TYPE_FOLDER) {
1530                                 shortcuts.add(values);
1531                             } else {
1532                                 folders.add(values);
1533                             }
1534                         }
1535 
1536                         // Now that we have all the hotseat icons, let's go through them left-right
1537                         // and assign valid locations for them in the new hotseat
1538                         final int N = hotseat.size();
1539                         for (int idx=0; idx<N; idx++) {
1540                             int hotseatX = hotseat.keyAt(idx);
1541                             ContentValues values = hotseat.valueAt(idx);
1542 
1543                             if (hotseatX == grid.hotseatAllAppsRank) {
1544                                 // let's drop this in the next available hole in the hotseat
1545                                 while (++hotseatX < hotseatWidth) {
1546                                     if (hotseat.get(hotseatX) == null) {
1547                                         // found a spot! move it here
1548                                         values.put(LauncherSettings.Favorites.SCREEN,
1549                                                 hotseatX);
1550                                         break;
1551                                     }
1552                                 }
1553                             }
1554                             if (hotseatX >= hotseatWidth) {
1555                                 // no room for you in the hotseat? it's off to the desktop with you
1556                                 values.put(LauncherSettings.Favorites.CONTAINER,
1557                                            Favorites.CONTAINER_DESKTOP);
1558                             }
1559                         }
1560 
1561                         final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>();
1562                         // Folders first
1563                         allItems.addAll(folders);
1564                         // Then shortcuts
1565                         allItems.addAll(shortcuts);
1566 
1567                         // Layout all the folders
1568                         for (ContentValues values: allItems) {
1569                             if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) !=
1570                                     LauncherSettings.Favorites.CONTAINER_DESKTOP) {
1571                                 // Hotseat items and folder items have already had their
1572                                 // location information set. Nothing to be done here.
1573                                 continue;
1574                             }
1575                             values.put(LauncherSettings.Favorites.SCREEN, curScreen);
1576                             values.put(LauncherSettings.Favorites.CELLX, curX);
1577                             values.put(LauncherSettings.Favorites.CELLY, curY);
1578                             curX = (curX + 1) % width;
1579                             if (curX == 0) {
1580                                 curY = (curY + 1);
1581                             }
1582                             // Leave the last row of icons blank on every screen
1583                             if (curY == height - 1) {
1584                                 curScreen = (int) generateNewScreenId();
1585                                 curY = 0;
1586                             }
1587                         }
1588 
1589                         if (allItems.size() > 0) {
1590                             db.beginTransaction();
1591                             try {
1592                                 for (ContentValues row: allItems) {
1593                                     if (row == null) continue;
1594                                     if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
1595                                             < 0) {
1596                                         return;
1597                                     } else {
1598                                         count++;
1599                                     }
1600                                 }
1601                                 db.setTransactionSuccessful();
1602                             } finally {
1603                                 db.endTransaction();
1604                             }
1605                         }
1606 
1607                         db.beginTransaction();
1608                         try {
1609                             for (i=0; i<=curScreen; i++) {
1610                                 final ContentValues values = new ContentValues();
1611                                 values.put(LauncherSettings.WorkspaceScreens._ID, i);
1612                                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
1613                                 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
1614                                         < 0) {
1615                                     return;
1616                                 }
1617                             }
1618                             db.setTransactionSuccessful();
1619                         } finally {
1620                             db.endTransaction();
1621                         }
1622                     }
1623                 } finally {
1624                     c.close();
1625                 }
1626             }
1627 
1628             Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into "
1629                     + (curScreen+1) + " screens", true);
1630 
1631             // ensure that new screens are created to hold these icons
1632             setFlagJustLoadedOldDb();
1633 
1634             // Update max IDs; very important since we just grabbed IDs from another database
1635             mMaxItemId = initializeMaxItemId(db);
1636             mMaxScreenId = initializeMaxScreenId(db);
1637             if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId);
1638         }
1639     }
1640 
1641     /**
1642      * Build a query string that will match any row where the column matches
1643      * anything in the values list.
1644      */
buildOrWhereString(String column, int[] values)1645     private static String buildOrWhereString(String column, int[] values) {
1646         StringBuilder selectWhere = new StringBuilder();
1647         for (int i = values.length - 1; i >= 0; i--) {
1648             selectWhere.append(column).append("=").append(values[i]);
1649             if (i > 0) {
1650                 selectWhere.append(" OR ");
1651             }
1652         }
1653         return selectWhere.toString();
1654     }
1655 
1656     static class SqlArguments {
1657         public final String table;
1658         public final String where;
1659         public final String[] args;
1660 
SqlArguments(Uri url, String where, String[] args)1661         SqlArguments(Uri url, String where, String[] args) {
1662             if (url.getPathSegments().size() == 1) {
1663                 this.table = url.getPathSegments().get(0);
1664                 this.where = where;
1665                 this.args = args;
1666             } else if (url.getPathSegments().size() != 2) {
1667                 throw new IllegalArgumentException("Invalid URI: " + url);
1668             } else if (!TextUtils.isEmpty(where)) {
1669                 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1670             } else {
1671                 this.table = url.getPathSegments().get(0);
1672                 this.where = "_id=" + ContentUris.parseId(url);
1673                 this.args = null;
1674             }
1675         }
1676 
SqlArguments(Uri url)1677         SqlArguments(Uri url) {
1678             if (url.getPathSegments().size() == 1) {
1679                 table = url.getPathSegments().get(0);
1680                 where = null;
1681                 args = null;
1682             } else {
1683                 throw new IllegalArgumentException("Invalid URI: " + url);
1684             }
1685         }
1686     }
1687 }
1688