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