1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3;
18 
19 import android.appwidget.AppWidgetHost;
20 import android.appwidget.AppWidgetManager;
21 import android.content.ComponentName;
22 import android.content.ContentProvider;
23 import android.content.ContentProviderOperation;
24 import android.content.ContentProviderResult;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.OperationApplicationException;
30 import android.content.SharedPreferences;
31 import android.content.pm.PackageManager.NameNotFoundException;
32 import android.content.res.Resources;
33 import android.database.Cursor;
34 import android.database.SQLException;
35 import android.database.sqlite.SQLiteDatabase;
36 import android.database.sqlite.SQLiteOpenHelper;
37 import android.database.sqlite.SQLiteQueryBuilder;
38 import android.database.sqlite.SQLiteStatement;
39 import android.net.Uri;
40 import android.os.Binder;
41 import android.os.Bundle;
42 import android.os.Handler;
43 import android.os.Message;
44 import android.os.Process;
45 import android.os.Trace;
46 import android.os.UserHandle;
47 import android.os.UserManager;
48 import android.text.TextUtils;
49 import android.util.Log;
50 
51 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
52 import com.android.launcher3.LauncherSettings.Favorites;
53 import com.android.launcher3.LauncherSettings.WorkspaceScreens;
54 import com.android.launcher3.compat.UserManagerCompat;
55 import com.android.launcher3.config.FeatureFlags;
56 import com.android.launcher3.config.ProviderConfig;
57 import com.android.launcher3.dynamicui.ExtractionUtils;
58 import com.android.launcher3.graphics.IconShapeOverride;
59 import com.android.launcher3.logging.FileLog;
60 import com.android.launcher3.provider.LauncherDbUtils;
61 import com.android.launcher3.provider.RestoreDbTask;
62 import com.android.launcher3.util.ManagedProfileHeuristic;
63 import com.android.launcher3.util.NoLocaleSqliteContext;
64 import com.android.launcher3.util.Preconditions;
65 import com.android.launcher3.util.Thunk;
66 
67 import java.io.FileDescriptor;
68 import java.io.PrintWriter;
69 import java.lang.reflect.Method;
70 import java.net.URISyntaxException;
71 import java.util.ArrayList;
72 import java.util.Collections;
73 import java.util.HashSet;
74 
75 public class LauncherProvider extends ContentProvider {
76     private static final String TAG = "LauncherProvider";
77     private static final boolean LOGD = false;
78 
79     /**
80      * Represents the schema of the database. Changes in scheme need not be backwards compatible.
81      */
82     private static final int SCHEMA_VERSION = 27;
83     /**
84      * Represents the actual data. It could include additional validations and normalizations added
85      * overtime. These must be backwards compatible, else we risk breaking old devices during
86      * restore or binary version downgrade.
87      */
88     private static final int DATA_VERSION = 3;
89 
90     private static final String PREF_KEY_DATA_VERISON = "provider_data_version";
91 
92     public static final String AUTHORITY = ProviderConfig.AUTHORITY;
93 
94     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
95 
96     private static final String RESTRICTION_PACKAGE_NAME = "workspace.configuration.package.name";
97 
98     private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
99     private Handler mListenerHandler;
100 
101     protected DatabaseHelper mOpenHelper;
102 
103     /**
104      * $ adb shell dumpsys activity provider com.android.launcher3
105      */
106     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)107     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
108         LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
109         if (appState == null || !appState.getModel().isModelLoaded()) {
110             return;
111         }
112         appState.getModel().dumpState("", fd, writer, args);
113     }
114 
115     @Override
onCreate()116     public boolean onCreate() {
117         if (ProviderConfig.IS_DOGFOOD_BUILD) {
118             Log.d(TAG, "Launcher process started");
119         }
120         mListenerHandler = new Handler(mListenerWrapper);
121 
122         // The content provider exists for the entire duration of the launcher main process and
123         // is the first component to get created. Initializing FileLog here ensures that it's
124         // always available in the main process.
125         FileLog.setDir(getContext().getApplicationContext().getFilesDir());
126         IconShapeOverride.apply(getContext());
127         SessionCommitReceiver.applyDefaultUserPrefs(getContext());
128         return true;
129     }
130 
131     /**
132      * Sets a provider listener.
133      */
setLauncherProviderChangeListener(LauncherProviderChangeListener listener)134     public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
135         Preconditions.assertUIThread();
136         mListenerWrapper.mListener = listener;
137     }
138 
139     @Override
getType(Uri uri)140     public String getType(Uri uri) {
141         SqlArguments args = new SqlArguments(uri, null, null);
142         if (TextUtils.isEmpty(args.where)) {
143             return "vnd.android.cursor.dir/" + args.table;
144         } else {
145             return "vnd.android.cursor.item/" + args.table;
146         }
147     }
148 
149     /**
150      * Overridden in tests
151      */
createDbIfNotExists()152     protected synchronized void createDbIfNotExists() {
153         if (mOpenHelper == null) {
154             if (LauncherAppState.PROFILE_STARTUP) {
155                 Trace.beginSection("Opening workspace DB");
156             }
157             mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
158 
159             if (RestoreDbTask.isPending(getContext())) {
160                 if (!RestoreDbTask.performRestore(mOpenHelper)) {
161                     mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
162                 }
163                 // Set is pending to false irrespective of the result, so that it doesn't get
164                 // executed again.
165                 RestoreDbTask.setPending(getContext(), false);
166             }
167 
168             if (LauncherAppState.PROFILE_STARTUP) {
169                 Trace.endSection();
170             }
171         }
172     }
173 
174     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)175     public Cursor query(Uri uri, String[] projection, String selection,
176             String[] selectionArgs, String sortOrder) {
177         createDbIfNotExists();
178 
179         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
180         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
181         qb.setTables(args.table);
182 
183         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
184         Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
185         result.setNotificationUri(getContext().getContentResolver(), uri);
186 
187         return result;
188     }
189 
dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values)190     @Thunk static long dbInsertAndCheck(DatabaseHelper helper,
191             SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
192         if (values == null) {
193             throw new RuntimeException("Error: attempting to insert null values");
194         }
195         if (!values.containsKey(LauncherSettings.ChangeLogColumns._ID)) {
196             throw new RuntimeException("Error: attempting to add item without specifying an id");
197         }
198         helper.checkId(table, values);
199         return db.insert(table, nullColumnHack, values);
200     }
201 
reloadLauncherIfExternal()202     private void reloadLauncherIfExternal() {
203         if (Utilities.ATLEAST_MARSHMALLOW && Binder.getCallingPid() != Process.myPid()) {
204             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
205             if (app != null) {
206                 app.getModel().forceReload();
207             }
208         }
209     }
210 
211     @Override
insert(Uri uri, ContentValues initialValues)212     public Uri insert(Uri uri, ContentValues initialValues) {
213         createDbIfNotExists();
214         SqlArguments args = new SqlArguments(uri);
215 
216         // In very limited cases, we support system|signature permission apps to modify the db.
217         if (Binder.getCallingPid() != Process.myPid()) {
218             if (!initializeExternalAdd(initialValues)) {
219                 return null;
220             }
221         }
222 
223         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
224         addModifiedTime(initialValues);
225         final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
226         if (rowId < 0) return null;
227 
228         uri = ContentUris.withAppendedId(uri, rowId);
229         notifyListeners();
230 
231         if (Utilities.ATLEAST_MARSHMALLOW) {
232             reloadLauncherIfExternal();
233         } else {
234             // Deprecated behavior to support legacy devices which rely on provider callbacks.
235             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
236             if (app != null && "true".equals(uri.getQueryParameter("isExternalAdd"))) {
237                 app.getModel().forceReload();
238             }
239 
240             String notify = uri.getQueryParameter("notify");
241             if (notify == null || "true".equals(notify)) {
242                 getContext().getContentResolver().notifyChange(uri, null);
243             }
244         }
245         return uri;
246     }
247 
initializeExternalAdd(ContentValues values)248     private boolean initializeExternalAdd(ContentValues values) {
249         // 1. Ensure that externally added items have a valid item id
250         long id = mOpenHelper.generateNewItemId();
251         values.put(LauncherSettings.Favorites._ID, id);
252 
253         // 2. In the case of an app widget, and if no app widget id is specified, we
254         // attempt allocate and bind the widget.
255         Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
256         if (itemType != null &&
257                 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
258                 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
259 
260             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
261             ComponentName cn = ComponentName.unflattenFromString(
262                     values.getAsString(Favorites.APPWIDGET_PROVIDER));
263 
264             if (cn != null) {
265                 try {
266                     AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
267                     int appWidgetId = widgetHost.allocateAppWidgetId();
268                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
269                     if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
270                         widgetHost.deleteAppWidgetId(appWidgetId);
271                         return false;
272                     }
273                 } catch (RuntimeException e) {
274                     Log.e(TAG, "Failed to initialize external widget", e);
275                     return false;
276                 }
277             } else {
278                 return false;
279             }
280         }
281 
282         // Add screen id if not present
283         long screenId = values.getAsLong(LauncherSettings.Favorites.SCREEN);
284         SQLiteStatement stmp = null;
285         try {
286             stmp = mOpenHelper.getWritableDatabase().compileStatement(
287                     "INSERT OR IGNORE INTO workspaceScreens (_id, screenRank) " +
288                             "select ?, (ifnull(MAX(screenRank), -1)+1) from workspaceScreens");
289             stmp.bindLong(1, screenId);
290 
291             ContentValues valuesInserted = new ContentValues();
292             valuesInserted.put(LauncherSettings.BaseLauncherColumns._ID, stmp.executeInsert());
293             mOpenHelper.checkId(WorkspaceScreens.TABLE_NAME, valuesInserted);
294             return true;
295         } catch (Exception e) {
296             return false;
297         } finally {
298             Utilities.closeSilently(stmp);
299         }
300     }
301 
302     @Override
bulkInsert(Uri uri, ContentValues[] values)303     public int bulkInsert(Uri uri, ContentValues[] values) {
304         createDbIfNotExists();
305         SqlArguments args = new SqlArguments(uri);
306 
307         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
308         db.beginTransaction();
309         try {
310             int numValues = values.length;
311             for (int i = 0; i < numValues; i++) {
312                 addModifiedTime(values[i]);
313                 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
314                     return 0;
315                 }
316             }
317             db.setTransactionSuccessful();
318         } finally {
319             db.endTransaction();
320         }
321 
322         notifyListeners();
323         reloadLauncherIfExternal();
324         return values.length;
325     }
326 
327     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)328     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
329             throws OperationApplicationException {
330         createDbIfNotExists();
331         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
332         db.beginTransaction();
333         try {
334             ContentProviderResult[] result =  super.applyBatch(operations);
335             db.setTransactionSuccessful();
336             reloadLauncherIfExternal();
337             return result;
338         } finally {
339             db.endTransaction();
340         }
341     }
342 
343     @Override
delete(Uri uri, String selection, String[] selectionArgs)344     public int delete(Uri uri, String selection, String[] selectionArgs) {
345         createDbIfNotExists();
346         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
347 
348         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
349 
350         if (Binder.getCallingPid() != Process.myPid()
351                 && Favorites.TABLE_NAME.equalsIgnoreCase(args.table)) {
352             mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
353         }
354         int count = db.delete(args.table, args.where, args.args);
355         if (count > 0) {
356             notifyListeners();
357             reloadLauncherIfExternal();
358         }
359         return count;
360     }
361 
362     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)363     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
364         createDbIfNotExists();
365         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
366 
367         addModifiedTime(values);
368         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
369         int count = db.update(args.table, values, args.where, args.args);
370         if (count > 0) notifyListeners();
371 
372         reloadLauncherIfExternal();
373         return count;
374     }
375 
376     @Override
call(String method, final String arg, final Bundle extras)377     public Bundle call(String method, final String arg, final Bundle extras) {
378         if (Binder.getCallingUid() != Process.myUid()) {
379             return null;
380         }
381         createDbIfNotExists();
382 
383         switch (method) {
384             case LauncherSettings.Settings.METHOD_SET_EXTRACTED_COLORS_AND_WALLPAPER_ID: {
385                 String extractedColors = extras.getString(
386                         LauncherSettings.Settings.EXTRA_EXTRACTED_COLORS);
387                 int wallpaperId = extras.getInt(LauncherSettings.Settings.EXTRA_WALLPAPER_ID);
388                 Utilities.getPrefs(getContext()).edit()
389                         .putString(ExtractionUtils.EXTRACTED_COLORS_PREFERENCE_KEY, extractedColors)
390                         .putInt(ExtractionUtils.WALLPAPER_ID_PREFERENCE_KEY, wallpaperId)
391                         .apply();
392                 mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_EXTRACTED_COLORS_CHANGED);
393                 Bundle result = new Bundle();
394                 result.putString(LauncherSettings.Settings.EXTRA_VALUE, extractedColors);
395                 return result;
396             }
397             case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
398                 clearFlagEmptyDbCreated();
399                 return null;
400             }
401             case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
402                 Bundle result = new Bundle();
403                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
404                         Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false));
405                 return result;
406             }
407             case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
408                 Bundle result = new Bundle();
409                 result.putSerializable(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders());
410                 return result;
411             }
412             case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
413                 Bundle result = new Bundle();
414                 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId());
415                 return result;
416             }
417             case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
418                 Bundle result = new Bundle();
419                 result.putLong(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId());
420                 return result;
421             }
422             case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
423                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
424                 return null;
425             }
426             case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
427                 loadDefaultFavoritesIfNecessary();
428                 return null;
429             }
430             case LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS: {
431                 mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
432                 return null;
433             }
434         }
435         return null;
436     }
437 
438     /**
439      * Deletes any empty folder from the DB.
440      * @return Ids of deleted folders.
441      */
deleteEmptyFolders()442     private ArrayList<Long> deleteEmptyFolders() {
443         ArrayList<Long> folderIds = new ArrayList<>();
444         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
445         db.beginTransaction();
446         try {
447             // Select folders whose id do not match any container value.
448             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
449                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
450                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT " +
451                             LauncherSettings.Favorites.CONTAINER + " FROM "
452                                 + Favorites.TABLE_NAME + ")";
453             Cursor c = db.query(Favorites.TABLE_NAME,
454                     new String[] {LauncherSettings.Favorites._ID},
455                     selection, null, null, null, null);
456             while (c.moveToNext()) {
457                 folderIds.add(c.getLong(0));
458             }
459             c.close();
460             if (!folderIds.isEmpty()) {
461                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
462                         LauncherSettings.Favorites._ID, folderIds), null);
463             }
464             db.setTransactionSuccessful();
465         } catch (SQLException ex) {
466             Log.e(TAG, ex.getMessage(), ex);
467             folderIds.clear();
468         } finally {
469             db.endTransaction();
470         }
471         return folderIds;
472     }
473 
474     /**
475      * Overridden in tests
476      */
notifyListeners()477     protected void notifyListeners() {
478         mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED);
479     }
480 
addModifiedTime(ContentValues values)481     @Thunk static void addModifiedTime(ContentValues values) {
482         values.put(LauncherSettings.ChangeLogColumns.MODIFIED, System.currentTimeMillis());
483     }
484 
clearFlagEmptyDbCreated()485     private void clearFlagEmptyDbCreated() {
486         Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
487     }
488 
489     /**
490      * Loads the default workspace based on the following priority scheme:
491      *   1) From the app restrictions
492      *   2) From a package provided by play store
493      *   3) From a partner configuration APK, already in the system image
494      *   4) The default configuration for the particular device
495      */
loadDefaultFavoritesIfNecessary()496     synchronized private void loadDefaultFavoritesIfNecessary() {
497         SharedPreferences sp = Utilities.getPrefs(getContext());
498 
499         if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
500             Log.d(TAG, "loading default workspace");
501 
502             AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
503             AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
504             if (loader == null) {
505                 loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
506             }
507             if (loader == null) {
508                 final Partner partner = Partner.get(getContext().getPackageManager());
509                 if (partner != null && partner.hasDefaultLayout()) {
510                     final Resources partnerRes = partner.getResources();
511                     int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
512                             "xml", partner.getPackageName());
513                     if (workspaceResId != 0) {
514                         loader = new DefaultLayoutParser(getContext(), widgetHost,
515                                 mOpenHelper, partnerRes, workspaceResId);
516                     }
517                 }
518             }
519 
520             final boolean usingExternallyProvidedLayout = loader != null;
521             if (loader == null) {
522                 loader = getDefaultLayoutParser(widgetHost);
523             }
524 
525             // There might be some partially restored DB items, due to buggy restore logic in
526             // previous versions of launcher.
527             mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
528             // Populate favorites table with initial favorites
529             if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
530                     && usingExternallyProvidedLayout) {
531                 // Unable to load external layout. Cleanup and load the internal layout.
532                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
533                 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
534                         getDefaultLayoutParser(widgetHost));
535             }
536             clearFlagEmptyDbCreated();
537         }
538     }
539 
540     /**
541      * Creates workspace loader from an XML resource listed in the app restrictions.
542      *
543      * @return the loader if the restrictions are set and the resource exists; null otherwise.
544      */
createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost)545     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
546         Context ctx = getContext();
547         UserManager um = (UserManager) ctx.getSystemService(Context.USER_SERVICE);
548         Bundle bundle = um.getApplicationRestrictions(ctx.getPackageName());
549         if (bundle == null) {
550             return null;
551         }
552 
553         String packageName = bundle.getString(RESTRICTION_PACKAGE_NAME);
554         if (packageName != null) {
555             try {
556                 Resources targetResources = ctx.getPackageManager()
557                         .getResourcesForApplication(packageName);
558                 return AutoInstallsLayout.get(ctx, packageName, targetResources,
559                         widgetHost, mOpenHelper);
560             } catch (NameNotFoundException e) {
561                 Log.e(TAG, "Target package for restricted profile not found", e);
562                 return null;
563             }
564         }
565         return null;
566     }
567 
getDefaultLayoutParser(AppWidgetHost widgetHost)568     private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
569         int defaultLayout = LauncherAppState.getIDP(getContext()).defaultLayoutId;
570         return new DefaultLayoutParser(getContext(), widgetHost,
571                 mOpenHelper, getContext().getResources(), defaultLayout);
572     }
573 
574     /**
575      * The class is subclassed in tests to create an in-memory db.
576      */
577     public static class DatabaseHelper extends SQLiteOpenHelper implements LayoutParserCallback {
578         private final Handler mWidgetHostResetHandler;
579         private final Context mContext;
580         private long mMaxItemId = -1;
581         private long mMaxScreenId = -1;
582 
DatabaseHelper(Context context, Handler widgetHostResetHandler)583         DatabaseHelper(Context context, Handler widgetHostResetHandler) {
584             this(context, widgetHostResetHandler, LauncherFiles.LAUNCHER_DB);
585             // Table creation sometimes fails silently, which leads to a crash loop.
586             // This way, we will try to create a table every time after crash, so the device
587             // would eventually be able to recover.
588             if (!tableExists(Favorites.TABLE_NAME) || !tableExists(WorkspaceScreens.TABLE_NAME)) {
589                 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
590                 // This operation is a no-op if the table already exists.
591                 addFavoritesTable(getWritableDatabase(), true);
592                 addWorkspacesTable(getWritableDatabase(), true);
593             }
594 
595             initIds();
596         }
597 
598         /**
599          * Constructor used in tests and for restore.
600          */
DatabaseHelper( Context context, Handler widgetHostResetHandler, String tableName)601         public DatabaseHelper(
602                 Context context, Handler widgetHostResetHandler, String tableName) {
603             super(new NoLocaleSqliteContext(context), tableName, null, SCHEMA_VERSION);
604             mContext = context;
605             mWidgetHostResetHandler = widgetHostResetHandler;
606         }
607 
initIds()608         protected void initIds() {
609             // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
610             // the DB here
611             if (mMaxItemId == -1) {
612                 mMaxItemId = initializeMaxItemId(getWritableDatabase());
613             }
614             if (mMaxScreenId == -1) {
615                 mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
616             }
617         }
618 
tableExists(String tableName)619         private boolean tableExists(String tableName) {
620             Cursor c = getReadableDatabase().query(
621                     true, "sqlite_master", new String[] {"tbl_name"},
622                     "tbl_name = ?", new String[] {tableName},
623                     null, null, null, null, null);
624             try {
625                 return c.getCount() > 0;
626             } finally {
627                 c.close();
628             }
629         }
630 
631         @Override
onCreate(SQLiteDatabase db)632         public void onCreate(SQLiteDatabase db) {
633             if (LOGD) Log.d(TAG, "creating new launcher database");
634 
635             mMaxItemId = 1;
636             mMaxScreenId = 0;
637 
638             addFavoritesTable(db, false);
639             addWorkspacesTable(db, false);
640 
641             // Fresh and clean launcher DB.
642             mMaxItemId = initializeMaxItemId(db);
643             onEmptyDbCreated();
644         }
645 
646         /**
647          * Overriden in tests.
648          */
onEmptyDbCreated()649         protected void onEmptyDbCreated() {
650             // Database was just created, so wipe any previous widgets
651             if (mWidgetHostResetHandler != null) {
652                 newLauncherWidgetHost().deleteHost();
653                 mWidgetHostResetHandler.sendEmptyMessage(
654                         ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET);
655             }
656 
657             // Set the flag for empty DB
658             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
659 
660             // When a new DB is created, remove all previously stored managed profile information.
661             ManagedProfileHeuristic.processAllUsers(Collections.<UserHandle>emptyList(),
662                     mContext);
663         }
664 
getDefaultUserSerial()665         public long getDefaultUserSerial() {
666             return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(
667                     Process.myUserHandle());
668         }
669 
addFavoritesTable(SQLiteDatabase db, boolean optional)670         private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
671             Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
672         }
673 
addWorkspacesTable(SQLiteDatabase db, boolean optional)674         private void addWorkspacesTable(SQLiteDatabase db, boolean optional) {
675             String ifNotExists = optional ? " IF NOT EXISTS " : "";
676             db.execSQL("CREATE TABLE " + ifNotExists + WorkspaceScreens.TABLE_NAME + " (" +
677                     LauncherSettings.WorkspaceScreens._ID + " INTEGER PRIMARY KEY," +
678                     LauncherSettings.WorkspaceScreens.SCREEN_RANK + " INTEGER," +
679                     LauncherSettings.ChangeLogColumns.MODIFIED + " INTEGER NOT NULL DEFAULT 0" +
680                     ");");
681         }
682 
removeOrphanedItems(SQLiteDatabase db)683         private void removeOrphanedItems(SQLiteDatabase db) {
684             // Delete items directly on the workspace who's screen id doesn't exist
685             //  "DELETE FROM favorites WHERE screen NOT IN (SELECT _id FROM workspaceScreens)
686             //   AND container = -100"
687             String removeOrphanedDesktopItems = "DELETE FROM " + Favorites.TABLE_NAME +
688                     " WHERE " +
689                     LauncherSettings.Favorites.SCREEN + " NOT IN (SELECT " +
690                     LauncherSettings.WorkspaceScreens._ID + " FROM " + WorkspaceScreens.TABLE_NAME + ")" +
691                     " AND " +
692                     LauncherSettings.Favorites.CONTAINER + " = " +
693                     LauncherSettings.Favorites.CONTAINER_DESKTOP;
694             db.execSQL(removeOrphanedDesktopItems);
695 
696             // Delete items contained in folders which no longer exist (after above statement)
697             //  "DELETE FROM favorites  WHERE container <> -100 AND container <> -101 AND container
698             //   NOT IN (SELECT _id FROM favorites WHERE itemType = 2)"
699             String removeOrphanedFolderItems = "DELETE FROM " + Favorites.TABLE_NAME +
700                     " WHERE " +
701                     LauncherSettings.Favorites.CONTAINER + " <> " +
702                     LauncherSettings.Favorites.CONTAINER_DESKTOP +
703                     " AND "
704                     + LauncherSettings.Favorites.CONTAINER + " <> " +
705                     LauncherSettings.Favorites.CONTAINER_HOTSEAT +
706                     " AND "
707                     + LauncherSettings.Favorites.CONTAINER + " NOT IN (SELECT " +
708                     LauncherSettings.Favorites._ID + " FROM " + Favorites.TABLE_NAME +
709                     " WHERE " + LauncherSettings.Favorites.ITEM_TYPE + " = " +
710                     LauncherSettings.Favorites.ITEM_TYPE_FOLDER + ")";
711             db.execSQL(removeOrphanedFolderItems);
712         }
713 
714         @Override
onOpen(SQLiteDatabase db)715         public void onOpen(SQLiteDatabase db) {
716             super.onOpen(db);
717             SharedPreferences prefs = mContext
718                     .getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, 0);
719             int oldVersion = prefs.getInt(PREF_KEY_DATA_VERISON, 0);
720             if (oldVersion != DATA_VERSION) {
721                 // Only run the data upgrade path for an existing db.
722                 if (!Utilities.getPrefs(mContext).getBoolean(EMPTY_DATABASE_CREATED, false)) {
723                     db.beginTransaction();
724                     try {
725                         onDataUpgrade(db, oldVersion);
726                         db.setTransactionSuccessful();
727                     } catch (Exception e) {
728                         Log.d(TAG, "Error updating data version, ignoring", e);
729                         return;
730                     } finally {
731                         db.endTransaction();
732                     }
733                 }
734                 prefs.edit().putInt(PREF_KEY_DATA_VERISON, DATA_VERSION).apply();
735             }
736         }
737 
738         /**
739          * Called when the data is updated as part of app update. It can be called multiple times
740          * with old version, even though it had been run before. The changes made here must be
741          * backwards compatible, else we risk breaking old devices during restore or binary
742          * version downgrade.
743          */
onDataUpgrade(SQLiteDatabase db, int oldVersion)744         protected void onDataUpgrade(SQLiteDatabase db, int oldVersion) {
745             switch (oldVersion) {
746                 case 0:
747                 case 1: {
748                     // Remove "profile extra"
749                     UserManagerCompat um = UserManagerCompat.getInstance(mContext);
750                     for (UserHandle user : um.getUserProfiles()) {
751                         long serial = um.getSerialNumberForUser(user);
752                         String sql = "update favorites set intent = replace(intent, "
753                                 + "';l.profile=" + serial + ";', ';') where itemType = 0;";
754                         db.execSQL(sql);
755                     }
756                 }
757                 case 2:
758                 case 3:
759                     // data updated
760                     return;
761             }
762         }
763 
764         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)765         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
766             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
767             switch (oldVersion) {
768                 // The version cannot be lower that 12, as Launcher3 never supported a lower
769                 // version of the DB.
770                 case 12: {
771                     // With the new shrink-wrapped and re-orderable workspaces, it makes sense
772                     // to persist workspace screens and their relative order.
773                     mMaxScreenId = 0;
774                     addWorkspacesTable(db, false);
775                 }
776                 case 13: {
777                     db.beginTransaction();
778                     try {
779                         // Insert new column for holding widget provider name
780                         db.execSQL("ALTER TABLE favorites " +
781                                 "ADD COLUMN appWidgetProvider TEXT;");
782                         db.setTransactionSuccessful();
783                     } catch (SQLException ex) {
784                         Log.e(TAG, ex.getMessage(), ex);
785                         // Old version remains, which means we wipe old data
786                         break;
787                     } finally {
788                         db.endTransaction();
789                     }
790                 }
791                 case 14: {
792                     db.beginTransaction();
793                     try {
794                         // Insert new column for holding update timestamp
795                         db.execSQL("ALTER TABLE favorites " +
796                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
797                         db.execSQL("ALTER TABLE workspaceScreens " +
798                                 "ADD COLUMN modified INTEGER NOT NULL DEFAULT 0;");
799                         db.setTransactionSuccessful();
800                     } catch (SQLException ex) {
801                         Log.e(TAG, ex.getMessage(), ex);
802                         // Old version remains, which means we wipe old data
803                         break;
804                     } finally {
805                         db.endTransaction();
806                     }
807                 }
808                 case 15: {
809                     if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
810                         // Old version remains, which means we wipe old data
811                         break;
812                     }
813                 }
814                 case 16: {
815                     // No-op
816                 }
817                 case 17: {
818                     // No-op
819                 }
820                 case 18: {
821                     // Due to a data loss bug, some users may have items associated with screen ids
822                     // which no longer exist. Since this can cause other problems, and since the user
823                     // will never see these items anyway, we use database upgrade as an opportunity to
824                     // clean things up.
825                     removeOrphanedItems(db);
826                 }
827                 case 19: {
828                     // Add userId column
829                     if (!addProfileColumn(db)) {
830                         // Old version remains, which means we wipe old data
831                         break;
832                     }
833                 }
834                 case 20:
835                     if (!updateFolderItemsRank(db, true)) {
836                         break;
837                     }
838                 case 21:
839                     // Recreate workspace table with screen id a primary key
840                     if (!recreateWorkspaceTable(db)) {
841                         break;
842                     }
843                 case 22: {
844                     if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
845                         // Old version remains, which means we wipe old data
846                         break;
847                     }
848                 }
849                 case 23:
850                     // No-op
851                 case 24:
852                     ManagedProfileHeuristic.markExistingUsersForNoFolderCreation(mContext);
853                 case 25:
854                     convertShortcutsToLauncherActivities(db);
855                 case 26:
856                     // QSB was moved to the grid. Clear the first row on screen 0.
857                     if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
858                             !LauncherDbUtils.prepareScreenZeroToHostQsb(mContext, db)) {
859                         break;
860                     }
861                 case 27:
862                     // DB Upgraded successfully
863                     return;
864             }
865 
866             // DB was not upgraded
867             Log.w(TAG, "Destroying all old data.");
868             createEmptyDB(db);
869         }
870 
871         @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)872         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
873             if (oldVersion == 28 && newVersion == 27) {
874                 // TODO: remove this check. This is only applicable for internal development/testing
875                 // and for any released version of Launcher.
876                 return;
877             }
878             // This shouldn't happen -- throw our hands up in the air and start over.
879             Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion +
880                     ". Wiping databse.");
881             createEmptyDB(db);
882         }
883 
884         /**
885          * Clears all the data for a fresh start.
886          */
createEmptyDB(SQLiteDatabase db)887         public void createEmptyDB(SQLiteDatabase db) {
888             db.beginTransaction();
889             try {
890                 db.execSQL("DROP TABLE IF EXISTS " + Favorites.TABLE_NAME);
891                 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
892                 onCreate(db);
893                 db.setTransactionSuccessful();
894             } finally {
895                 db.endTransaction();
896             }
897         }
898 
899         /**
900          * Removes widgets which are registered to the Launcher's host, but are not present
901          * in our model.
902          */
removeGhostWidgets(SQLiteDatabase db)903         public void removeGhostWidgets(SQLiteDatabase db) {
904             // Get all existing widget ids.
905             final AppWidgetHost host = newLauncherWidgetHost();
906             final int[] allWidgets;
907             try {
908                 Method getter = AppWidgetHost.class.getDeclaredMethod("getAppWidgetIds");
909                 getter.setAccessible(true);
910                 allWidgets = (int[]) getter.invoke(host);
911             } catch (Exception e) {
912                 Log.e(TAG, "getAppWidgetIds not supported", e);
913                 return;
914             }
915             try {
916                 Cursor c = db.query(Favorites.TABLE_NAME,
917                         new String[] {Favorites.APPWIDGET_ID },
918                         "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null, null, null);
919                 HashSet<Integer> validWidgets = new HashSet<>();
920                 while (c.moveToNext()) {
921                     validWidgets.add(c.getInt(0));
922                 }
923                 c.close();
924 
925                 for (int widgetId : allWidgets) {
926                     if (!validWidgets.contains(widgetId)) {
927                         try {
928                             FileLog.d(TAG, "Deleting invalid widget " + widgetId);
929                             host.deleteAppWidgetId(widgetId);
930                         } catch (RuntimeException e) {
931                             // Ignore
932                         }
933                     }
934                 }
935             } catch (SQLException ex) {
936                 Log.w(TAG, "Error getting widgets list", ex);
937             }
938         }
939 
940         /**
941          * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
942          * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
943          */
convertShortcutsToLauncherActivities(SQLiteDatabase db)944         @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
945             db.beginTransaction();
946             Cursor c = null;
947             SQLiteStatement updateStmt = null;
948 
949             try {
950                 // Only consider the primary user as other users can't have a shortcut.
951                 long userSerial = getDefaultUserSerial();
952                 c = db.query(Favorites.TABLE_NAME, new String[] {
953                         Favorites._ID,
954                         Favorites.INTENT,
955                     }, "itemType=" + Favorites.ITEM_TYPE_SHORTCUT + " AND profileId=" + userSerial,
956                     null, null, null, null);
957 
958                 updateStmt = db.compileStatement("UPDATE favorites SET itemType="
959                         + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?");
960 
961                 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
962                 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
963 
964                 while (c.moveToNext()) {
965                     String intentDescription = c.getString(intentIndex);
966                     Intent intent;
967                     try {
968                         intent = Intent.parseUri(intentDescription, 0);
969                     } catch (URISyntaxException e) {
970                         Log.e(TAG, "Unable to parse intent", e);
971                         continue;
972                     }
973 
974                     if (!Utilities.isLauncherAppTarget(intent)) {
975                         continue;
976                     }
977 
978                     long id = c.getLong(idIndex);
979                     updateStmt.bindLong(1, id);
980                     updateStmt.executeUpdateDelete();
981                 }
982                 db.setTransactionSuccessful();
983             } catch (SQLException ex) {
984                 Log.w(TAG, "Error deduping shortcuts", ex);
985             } finally {
986                 db.endTransaction();
987                 if (c != null) {
988                     c.close();
989                 }
990                 if (updateStmt != null) {
991                     updateStmt.close();
992                 }
993             }
994         }
995 
996         /**
997          * Recreates workspace table and migrates data to the new table.
998          */
recreateWorkspaceTable(SQLiteDatabase db)999         public boolean recreateWorkspaceTable(SQLiteDatabase db) {
1000             db.beginTransaction();
1001             try {
1002                 Cursor c = db.query(WorkspaceScreens.TABLE_NAME,
1003                         new String[] {LauncherSettings.WorkspaceScreens._ID},
1004                         null, null, null, null,
1005                         LauncherSettings.WorkspaceScreens.SCREEN_RANK);
1006                 ArrayList<Long> sortedIDs = new ArrayList<Long>();
1007                 long maxId = 0;
1008                 try {
1009                     while (c.moveToNext()) {
1010                         Long id = c.getLong(0);
1011                         if (!sortedIDs.contains(id)) {
1012                             sortedIDs.add(id);
1013                             maxId = Math.max(maxId, id);
1014                         }
1015                     }
1016                 } finally {
1017                     c.close();
1018                 }
1019 
1020                 db.execSQL("DROP TABLE IF EXISTS " + WorkspaceScreens.TABLE_NAME);
1021                 addWorkspacesTable(db, false);
1022 
1023                 // Add all screen ids back
1024                 int total = sortedIDs.size();
1025                 for (int i = 0; i < total; i++) {
1026                     ContentValues values = new ContentValues();
1027                     values.put(LauncherSettings.WorkspaceScreens._ID, sortedIDs.get(i));
1028                     values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, i);
1029                     addModifiedTime(values);
1030                     db.insertOrThrow(WorkspaceScreens.TABLE_NAME, null, values);
1031                 }
1032                 db.setTransactionSuccessful();
1033                 mMaxScreenId = maxId;
1034             } catch (SQLException ex) {
1035                 // Old version remains, which means we wipe old data
1036                 Log.e(TAG, ex.getMessage(), ex);
1037                 return false;
1038             } finally {
1039                 db.endTransaction();
1040             }
1041             return true;
1042         }
1043 
updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn)1044         @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
1045             db.beginTransaction();
1046             try {
1047                 if (addRankColumn) {
1048                     // Insert new column for holding rank
1049                     db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
1050                 }
1051 
1052                 // Get a map for folder ID to folder width
1053                 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
1054                         + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
1055                         + " GROUP BY container;",
1056                         new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
1057 
1058                 while (c.moveToNext()) {
1059                     db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
1060                             + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
1061                             new Object[] {c.getLong(1) + 1, c.getLong(0)});
1062                 }
1063 
1064                 c.close();
1065                 db.setTransactionSuccessful();
1066             } catch (SQLException ex) {
1067                 // Old version remains, which means we wipe old data
1068                 Log.e(TAG, ex.getMessage(), ex);
1069                 return false;
1070             } finally {
1071                 db.endTransaction();
1072             }
1073             return true;
1074         }
1075 
addProfileColumn(SQLiteDatabase db)1076         private boolean addProfileColumn(SQLiteDatabase db) {
1077             return addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial());
1078         }
1079 
addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue)1080         private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
1081             db.beginTransaction();
1082             try {
1083                 db.execSQL("ALTER TABLE favorites ADD COLUMN "
1084                         + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
1085                 db.setTransactionSuccessful();
1086             } catch (SQLException ex) {
1087                 Log.e(TAG, ex.getMessage(), ex);
1088                 return false;
1089             } finally {
1090                 db.endTransaction();
1091             }
1092             return true;
1093         }
1094 
1095         // Generates a new ID to use for an object in your database. This method should be only
1096         // called from the main UI thread. As an exception, we do call it when we call the
1097         // constructor from the worker thread; however, this doesn't extend until after the
1098         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1099         // after that point
1100         @Override
generateNewItemId()1101         public long generateNewItemId() {
1102             if (mMaxItemId < 0) {
1103                 throw new RuntimeException("Error: max item id was not initialized");
1104             }
1105             mMaxItemId += 1;
1106             return mMaxItemId;
1107         }
1108 
newLauncherWidgetHost()1109         public AppWidgetHost newLauncherWidgetHost() {
1110             return new AppWidgetHost(mContext, Launcher.APPWIDGET_HOST_ID);
1111         }
1112 
1113         @Override
insertAndCheck(SQLiteDatabase db, ContentValues values)1114         public long insertAndCheck(SQLiteDatabase db, ContentValues values) {
1115             return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
1116         }
1117 
checkId(String table, ContentValues values)1118         public void checkId(String table, ContentValues values) {
1119             long id = values.getAsLong(LauncherSettings.BaseLauncherColumns._ID);
1120             if (WorkspaceScreens.TABLE_NAME.equals(table)) {
1121                 mMaxScreenId = Math.max(id, mMaxScreenId);
1122             }  else {
1123                 mMaxItemId = Math.max(id, mMaxItemId);
1124             }
1125         }
1126 
initializeMaxItemId(SQLiteDatabase db)1127         private long initializeMaxItemId(SQLiteDatabase db) {
1128             return getMaxId(db, Favorites.TABLE_NAME);
1129         }
1130 
1131         // Generates a new ID to use for an workspace screen in your database. This method
1132         // should be only called from the main UI thread. As an exception, we do call it when we
1133         // call the constructor from the worker thread; however, this doesn't extend until after the
1134         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1135         // after that point
generateNewScreenId()1136         public long generateNewScreenId() {
1137             if (mMaxScreenId < 0) {
1138                 throw new RuntimeException("Error: max screen id was not initialized");
1139             }
1140             mMaxScreenId += 1;
1141             return mMaxScreenId;
1142         }
1143 
initializeMaxScreenId(SQLiteDatabase db)1144         private long initializeMaxScreenId(SQLiteDatabase db) {
1145             return getMaxId(db, WorkspaceScreens.TABLE_NAME);
1146         }
1147 
loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader)1148         @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
1149             ArrayList<Long> screenIds = new ArrayList<Long>();
1150             // TODO: Use multiple loaders with fall-back and transaction.
1151             int count = loader.loadLayout(db, screenIds);
1152 
1153             // Add the screens specified by the items above
1154             Collections.sort(screenIds);
1155             int rank = 0;
1156             ContentValues values = new ContentValues();
1157             for (Long id : screenIds) {
1158                 values.clear();
1159                 values.put(LauncherSettings.WorkspaceScreens._ID, id);
1160                 values.put(LauncherSettings.WorkspaceScreens.SCREEN_RANK, rank);
1161                 if (dbInsertAndCheck(this, db, WorkspaceScreens.TABLE_NAME, null, values) < 0) {
1162                     throw new RuntimeException("Failed initialize screen table"
1163                             + "from default layout");
1164                 }
1165                 rank++;
1166             }
1167 
1168             // Ensure that the max ids are initialized
1169             mMaxItemId = initializeMaxItemId(db);
1170             mMaxScreenId = initializeMaxScreenId(db);
1171 
1172             return count;
1173         }
1174     }
1175 
1176     /**
1177      * @return the max _id in the provided table.
1178      */
getMaxId(SQLiteDatabase db, String table)1179     @Thunk static long getMaxId(SQLiteDatabase db, String table) {
1180         Cursor c = db.rawQuery("SELECT MAX(_id) FROM " + table, null);
1181         // get the result
1182         long id = -1;
1183         if (c != null && c.moveToNext()) {
1184             id = c.getLong(0);
1185         }
1186         if (c != null) {
1187             c.close();
1188         }
1189 
1190         if (id == -1) {
1191             throw new RuntimeException("Error: could not query max id in " + table);
1192         }
1193 
1194         return id;
1195     }
1196 
1197     static class SqlArguments {
1198         public final String table;
1199         public final String where;
1200         public final String[] args;
1201 
SqlArguments(Uri url, String where, String[] args)1202         SqlArguments(Uri url, String where, String[] args) {
1203             if (url.getPathSegments().size() == 1) {
1204                 this.table = url.getPathSegments().get(0);
1205                 this.where = where;
1206                 this.args = args;
1207             } else if (url.getPathSegments().size() != 2) {
1208                 throw new IllegalArgumentException("Invalid URI: " + url);
1209             } else if (!TextUtils.isEmpty(where)) {
1210                 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1211             } else {
1212                 this.table = url.getPathSegments().get(0);
1213                 this.where = "_id=" + ContentUris.parseId(url);
1214                 this.args = null;
1215             }
1216         }
1217 
SqlArguments(Uri url)1218         SqlArguments(Uri url) {
1219             if (url.getPathSegments().size() == 1) {
1220                 table = url.getPathSegments().get(0);
1221                 where = null;
1222                 args = null;
1223             } else {
1224                 throw new IllegalArgumentException("Invalid URI: " + url);
1225             }
1226         }
1227     }
1228 
1229     private static class ChangeListenerWrapper implements Handler.Callback {
1230 
1231         private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1;
1232         private static final int MSG_EXTRACTED_COLORS_CHANGED = 2;
1233         private static final int MSG_APP_WIDGET_HOST_RESET = 3;
1234 
1235         private LauncherProviderChangeListener mListener;
1236 
1237         @Override
handleMessage(Message msg)1238         public boolean handleMessage(Message msg) {
1239             if (mListener != null) {
1240                 switch (msg.what) {
1241                     case MSG_LAUNCHER_PROVIDER_CHANGED:
1242                         mListener.onLauncherProviderChanged();
1243                         break;
1244                     case MSG_EXTRACTED_COLORS_CHANGED:
1245                         mListener.onExtractedColorsChanged();
1246                         break;
1247                     case MSG_APP_WIDGET_HOST_RESET:
1248                         mListener.onAppWidgetHostReset();
1249                         break;
1250                 }
1251             }
1252             return true;
1253         }
1254     }
1255 }
1256