• 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.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
20 import static com.android.launcher3.provider.LauncherDbUtils.copyTable;
21 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
22 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
23 
24 import android.annotation.TargetApi;
25 import android.app.backup.BackupManager;
26 import android.appwidget.AppWidgetManager;
27 import android.content.ComponentName;
28 import android.content.ContentProvider;
29 import android.content.ContentProviderOperation;
30 import android.content.ContentProviderResult;
31 import android.content.ContentUris;
32 import android.content.ContentValues;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.OperationApplicationException;
36 import android.content.SharedPreferences;
37 import android.content.pm.ProviderInfo;
38 import android.database.Cursor;
39 import android.database.DatabaseUtils;
40 import android.database.SQLException;
41 import android.database.sqlite.SQLiteDatabase;
42 import android.database.sqlite.SQLiteQueryBuilder;
43 import android.database.sqlite.SQLiteStatement;
44 import android.net.Uri;
45 import android.os.Binder;
46 import android.os.Build;
47 import android.os.Bundle;
48 import android.os.Process;
49 import android.os.UserHandle;
50 import android.os.UserManager;
51 import android.provider.BaseColumns;
52 import android.provider.Settings;
53 import android.text.TextUtils;
54 import android.util.Log;
55 import android.util.Xml;
56 
57 import androidx.annotation.NonNull;
58 
59 import com.android.launcher3.AutoInstallsLayout.LayoutParserCallback;
60 import com.android.launcher3.LauncherSettings.Favorites;
61 import com.android.launcher3.config.FeatureFlags;
62 import com.android.launcher3.logging.FileLog;
63 import com.android.launcher3.model.DbDowngradeHelper;
64 import com.android.launcher3.pm.UserCache;
65 import com.android.launcher3.provider.LauncherDbUtils;
66 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
67 import com.android.launcher3.provider.RestoreDbTask;
68 import com.android.launcher3.util.IOUtils;
69 import com.android.launcher3.util.IntArray;
70 import com.android.launcher3.util.IntSet;
71 import com.android.launcher3.util.NoLocaleSQLiteHelper;
72 import com.android.launcher3.util.PackageManagerHelper;
73 import com.android.launcher3.util.Partner;
74 import com.android.launcher3.util.Thunk;
75 import com.android.launcher3.widget.LauncherWidgetHolder;
76 
77 import org.xmlpull.v1.XmlPullParser;
78 
79 import java.io.File;
80 import java.io.FileDescriptor;
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 import java.util.concurrent.TimeUnit;
89 import java.util.function.Supplier;
90 import java.util.stream.Collectors;
91 
92 public class LauncherProvider extends ContentProvider {
93     private static final String TAG = "LauncherProvider";
94     private static final boolean LOGD = false;
95 
96     private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json";
97     private static final long RESTORE_BACKUP_TABLE_DELAY = TimeUnit.SECONDS.toMillis(30);
98 
99     /**
100      * Represents the schema of the database. Changes in scheme need not be backwards compatible.
101      * When increasing the scheme version, ensure that downgrade_schema.json is updated
102      */
103     public static final int SCHEMA_VERSION = 31;
104 
105     public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".settings";
106     public static final String KEY_LAYOUT_PROVIDER_AUTHORITY = "KEY_LAYOUT_PROVIDER_AUTHORITY";
107 
108     private static final int TEST_WORKSPACE_LAYOUT_RES_XML = R.xml.default_test_workspace;
109     private static final int TEST2_WORKSPACE_LAYOUT_RES_XML = R.xml.default_test2_workspace;
110     private static final int TAPL_WORKSPACE_LAYOUT_RES_XML = R.xml.default_tapl_test_workspace;
111 
112     static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
113 
114     protected DatabaseHelper mOpenHelper;
115     protected String mProviderAuthority;
116 
117     private long mLastRestoreTimestamp = 0L;
118 
119     private int mDefaultWorkspaceLayoutOverride = 0;
120 
121     /**
122      * $ adb shell dumpsys activity provider com.android.launcher3
123      */
124     @Override
dump(FileDescriptor fd, PrintWriter writer, String[] args)125     public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
126         LauncherAppState appState = LauncherAppState.getInstanceNoCreate();
127         if (appState == null || !appState.getModel().isModelLoaded()) {
128             return;
129         }
130         appState.getModel().dumpState("", fd, writer, args);
131     }
132 
133     @Override
onCreate()134     public boolean onCreate() {
135         if (FeatureFlags.IS_STUDIO_BUILD) {
136             Log.d(TAG, "Launcher process started");
137         }
138 
139         // The content provider exists for the entire duration of the launcher main process and
140         // is the first component to get created.
141         MainProcessInitializer.initialize(getContext().getApplicationContext());
142         return true;
143     }
144 
145     @Override
getType(Uri uri)146     public String getType(Uri uri) {
147         SqlArguments args = new SqlArguments(uri, null, null);
148         if (TextUtils.isEmpty(args.where)) {
149             return "vnd.android.cursor.dir/" + args.table;
150         } else {
151             return "vnd.android.cursor.item/" + args.table;
152         }
153     }
154 
155     /**
156      * Overridden in tests
157      */
createDbIfNotExists()158     protected synchronized void createDbIfNotExists() {
159         if (mOpenHelper == null) {
160             mOpenHelper = DatabaseHelper.createDatabaseHelper(
161                     getContext(), false /* forMigration */);
162 
163             RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper);
164         }
165     }
166 
prepForMigration(String dbFile, String targetTableName, Supplier<DatabaseHelper> src, Supplier<DatabaseHelper> dst)167     private synchronized boolean prepForMigration(String dbFile, String targetTableName,
168             Supplier<DatabaseHelper> src, Supplier<DatabaseHelper> dst) {
169         if (TextUtils.equals(dbFile, mOpenHelper.getDatabaseName())) {
170             Log.e(TAG, "prepForMigration - target db is same as current: " + dbFile);
171             return false;
172         }
173 
174         final DatabaseHelper helper = src.get();
175         mOpenHelper = dst.get();
176         copyTable(helper.getReadableDatabase(), Favorites.TABLE_NAME,
177                 mOpenHelper.getWritableDatabase(), targetTableName, getContext());
178         helper.close();
179         return true;
180     }
181 
182     @Override
query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)183     public Cursor query(Uri uri, String[] projection, String selection,
184             String[] selectionArgs, String sortOrder) {
185         createDbIfNotExists();
186 
187         SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
188         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
189         qb.setTables(args.table);
190 
191         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
192         Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
193         final Bundle extra = new Bundle();
194         extra.putString(LauncherSettings.Settings.EXTRA_DB_NAME, mOpenHelper.getDatabaseName());
195         result.setExtras(extra);
196         result.setNotificationUri(getContext().getContentResolver(), uri);
197 
198         return result;
199     }
200 
dbInsertAndCheck(DatabaseHelper helper, SQLiteDatabase db, String table, String nullColumnHack, ContentValues values)201     @Thunk static int dbInsertAndCheck(DatabaseHelper helper,
202             SQLiteDatabase db, String table, String nullColumnHack, ContentValues values) {
203         if (values == null) {
204             throw new RuntimeException("Error: attempting to insert null values");
205         }
206         if (!values.containsKey(LauncherSettings.Favorites._ID)) {
207             throw new RuntimeException("Error: attempting to add item without specifying an id");
208         }
209         helper.checkId(values);
210         return (int) db.insert(table, nullColumnHack, values);
211     }
212 
reloadLauncherIfExternal()213     private void reloadLauncherIfExternal() {
214         if (Binder.getCallingPid() != Process.myPid()) {
215             LauncherAppState app = LauncherAppState.getInstanceNoCreate();
216             if (app != null) {
217                 app.getModel().forceReload();
218             }
219         }
220     }
221 
222     @Override
insert(Uri uri, ContentValues initialValues)223     public Uri insert(Uri uri, ContentValues initialValues) {
224         createDbIfNotExists();
225         SqlArguments args = new SqlArguments(uri);
226 
227         // In very limited cases, we support system|signature permission apps to modify the db.
228         if (Binder.getCallingPid() != Process.myPid()) {
229             if (!initializeExternalAdd(initialValues)) {
230                 return null;
231             }
232         }
233 
234         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
235         addModifiedTime(initialValues);
236         final int rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
237         if (rowId < 0) return null;
238         onAddOrDeleteOp(db);
239 
240         uri = ContentUris.withAppendedId(uri, rowId);
241         reloadLauncherIfExternal();
242         return uri;
243     }
244 
initializeExternalAdd(ContentValues values)245     private boolean initializeExternalAdd(ContentValues values) {
246         // 1. Ensure that externally added items have a valid item id
247         int id = mOpenHelper.generateNewItemId();
248         values.put(LauncherSettings.Favorites._ID, id);
249 
250         // 2. In the case of an app widget, and if no app widget id is specified, we
251         // attempt allocate and bind the widget.
252         Integer itemType = values.getAsInteger(LauncherSettings.Favorites.ITEM_TYPE);
253         if (itemType != null &&
254                 itemType.intValue() == LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET &&
255                 !values.containsKey(LauncherSettings.Favorites.APPWIDGET_ID)) {
256 
257             final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getContext());
258             ComponentName cn = ComponentName.unflattenFromString(
259                     values.getAsString(Favorites.APPWIDGET_PROVIDER));
260 
261             if (cn != null) {
262                 LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
263                 try {
264                     int appWidgetId = widgetHolder.allocateAppWidgetId();
265                     values.put(LauncherSettings.Favorites.APPWIDGET_ID, appWidgetId);
266                     if (!appWidgetManager.bindAppWidgetIdIfAllowed(appWidgetId,cn)) {
267                         widgetHolder.deleteAppWidgetId(appWidgetId);
268                         return false;
269                     }
270                 } catch (RuntimeException e) {
271                     Log.e(TAG, "Failed to initialize external widget", e);
272                     return false;
273                 } finally {
274                     // Necessary to destroy the holder to free up possible activity context
275                     widgetHolder.destroy();
276                 }
277             } else {
278                 return false;
279             }
280         }
281 
282         return true;
283     }
284 
285     @Override
bulkInsert(Uri uri, ContentValues[] values)286     public int bulkInsert(Uri uri, ContentValues[] values) {
287         createDbIfNotExists();
288         SqlArguments args = new SqlArguments(uri);
289 
290         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
291         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
292             int numValues = values.length;
293             for (int i = 0; i < numValues; i++) {
294                 addModifiedTime(values[i]);
295                 if (dbInsertAndCheck(mOpenHelper, db, args.table, null, values[i]) < 0) {
296                     return 0;
297                 }
298             }
299             onAddOrDeleteOp(db);
300             t.commit();
301         }
302 
303         reloadLauncherIfExternal();
304         return values.length;
305     }
306 
307     @TargetApi(Build.VERSION_CODES.M)
308     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)309     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
310             throws OperationApplicationException {
311         createDbIfNotExists();
312         try (SQLiteTransaction t = new SQLiteTransaction(mOpenHelper.getWritableDatabase())) {
313             boolean isAddOrDelete = false;
314 
315             final int numOperations = operations.size();
316             final ContentProviderResult[] results = new ContentProviderResult[numOperations];
317             for (int i = 0; i < numOperations; i++) {
318                 ContentProviderOperation op = operations.get(i);
319                 results[i] = op.apply(this, results, i);
320 
321                 isAddOrDelete |= (op.isInsert() || op.isDelete()) &&
322                         results[i].count != null && results[i].count > 0;
323             }
324             if (isAddOrDelete) {
325                 onAddOrDeleteOp(t.getDb());
326             }
327 
328             t.commit();
329             reloadLauncherIfExternal();
330             return results;
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             onAddOrDeleteOp(db);
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         reloadLauncherIfExternal();
362         return count;
363     }
364 
365     @Override
call(String method, final String arg, final Bundle extras)366     public Bundle call(String method, final String arg, final Bundle extras) {
367         if (Binder.getCallingUid() != Process.myUid()) {
368             return null;
369         }
370         createDbIfNotExists();
371 
372         switch (method) {
373             case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
374                 clearFlagEmptyDbCreated();
375                 return null;
376             }
377             case LauncherSettings.Settings.METHOD_WAS_EMPTY_DB_CREATED : {
378                 Bundle result = new Bundle();
379                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
380                         LauncherPrefs.getPrefs(getContext()).getBoolean(
381                                 mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false));
382                 return result;
383             }
384             case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
385                 Bundle result = new Bundle();
386                 result.putIntArray(LauncherSettings.Settings.EXTRA_VALUE, deleteEmptyFolders()
387                         .toArray());
388                 return result;
389             }
390             case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
391                 Bundle result = new Bundle();
392                 result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
393                         mOpenHelper.generateNewItemId());
394                 return result;
395             }
396             case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
397                 Bundle result = new Bundle();
398                 result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
399                         mOpenHelper.getNewScreenId());
400                 return result;
401             }
402             case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
403                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
404                 return null;
405             }
406             case LauncherSettings.Settings.METHOD_SET_USE_TEST_WORKSPACE_LAYOUT_FLAG: {
407                 switch (arg) {
408                     case LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TEST:
409                         mDefaultWorkspaceLayoutOverride = TEST_WORKSPACE_LAYOUT_RES_XML;
410                         break;
411                     case LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TEST2:
412                         mDefaultWorkspaceLayoutOverride = TEST2_WORKSPACE_LAYOUT_RES_XML;
413                         break;
414                     case LauncherSettings.Settings.ARG_DEFAULT_WORKSPACE_LAYOUT_TAPL:
415                         mDefaultWorkspaceLayoutOverride = TAPL_WORKSPACE_LAYOUT_RES_XML;
416                         break;
417                     default:
418                         mDefaultWorkspaceLayoutOverride = 0;
419                         break;
420                 }
421                 return null;
422             }
423             case LauncherSettings.Settings.METHOD_CLEAR_USE_TEST_WORKSPACE_LAYOUT_FLAG: {
424                 mDefaultWorkspaceLayoutOverride = 0;
425                 return null;
426             }
427             case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
428                 loadDefaultFavoritesIfNecessary();
429                 return null;
430             }
431             case LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS: {
432                 mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
433                 return null;
434             }
435             case LauncherSettings.Settings.METHOD_NEW_TRANSACTION: {
436                 Bundle result = new Bundle();
437                 result.putBinder(LauncherSettings.Settings.EXTRA_VALUE,
438                         new SQLiteTransaction(mOpenHelper.getWritableDatabase()));
439                 return result;
440             }
441             case LauncherSettings.Settings.METHOD_REFRESH_BACKUP_TABLE: {
442                 mOpenHelper.mBackupTableExists = tableExists(mOpenHelper.getReadableDatabase(),
443                         Favorites.BACKUP_TABLE_NAME);
444                 return null;
445             }
446             case LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE: {
447                 mOpenHelper.mHotseatRestoreTableExists = tableExists(
448                         mOpenHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
449                 return null;
450             }
451             case LauncherSettings.Settings.METHOD_RESTORE_BACKUP_TABLE: {
452                 final long ts = System.currentTimeMillis();
453                 if (ts - mLastRestoreTimestamp > RESTORE_BACKUP_TABLE_DELAY) {
454                     mLastRestoreTimestamp = ts;
455                     RestoreDbTask.restoreIfPossible(
456                             getContext(), mOpenHelper, new BackupManager(getContext()));
457                 }
458                 return null;
459             }
460             case LauncherSettings.Settings.METHOD_UPDATE_CURRENT_OPEN_HELPER: {
461                 Bundle result = new Bundle();
462                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
463                         prepForMigration(
464                                 arg /* dbFile */,
465                                 Favorites.TMP_TABLE,
466                                 () -> mOpenHelper,
467                                 () -> DatabaseHelper.createDatabaseHelper(
468                                         getContext(), true /* forMigration */)));
469                 return result;
470             }
471             case LauncherSettings.Settings.METHOD_PREP_FOR_PREVIEW: {
472                 Bundle result = new Bundle();
473                 result.putBoolean(LauncherSettings.Settings.EXTRA_VALUE,
474                         prepForMigration(
475                                 arg /* dbFile */,
476                                 Favorites.PREVIEW_TABLE_NAME,
477                                 () -> DatabaseHelper.createDatabaseHelper(
478                                         getContext(), arg, true /* forMigration */),
479                                 () -> mOpenHelper));
480                 return result;
481             }
482             case LauncherSettings.Settings.METHOD_SWITCH_DATABASE: {
483                 if (TextUtils.equals(arg, mOpenHelper.getDatabaseName())) return null;
484                 final DatabaseHelper helper = mOpenHelper;
485                 if (extras == null || !extras.containsKey(KEY_LAYOUT_PROVIDER_AUTHORITY)) {
486                     mProviderAuthority = null;
487                 } else {
488                     mProviderAuthority = extras.getString(KEY_LAYOUT_PROVIDER_AUTHORITY);
489                 }
490                 mOpenHelper = DatabaseHelper.createDatabaseHelper(
491                         getContext(), arg, false /* forMigration */);
492                 helper.close();
493                 LauncherAppState app = LauncherAppState.getInstanceNoCreate();
494                 if (app == null) return null;
495                 app.getModel().forceReload();
496                 return null;
497             }
498         }
499         return null;
500     }
501 
onAddOrDeleteOp(SQLiteDatabase db)502     private void onAddOrDeleteOp(SQLiteDatabase db) {
503         mOpenHelper.onAddOrDeleteOp(db);
504     }
505 
506     /**
507      * Deletes any empty folder from the DB.
508      * @return Ids of deleted folders.
509      */
deleteEmptyFolders()510     private IntArray deleteEmptyFolders() {
511         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
512         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
513             // Select folders whose id do not match any container value.
514             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
515                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
516                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT " +
517                             LauncherSettings.Favorites.CONTAINER + " FROM "
518                                 + Favorites.TABLE_NAME + ")";
519 
520             IntArray folderIds = LauncherDbUtils.queryIntArray(false, db, Favorites.TABLE_NAME,
521                     Favorites._ID, selection, null, null);
522             if (!folderIds.isEmpty()) {
523                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
524                         LauncherSettings.Favorites._ID, folderIds), null);
525             }
526             t.commit();
527             return folderIds;
528         } catch (SQLException ex) {
529             Log.e(TAG, ex.getMessage(), ex);
530             return new IntArray();
531         }
532     }
533 
addModifiedTime(ContentValues values)534     @Thunk static void addModifiedTime(ContentValues values) {
535         values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis());
536     }
537 
clearFlagEmptyDbCreated()538     private void clearFlagEmptyDbCreated() {
539         LauncherPrefs.getPrefs(getContext()).edit()
540                 .remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit();
541     }
542 
543     /**
544      * Loads the default workspace based on the following priority scheme:
545      *   1) From the app restrictions
546      *   2) From a package provided by play store
547      *   3) From a partner configuration APK, already in the system image
548      *   4) The default configuration for the particular device
549      */
loadDefaultFavoritesIfNecessary()550     synchronized private void loadDefaultFavoritesIfNecessary() {
551         SharedPreferences sp = LauncherPrefs.getPrefs(getContext());
552 
553         if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false)) {
554             Log.d(TAG, "loading default workspace");
555 
556             LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
557             try {
558                 AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder);
559                 if (loader == null) {
560                     loader = AutoInstallsLayout.get(getContext(), widgetHolder, mOpenHelper);
561                 }
562                 if (loader == null) {
563                     final Partner partner = Partner.get(getContext().getPackageManager());
564                     if (partner != null) {
565                         int workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT);
566                         if (workspaceResId != 0) {
567                             loader = new DefaultLayoutParser(getContext(), widgetHolder,
568                                     mOpenHelper, partner.getResources(), workspaceResId);
569                         }
570                     }
571                 }
572 
573                 final boolean usingExternallyProvidedLayout = loader != null;
574                 if (loader == null) {
575                     loader = getDefaultLayoutParser(widgetHolder);
576                 }
577 
578                 // There might be some partially restored DB items, due to buggy restore logic in
579                 // previous versions of launcher.
580                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
581                 // Populate favorites table with initial favorites
582                 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
583                         && usingExternallyProvidedLayout) {
584                     // Unable to load external layout. Cleanup and load the internal layout.
585                     mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
586                     mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
587                             getDefaultLayoutParser(widgetHolder));
588                 }
589                 clearFlagEmptyDbCreated();
590             } finally {
591                 widgetHolder.destroy();
592             }
593         }
594     }
595 
596     /**
597      * Creates workspace loader from an XML resource listed in the app restrictions.
598      *
599      * @return the loader if the restrictions are set and the resource exists; null otherwise.
600      */
createWorkspaceLoaderFromAppRestriction( LauncherWidgetHolder widgetHolder)601     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
602             LauncherWidgetHolder widgetHolder) {
603         Context ctx = getContext();
604         final String authority;
605         if (!TextUtils.isEmpty(mProviderAuthority)) {
606             authority = mProviderAuthority;
607         } else {
608             authority = Settings.Secure.getString(ctx.getContentResolver(),
609                     "launcher3.layout.provider");
610         }
611         if (TextUtils.isEmpty(authority)) {
612             return null;
613         }
614 
615         ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0);
616         if (pi == null) {
617             Log.e(TAG, "No provider found for authority " + authority);
618             return null;
619         }
620         Uri uri = getLayoutUri(authority, ctx);
621         try (InputStream in = ctx.getContentResolver().openInputStream(uri)) {
622             // Read the full xml so that we fail early in case of any IO error.
623             String layout = new String(IOUtils.toByteArray(in));
624             XmlPullParser parser = Xml.newPullParser();
625             parser.setInput(new StringReader(layout));
626 
627             Log.d(TAG, "Loading layout from " + authority);
628             return new AutoInstallsLayout(ctx, widgetHolder, mOpenHelper,
629                     ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo),
630                     () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
631         } catch (Exception e) {
632             Log.e(TAG, "Error getting layout stream from: " + authority , e);
633             return null;
634         }
635     }
636 
getLayoutUri(String authority, Context ctx)637     public static Uri getLayoutUri(String authority, Context ctx) {
638         InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
639         return new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
640                 .appendQueryParameter("version", "1")
641                 .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
642                 .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
643                 .appendQueryParameter("hotseatSize", Integer.toString(grid.numDatabaseHotseatIcons))
644                 .build();
645     }
646 
getDefaultLayoutParser(LauncherWidgetHolder widgetHolder)647     private DefaultLayoutParser getDefaultLayoutParser(LauncherWidgetHolder widgetHolder) {
648         InvariantDeviceProfile idp = LauncherAppState.getIDP(getContext());
649         int defaultLayout = mDefaultWorkspaceLayoutOverride > 0
650                 ? mDefaultWorkspaceLayoutOverride : idp.defaultLayoutId;
651 
652         if (getContext().getSystemService(UserManager.class).isDemoUser()
653                 && idp.demoModeLayoutId != 0) {
654             defaultLayout = idp.demoModeLayoutId;
655         }
656 
657         return new DefaultLayoutParser(getContext(), widgetHolder,
658                 mOpenHelper, getContext().getResources(), defaultLayout);
659     }
660 
661     /**
662      * The class is subclassed in tests to create an in-memory db.
663      */
664     public static class DatabaseHelper extends NoLocaleSQLiteHelper implements
665             LayoutParserCallback {
666         private final Context mContext;
667         private final boolean mForMigration;
668         private int mMaxItemId = -1;
669         private boolean mBackupTableExists;
670         private boolean mHotseatRestoreTableExists;
671 
createDatabaseHelper(Context context, boolean forMigration)672         static DatabaseHelper createDatabaseHelper(Context context, boolean forMigration) {
673             return createDatabaseHelper(context, null, forMigration);
674         }
675 
createDatabaseHelper(Context context, String dbName, boolean forMigration)676         static DatabaseHelper createDatabaseHelper(Context context, String dbName,
677                 boolean forMigration) {
678             if (dbName == null) {
679                 dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile;
680             }
681             DatabaseHelper databaseHelper = new DatabaseHelper(context, dbName, forMigration);
682             // Table creation sometimes fails silently, which leads to a crash loop.
683             // This way, we will try to create a table every time after crash, so the device
684             // would eventually be able to recover.
685             if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
686                 Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
687                 // This operation is a no-op if the table already exists.
688                 databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true);
689             }
690             databaseHelper.mHotseatRestoreTableExists = tableExists(
691                     databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
692 
693             databaseHelper.initIds();
694             return databaseHelper;
695         }
696 
697         /**
698          * Constructor used in tests and for restore.
699          */
DatabaseHelper(Context context, String dbName, boolean forMigration)700         public DatabaseHelper(Context context, String dbName, boolean forMigration) {
701             super(context, dbName, SCHEMA_VERSION);
702             mContext = context;
703             mForMigration = forMigration;
704         }
705 
initIds()706         protected void initIds() {
707             // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
708             // the DB here
709             if (mMaxItemId == -1) {
710                 mMaxItemId = initializeMaxItemId(getWritableDatabase());
711             }
712         }
713 
714         @Override
onCreate(SQLiteDatabase db)715         public void onCreate(SQLiteDatabase db) {
716             if (LOGD) Log.d(TAG, "creating new launcher database");
717 
718             mMaxItemId = 1;
719 
720             addFavoritesTable(db, false);
721 
722             // Fresh and clean launcher DB.
723             mMaxItemId = initializeMaxItemId(db);
724             if (!mForMigration) {
725                 onEmptyDbCreated();
726             }
727         }
728 
onAddOrDeleteOp(SQLiteDatabase db)729         protected void onAddOrDeleteOp(SQLiteDatabase db) {
730             if (mBackupTableExists) {
731                 dropTable(db, Favorites.BACKUP_TABLE_NAME);
732                 mBackupTableExists = false;
733             }
734             if (mHotseatRestoreTableExists) {
735                 dropTable(db, Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
736                 mHotseatRestoreTableExists = false;
737             }
738         }
739 
740         /**
741          * Re-composite given key in respect to database. If the current db is
742          * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
743          * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
744          * string will be "EMPTY_DATABASE_CREATED@minimal.db".
745          */
getKey(final String key)746         String getKey(final String key) {
747             if (TextUtils.equals(getDatabaseName(), LauncherFiles.LAUNCHER_DB)) {
748                 return key;
749             }
750             return key + "@" + getDatabaseName();
751         }
752 
753         /**
754          * Overriden in tests.
755          */
onEmptyDbCreated()756         protected void onEmptyDbCreated() {
757             // Set the flag for empty DB
758             LauncherPrefs.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true)
759                     .commit();
760         }
761 
getSerialNumberForUser(UserHandle user)762         public long getSerialNumberForUser(UserHandle user) {
763             return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
764         }
765 
getDefaultUserSerial()766         public long getDefaultUserSerial() {
767             return getSerialNumberForUser(Process.myUserHandle());
768         }
769 
addFavoritesTable(SQLiteDatabase db, boolean optional)770         private void addFavoritesTable(SQLiteDatabase db, boolean optional) {
771             Favorites.addTableToDb(db, getDefaultUserSerial(), optional);
772         }
773 
774         @Override
onOpen(SQLiteDatabase db)775         public void onOpen(SQLiteDatabase db) {
776             super.onOpen(db);
777 
778             File schemaFile = mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE);
779             if (!schemaFile.exists()) {
780                 handleOneTimeDataUpgrade(db);
781             }
782             DbDowngradeHelper.updateSchemaFile(schemaFile, SCHEMA_VERSION, mContext);
783         }
784 
785         /**
786          * One-time data updated before support of onDowngrade was added. This update is backwards
787          * compatible and can safely be run multiple times.
788          * Note: No new logic should be added here after release, as the new logic might not get
789          * executed on an existing device.
790          * TODO: Move this to db upgrade path, once the downgrade path is released.
791          */
handleOneTimeDataUpgrade(SQLiteDatabase db)792         protected void handleOneTimeDataUpgrade(SQLiteDatabase db) {
793             // Remove "profile extra"
794             UserCache um = UserCache.INSTANCE.get(mContext);
795             for (UserHandle user : um.getUserProfiles()) {
796                 long serial = um.getSerialNumberForUser(user);
797                 String sql = "update favorites set intent = replace(intent, "
798                         + "';l.profile=" + serial + ";', ';') where itemType = 0;";
799                 db.execSQL(sql);
800             }
801         }
802 
803         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)804         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
805             if (LOGD) Log.d(TAG, "onUpgrade triggered: " + oldVersion);
806             switch (oldVersion) {
807                 // The version cannot be lower that 12, as Launcher3 never supported a lower
808                 // version of the DB.
809                 case 12:
810                     // No-op
811                 case 13: {
812                     try (SQLiteTransaction t = new SQLiteTransaction(db)) {
813                         // Insert new column for holding widget provider name
814                         db.execSQL("ALTER TABLE favorites " +
815                                 "ADD COLUMN appWidgetProvider TEXT;");
816                         t.commit();
817                     } catch (SQLException ex) {
818                         Log.e(TAG, ex.getMessage(), ex);
819                         // Old version remains, which means we wipe old data
820                         break;
821                     }
822                 }
823                 case 14: {
824                     if (!addIntegerColumn(db, Favorites.MODIFIED, 0)) {
825                         // Old version remains, which means we wipe old data
826                         break;
827                     }
828                 }
829                 case 15: {
830                     if (!addIntegerColumn(db, Favorites.RESTORED, 0)) {
831                         // Old version remains, which means we wipe old data
832                         break;
833                     }
834                 }
835                 case 16:
836                     // No-op
837                 case 17:
838                     // No-op
839                 case 18:
840                     // No-op
841                 case 19: {
842                     // Add userId column
843                     if (!addIntegerColumn(db, Favorites.PROFILE_ID, getDefaultUserSerial())) {
844                         // Old version remains, which means we wipe old data
845                         break;
846                     }
847                 }
848                 case 20:
849                     if (!updateFolderItemsRank(db, true)) {
850                         break;
851                     }
852                 case 21:
853                     // No-op
854                 case 22: {
855                     if (!addIntegerColumn(db, Favorites.OPTIONS, 0)) {
856                         // Old version remains, which means we wipe old data
857                         break;
858                     }
859                 }
860                 case 23:
861                     // No-op
862                 case 24:
863                     // No-op
864                 case 25:
865                     convertShortcutsToLauncherActivities(db);
866                 case 26:
867                     // QSB was moved to the grid. Ignore overlapping items
868                 case 27: {
869                     // Update the favorites table so that the screen ids are ordered based on
870                     // workspace page rank.
871                     IntArray finalScreens = LauncherDbUtils.queryIntArray(false, db,
872                             "workspaceScreens", BaseColumns._ID, null, null, "screenRank");
873                     int[] original = finalScreens.toArray();
874                     Arrays.sort(original);
875                     String updatemap = "";
876                     for (int i = 0; i < original.length; i++) {
877                         if (finalScreens.get(i) != original[i]) {
878                             updatemap += String.format(Locale.ENGLISH, " WHEN %1$s=%2$d THEN %3$d",
879                                     Favorites.SCREEN, finalScreens.get(i), original[i]);
880                         }
881                     }
882                     if (!TextUtils.isEmpty(updatemap)) {
883                         String query = String.format(Locale.ENGLISH,
884                                 "UPDATE %1$s SET %2$s=CASE %3$s ELSE %2$s END WHERE %4$s = %5$d",
885                                 Favorites.TABLE_NAME, Favorites.SCREEN, updatemap,
886                                 Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP);
887                         db.execSQL(query);
888                     }
889                     dropTable(db, "workspaceScreens");
890                 }
891                 case 28: {
892                     boolean columnAdded = addIntegerColumn(
893                             db, Favorites.APPWIDGET_SOURCE, Favorites.CONTAINER_UNKNOWN);
894                     if (!columnAdded) {
895                         // Old version remains, which means we wipe old data
896                         break;
897                     }
898                 }
899                 case 29: {
900                     // Remove widget panel related leftover workspace items
901                     db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
902                             Favorites.SCREEN, IntArray.wrap(-777, -778)), null);
903                 }
904                 case 30: {
905                     if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
906                         // Clean up first row in screen 0 as it might contain junk data.
907                         Log.d(TAG, "Cleaning up first row");
908                         db.delete(Favorites.TABLE_NAME,
909                                 String.format(Locale.ENGLISH,
910                                         "%1$s = %2$d AND %3$s = %4$d AND %5$s = %6$d",
911                                         Favorites.SCREEN, 0,
912                                         Favorites.CONTAINER, Favorites.CONTAINER_DESKTOP,
913                                         Favorites.CELLY, 0), null);
914                     }
915                     return;
916                 }
917                 case 31: {
918                     // DB Upgraded successfully
919                     return;
920                 }
921             }
922 
923             // DB was not upgraded
924             Log.w(TAG, "Destroying all old data.");
925             createEmptyDB(db);
926         }
927 
928         @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)929         public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
930             try {
931                 DbDowngradeHelper.parse(mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE))
932                         .onDowngrade(db, oldVersion, newVersion);
933             } catch (Exception e) {
934                 Log.d(TAG, "Unable to downgrade from: " + oldVersion + " to " + newVersion +
935                         ". Wiping databse.", e);
936                 createEmptyDB(db);
937             }
938         }
939 
940         /**
941          * Clears all the data for a fresh start.
942          */
createEmptyDB(SQLiteDatabase db)943         public void createEmptyDB(SQLiteDatabase db) {
944             try (SQLiteTransaction t = new SQLiteTransaction(db)) {
945                 dropTable(db, Favorites.TABLE_NAME);
946                 dropTable(db, "workspaceScreens");
947                 onCreate(db);
948                 t.commit();
949             }
950         }
951 
952         /**
953          * Removes widgets which are registered to the Launcher's host, but are not present
954          * in our model.
955          */
removeGhostWidgets(SQLiteDatabase db)956         public void removeGhostWidgets(SQLiteDatabase db) {
957             // Get all existing widget ids.
958             final LauncherWidgetHolder holder = newLauncherWidgetHolder();
959             try {
960                 final int[] allWidgets;
961                 try {
962                     // Although the method was defined in O, it has existed since the beginning of
963                     // time, so it might work on older platforms as well.
964                     allWidgets = holder.getAppWidgetIds();
965                 } catch (IncompatibleClassChangeError e) {
966                     Log.e(TAG, "getAppWidgetIds not supported", e);
967                     return;
968                 }
969                 final IntSet validWidgets = IntSet.wrap(LauncherDbUtils.queryIntArray(false, db,
970                         Favorites.TABLE_NAME, Favorites.APPWIDGET_ID,
971                         "itemType=" + Favorites.ITEM_TYPE_APPWIDGET, null, null));
972                 boolean isAnyWidgetRemoved = false;
973                 for (int widgetId : allWidgets) {
974                     if (!validWidgets.contains(widgetId)) {
975                         try {
976                             FileLog.d(TAG, "Deleting invalid widget " + widgetId);
977                             holder.deleteAppWidgetId(widgetId);
978                             isAnyWidgetRemoved = true;
979                         } catch (RuntimeException e) {
980                             // Ignore
981                         }
982                     }
983                 }
984                 if (isAnyWidgetRemoved) {
985                     final String allWidgetsIds = Arrays.stream(allWidgets).mapToObj(String::valueOf)
986                             .collect(Collectors.joining(",", "[", "]"));
987                     final String validWidgetsIds = Arrays.stream(
988                                     validWidgets.getArray().toArray()).mapToObj(String::valueOf)
989                             .collect(Collectors.joining(",", "[", "]"));
990                     FileLog.d(TAG, "One or more widgets was removed. db_path=" + db.getPath()
991                             + " allWidgetsIds=" + allWidgetsIds
992                             + ", validWidgetsIds=" + validWidgetsIds);
993                 }
994             } finally {
995                 holder.destroy();
996             }
997         }
998 
999         /**
1000          * Replaces all shortcuts of type {@link Favorites#ITEM_TYPE_SHORTCUT} which have a valid
1001          * launcher activity target with {@link Favorites#ITEM_TYPE_APPLICATION}.
1002          */
convertShortcutsToLauncherActivities(SQLiteDatabase db)1003         @Thunk void convertShortcutsToLauncherActivities(SQLiteDatabase db) {
1004             try (SQLiteTransaction t = new SQLiteTransaction(db);
1005                  // Only consider the primary user as other users can't have a shortcut.
1006                  Cursor c = db.query(Favorites.TABLE_NAME,
1007                          new String[] { Favorites._ID, Favorites.INTENT},
1008                          "itemType=" + Favorites.ITEM_TYPE_SHORTCUT +
1009                                  " AND profileId=" + getDefaultUserSerial(),
1010                          null, null, null, null);
1011                  SQLiteStatement updateStmt = db.compileStatement("UPDATE favorites SET itemType="
1012                          + Favorites.ITEM_TYPE_APPLICATION + " WHERE _id=?")
1013             ) {
1014                 final int idIndex = c.getColumnIndexOrThrow(Favorites._ID);
1015                 final int intentIndex = c.getColumnIndexOrThrow(Favorites.INTENT);
1016 
1017                 while (c.moveToNext()) {
1018                     String intentDescription = c.getString(intentIndex);
1019                     Intent intent;
1020                     try {
1021                         intent = Intent.parseUri(intentDescription, 0);
1022                     } catch (URISyntaxException e) {
1023                         Log.e(TAG, "Unable to parse intent", e);
1024                         continue;
1025                     }
1026 
1027                     if (!PackageManagerHelper.isLauncherAppTarget(intent)) {
1028                         continue;
1029                     }
1030 
1031                     int id = c.getInt(idIndex);
1032                     updateStmt.bindLong(1, id);
1033                     updateStmt.executeUpdateDelete();
1034                 }
1035                 t.commit();
1036             } catch (SQLException ex) {
1037                 Log.w(TAG, "Error deduping shortcuts", ex);
1038             }
1039         }
1040 
updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn)1041         @Thunk boolean updateFolderItemsRank(SQLiteDatabase db, boolean addRankColumn) {
1042             try (SQLiteTransaction t = new SQLiteTransaction(db)) {
1043                 if (addRankColumn) {
1044                     // Insert new column for holding rank
1045                     db.execSQL("ALTER TABLE favorites ADD COLUMN rank INTEGER NOT NULL DEFAULT 0;");
1046                 }
1047 
1048                 // Get a map for folder ID to folder width
1049                 Cursor c = db.rawQuery("SELECT container, MAX(cellX) FROM favorites"
1050                         + " WHERE container IN (SELECT _id FROM favorites WHERE itemType = ?)"
1051                         + " GROUP BY container;",
1052                         new String[] {Integer.toString(LauncherSettings.Favorites.ITEM_TYPE_FOLDER)});
1053 
1054                 while (c.moveToNext()) {
1055                     db.execSQL("UPDATE favorites SET rank=cellX+(cellY*?) WHERE "
1056                             + "container=? AND cellX IS NOT NULL AND cellY IS NOT NULL;",
1057                             new Object[] {c.getLong(1) + 1, c.getLong(0)});
1058                 }
1059 
1060                 c.close();
1061                 t.commit();
1062             } catch (SQLException ex) {
1063                 // Old version remains, which means we wipe old data
1064                 Log.e(TAG, ex.getMessage(), ex);
1065                 return false;
1066             }
1067             return true;
1068         }
1069 
addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue)1070         private boolean addIntegerColumn(SQLiteDatabase db, String columnName, long defaultValue) {
1071             try (SQLiteTransaction t = new SQLiteTransaction(db)) {
1072                 db.execSQL("ALTER TABLE favorites ADD COLUMN "
1073                         + columnName + " INTEGER NOT NULL DEFAULT " + defaultValue + ";");
1074                 t.commit();
1075             } catch (SQLException ex) {
1076                 Log.e(TAG, ex.getMessage(), ex);
1077                 return false;
1078             }
1079             return true;
1080         }
1081 
1082         // Generates a new ID to use for an object in your database. This method should be only
1083         // called from the main UI thread. As an exception, we do call it when we call the
1084         // constructor from the worker thread; however, this doesn't extend until after the
1085         // constructor is called, and we only pass a reference to LauncherProvider to LauncherApp
1086         // after that point
1087         @Override
generateNewItemId()1088         public int generateNewItemId() {
1089             if (mMaxItemId < 0) {
1090                 throw new RuntimeException("Error: max item id was not initialized");
1091             }
1092             mMaxItemId += 1;
1093             return mMaxItemId;
1094         }
1095 
1096         /**
1097          * @return A new {@link LauncherWidgetHolder} based on the current context
1098          */
1099         @NonNull
newLauncherWidgetHolder()1100         public LauncherWidgetHolder newLauncherWidgetHolder() {
1101             return LauncherWidgetHolder.newInstance(mContext);
1102         }
1103 
1104         @Override
insertAndCheck(SQLiteDatabase db, ContentValues values)1105         public int insertAndCheck(SQLiteDatabase db, ContentValues values) {
1106             return dbInsertAndCheck(this, db, Favorites.TABLE_NAME, null, values);
1107         }
1108 
checkId(ContentValues values)1109         public void checkId(ContentValues values) {
1110             int id = values.getAsInteger(Favorites._ID);
1111             mMaxItemId = Math.max(id, mMaxItemId);
1112         }
1113 
initializeMaxItemId(SQLiteDatabase db)1114         private int initializeMaxItemId(SQLiteDatabase db) {
1115             return getMaxId(db, "SELECT MAX(%1$s) FROM %2$s", Favorites._ID, Favorites.TABLE_NAME);
1116         }
1117 
1118         // Returns a new ID to use for an workspace screen in your database that is greater than all
1119         // existing screen IDs.
getNewScreenId()1120         private int getNewScreenId() {
1121             return getMaxId(getWritableDatabase(),
1122                     "SELECT MAX(%1$s) FROM %2$s WHERE %3$s = %4$d AND %1$s >= 0",
1123                     Favorites.SCREEN, Favorites.TABLE_NAME, Favorites.CONTAINER,
1124                     Favorites.CONTAINER_DESKTOP) + 1;
1125         }
1126 
loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader)1127         @Thunk int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
1128             // TODO: Use multiple loaders with fall-back and transaction.
1129             int count = loader.loadLayout(db, new IntArray());
1130 
1131             // Ensure that the max ids are initialized
1132             mMaxItemId = initializeMaxItemId(db);
1133             return count;
1134         }
1135     }
1136 
1137     /**
1138      * @return the max _id in the provided table.
1139      */
getMaxId(SQLiteDatabase db, String query, Object... args)1140     @Thunk static int getMaxId(SQLiteDatabase db, String query, Object... args) {
1141         int max = 0;
1142         try (SQLiteStatement prog = db.compileStatement(
1143                 String.format(Locale.ENGLISH, query, args))) {
1144             max = (int) DatabaseUtils.longForQuery(prog, null);
1145             if (max < 0) {
1146                 throw new RuntimeException("Error: could not query max id");
1147             }
1148         } catch (IllegalArgumentException exception) {
1149             String message = exception.getMessage();
1150             if (message.contains("re-open") && message.contains("already-closed")) {
1151                 // Don't crash trying to end a transaction an an already closed DB. See b/173162852.
1152             } else {
1153                 throw exception;
1154             }
1155         }
1156         return max;
1157     }
1158 
1159     static class SqlArguments {
1160         public final String table;
1161         public final String where;
1162         public final String[] args;
1163 
SqlArguments(Uri url, String where, String[] args)1164         SqlArguments(Uri url, String where, String[] args) {
1165             if (url.getPathSegments().size() == 1) {
1166                 this.table = url.getPathSegments().get(0);
1167                 this.where = where;
1168                 this.args = args;
1169             } else if (url.getPathSegments().size() != 2) {
1170                 throw new IllegalArgumentException("Invalid URI: " + url);
1171             } else if (!TextUtils.isEmpty(where)) {
1172                 throw new UnsupportedOperationException("WHERE clause not supported: " + url);
1173             } else {
1174                 this.table = url.getPathSegments().get(0);
1175                 this.where = "_id=" + ContentUris.parseId(url);
1176                 this.args = null;
1177             }
1178         }
1179 
SqlArguments(Uri url)1180         SqlArguments(Uri url) {
1181             if (url.getPathSegments().size() == 1) {
1182                 table = url.getPathSegments().get(0);
1183                 where = null;
1184                 args = null;
1185             } else {
1186                 throw new IllegalArgumentException("Invalid URI: " + url);
1187             }
1188         }
1189     }
1190 }
1191