Home | History | Annotate | Download | only in launcher3
      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.annotation.TargetApi;
     20 import android.appwidget.AppWidgetHost;
     21 import android.appwidget.AppWidgetManager;
     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.pm.PackageManager.NameNotFoundException;
     34 import android.content.res.Resources;
     35 import android.database.Cursor;
     36 import android.database.SQLException;
     37 import android.database.sqlite.SQLiteDatabase;
     38 import android.database.sqlite.SQLiteOpenHelper;
     39 import android.database.sqlite.SQLiteQueryBuilder;
     40 import android.database.sqlite.SQLiteStatement;
     41 import android.net.Uri;
     42 import android.os.Binder;
     43 import android.os.Build;
     44 import android.os.Bundle;
     45 import android.os.Process;
     46 import android.os.StrictMode;
     47 import android.os.UserManager;
     48 import android.text.TextUtils;
     49 import android.util.Log;
     50 import android.util.SparseArray;
     51 
     52 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
     53 import com.android.launcher3.LauncherSettings.Favorites;
     54 import com.android.launcher3.compat.UserHandleCompat;
     55 import com.android.launcher3.compat.UserManagerCompat;
     56 import com.android.launcher3.config.ProviderConfig;
     57 import com.android.launcher3.util.ManagedProfileHeuristic;
     58 import com.android.launcher3.util.Thunk;
     59 
     60 import java.net.URISyntaxException;
     61 import java.util.ArrayList;
     62 import java.util.Collections;
     63 import java.util.HashSet;
     64 import java.util.List;
     65 
     66 public class LauncherProvider extends ContentProvider {
     67     private static final String TAG = "LauncherProvider";
     68     private static final boolean LOGD = false;
     69 
     70     private static final int DATABASE_VERSION = 26;
     71 
     72     public static final String AUTHORITY = ProviderConfig.AUTHORITY;
     73 
     74     static final String TABLE_FAVORITES = LauncherSettings.Favorites.TABLE_NAME;
     75     static final String TABLE_WORKSPACE_SCREENS = LauncherSettings.WorkspaceScreens.TABLE_NAME;
     76     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
     77 
     78     private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
     79 
     80     @Thunk LauncherProviderChangeListener mListener;
     81     protected DatabaseHelper mOpenHelper;
     82 
     83     @Override
     84     public boolean onCreate() {
     85         final Context context = getContext();
     86         // The content provider exists for the entire duration of the launcher main process and
     87         // is the first component to get created. Initializing application context here ensures
     88         // that LauncherAppState always exists in the main process.
     89         LauncherAppState.setApplicationContext(context.getApplicationContext());
     90         StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
     91         mOpenHelper = new DatabaseHelper(context);
     92         StrictMode.setThreadPolicy(oldPolicy);
     93         LauncherAppState.setLauncherProvider(this);
     94         return true;
     95     }
     96 
     97     public boolean wasNewDbCreated() {
     98         return mOpenHelper.wasNewDbCreated();
     99     }
    100 
    101     public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
    102         mListener = listener;
    103         mOpenHelper.mListener = mListener;
    104     }
    105 
    106     @Override
    107     public String getType(Uri uri) {
    108         SqlArguments args = new SqlArguments(uri, null, null);
    109         if (TextUtils.isEmpty(args.where)) {
    110             return "vnd.android.cursor.dir/" + args.table;
    111         } else {
    112             return "vnd.android.cursor.item/" + args.table;
    113         }
    114     }
    115 
    116     /**
    117      * Overridden in tests
    118      */
    119     protected synchronized void createDbIfNotExists() {
    120         if (mOpenHelper == null) {
    121             mOpenHelper = new DatabaseHelper(getContext());
    122         }
    123     }
    124 
    125     @Override
    126     public Cursor query(Uri uri, String[] projection, String selection,
    127             String[] selectionArgs, String sortOrder) {
    128 
    129         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
    130         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
    131         qb.setTables(args.table);
    132 
    133         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    134         Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
    135         result.setNotificationUri(getContext().getContentResolver(), uri);
    136 
    137         return result;
    138     }
    139 
    140     @Thunk static long dbInsertAndCheck(DatabaseHelper helper,
    141             SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
    142         if (values == null) {
    143             throw new RuntimeException("Error: attempting to insert null values");
    144         }
    145         if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
    146             throw new RuntimeException("Error: attempting to add item without specifying an id");
    147         }
    148         helper.checkId(table, values);
    149         return db.insert(table, nullColumnHack, values);
    150     }
    151 
    152     private void reloadLauncherIfExternal() {
    153         if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) {
    154             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
    155             if (app != null) {
    156                 app.reloadWorkspace();
    157             }
    158         }
    159     }
    160 
    161     @Override
    162     public Uri insert(Uri uri, ContentValues initialValues) {
    163         SqlArguments args = new SqlArguments(uri);
    164 
    165         // In very limited cases, we support system|signature permission apps to modify the db.
    166         if (Binder.getCallingPid() != Process.myPid()) {
    167             if (!mOpenHelper.initializeExternalAdd(initialValues)) {
    168                 return null;
    169             }
    170         }
    171 
    172         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    173         addModifiedTime(initialValues);
    174         final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
    175         if (rowId < 0) return null;
    176 
    177         uri = ContentUris.withAppendedId(uri, rowId);
    178         notifyListeners();
    179 
    180         if (Utilities.ATLEAST_MARSHMALLOW) {
    181             reloadLauncherIfExternal();
    182         } else {
    183             // Deprecated behavior to support legacy devices which rely on provider callbacks.
    184             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
    185             if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
    186                 app.reloadWorkspace();
    187             }
    188 
    189             String notify = uri.getQueryParameter("notify");
    190             if (notify == null || "true".equals(notify)) {
    191                 getContext().getContentResolver().notifyChange(uri, null);
    192             }
    193         }
    194         return uri;
    195     }
    196 
    197 
    198     @Override
    199     public int bulkInsert(Uri uri, ContentValues[] values) {
    200         SqlArguments args = new SqlArguments(uri);
    201 
    202         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    203         db.beginTransaction();
    204         try {
    205             int numValues = values.length;
    206             for (int i = 0; i < numValues; i++) {
    207                 addModifiedTime(values[i]);
    208                 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
    209                     return 0;
    210                 }
    211             }
    212             db.setTransactionSuccessful();
    213         } finally {
    214             db.endTransaction();
    215         }
    216 
    217         notifyListeners();
    218         reloadLauncherIfExternal();
    219         return values.length;
    220     }
    221 
    222     @Override
    223     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
    224             throws OperationApplicationException {
    225         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    226         db.beginTransaction();
    227         try {
    228             ContentProviderResult[] result =  super.applyBatch(operations);
    229             db.setTransactionSuccessful();
    230             reloadLauncherIfExternal();
    231             return result;
    232         } finally {
    233             db.endTransaction();
    234         }
    235     }
    236 
    237     @Override
    238     public int delete(Uri uri, String selection, String[] selectionArgs) {
    239         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
    240 
    241         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    242         int count = db.delete(args.table, args.where, args.args);
    243         if (count > 0) notifyListeners();
    244 
    245         reloadLauncherIfExternal();
    246         return count;
    247     }
    248 
    249     @Override
    250     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    251         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
    252 
    253         addModifiedTime(values);
    254         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    255         int count = db.update(args.table, values, args.where, args.args);
    256         if (count > 0) notifyListeners();
    257 
    258         reloadLauncherIfExternal();
    259         return count;
    260     }
    261 
    262     @Override
    263     public Bundle call(String method, String arg, Bundle extras) {
    264         if (Binder.getCallingUid() != Process.myUid()) {
    265             return null;
    266         }
    267 
    268         switch (method) {
    269             case LauncherSettings.Settings.METHOD_GET_BOOLEAN: {
    270                 Bundle result = new Bundle();
    271                 if (Utilities.ALLOW_ROTATION_PREFERENCE_KEY.equals(arg)) {
    272                     result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
    273                             Utilities.isAllowRotationPrefEnabled(getContext()));
    274                 } else {
    275                     result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
    276                             Utilities.getPrefs(getContext()).getBoolean(arg, extras.getBoolean(
    277                                     LauncherSettings.Settings.EXTRA_DEFAULT_VALUE)));
    278                 }
    279                 return result;
    280             }
    281             case LauncherSettings.Settings.METHOD_SET_BOOLEAN: {
    282                 boolean value = extras.getBoolean(LauncherSettings.Settings.EXTRA_VALUE);
    283                 Utilities.getPrefs(getContext()).edit().putBoolean(arg, value).apply();
    284                 if (mListener != null) {
    285                     mListener.onSettingsChanged(arg, value);
    286                 }
    287                 if (extras.getBoolean(LauncherSettings.Settings.NOTIFY_BACKUP)) {
    288                     LauncherBackupAgentHelper.dataChanged(getContext());
    289                 }
    290                 Bundle result = new Bundle();
    291                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE, value);
    292                 return result;
    293             }
    294         }
    295         return null;
    296     }
    297 
    298     /**
    299      * Deletes any empty folder from the DB.
    300      * @return Ids of deleted folders.
    301      */
    302     public List<Long> deleteEmptyFolders() {
    303         ArrayList<Long> folderIds = new ArrayList<Long>();
    304         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
    305         db.beginTransaction();
    306         try {
    307             // Select folders whose id do not match any container value.
    308             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
    309                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
    310                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT " +
    311                             LauncherSettings.Favorites.CONTAINER + " FROM "
    312                                 + TABLE_FAVORITES + ")";
    313             Cursor c = db.query(TABLE_FAVORITES,
    314                     new String[] {LauncherSettings.Favorites._ID},
    315                     selection, null, null, null, null);
    316             while (c.moveToNext()) {
    317                 folderIds.add(c.getLong(0));
    318             }
    319             c.close();
    320             if (folderIds.size() > 0) {
    321                 db.delete(TABLE_FAVORITES, Utilities.createDbSelectionQuery(
    322                         LauncherSettings.Favorites._ID, folderIds), null);
    323             }
    324             db.setTransactionSuccessful();
    325         } catch (SQLException ex) {
    326             Log.e(TAG, ex.getMessage(), ex);
    327             folderIds.clear();
    328         } finally {
    329             db.endTransaction();
    330         }
    331         return folderIds;
    332     }
    333 
    334     /**
    335      * Overridden in tests
    336      */
    337     protected void notifyListeners() {
    338         // always notify the backup agent
    339         LauncherBackupAgentHelper.dataChanged(getContext());
    340         if (mListener != null) {
    341             mListener.onLauncherProviderChange();
    342         }
    343     }
    344 
    345     @Thunk static void addModifiedTime(ContentValues values) {
    346         values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
    347     }
    348 
    349     public long generateNewItemId() {
    350         return mOpenHelper.generateNewItemId();
    351     }
    352 
    353     public long generateNewScreenId() {
    354         return mOpenHelper.generateNewScreenId();
    355     }
    356 
    357     /**
    358      * Clears all the data for a fresh start.
    359      */
    360     synchronized public void createEmptyDB() {
    361         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
    362     }
    363 
    364     public void clearFlagEmptyDbCreated() {
    365         Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
    366     }
    367 
    368     /**
    369      * Loads the default workspace based on the following priority scheme:
    370      *   1) From the app restrictions
    371      *   2) From a package provided by play store
    372      *   3) From a partner configuration APK, already in the system image
    373      *   4) The default configuration for the particular device
    374      */
    375     synchronized public void loadDefaultFavoritesIfNecessary() {
    376         SharedPreferences sp = Utilities.getPrefs(getContext());
    377 
    378         if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
    379             Log.d(TAG, "loading default workspace");
    380 
    381             AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction();
    382             if (loader == null) {
    383                 loader = AutoInstallsLayout.get(getContext(),
    384                         mOpenHelper.mAppWidgetHost, mOpenHelper);
    385             }
    386             if (loader == null) {
    387                 final Partner partner = Partner.get(getContext().getPackageManager());
    388                 if (partner != null && partner.hasDefaultLayout()) {
    389                     final Resources partnerRes = partner.getResources();
    390                     int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
    391                             "xml", partner.getPackageName());
    392                     if (workspaceResId != 0) {
    393                         loader = new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
    394                                 mOpenHelper, partnerRes, workspaceResId);
    395                     }
    396                 }
    397             }
    398 
    399             final boolean usingExternallyProvidedLayout = loader != null;
    400             if (loader == null) {
    401                 loader = getDefaultLayoutParser();
    402             }
    403 
    404             // There might be some partially restored DB items, due to buggy restore logic in
    405             // previous versions of launcher.
    406             createEmptyDB();
    407             // Populate favorites table with initial favorites
    408             if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
    409                     && usingExternallyProvidedLayout) {
    410                 // Unable to load external layout. Cleanup and load the internal layout.
    411                 createEmptyDB();
    412                 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
    413                         getDefaultLayoutParser());
    414             }
    415             clearFlagEmptyDbCreated();
    416         }
    417     }
    418 
    419     /**
    420      * Creates workspace loader from an XML resource listed in the app restrictions.
    421      *
    422      * @return the loader if the restrictions are set and the resource exists; null otherwise.
    423      */
    424     @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    425     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction() {
    426         // UserManager.getApplicationRestrictions() requires minSdkVersion >= 18
    427         if (!Utilities.ATLEAST_JB_MR2) {
    428             return null;
    429         }
    430 
    431         Context ctx = getContext();
    432         UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
    433         Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName());
    434         if (bundle == null) {
    435             return null;
    436         }
    437 
    438         String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME);
    439         if (packageName != null) {
    440             try {
    441                 Resources targetResources = ctx.getPackageManager()
    442                         .getResourcesForApplication(packageName);
    443                 return AutoInstallsLayout.get(ctx, packageName, targetResources,
    444                         mOpenHelper.mAppWidgetHost, mOpenHelper);
    445             } catch (NameNotFoundException e) {
    446                 Log.e(TAG, "Target package for restricted profile not found", e);
    447                 return null;
    448             }
    449         }
    450         return null;
    451     }
    452 
    453     private DefaultLayoutParser getDefaultLayoutParser() {
    454         int defaultLayout = LauncherAppState.getInstance()
    455                 .getInvariantDeviceProfile().defaultLayoutId;
    456         return new DefaultLayoutParser(getContext(), mOpenHelper.mAppWidgetHost,
    457                 mOpenHelper, getContext().getResources(), defaultLayout);
    458     }
    459 
    460     public void migrateLauncher2Shortcuts() {
    461         mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
    462                 Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
    463     }
    464 
    465     public void updateFolderItemsRank() {
    466         mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false);
    467     }
    468 
    469     public void convertShortcutsToLauncherActivities() {
    470         mOpenHelper.convertShortcutsToLauncherActivities(mOpenHelper.getWritableDatabase());
    471     }
    472 
    473 
    474     public void deleteDatabase() {
    475         // Are you sure? (y/n)
    476         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
    477     }
    478 
    479     /**
    480      * The class is subclassed in tests to create an in-memory db.
    481      */
    482     protected static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
    483         private final Context mContext;
    484         @Thunk final AppWidgetHost mAppWidgetHost;
    485         private long mMaxItemId = -1;
    486         private long mMaxScreenId = -1;
    487 
    488         private boolean mNewDbCreated = false;
    489 
    490         @Thunk LauncherProviderChangeListener mListener;
    491 
    492         DatabaseHelper(Context context) {
    493             super(context, LauncherFiles.LAUNCHER_DB, null, DATABASE_VERSION);
    494             mContext = context;
    495             mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);
    496 
    497             // Table creation sometimes fails silently, which leads to a crash loop.
    498             // This way, we will try to create a table every time after crash, so the device
    499             // would eventually be able to recover.
    500             if (!tableExists(TABLE_FAVORITES) || !tableExists(TABLE_WORKSPACE_SCREENS)) {
    501                 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
    502                 // This operation is a no-op if the table already exists.
    503                 addFavoritesTable(getWritableDatabase(), true);
    504                 addWorkspacesTable(getWritableDatabase(), true);
    505             }
    506 
    507             // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
    508             // the DB here
    509             if (mMaxItemId == -1) {
    510                 mMaxItemId = initializeMaxItemId(getWritableDatabase());
    511             }
    512             if (mMaxScreenId == -1) {
    513                 mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
    514             }
    515         }
    516 
    517         /**
    518          * Constructor used only in tests.
    519          */
    520         public DatabaseHelper(Context context, String tableName) {
    521             super(context, tableName, null, DATABASE_VERSION);
    522             mContext = context;
    523 
    524             mAppWidgetHost = null;
    525             mMaxItemId = initializeMaxItemId(getWritableDatabase());
    526             mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
    527         }
    528 
    529         private boolean tableExists(String tableName) {
    530             Cursor c = getReadableDatabase().query(
    531                     true, "sqlite_master", new String[] {"tbl_name"},
    532                     "tbl_name = ?", new String[] {tableName},
    533                     null, null, null, null, null);
    534             try {
    535                 return c.getCount() > 0;
    536             } finally {
    537                 c.close();
    538             }
    539         }
    540 
    541         public boolean wasNewDbCreated() {
    542             return mNewDbCreated;
    543         }
    544 
    545         @Override
    546         public void onCreate(SQLiteDatabase db) {
    547             if (LOGD) Log.d(TAG, "creating new launcher database");
    548 
    549             mMaxItemId = 1;
    550             mMaxScreenId = 0;
    551             mNewDbCreated = true;
    552 
    553             addFavoritesTable(db, false);
    554             addWorkspacesTable(db, false);
    555 
    556             // Database was just created, so wipe any previous widgets
    557             if (mAppWidgetHost != null) {
    558                 mAppWidgetHost.deleteHost();
    559 
    560                 /**
    561                  * Send notification that we've deleted the {@link AppWidgetHost},
    562                  * probably as part of the initial database creation. The receiver may
    563                  * want to re-call {@link AppWidgetHost#startListening()} to ensure
    564                  * callbacks are correctly set.
    565                  */
    566                 new MainThreadExecutor().execute(new Runnable() {
    567 
    568                     @Override
    569                     public void run() {
    570                         if (mListener != null) {
    571                             mListener.onAppWidgetHostReset();
    572                         }
    573                     }
    574                 });
    575             }
    576 
    577             // Fresh and clean launcher DB.
    578             mMaxItemId = initializeMaxItemId(db);
    579             onEmptyDbCreated();
    580         }
    581 
    582         /**
    583          * Overriden in tests.
    584          */
    585         protected void onEmptyDbCreated() {
    586             // Set the flag for empty DB
    587             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
    588 
    589             // When a new DB is created, remove all previously stored managed profile information.
    590             ManagedProfileHeuristic.processAllUsers(Collections.<UserHandleCompat>emptyList(),
    591                     mContext);
    592         }
    593 
    594         protected long getDefaultUserSerial() {
    595             return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
    596                     UserHandleCompat.myUserHandle());
    597         }
    598 
    599         private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
    600             String ifNotExists = optional ? " IF NOT EXISTS " : "";
    601             db.execSQL("CREATE TABLE " + ifNotExists + TABLE_FAVORITES + " (" +
    602                     "_id INTEGER PRIMARY KEY," +
    603                     "title TEXT," +
    604                     "intent TEXT," +
    605                     "container INTEGER," +
    606                     "screen INTEGER," +
    607                     "cellX INTEGER," +
    608                     "cellY INTEGER," +
    609                     "spanX INTEGER," +
    610                     "spanY INTEGER," +
    611                     "itemType INTEGER," +
    612                     "appWidgetId INTEGER NOT NULL DEFAULT -1," +
    613                     "isShortcut INTEGER," +
    614                     "iconType INTEGER," +
    615                     "iconPackage TEXT," +
    616                     "iconResource TEXT," +
    617                     "icon BLOB," +
    618                     "uri TEXT," +
    619                     "displayMode INTEGER," +
    620                     "appWidgetProvider TEXT," +
    621                     "modified INTEGER NOT NULL DEFAULT 0," +
    622                     "restored INTEGER NOT NULL DEFAULT 0," +
    623                     "profileId INTEGER DEFAULT " + getDefaultUserSerial() + "," +
    624                     "rank INTEGER NOT NULL DEFAULT 0," +
    625                     "options INTEGER NOT NULL DEFAULT 0" +
    626                     ");");
    627         }
    628 
    629         private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
    630             String ifNotExists = optional ? " IF NOT EXISTS " : "";
    631             db.execSQL("CREATE TABLE " + ifNotExists + TABLE_WORKSPACE_SCREENS + " (" +
    632                     LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
    633                     LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
    634                     LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
    635                     ");");
    636         }
    637 
    638         private void removeOrphanedItems(SQLiteDatabase db) {
    639             // Delete items directly on the workspace who's screen id doesn't exist
    640             //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
    641             //   AND container = -100"
    642             String removeOrphanedDesktopItems = "DELETE FROM " + TABLE_FAVORITES +
    643                     " WHERE " +
    644                     LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
    645                     LauncherSettings.WorkspaceScreens._ID + " FROM " + TABLE_WORKSPACE_SCREENS + ")" +
    646                     " AND " +
    647                     LauncherSettings.Favorites.CONTAINER + " = " +
    648                     LauncherSettings.Favorites.CONTAINER_DESKTOP;
    649             db.execSQL(removeOrphanedDesktopItems);
    650 
    651             // Delete items contained in folders which no longer exist (after above statement)
    652             //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
    653             //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
    654             String removeOrphanedFolderItems = "DELETE FROM " + TABLE_FAVORITES +
    655                     " WHERE " +
    656                     LauncherSettings.Favorites.CONTAINER + " <> " +
    657                     LauncherSettings.Favorites.CONTAINER_DESKTOP +
    658                     " AND "
    659                     + LauncherSettings.Favorites.CONTAINER + " <> " +
    660                     LauncherSettings.Favorites.CONTAINER_HOTSEAT +
    661                     " AND "
    662                     + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
    663                     LauncherSettings.Favorites._ID + " FROM " + TABLE_FAVORITES +
    664                     " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
    665                     LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
    666             db.execSQL(removeOrphanedFolderItems);
    667         }
    668 
    669         private void setFlagJustLoadedOldDb() {
    670             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
    671         }
    672 
    673         @Override
    674         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    675             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
    676             switch (oldVersion) {
    677                 // The version cannot be lower that 12, as Launcher3 never supported a lower
    678                 // version of the DB.
    679                 case 12: {
    680                     // With the new shrink-wrapped and re-orderable workspaces, it makes sense
    681                     // to persist workspace screens and their relative order.
    682                     mMaxScreenId = 0;
    683                     addWorkspacesTable(db, false);
    684                 }
    685                 case 13: {
    686                     db.beginTransaction();
    687                     try {
    688                         // Insert new column for holding widget provider name
    689                         db.execSQL("ALTER TABLE favorites " +
    690                                 "ADD COLUMN appWidgetProvider TEXT;");
    691                         db.setTransactionSuccessful();
    692                     } catch (SQLException ex) {
    693                         Log.e(TAG, ex.getMessage(), ex);
    694                         // Old version remains, which means we wipe old data
    695                         break;
    696                     } finally {
    697                         db.endTransaction();
    698                     }
    699                 }
    700                 case 14: {
    701                     db.beginTransaction();
    702                     try {
    703                         // Insert new column for holding update timestamp
    704                         db.execSQL("ALTER TABLE favorites " +
    705                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
    706                         db.execSQL("ALTER TABLE workspaceScreens " +
    707                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
    708                         db.setTransactionSuccessful();
    709                     } catch (SQLException ex) {
    710                         Log.e(TAG, ex.getMessage(), ex);
    711                         // Old version remains, which means we wipe old data
    712                         break;
    713                     } finally {
    714                         db.endTransaction();
    715                     }
    716                 }
    717                 case 15: {
    718                     if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
    719                         // Old version remains, which means we wipe old data
    720                         break;
    721                     }
    722                 }
    723                 case 16: {
    724                     // We use the db version upgrade here to identify users who may not have seen
    725                     // clings yet (because they weren't available), but for whom the clings are now
    726                     // available (tablet users). Because one of the possible cling flows (migration)
    727                     // is very destructive (wipes out workspaces), we want to prevent this from showing
    728                     // until clear data. We do so by marking that the clings have been shown.
    729                     LauncherClings.markFirstRunClingDismissed(mContext);
    730                 }
    731                 case 17: {
    732                     // No-op
    733                 }
    734                 case 18: {
    735                     // Due to a data loss bug, some users may have items associated with screen ids
    736                     // which no longer exist. Since this can cause other problems, and since the user
    737                     // will never see these items anyway, we use database upgrade as an opportunity to
    738                     // clean things up.
    739                     removeOrphanedItems(db);
    740                 }
    741                 case 19: {
    742                     // Add userId column
    743                     if (!addProfileColumn(db)) {
    744                         // Old version remains, which means we wipe old data
    745                         break;
    746                     }
    747                 }
    748                 case 20:
    749                     if (!updateFolderItemsRank(db, true)) {
    750                         break;
    751                     }
    752                 case 21:
    753                     // Recreate workspace table with screen id a primary key
    754                     if (!recreateWorkspaceTable(db)) {
    755                         break;
    756                     }
    757                 case 22: {
    758                     if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
    759                         // Old version remains, which means we wipe old data
    760                         break;
    761                     }
    762                 }
    763                 case 23:
    764                     // No-op
    765                 case 24:
    766                     ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext);
    767                 case 25:
    768                     convertShortcutsToLauncherActivities(db);
    769                 case 26: {
    770                     // DB Upgraded successfully
    771                     return;
    772                 }
    773             }
    774 
    775             // DB was not upgraded
    776             Log.w(TAG, "Destroying all old data.");
    777             createEmptyDB(db);
    778         }
    779 
    780         @Override
    781         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    782             // This shouldn't happen -- throw our hands up in the air and start over.
    783             Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
    784                     ". Wiping databse.");
    785             createEmptyDB(db);
    786         }
    787 
    788         /**
    789          * Clears all the data for a fresh start.
    790          */
    791         public void createEmptyDB(SQLiteDatabase db) {
    792             db.execSQL("DROP TABLE IF EXISTS " + TABLE_FAVORITES);
    793             db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
    794             onCreate(db);
    795         }
    796 
    797         /**
    798          * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
    799          * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
    800          */
    801         @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
    802             db.beginTransaction();
    803             Cursor c = null;
    804             SQLiteStatement updateStmt = null;
    805 
    806             try {
    807                 // Only consider the primary user as other users can't have a shortcut.
    808                 long userSerial = UserManagerCompat.getInstance(mContext)
    809                         .getSerialNumberForUser(UserHandleCompat.myUserHandle());
    810                 c = db.query(TABLE_FAVORITES, new String[] {
    811                         Favorites._ID,
    812                         Favorites.INTENT,
    813                     }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
    814                     null, null, null, null);
    815 
    816                 updateStmt = db.compileStatement("UPDATE favorites SET itemType="
    817                         + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?");
    818 
    819                 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
    820                 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
    821 
    822                 while (c.moveToNext()) {
    823                     String intentDescription = c.getString(intentIndex);
    824                     Intent intent;
    825                     try {
    826                         intent = Intent.parseUri(intentDescription, 0);
    827                     } catch (URISyntaxException e) {
    828                         Log.e(TAG, "Unable to parse intent", e);
    829                         continue;
    830                     }
    831 
    832                     if (!Utilities.isLauncherAppTarget(intent)) {
    833                         continue;
    834                     }
    835 
    836                     long id = c.getLong(idIndex);
    837                     updateStmt.bindLong(1, id);
    838                     updateStmt.executeUpdateDelete();
    839                 }
    840                 db.setTransactionSuccessful();
    841             } catch (SQLException ex) {
    842                 Log.w(TAG, "Error deduping shortcuts", ex);
    843             } finally {
    844                 db.endTransaction();
    845                 if (c != null) {
    846                     c.close();
    847                 }
    848                 if (updateStmt != null) {
    849                     updateStmt.close();
    850                 }
    851             }
    852         }
    853 
    854         /**
    855          * Recreates workspace table and migrates data to the new table.
    856          */
    857         public boolean recreateWorkspaceTable(SQLiteDatabase db) {
    858             db.beginTransaction();
    859             try {
    860                 Cursor c = db.query(TABLE_WORKSPACE_SCREENS,
    861                         new String[] {LauncherSettings.WorkspaceScreens._ID},
    862                         null, null, null, null,
    863                         LauncherSettings.WorkspaceScreens.SCREEN_RANK);
    864                 ArrayList<Long> sortedIDs = new ArrayList<Long>();
    865                 long maxId = 0;
    866                 try {
    867                     while (c.moveToNext()) {
    868                         Long id = c.getLong(0);
    869                         if (!sortedIDs.contains(id)) {
    870                             sortedIDs.add(id);
    871                             maxId = Math.max(maxId, id);
    872                         }
    873                     }
    874                 } finally {
    875                     c.close();
    876                 }
    877 
    878                 db.execSQL("DROP TABLE IF EXISTS " + TABLE_WORKSPACE_SCREENS);
    879                 addWorkspacesTable(db, false);
    880 
    881                 // Add all screen ids back
    882                 int total = sortedIDs.size();
    883                 for (int i = 0; i < total; i++) {
    884                     ContentValues values = new ContentValues();
    885                     values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i));
    886                     values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
    887                     addModifiedTime(values);
    888                     db.insertOrThrow(TABLE_WORKSPACE_SCREENS, null, values);
    889                 }
    890                 db.setTransactionSuccessful();
    891                 mMaxScreenId = maxId;
    892             } catch (SQLException ex) {
    893                 // Old version remains, which means we wipe old data
    894                 Log.e(TAG, ex.getMessage(), ex);
    895                 return false;
    896             } finally {
    897                 db.endTransaction();
    898             }
    899             return true;
    900         }
    901 
    902         @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
    903             db.beginTransaction();
    904             try {
    905                 if (addRankColumn) {
    906                     // Insert new column for holding rank
    907                     db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
    908                 }
    909 
    910                 // Get a map for folder ID to folder width
    911                 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
    912                         + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
    913                         + " GROUP BY container;",
    914                         new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
    915 
    916                 while (c.moveToNext()) {
    917                     db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
    918                             + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
    919                             new Object[] {c.getLong(1) + 1, c.getLong(0)});
    920                 }
    921 
    922                 c.close();
    923                 db.setTransactionSuccessful();
    924             } catch (SQLException ex) {
    925                 // Old version remains, which means we wipe old data
    926                 Log.e(TAG, ex.getMessage(), ex);
    927                 return false;
    928             } finally {
    929                 db.endTransaction();
    930             }
    931             return true;
    932         }
    933 
    934         private boolean addProfileColumn(SQLiteDatabase db) {
    935             UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
    936             // Default to the serial number of this user, for older
    937             // shortcuts.
    938             long userSerialNumber = userManager.getSerialNumberForUser(
    939                     UserHandleCompat.myUserHandle());
    940             return addIntegerColumn(db, Favorites.PROFILE_ID, userSerialNumber);
    941         }
    942 
    943         private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
    944             db.beginTransaction();
    945             try {
    946                 db.execSQL("ALTER TABLE favorites ADD COLUMN "
    947                         + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
    948                 db.setTransactionSuccessful();
    949             } catch (SQLException ex) {
    950                 Log.e(TAG, ex.getMessage(), ex);
    951                 return false;
    952             } finally {
    953                 db.endTransaction();
    954             }
    955             return true;
    956         }
    957 
    958         // Generates a new ID to use for an object in your database. This method should be only
    959         // called from the main UI thread. As an exception, we do call it when we call the
    960         // constructor from the worker thread; however, this doesn't extend until after the
    961         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
    962         // after that point
    963         @Override
    964         public long generateNewItemId() {
    965             if (mMaxItemId < 0) {
    966                 throw new RuntimeException("Error: max item id was not initialized");
    967             }
    968             mMaxItemId += 1;
    969             return mMaxItemId;
    970         }
    971 
    972         @Override
    973         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
    974             return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
    975         }
    976 
    977         public void checkId(String table, ContentValues values) {
    978             long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
    979             if (table == LauncherProvider.TABLE_WORKSPACE_SCREENS) {
    980                 mMaxScreenId = Math.max(id, mMaxScreenId);
    981             }  else {
    982                 mMaxItemId = Math.max(id, mMaxItemId);
    983             }
    984         }
    985 
    986         private long initializeMaxItemId(SQLiteDatabase db) {
    987             return getMaxId(db, TABLE_FAVORITES);
    988         }
    989 
    990         // Generates a new ID to use for an workspace screen in your database. This method
    991         // should be only called from the main UI thread. As an exception, we do call it when we
    992         // call the constructor from the worker thread; however, this doesn't extend until after the
    993         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
    994         // after that point
    995         public long generateNewScreenId() {
    996             if (mMaxScreenId < 0) {
    997                 throw new RuntimeException("Error: max screen id was not initialized");
    998             }
    999             mMaxScreenId += 1;
   1000             return mMaxScreenId;
   1001         }
   1002 
   1003         private long initializeMaxScreenId(SQLiteDatabase db) {
   1004             return getMaxId(db, TABLE_WORKSPACE_SCREENS);
   1005         }
   1006 
   1007         @Thunk boolean initializeExternalAdd(ContentValues values) {
   1008             // 1. Ensure that externally added items have a valid item id
   1009             long id = generateNewItemId();
   1010             values.put(LauncherSettings.Favorites._ID, id);
   1011 
   1012             // 2. In the case of an app widget, and if no app widget id is specified, we
   1013             // attempt allocate and bind the widget.
   1014             Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
   1015             if (itemType != null &&
   1016                     itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
   1017                     !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
   1018 
   1019                 final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
   1020                 ComponentName cn = ComponentName.unflattenFromString(
   1021                         values.getAsString(Favorites.APPWIDGET_PROVIDER));
   1022 
   1023                 if (cn != null) {
   1024                     try {
   1025                         int appWidgetId = mAppWidgetHost.allocateAppWidgetId();
   1026                         values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
   1027                         if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
   1028                             return false;
   1029                         }
   1030                     } catch (RuntimeException e) {
   1031                         Log.e(TAG, "Failed to initialize external widget", e);
   1032                         return false;
   1033                     }
   1034                 } else {
   1035                     return false;
   1036                 }
   1037             }
   1038 
   1039             // Add screen id if not present
   1040             long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
   1041             if (!addScreenIdIfNecessary(screenId)) {
   1042                 return false;
   1043             }
   1044             return true;
   1045         }
   1046 
   1047         // Returns true of screen id exists, or if successfully added
   1048         private boolean addScreenIdIfNecessary(long screenId) {
   1049             if (!hasScreenId(screenId)) {
   1050                 int rank = getMaxScreenRank() + 1;
   1051 
   1052                 ContentValues v = new ContentValues();
   1053                 v.put(LauncherSettings.WorkspaceScreens._ID, screenId);
   1054                 v.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
   1055                 if (dbInsertAndCheck(this, getWritableDatabase(),
   1056                         TABLE_WORKSPACE_SCREENS, null, v) < 0) {
   1057                     return false;
   1058                 }
   1059             }
   1060             return true;
   1061         }
   1062 
   1063         private boolean hasScreenId(long screenId) {
   1064             SQLiteDatabase db = getWritableDatabase();
   1065             Cursor c = db.rawQuery("SELECT * FROM " + TABLE_WORKSPACE_SCREENS + " WHERE "
   1066                     + LauncherSettings.WorkspaceScreens._ID + " = " + screenId, null);
   1067             if (c != null) {
   1068                 int count = c.getCount();
   1069                 c.close();
   1070                 return count > 0;
   1071             } else {
   1072                 return false;
   1073             }
   1074         }
   1075 
   1076         private int getMaxScreenRank() {
   1077             SQLiteDatabase db = getWritableDatabase();
   1078             Cursor c = db.rawQuery("SELECT MAX(" + LauncherSettings.WorkspaceScreens.SCREEN_RANK
   1079                     + ") FROM " + TABLE_WORKSPACE_SCREENS, null);
   1080 
   1081             // get the result
   1082             final int maxRankIndex = 0;
   1083             int rank = -1;
   1084             if (c != null && c.moveToNext()) {
   1085                 rank = c.getInt(maxRankIndex);
   1086             }
   1087             if (c != null) {
   1088                 c.close();
   1089             }
   1090 
   1091             return rank;
   1092         }
   1093 
   1094         @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
   1095             ArrayList<Long> screenIds = new ArrayList<Long>();
   1096             // TODO: Use multiple loaders with fall-back and transaction.
   1097             int count = loader.loadLayout(db, screenIds);
   1098 
   1099             // Add the screens specified by the items above
   1100             Collections.sort(screenIds);
   1101             int rank = 0;
   1102             ContentValues values = new ContentValues();
   1103             for (Long id : screenIds) {
   1104                 values.clear();
   1105                 values.put(LauncherSettings.WorkspaceScreens._ID, id);
   1106                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
   1107                 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values) < 0) {
   1108                     throw new RuntimeException("Failed initialize screen table"
   1109                             + "from default layout");
   1110                 }
   1111                 rank++;
   1112             }
   1113 
   1114             // Ensure that the max ids are initialized
   1115             mMaxItemId = initializeMaxItemId(db);
   1116             mMaxScreenId = initializeMaxScreenId(db);
   1117 
   1118             return count;
   1119         }
   1120 
   1121         @Thunk void migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri) {
   1122             final ContentResolver resolver = mContext.getContentResolver();
   1123             Cursor c = null;
   1124             int count = 0;
   1125             int curScreen = 0;
   1126 
   1127             try {
   1128                 c = resolver.query(uri, null, null, null, "title ASC");
   1129             } catch (Exception e) {
   1130                 // Ignore
   1131             }
   1132 
   1133             // We already have a favorites database in the old provider
   1134             if (c != null) {
   1135                 try {
   1136                     if (c.getCount() > 0) {
   1137                         final int idIndex = c.getColumnIndexOrThrow(LauncherSettings.Favorites._ID);
   1138                         final int intentIndex
   1139                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT);
   1140                         final int titleIndex
   1141                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE);
   1142                         final int iconTypeIndex
   1143                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_TYPE);
   1144                         final int iconIndex
   1145                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON);
   1146                         final int iconPackageIndex
   1147                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE);
   1148                         final int iconResourceIndex
   1149                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE);
   1150                         final int containerIndex
   1151                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER);
   1152                         final int itemTypeIndex
   1153                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE);
   1154                         final int screenIndex
   1155                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN);
   1156                         final int cellXIndex
   1157                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX);
   1158                         final int cellYIndex
   1159                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY);
   1160                         final int uriIndex
   1161                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.URI);
   1162                         final int displayModeIndex
   1163                                 = c.getColumnIndexOrThrow(LauncherSettings.Favorites.DISPLAY_MODE);
   1164                         final int profileIndex
   1165                                 = c.getColumnIndex(LauncherSettings.Favorites.PROFILE_ID);
   1166 
   1167                         int i = 0;
   1168                         int curX = 0;
   1169                         int curY = 0;
   1170 
   1171                         final LauncherAppState app = LauncherAppState.getInstance();
   1172                         final InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
   1173                         final int width = (int) profile.numColumns;
   1174                         final int height = (int) profile.numRows;
   1175                         final int hotseatWidth = (int) profile.numHotseatIcons;
   1176 
   1177                         final HashSet<String> seenIntents = new HashSet<String>(c.getCount());
   1178 
   1179                         final ArrayList<ContentValues> shortcuts = new ArrayList<ContentValues>();
   1180                         final ArrayList<ContentValues> folders = new ArrayList<ContentValues>();
   1181                         final SparseArray<ContentValues> hotseat = new SparseArray<ContentValues>();
   1182 
   1183                         while (c.moveToNext()) {
   1184                             final int itemType = c.getInt(itemTypeIndex);
   1185                             if (itemType != Favorites.ITEM_TYPE_APPLICATION
   1186                                     && itemType != Favorites.ITEM_TYPE_SHORTCUT
   1187                                     && itemType != Favorites.ITEM_TYPE_FOLDER) {
   1188                                 continue;
   1189                             }
   1190 
   1191                             final int cellX = c.getInt(cellXIndex);
   1192                             final int cellY = c.getInt(cellYIndex);
   1193                             final int screen = c.getInt(screenIndex);
   1194                             int container = c.getInt(containerIndex);
   1195                             final String intentStr = c.getString(intentIndex);
   1196 
   1197                             UserManagerCompat userManager = UserManagerCompat.getInstance(mContext);
   1198                             UserHandleCompat userHandle;
   1199                             final long userSerialNumber;
   1200                             if (profileIndex != -1 && !c.isNull(profileIndex)) {
   1201                                 userSerialNumber = c.getInt(profileIndex);
   1202                                 userHandle = userManager.getUserForSerialNumber(userSerialNumber);
   1203                             } else {
   1204                                 // Default to the serial number of this user, for older
   1205                                 // shortcuts.
   1206                                 userHandle = UserHandleCompat.myUserHandle();
   1207                                 userSerialNumber = userManager.getSerialNumberForUser(userHandle);
   1208                             }
   1209 
   1210                             if (userHandle == null) {
   1211                                 Launcher.addDumpLog(TAG, "skipping deleted user", true);
   1212                                 continue;
   1213                             }
   1214 
   1215                             Launcher.addDumpLog(TAG, "migrating \""
   1216                                 + c.getString(titleIndex) + "\" ("
   1217                                 + cellX + "," + cellY + "@"
   1218                                 + LauncherSettings.Favorites.containerToString(container)
   1219                                 + "/" + screen
   1220                                 + "): " + intentStr, true);
   1221 
   1222                             if (itemType != Favorites.ITEM_TYPE_FOLDER) {
   1223 
   1224                                 final Intent intent;
   1225                                 final ComponentName cn;
   1226                                 try {
   1227                                     intent = Intent.parseUri(intentStr, 0);
   1228                                 } catch (URISyntaxException e) {
   1229                                     // bogus intent?
   1230                                     Launcher.addDumpLog(TAG,
   1231                                             "skipping invalid intent uri", true);
   1232                                     continue;
   1233                                 }
   1234 
   1235                                 cn = intent.getComponent();
   1236                                 if (TextUtils.isEmpty(intentStr)) {
   1237                                     // no intent? no icon
   1238                                     Launcher.addDumpLog(TAG, "skipping empty intent", true);
   1239                                     continue;
   1240                                 } else if (cn != null &&
   1241                                         !LauncherModel.isValidPackageActivity(mContext, cn,
   1242                                                 userHandle)) {
   1243                                     // component no longer exists.
   1244                                     Launcher.addDumpLog(TAG, "skipping item whose component " +
   1245                                             "no longer exists.", true);
   1246                                     continue;
   1247                                 } else if (container ==
   1248                                         LauncherSettings.Favorites.CONTAINER_DESKTOP) {
   1249                                     // Dedupe icons directly on the workspace
   1250 
   1251                                     // Canonicalize
   1252                                     // the Play Store sets the package parameter, but Launcher
   1253                                     // does not, so we clear that out to keep them the same.
   1254                                     // Also ignore intent flags for the purposes of deduping.
   1255                                     intent.setPackage(null);
   1256                                     int flags = intent.getFlags();
   1257                                     intent.setFlags(0);
   1258                                     final String key = intent.toUri(0);
   1259                                     intent.setFlags(flags);
   1260                                     if (seenIntents.contains(key)) {
   1261                                         Launcher.addDumpLog(TAG, "skipping duplicate", true);
   1262                                         continue;
   1263                                     } else {
   1264                                         seenIntents.add(key);
   1265                                     }
   1266                                 }
   1267                             }
   1268 
   1269                             ContentValues values = new ContentValues(c.getColumnCount());
   1270                             values.put(LauncherSettings.Favorites._ID, c.getInt(idIndex));
   1271                             values.put(LauncherSettings.Favorites.INTENT, intentStr);
   1272                             values.put(LauncherSettings.Favorites.TITLE, c.getString(titleIndex));
   1273                             values.put(LauncherSettings.Favorites.ICON_TYPE,
   1274                                     c.getInt(iconTypeIndex));
   1275                             values.put(LauncherSettings.Favorites.ICON, c.getBlob(iconIndex));
   1276                             values.put(LauncherSettings.Favorites.ICON_PACKAGE,
   1277                                     c.getString(iconPackageIndex));
   1278                             values.put(LauncherSettings.Favorites.ICON_RESOURCE,
   1279                                     c.getString(iconResourceIndex));
   1280                             values.put(LauncherSettings.Favorites.ITEM_TYPE, itemType);
   1281                             values.put(LauncherSettings.Favorites.APPWIDGET_ID, -1);
   1282                             values.put(LauncherSettings.Favorites.URI, c.getString(uriIndex));
   1283                             values.put(LauncherSettings.Favorites.DISPLAY_MODE,
   1284                                     c.getInt(displayModeIndex));
   1285                             values.put(LauncherSettings.Favorites.PROFILE_ID, userSerialNumber);
   1286 
   1287                             if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
   1288                                 hotseat.put(screen, values);
   1289                             }
   1290 
   1291                             if (container != LauncherSettings.Favorites.CONTAINER_DESKTOP) {
   1292                                 // In a folder or in the hotseat, preserve position
   1293                                 values.put(LauncherSettings.Favorites.SCREEN, screen);
   1294                                 values.put(LauncherSettings.Favorites.CELLX, cellX);
   1295                                 values.put(LauncherSettings.Favorites.CELLY, cellY);
   1296                             } else {
   1297                                 // For items contained directly on one of the workspace screen,
   1298                                 // we'll determine their location (screen, x, y) in a second pass.
   1299                             }
   1300 
   1301                             values.put(LauncherSettings.Favorites.CONTAINER, container);
   1302 
   1303                             if (itemType != Favorites.ITEM_TYPE_FOLDER) {
   1304                                 shortcuts.add(values);
   1305                             } else {
   1306                                 folders.add(values);
   1307                             }
   1308                         }
   1309 
   1310                         // Now that we have all the hotseat icons, let's go through them left-right
   1311                         // and assign valid locations for them in the new hotseat
   1312                         final int N = hotseat.size();
   1313                         for (int idx=0; idx<N; idx++) {
   1314                             int hotseatX = hotseat.keyAt(idx);
   1315                             ContentValues values = hotseat.valueAt(idx);
   1316 
   1317                             if (hotseatX == profile.hotseatAllAppsRank) {
   1318                                 // let's drop this in the next available hole in the hotseat
   1319                                 while (++hotseatX < hotseatWidth) {
   1320                                     if (hotseat.get(hotseatX) == null) {
   1321                                         // found a spot! move it here
   1322                                         values.put(LauncherSettings.Favorites.SCREEN,
   1323                                                 hotseatX);
   1324                                         break;
   1325                                     }
   1326                                 }
   1327                             }
   1328                             if (hotseatX >= hotseatWidth) {
   1329                                 // no room for you in the hotseat? it's off to the desktop with you
   1330                                 values.put(LauncherSettings.Favorites.CONTAINER,
   1331                                            Favorites.CONTAINER_DESKTOP);
   1332                             }
   1333                         }
   1334 
   1335                         final ArrayList<ContentValues> allItems = new ArrayList<ContentValues>();
   1336                         // Folders first
   1337                         allItems.addAll(folders);
   1338                         // Then shortcuts
   1339                         allItems.addAll(shortcuts);
   1340 
   1341                         // Layout all the folders
   1342                         for (ContentValues values: allItems) {
   1343                             if (values.getAsInteger(LauncherSettings.Favorites.CONTAINER) !=
   1344                                     LauncherSettings.Favorites.CONTAINER_DESKTOP) {
   1345                                 // Hotseat items and folder items have already had their
   1346                                 // location information set. Nothing to be done here.
   1347                                 continue;
   1348                             }
   1349                             values.put(LauncherSettings.Favorites.SCREEN, curScreen);
   1350                             values.put(LauncherSettings.Favorites.CELLX, curX);
   1351                             values.put(LauncherSettings.Favorites.CELLY, curY);
   1352                             curX = (curX + 1) % width;
   1353                             if (curX == 0) {
   1354                                 curY = (curY + 1);
   1355                             }
   1356                             // Leave the last row of icons blank on every screen
   1357                             if (curY == height - 1) {
   1358                                 curScreen = (int) generateNewScreenId();
   1359                                 curY = 0;
   1360                             }
   1361                         }
   1362 
   1363                         if (allItems.size() > 0) {
   1364                             db.beginTransaction();
   1365                             try {
   1366                                 for (ContentValues row: allItems) {
   1367                                     if (row == null) continue;
   1368                                     if (dbInsertAndCheck(this, db, TABLE_FAVORITES, null, row)
   1369                                             < 0) {
   1370                                         return;
   1371                                     } else {
   1372                                         count++;
   1373                                     }
   1374                                 }
   1375                                 db.setTransactionSuccessful();
   1376                             } finally {
   1377                                 db.endTransaction();
   1378                             }
   1379                         }
   1380 
   1381                         db.beginTransaction();
   1382                         try {
   1383                             for (i=0; i<=curScreen; i++) {
   1384                                 final ContentValues values = new ContentValues();
   1385                                 values.put(LauncherSettings.WorkspaceScreens._ID, i);
   1386                                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
   1387                                 if (dbInsertAndCheck(this, db, TABLE_WORKSPACE_SCREENS, null, values)
   1388                                         < 0) {
   1389                                     return;
   1390                                 }
   1391                             }
   1392                             db.setTransactionSuccessful();
   1393                         } finally {
   1394                             db.endTransaction();
   1395                         }
   1396 
   1397                         updateFolderItemsRank(db, false);
   1398                     }
   1399                 } finally {
   1400                     c.close();
   1401                 }
   1402             }
   1403 
   1404             Launcher.addDumpLog(TAG, "migrated " + count + " icons from Launcher2 into "
   1405                     + (curScreen+1) + " screens", true);
   1406 
   1407             // ensure that new screens are created to hold these icons
   1408             setFlagJustLoadedOldDb();
   1409 
   1410             // Update max IDs; very important since we just grabbed IDs from another database
   1411             mMaxItemId = initializeMaxItemId(db);
   1412             mMaxScreenId = initializeMaxScreenId(db);
   1413             if (LOGD) Log.d(TAG, "mMaxItemId: " + mMaxItemId + " mMaxScreenId: " + mMaxScreenId);
   1414         }
   1415     }
   1416 
   1417     /**
   1418      * @return the max _id in the provided table.
   1419      */
   1420     @Thunk static long getMaxId(SQLiteDatabase db, String table) {
   1421         Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null);
   1422         // get the result
   1423         long id = -1;
   1424         if (c != null && c.moveToNext()) {
   1425             id = c.getLong(0);
   1426         }
   1427         if (c != null) {
   1428             c.close();
   1429         }
   1430 
   1431         if (id == -1) {
   1432             throw new RuntimeException("Error: could not query max id in " + table);
   1433         }
   1434 
   1435         return id;
   1436     }
   1437 
   1438     static class SqlArguments {
   1439         public final String table;
   1440         public final String where;
   1441         public final String[] args;
   1442 
   1443         SqlArguments(Uri url, String where, String[] args) {
   1444             if (url.getPathSegments().size() == 1) {
   1445                 this.table = url.getPathSegments().get(0);
   1446                 this.where = where;
   1447                 this.args = args;
   1448             } else if (url.getPathSegments().size() != 2) {
   1449                 throw new IllegalArgumentException("Invalid URI: " + url);
   1450             } else if (!TextUtils.isEmpty(where)) {
   1451                 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
   1452             } else {
   1453                 this.table = url.getPathSegments().get(0);
   1454                 this.where = "_id=" + ContentUris.parseId(url);
   1455                 this.args = null;
   1456             }
   1457         }
   1458 
   1459         SqlArguments(Uri url) {
   1460             if (url.getPathSegments().size() == 1) {
   1461                 table = url.getPathSegments().get(0);
   1462                 where = null;
   1463                 args = null;
   1464             } else {
   1465                 throw new IllegalArgumentException("Invalid URI: " + url);
   1466             }
   1467         }
   1468     }
   1469 }
   1470