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