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