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
onCreate()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 
wasNewDbCreated()97     public boolean wasNewDbCreated() {
98         return mOpenHelper.wasNewDbCreated();
99     }
100 
setLauncherProviderChangeListener(LauncherProviderChangeListener listener)101     public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
102         mListener = listener;
103         mOpenHelper.mListener = mListener;
104     }
105 
106     @Override
getType(Uri uri)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      */
createDbIfNotExists()119     protected synchronized void createDbIfNotExists() {
120         if (mOpenHelper == null) {
121             mOpenHelper = new DatabaseHelper(getContext());
122         }
123     }
124 
125     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)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 
dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values)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 
reloadLauncherIfExternal()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
insert(Uri uri, ContentValues initialValues)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
bulkInsert(Uri uri, ContentValues[] values)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
applyBatch(ArrayList<ContentProviderOperation> operations)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
delete(Uri uri, String selection, String[] selectionArgs)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
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)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
call(String method, String arg, Bundle extras)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      */
deleteEmptyFolders()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      */
notifyListeners()337     protected void notifyListeners() {
338         // always notify the backup agent
339         LauncherBackupAgentHelper.dataChanged(getContext());
340         if (mListener != null) {
341             mListener.onLauncherProviderChange();
342         }
343     }
344 
addModifiedTime(ContentValues values)345     @Thunk static void addModifiedTime(ContentValues values) {
346         values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
347     }
348 
generateNewItemId()349     public long generateNewItemId() {
350         return mOpenHelper.generateNewItemId();
351     }
352 
generateNewScreenId()353     public long generateNewScreenId() {
354         return mOpenHelper.generateNewScreenId();
355     }
356 
357     /**
358      * Clears all the data for a fresh start.
359      */
createEmptyDB()360     synchronized public void createEmptyDB() {
361         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
362     }
363 
clearFlagEmptyDbCreated()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      */
loadDefaultFavoritesIfNecessary()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)
createWorkspaceLoaderFromAppRestriction()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 
getDefaultLayoutParser()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 
migrateLauncher2Shortcuts()460     public void migrateLauncher2Shortcuts() {
461         mOpenHelper.migrateLauncher2Shortcuts(mOpenHelper.getWritableDatabase(),
462                 Uri.parse(getContext().getString(R.string.old_launcher_provider_uri)));
463     }
464 
updateFolderItemsRank()465     public void updateFolderItemsRank() {
466         mOpenHelper.updateFolderItemsRank(mOpenHelper.getWritableDatabase(), false);
467     }
468 
convertShortcutsToLauncherActivities()469     public void convertShortcutsToLauncherActivities() {
470         mOpenHelper.convertShortcutsToLauncherActivities(mOpenHelper.getWritableDatabase());
471     }
472 
473 
deleteDatabase()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 
DatabaseHelper(Context context)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          */
DatabaseHelper(Context context, String tableName)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 
tableExists(String tableName)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 
wasNewDbCreated()541         public boolean wasNewDbCreated() {
542             return mNewDbCreated;
543         }
544 
545         @Override
onCreate(SQLiteDatabase db)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          */
onEmptyDbCreated()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 
getDefaultUserSerial()594         protected long getDefaultUserSerial() {
595             return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
596                     UserHandleCompat.myUserHandle());
597         }
598 
addFavoritesTable(SQLiteDatabase db, boolean optional)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 
addWorkspacesTable(SQLiteDatabase db, boolean optional)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 
removeOrphanedItems(SQLiteDatabase db)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 
setFlagJustLoadedOldDb()669         private void setFlagJustLoadedOldDb() {
670             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, false).commit();
671         }
672 
673         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)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
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)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          */
createEmptyDB(SQLiteDatabase db)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          */
convertShortcutsToLauncherActivities(SQLiteDatabase db)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          */
recreateWorkspaceTable(SQLiteDatabase db)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 
updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn)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 
addProfileColumn(SQLiteDatabase db)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 
addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue)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
generateNewItemId()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
insertAndCheck(SQLiteDatabase db, ContentValues values)973         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
974             return dbInsertAndCheck(this, db, TABLE_FAVORITES, null, values);
975         }
976 
checkId(String table, ContentValues values)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 
initializeMaxItemId(SQLiteDatabase db)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
generateNewScreenId()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 
initializeMaxScreenId(SQLiteDatabase db)1003         private long initializeMaxScreenId(SQLiteDatabase db) {
1004             return getMaxId(db, TABLE_WORKSPACE_SCREENS);
1005         }
1006 
initializeExternalAdd(ContentValues values)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
addScreenIdIfNecessary(long screenId)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 
hasScreenId(long screenId)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 
getMaxScreenRank()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 
loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader)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 
migrateLauncher2Shortcuts(SQLiteDatabase db, Uri uri)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      */
getMaxId(SQLiteDatabase db, String table)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 
SqlArguments(Uri url, String where, String[] args)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 
SqlArguments(Uri url)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