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