OSDN Git Service

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