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