• 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 static com.android.launcher3.provider.LauncherDbUtils.dropTable;
20 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
21 
22 import android.annotation.TargetApi;
23 import android.app.backup.BackupManager;
24 import android.appwidget.AppWidgetHost;
25 import android.appwidget.AppWidgetManager;
26 import android.content.ComponentName;
27 import android.content.ContentProvider;
28 import android.content.ContentProviderOperation;
29 import android.content.ContentProviderResult;
30 import android.content.ContentUris;
31 import android.content.ContentValues;
32 import android.content.Context;
33 import android.content.Intent;
34 import android.content.OperationApplicationException;
35 import android.content.SharedPreferences;
36 import android.content.pm.PackageManager.NameNotFoundException;
37 import android.content.pm.ProviderInfo;
38 import android.content.res.Resources;
39 import android.database.Cursor;
40 import android.database.DatabaseUtils;
41 import android.database.SQLException;
42 import android.database.sqlite.SQLiteDatabase;
43 import android.database.sqlite.SQLiteQueryBuilder;
44 import android.database.sqlite.SQLiteStatement;
45 import android.net.Uri;
46 import android.os.Binder;
47 import android.os.Build;
48 import android.os.Bundle;
49 import android.os.Handler;
50 import android.os.Message;
51 import android.os.Process;
52 import android.os.UserHandle;
53 import android.os.UserManager;
54 import android.provider.BaseColumns;
55 import android.provider.Settings;
56 import android.text.TextUtils;
57 import android.util.Log;
58 import android.util.Xml;
59 
60 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
61 import com.android.launcher3.LauncherSettings.Favorites;
62 import com.android.launcher3.compat.UserManagerCompat;
63 import com.android.launcher3.config.FeatureFlags;
64 import com.android.launcher3.logging.FileLog;
65 import com.android.launcher3.model.DbDowngradeHelper;
66 import com.android.launcher3.provider.LauncherDbUtils;
67 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
68 import com.android.launcher3.provider.RestoreDbTask;
69 import com.android.launcher3.util.IOUtils;
70 import com.android.launcher3.util.IntArray;
71 import com.android.launcher3.util.IntSet;
72 import com.android.launcher3.util.NoLocaleSQLiteHelper;
73 import com.android.launcher3.util.Preconditions;
74 import com.android.launcher3.util.Thunk;
75 
76 import org.xmlpull.v1.XmlPullParser;
77 
78 import java.io.File;
79 import java.io.FileDescriptor;
80 import java.io.IOException;
81 import java.io.InputStream;
82 import java.io.PrintWriter;
83 import java.io.StringReader;
84 import java.net.URISyntaxException;
85 import java.util.ArrayList;
86 import java.util.Arrays;
87 import java.util.Locale;
88 
89 public class LauncherProvider extends ContentProvider {
90     private static final String TAG = "LauncherProvider";
91     private static final boolean LOGD = false;
92 
93     private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
94 
95     /**
96      * Represents the schema of the database. Changes in scheme need not be backwards compatible.
97      * When increasing the scheme version, ensure that downgrade_schema.json is updated
98      */
99     public static final int SCHEMA_VERSION = 28;
100 
101     public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".settings";
102 
103     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
104 
105     private final ChangeListenerWrapper mListenerWrapper = new ChangeListenerWrapper();
106     private Handler mListenerHandler;
107 
108     protected DatabaseHelper mOpenHelper;
109 
110     /**
111      * $ adb shell dumpsys activity provider com.android.launcher3
112      */
113     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)114     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
115         LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
116         if (appState == null || !appState.getModel().isModelLoaded()) {
117             return;
118         }
119         appState.getModel().dumpState("", fd, writer, args);
120     }
121 
122     @Override
onCreate()123     public boolean onCreate() {
124         if (FeatureFlags.IS_DOGFOOD_BUILD) {
125             Log.d(TAG, "Launcher process started");
126         }
127         mListenerHandler = new Handler(mListenerWrapper);
128 
129         // The content provider exists for the entire duration of the launcher main process and
130         // is the first component to get created.
131         MainProcessInitializer.initialize(getContext().getApplicationContext());
132         return true;
133     }
134 
135     /**
136      * Sets a provider listener.
137      */
setLauncherProviderChangeListener(LauncherProviderChangeListener listener)138     public void setLauncherProviderChangeListener(LauncherProviderChangeListener listener) {
139         Preconditions.assertUIThread();
140         mListenerWrapper.mListener = listener;
141     }
142 
143     @Override
getType(Uri uri)144     public String getType(Uri uri) {
145         SqlArguments args = new SqlArguments(uri, null, null);
146         if (TextUtils.isEmpty(args.where)) {
147             return "vnd.android.cursor.dir/" + args.table;
148         } else {
149             return "vnd.android.cursor.item/" + args.table;
150         }
151     }
152 
153     /**
154      * Overridden in tests
155      */
createDbIfNotExists()156     protected synchronized void createDbIfNotExists() {
157         if (mOpenHelper == null) {
158             mOpenHelper = new DatabaseHelper(getContext(), mListenerHandler);
159 
160             if (RestoreDbTask.isPending(getContext())) {
161                 if (!RestoreDbTask.performRestore(getContext(), mOpenHelper,
162                         new BackupManager(getContext()))) {
163                     mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
164                 }
165                 // Set is pending to false irrespective of the result, so that it doesn't get
166                 // executed again.
167                 RestoreDbTask.setPending(getContext(), false);
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 int 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.Favorites._ID)) {
194             throw new RuntimeException("Error: attempting to add item without specifying an id");
195         }
196         helper.checkId(values);
197         return (int) db.insert(table, nullColumnHack, values);
198     }
199 
reloadLauncherIfExternal()200     private void reloadLauncherIfExternal() {
201         if (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 int rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
224         if (rowId < 0) return null;
225         mOpenHelper.onAddOrDeleteOp(db);
226 
227         uri = ContentUris.withAppendedId(uri, rowId);
228         notifyListeners();
229         reloadLauncherIfExternal();
230         return uri;
231     }
232 
initializeExternalAdd(ContentValues values)233     private boolean initializeExternalAdd(ContentValues values) {
234         // 1. Ensure that externally added items have a valid item id
235         int id = mOpenHelper.generateNewItemId();
236         values.put(LauncherSettings.Favorites._ID, id);
237 
238         // 2. In the case of an app widget, and if no app widget id is specified, we
239         // attempt allocate and bind the widget.
240         Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
241         if (itemType != null &&
242                 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
243                 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
244 
245             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
246             ComponentName cn = ComponentName.unflattenFromString(
247                     values.getAsString(Favorites.APPWIDGET_PROVIDER));
248 
249             if (cn != null) {
250                 try {
251                     AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
252                     int appWidgetId = widgetHost.allocateAppWidgetId();
253                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
254                     if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
255                         widgetHost.deleteAppWidgetId(appWidgetId);
256                         return false;
257                     }
258                 } catch (RuntimeException e) {
259                     Log.e(TAG, "Failed to initialize external widget", e);
260                     return false;
261                 }
262             } else {
263                 return false;
264             }
265         }
266 
267         return true;
268     }
269 
270     @Override
bulkInsert(Uri uri, ContentValues[] values)271     public int bulkInsert(Uri uri, ContentValues[] values) {
272         createDbIfNotExists();
273         SqlArguments args = new SqlArguments(uri);
274 
275         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
276         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
277             int numValues = values.length;
278             for (int i = 0; i < numValues; i++) {
279                 addModifiedTime(values[i]);
280                 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
281                     return 0;
282                 }
283             }
284             mOpenHelper.onAddOrDeleteOp(db);
285             t.commit();
286         }
287 
288         notifyListeners();
289         reloadLauncherIfExternal();
290         return values.length;
291     }
292 
293     @TargetApi(Build.VERSION_CODES.M)
294     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)295     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
296             throws OperationApplicationException {
297         createDbIfNotExists();
298         try (SQLiteTransaction t = new SQLiteTransaction(mOpenHelper.getWritableDatabase())) {
299             boolean isAddOrDelete = false;
300 
301             final int numOperations = operations.size();
302             final ContentProviderResult[] results = new ContentProviderResult[numOperations];
303             for (int i = 0; i < numOperations; i++) {
304                 ContentProviderOperation op = operations.get(i);
305                 results[i] = op.apply(this, results, i);
306 
307                 isAddOrDelete |= (op.isInsert() || op.isDelete()) &&
308                         results[i].count != null && results[i].count > 0;
309             }
310             if (isAddOrDelete) {
311                 mOpenHelper.onAddOrDeleteOp(t.getDb());
312             }
313 
314             t.commit();
315             reloadLauncherIfExternal();
316             return results;
317         }
318     }
319 
320     @Override
delete(Uri uri, String selection, String[] selectionArgs)321     public int delete(Uri uri, String selection, String[] selectionArgs) {
322         createDbIfNotExists();
323         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
324 
325         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
326 
327         if (Binder.getCallingPid() != Process.myPid()
328                 && Favorites.TABLE_NAME.equalsIgnoreCase(args.table)) {
329             mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
330         }
331         int count = db.delete(args.table, args.where, args.args);
332         if (count > 0) {
333             mOpenHelper.onAddOrDeleteOp(db);
334             notifyListeners();
335             reloadLauncherIfExternal();
336         }
337         return count;
338     }
339 
340     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)341     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
342         createDbIfNotExists();
343         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
344 
345         addModifiedTime(values);
346         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
347         int count = db.update(args.table, values, args.where, args.args);
348         if (count > 0) notifyListeners();
349 
350         reloadLauncherIfExternal();
351         return count;
352     }
353 
354     @Override
call(String method, final String arg, final Bundle extras)355     public Bundle call(String method, final String arg, final Bundle extras) {
356         if (Binder.getCallingUid() != Process.myUid()) {
357             return null;
358         }
359         createDbIfNotExists();
360 
361         switch (method) {
362             case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
363                 clearFlagEmptyDbCreated();
364                 return null;
365             }
366             case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
367                 Bundle result = new Bundle();
368                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
369                         Utilities.getPrefs(getContext()).getBoolean(EMPTY_DATABASE_CREATED, false));
370                 return result;
371             }
372             case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
373                 Bundle result = new Bundle();
374                 result.putIntArray(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders()
375                         .toArray());
376                 return result;
377             }
378             case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
379                 Bundle result = new Bundle();
380                 result.putInt(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewItemId());
381                 return result;
382             }
383             case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
384                 Bundle result = new Bundle();
385                 result.putInt(LauncherSettings.Settings.EXTRA_VALUE, mOpenHelper.generateNewScreenId());
386                 return result;
387             }
388             case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
389                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
390                 return null;
391             }
392             case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
393                 loadDefaultFavoritesIfNecessary();
394                 return null;
395             }
396             case LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS: {
397                 mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
398                 return null;
399             }
400             case LauncherSettings.Settings.METHOD_NEW_TRANSACTION: {
401                 Bundle result = new Bundle();
402                 result.putBinder(LauncherSettings.Settings.EXTRA_VALUE,
403                         new SQLiteTransaction(mOpenHelper.getWritableDatabase()));
404                 return result;
405             }
406             case LauncherSettings.Settings.METHOD_REFRESH_BACKUP_TABLE: {
407                 mOpenHelper.mBackupTableExists =
408                         tableExists(mOpenHelper.getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
409                 return null;
410             }
411         }
412         return null;
413     }
414 
415     /**
416      * Deletes any empty folder from the DB.
417      * @return Ids of deleted folders.
418      */
deleteEmptyFolders()419     private IntArray deleteEmptyFolders() {
420         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
421         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
422             // Select folders whose id do not match any container value.
423             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
424                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
425                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT " +
426                             LauncherSettings.Favorites.CONTAINER + " FROM "
427                                 + Favorites.TABLE_NAME + ")";
428 
429             IntArray folderIds = LauncherDbUtils.queryIntArray(db, Favorites.TABLE_NAME,
430                     Favorites._ID, selection, null, null);
431             if (!folderIds.isEmpty()) {
432                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
433                         LauncherSettings.Favorites._ID, folderIds), null);
434             }
435             t.commit();
436             return folderIds;
437         } catch (SQLException ex) {
438             Log.e(TAG, ex.getMessage(), ex);
439             return new IntArray();
440         }
441     }
442 
443     /**
444      * Overridden in tests
445      */
notifyListeners()446     protected void notifyListeners() {
447         mListenerHandler.sendEmptyMessage(ChangeListenerWrapper.MSG_LAUNCHER_PROVIDER_CHANGED);
448     }
449 
addModifiedTime(ContentValues values)450     @Thunk static void addModifiedTime(ContentValues values) {
451         values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis());
452     }
453 
clearFlagEmptyDbCreated()454     private void clearFlagEmptyDbCreated() {
455         Utilities.getPrefs(getContext()).edit().remove(EMPTY_DATABASE_CREATED).commit();
456     }
457 
458     /**
459      * Loads the default workspace based on the following priority scheme:
460      *   1) From the app restrictions
461      *   2) From a package provided by play store
462      *   3) From a partner configuration APK, already in the system image
463      *   4) The default configuration for the particular device
464      */
loadDefaultFavoritesIfNecessary()465     synchronized private void loadDefaultFavoritesIfNecessary() {
466         SharedPreferences sp = Utilities.getPrefs(getContext());
467 
468         if (sp.getBoolean(EMPTY_DATABASE_CREATED, false)) {
469             Log.d(TAG, "loading default workspace");
470 
471             AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost();
472             AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost);
473             if (loader == null) {
474                 loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper);
475             }
476             if (loader == null) {
477                 final Partner partner = Partner.get(getContext().getPackageManager());
478                 if (partner != null && partner.hasDefaultLayout()) {
479                     final Resources partnerRes = partner.getResources();
480                     int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT,
481                             "xml", partner.getPackageName());
482                     if (workspaceResId != 0) {
483                         loader = new DefaultLayoutParser(getContext(), widgetHost,
484                                 mOpenHelper, partnerRes, workspaceResId);
485                     }
486                 }
487             }
488 
489             final boolean usingExternallyProvidedLayout = loader != null;
490             if (loader == null) {
491                 loader = getDefaultLayoutParser(widgetHost);
492             }
493 
494             // There might be some partially restored DB items, due to buggy restore logic in
495             // previous versions of launcher.
496             mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
497             // Populate favorites table with initial favorites
498             if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
499                     && usingExternallyProvidedLayout) {
500                 // Unable to load external layout. Cleanup and load the internal layout.
501                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
502                 mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
503                         getDefaultLayoutParser(widgetHost));
504             }
505             clearFlagEmptyDbCreated();
506         }
507     }
508 
509     /**
510      * Creates workspace loader from an XML resource listed in the app restrictions.
511      *
512      * @return the loader if the restrictions are set and the resource exists; null otherwise.
513      */
createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost)514     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(AppWidgetHost widgetHost) {
515         Context ctx = getContext();
516         InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
517 
518         String authority = Settings.Secure.getString(ctx.getContentResolver(),
519                 "launcher3.layout.provider");
520         if (TextUtils.isEmpty(authority)) {
521             return null;
522         }
523 
524         ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0);
525         if (pi == null) {
526             Log.e(TAG, "No provider found for authority " + authority);
527             return null;
528         }
529         Uri uri = new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
530                 .appendQueryParameter("version", "1")
531                 .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
532                 .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
533                 .appendQueryParameter("hotseatSize", Integer.toString(grid.numHotseatIcons))
534                 .build();
535 
536         try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
537             // Read the full xml so that we fail early in case of any IO error.
538             String layout = new String(IOUtils.toByteArray(in));
539             XmlPullParser parser = Xml.newPullParser();
540             parser.setInput(new StringReader(layout));
541 
542             Log.d(TAG, "Loading layout from " + authority);
543             return new AutoInstallsLayout(ctx, widgetHost, mOpenHelper,
544                     ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
545                     () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
546         } catch (Exception e) {
547             Log.e(TAG, "Error getting layout stream from: " + authority , e);
548             return null;
549         }
550     }
551 
getDefaultLayoutParser(AppWidgetHost widgetHost)552     private DefaultLayoutParser getDefaultLayoutParser(AppWidgetHost widgetHost) {
553         InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
554         int defaultLayout = idp.defaultLayoutId;
555 
556         UserManagerCompat um = UserManagerCompat.getInstance(getContext());
557         if (um.isDemoUser() && idp.demoModeLayoutId != 0) {
558             defaultLayout = idp.demoModeLayoutId;
559         }
560 
561         return new DefaultLayoutParser(getContext(), widgetHost,
562                 mOpenHelper, getContext().getResources(), defaultLayout);
563     }
564 
565     /**
566      * The class is subclassed in tests to create an in-memory db.
567      */
568     public static class DatabaseHelper extends NoLocaleSQLiteHelper implements LayoutParserCallback {
569         private final BackupManager mBackupManager;
570         private final Handler mWidgetHostResetHandler;
571         private final Context mContext;
572         private int mMaxItemId = -1;
573         private int mMaxScreenId = -1;
574         private boolean mBackupTableExists;
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(getReadableDatabase(), Favorites.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             }
586             mBackupTableExists = tableExists(getReadableDatabase(), Favorites.BACKUP_TABLE_NAME);
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(context, tableName, SCHEMA_VERSION);
597             mContext = context;
598             mWidgetHostResetHandler = widgetHostResetHandler;
599             mBackupManager = new BackupManager(mContext);
600         }
601 
initIds()602         protected void initIds() {
603             // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
604             // the DB here
605             if (mMaxItemId == -1) {
606                 mMaxItemId = initializeMaxItemId(getWritableDatabase());
607             }
608             if (mMaxScreenId == -1) {
609                 mMaxScreenId = initializeMaxScreenId(getWritableDatabase());
610             }
611         }
612 
613         @Override
onCreate(SQLiteDatabase db)614         public void onCreate(SQLiteDatabase db) {
615             if (LOGD) Log.d(TAG, "creating new launcher database");
616 
617             mMaxItemId = 1;
618             mMaxScreenId = 0;
619 
620             addFavoritesTable(db, false);
621 
622             // Fresh and clean launcher DB.
623             mMaxItemId = initializeMaxItemId(db);
624             onEmptyDbCreated();
625         }
626 
onAddOrDeleteOp(SQLiteDatabase db)627         protected void onAddOrDeleteOp(SQLiteDatabase db) {
628             if (mBackupTableExists) {
629                 dropTable(db, Favorites.BACKUP_TABLE_NAME);
630                 mBackupTableExists = false;
631             }
632         }
633 
634         /**
635          * Overriden in tests.
636          */
onEmptyDbCreated()637         protected void onEmptyDbCreated() {
638             // Database was just created, so wipe any previous widgets
639             if (mWidgetHostResetHandler != null) {
640                 newLauncherWidgetHost().deleteHost();
641                 mWidgetHostResetHandler.sendEmptyMessage(
642                         ChangeListenerWrapper.MSG_APP_WIDGET_HOST_RESET);
643             }
644 
645             // Set the flag for empty DB
646             Utilities.getPrefs(mContext).edit().putBoolean(EMPTY_DATABASE_CREATED, true).commit();
647         }
648 
getSerialNumberForUser(UserHandle user)649         public long getSerialNumberForUser(UserHandle user) {
650             return UserManagerCompat.getInstance(mContext).getSerialNumberForUser(user);
651         }
652 
getDefaultUserSerial()653         public long getDefaultUserSerial() {
654             return getSerialNumberForUser(Process.myUserHandle());
655         }
656 
addFavoritesTable(SQLiteDatabase db, boolean optional)657         private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
658             Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
659         }
660 
661         @Override
onOpen(SQLiteDatabase db)662         public void onOpen(SQLiteDatabase db) {
663             super.onOpen(db);
664 
665             File schemaFile = mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE);
666             if (!schemaFile.exists()) {
667                 handleOneTimeDataUpgrade(db);
668             }
669             DbDowngradeHelper.updateSchemaFile(schemaFile, SCHEMA_VERSION, mContext);
670         }
671 
672         /**
673          * One-time data updated before support of onDowngrade was added. This update is backwards
674          * compatible and can safely be run multiple times.
675          * Note: No new logic should be added here after release, as the new logic might not get
676          * executed on an existing device.
677          * TODO: Move this to db upgrade path, once the downgrade path is released.
678          */
handleOneTimeDataUpgrade(SQLiteDatabase db)679         protected void handleOneTimeDataUpgrade(SQLiteDatabase db) {
680             // Remove "profile extra"
681             UserManagerCompat um = UserManagerCompat.getInstance(mContext);
682             for (UserHandle user : um.getUserProfiles()) {
683                 long serial = um.getSerialNumberForUser(user);
684                 String sql = "update favorites set intent = replace(intent, "
685                         + "';l.profile=" + serial + ";', ';') where itemType = 0;";
686                 db.execSQL(sql);
687             }
688         }
689 
690         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)691         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
692             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
693             switch (oldVersion) {
694                 // The version cannot be lower that 12, as Launcher3 never supported a lower
695                 // version of the DB.
696                 case 12:
697                     // No-op
698                 case 13: {
699                     try (SQLiteTransaction t = new SQLiteTransaction(db)) {
700                         // Insert new column for holding widget provider name
701                         db.execSQL("ALTER TABLE favorites " +
702                                 "ADD COLUMN appWidgetProvider TEXT;");
703                         t.commit();
704                     } catch (SQLException ex) {
705                         Log.e(TAG, ex.getMessage(), ex);
706                         // Old version remains, which means we wipe old data
707                         break;
708                     }
709                 }
710                 case 14: {
711                     if (!addIntegerColumn(db, Favorites.MODIFIED, 0)) {
712                         // Old version remains, which means we wipe old data
713                         break;
714                     }
715                 }
716                 case 15: {
717                     if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
718                         // Old version remains, which means we wipe old data
719                         break;
720                     }
721                 }
722                 case 16:
723                     // No-op
724                 case 17:
725                     // No-op
726                 case 18:
727                     // No-op
728                 case 19: {
729                     // Add userId column
730                     if (!addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial())) {
731                         // Old version remains, which means we wipe old data
732                         break;
733                     }
734                 }
735                 case 20:
736                     if (!updateFolderItemsRank(db, true)) {
737                         break;
738                     }
739                 case 21:
740                     // No-op
741                 case 22: {
742                     if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
743                         // Old version remains, which means we wipe old data
744                         break;
745                     }
746                 }
747                 case 23:
748                     // No-op
749                 case 24:
750                     // No-op
751                 case 25:
752                     convertShortcutsToLauncherActivities(db);
753                 case 26:
754                     // QSB was moved to the grid. Clear the first row on screen 0.
755                     if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
756                             !LauncherDbUtils.prepareScreenZeroToHostQsb(mContext, db)) {
757                         break;
758                     }
759                 case 27: {
760                     // Update the favorites table so that the screen ids are ordered based on
761                     // workspace page rank.
762                     IntArray finalScreens = LauncherDbUtils.queryIntArray(db, "workspaceScreens",
763                             BaseColumns._ID, null, null, "screenRank");
764                     int[] original = finalScreens.toArray();
765                     Arrays.sort(original);
766                     String updatemap = "";
767                     for (int i = 0; i < original.length; i++) {
768                         if (finalScreens.get(i) != original[i]) {
769                             updatemap += String.format(Locale.ENGLISH, " WHEN %1$s=%2$d THEN %3$d",
770                                     Favorites.SCREEN, finalScreens.get(i), original[i]);
771                         }
772                     }
773                     if (!TextUtils.isEmpty(updatemap)) {
774                         String query = String.format(Locale.ENGLISH,
775                                 "UPDATE %1$s SET %2$s=CASE %3$s ELSE %2$s END WHERE %4$s = %5$d",
776                                 Favorites.TABLE_NAME, Favorites.SCREEN, updatemap,
777                                 Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP);
778                         db.execSQL(query);
779                     }
780                     dropTable(db, "workspaceScreens");
781                 }
782                 case 28:
783                     // DB Upgraded successfully
784                     return;
785             }
786 
787             // DB was not upgraded
788             Log.w(TAG, "Destroying all old data.");
789             createEmptyDB(db);
790         }
791 
792         @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)793         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
794             try {
795                 DbDowngradeHelper.parse(mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE))
796                         .onDowngrade(db, oldVersion, newVersion);
797             } catch (Exception e) {
798                 Log.d(TAG, "Unable to downgrade from: " + oldVersion + " to " + newVersion +
799                         ". Wiping databse.", e);
800                 createEmptyDB(db);
801             }
802         }
803 
804         /**
805          * Clears all the data for a fresh start.
806          */
createEmptyDB(SQLiteDatabase db)807         public void createEmptyDB(SQLiteDatabase db) {
808             try (SQLiteTransaction t = new SQLiteTransaction(db)) {
809                 dropTable(db, Favorites.TABLE_NAME);
810                 dropTable(db, "workspaceScreens");
811                 onCreate(db);
812                 t.commit();
813             }
814         }
815 
816         /**
817          * Removes widgets which are registered to the Launcher's host, but are not present
818          * in our model.
819          */
820         @TargetApi(Build.VERSION_CODES.O)
removeGhostWidgets(SQLiteDatabase db)821         public void removeGhostWidgets(SQLiteDatabase db) {
822             // Get all existing widget ids.
823             final AppWidgetHost host = newLauncherWidgetHost();
824             final int[] allWidgets;
825             try {
826                 // Although the method was defined in O, it has existed since the beginning of time,
827                 // so it might work on older platforms as well.
828                 allWidgets = host.getAppWidgetIds();
829             } catch (IncompatibleClassChangeError e) {
830                 Log.e(TAG, "getAppWidgetIds not supported", e);
831                 return;
832             }
833             final IntSet validWidgets = IntSet.wrap(LauncherDbUtils.queryIntArray(db,
834                     Favorites.TABLE_NAME, Favorites.APPWIDGET_ID,
835                     "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null));
836             for (int widgetId : allWidgets) {
837                 if (!validWidgets.contains(widgetId)) {
838                     try {
839                         FileLog.d(TAG, "Deleting invalid widget " + widgetId);
840                         host.deleteAppWidgetId(widgetId);
841                     } catch (RuntimeException e) {
842                         // Ignore
843                     }
844                 }
845             }
846         }
847 
848         /**
849          * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
850          * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
851          */
convertShortcutsToLauncherActivities(SQLiteDatabase db)852         @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
853             try (SQLiteTransaction t = new SQLiteTransaction(db);
854                  // Only consider the primary user as other users can't have a shortcut.
855                  Cursor c = db.query(Favorites.TABLE_NAME,
856                          new String[] { Favorites._ID, Favorites.INTENT},
857                          "itemType=" + Favorites.ITEM_TYPE_SHORTCUT +
858                                  " AND profileId=" + getDefaultUserSerial(),
859                          null, null, null, null);
860                  SQLiteStatement updateStmt = db.compileStatement("UPDATE favorites SET itemType="
861                          + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?")
862             ) {
863                 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
864                 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
865 
866                 while (c.moveToNext()) {
867                     String intentDescription = c.getString(intentIndex);
868                     Intent intent;
869                     try {
870                         intent = Intent.parseUri(intentDescription, 0);
871                     } catch (URISyntaxException e) {
872                         Log.e(TAG, "Unable to parse intent", e);
873                         continue;
874                     }
875 
876                     if (!Utilities.isLauncherAppTarget(intent)) {
877                         continue;
878                     }
879 
880                     int id = c.getInt(idIndex);
881                     updateStmt.bindLong(1, id);
882                     updateStmt.executeUpdateDelete();
883                 }
884                 t.commit();
885             } catch (SQLException ex) {
886                 Log.w(TAG, "Error deduping shortcuts", ex);
887             }
888         }
889 
updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn)890         @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
891             try (SQLiteTransaction t = new SQLiteTransaction(db)) {
892                 if (addRankColumn) {
893                     // Insert new column for holding rank
894                     db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
895                 }
896 
897                 // Get a map for folder ID to folder width
898                 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
899                         + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
900                         + " GROUP BY container;",
901                         new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
902 
903                 while (c.moveToNext()) {
904                     db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
905                             + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
906                             new Object[] {c.getLong(1) + 1, c.getLong(0)});
907                 }
908 
909                 c.close();
910                 t.commit();
911             } catch (SQLException ex) {
912                 // Old version remains, which means we wipe old data
913                 Log.e(TAG, ex.getMessage(), ex);
914                 return false;
915             }
916             return true;
917         }
918 
addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue)919         private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
920             try (SQLiteTransaction t = new SQLiteTransaction(db)) {
921                 db.execSQL("ALTER TABLE favorites ADD COLUMN "
922                         + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
923                 t.commit();
924             } catch (SQLException ex) {
925                 Log.e(TAG, ex.getMessage(), ex);
926                 return false;
927             }
928             return true;
929         }
930 
931         // Generates a new ID to use for an object in your database. This method should be only
932         // called from the main UI thread. As an exception, we do call it when we call the
933         // constructor from the worker thread; however, this doesn't extend until after the
934         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
935         // after that point
936         @Override
generateNewItemId()937         public int generateNewItemId() {
938             if (mMaxItemId < 0) {
939                 throw new RuntimeException("Error: max item id was not initialized");
940             }
941             mMaxItemId += 1;
942             return mMaxItemId;
943         }
944 
newLauncherWidgetHost()945         public AppWidgetHost newLauncherWidgetHost() {
946             return new LauncherAppWidgetHost(mContext);
947         }
948 
949         @Override
insertAndCheck(SQLiteDatabase db, ContentValues values)950         public int insertAndCheck(SQLiteDatabase db, ContentValues values) {
951             return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
952         }
953 
checkId(ContentValues values)954         public void checkId(ContentValues values) {
955             int id = values.getAsInteger(Favorites._ID);
956             mMaxItemId = Math.max(id, mMaxItemId);
957 
958             Integer screen = values.getAsInteger(Favorites.SCREEN);
959             Integer container = values.getAsInteger(Favorites.CONTAINER);
960             if (screen != null && container != null
961                     && container.intValue() == Favorites.CONTAINER_DESKTOP) {
962                 mMaxScreenId = Math.max(screen, mMaxScreenId);
963             }
964         }
965 
initializeMaxItemId(SQLiteDatabase db)966         private int initializeMaxItemId(SQLiteDatabase db) {
967             return getMaxId(db, "SELECT MAX(%1$s) FROM %2$s", Favorites._ID, Favorites.TABLE_NAME);
968         }
969 
970         // Generates a new ID to use for an workspace screen in your database. This method
971         // should be only called from the main UI thread. As an exception, we do call it when we
972         // call the constructor from the worker thread; however, this doesn't extend until after the
973         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
974         // after that point
generateNewScreenId()975         public int generateNewScreenId() {
976             if (mMaxScreenId < 0) {
977                 throw new RuntimeException("Error: max screen id was not initialized");
978             }
979             mMaxScreenId += 1;
980             return mMaxScreenId;
981         }
982 
initializeMaxScreenId(SQLiteDatabase db)983         private int initializeMaxScreenId(SQLiteDatabase db) {
984             return getMaxId(db, "SELECT MAX(%1$s) FROM %2$s WHERE %3$s = %4$d",
985                     Favorites.SCREEN, Favorites.TABLE_NAME, Favorites.CONTAINER,
986                     Favorites.CONTAINER_DESKTOP);
987         }
988 
loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader)989         @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
990             // TODO: Use multiple loaders with fall-back and transaction.
991             int count = loader.loadLayout(db, new IntArray());
992 
993             // Ensure that the max ids are initialized
994             mMaxItemId = initializeMaxItemId(db);
995             mMaxScreenId = initializeMaxScreenId(db);
996             return count;
997         }
998     }
999 
1000     /**
1001      * @return the max _id in the provided table.
1002      */
getMaxId(SQLiteDatabase db, String query, Object... args)1003     @Thunk static int getMaxId(SQLiteDatabase db, String query, Object... args) {
1004         int max = (int) DatabaseUtils.longForQuery(db,
1005                 String.format(Locale.ENGLISH, query, args),
1006                 null);
1007         if (max < 0) {
1008             throw new RuntimeException("Error: could not query max id");
1009         }
1010         return max;
1011     }
1012 
1013     static class SqlArguments {
1014         public final String table;
1015         public final String where;
1016         public final String[] args;
1017 
SqlArguments(Uri url, String where, String[] args)1018         SqlArguments(Uri url, String where, String[] args) {
1019             if (url.getPathSegments().size() == 1) {
1020                 this.table = url.getPathSegments().get(0);
1021                 this.where = where;
1022                 this.args = args;
1023             } else if (url.getPathSegments().size() != 2) {
1024                 throw new IllegalArgumentException("Invalid URI: " + url);
1025             } else if (!TextUtils.isEmpty(where)) {
1026                 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1027             } else {
1028                 this.table = url.getPathSegments().get(0);
1029                 this.where = "_id=" + ContentUris.parseId(url);
1030                 this.args = null;
1031             }
1032         }
1033 
SqlArguments(Uri url)1034         SqlArguments(Uri url) {
1035             if (url.getPathSegments().size() == 1) {
1036                 table = url.getPathSegments().get(0);
1037                 where = null;
1038                 args = null;
1039             } else {
1040                 throw new IllegalArgumentException("Invalid URI: " + url);
1041             }
1042         }
1043     }
1044 
1045     private static class ChangeListenerWrapper implements Handler.Callback {
1046 
1047         private static final int MSG_LAUNCHER_PROVIDER_CHANGED = 1;
1048         private static final int MSG_APP_WIDGET_HOST_RESET = 2;
1049 
1050         private LauncherProviderChangeListener mListener;
1051 
1052         @Override
handleMessage(Message msg)1053         public boolean handleMessage(Message msg) {
1054             if (mListener != null) {
1055                 switch (msg.what) {
1056                     case MSG_LAUNCHER_PROVIDER_CHANGED:
1057                         mListener.onLauncherProviderChanged();
1058                         break;
1059                     case MSG_APP_WIDGET_HOST_RESET:
1060                         mListener.onAppWidgetHostReset();
1061                         break;
1062                 }
1063             }
1064             return true;
1065         }
1066     }
1067 }
1068