• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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 package com.android.launcher3.model;
17 
18 import static android.util.Base64.NO_PADDING;
19 import static android.util.Base64.NO_WRAP;
20 
21 import static com.android.launcher3.DefaultLayoutParser.RES_PARTNER_DEFAULT_LAYOUT;
22 import static com.android.launcher3.LauncherSettings.Favorites.addTableToDb;
23 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_KEY;
24 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_LABEL;
25 import static com.android.launcher3.LauncherSettings.Settings.LAYOUT_DIGEST_TAG;
26 import static com.android.launcher3.provider.LauncherDbUtils.tableExists;
27 
28 import android.app.blob.BlobHandle;
29 import android.app.blob.BlobStoreManager;
30 import android.content.ContentResolver;
31 import android.content.ContentValues;
32 import android.content.Context;
33 import android.content.pm.PackageManager;
34 import android.content.pm.ProviderInfo;
35 import android.content.res.Resources;
36 import android.database.Cursor;
37 import android.database.SQLException;
38 import android.database.sqlite.SQLiteDatabase;
39 import android.net.Uri;
40 import android.os.Bundle;
41 import android.os.ParcelFileDescriptor;
42 import android.os.Process;
43 import android.os.UserHandle;
44 import android.os.UserManager;
45 import android.provider.Settings;
46 import android.text.TextUtils;
47 import android.util.Base64;
48 import android.util.Log;
49 import android.util.Xml;
50 
51 import androidx.annotation.WorkerThread;
52 
53 import com.android.launcher3.AutoInstallsLayout;
54 import com.android.launcher3.AutoInstallsLayout.SourceResources;
55 import com.android.launcher3.ConstantItem;
56 import com.android.launcher3.DefaultLayoutParser;
57 import com.android.launcher3.InvariantDeviceProfile;
58 import com.android.launcher3.LauncherAppState;
59 import com.android.launcher3.LauncherFiles;
60 import com.android.launcher3.LauncherPrefs;
61 import com.android.launcher3.LauncherSettings;
62 import com.android.launcher3.LauncherSettings.Favorites;
63 import com.android.launcher3.Utilities;
64 import com.android.launcher3.pm.UserCache;
65 import com.android.launcher3.provider.LauncherDbUtils;
66 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
67 import com.android.launcher3.provider.RestoreDbTask;
68 import com.android.launcher3.util.IOUtils;
69 import com.android.launcher3.util.IntArray;
70 import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
71 import com.android.launcher3.util.Partner;
72 import com.android.launcher3.widget.LauncherWidgetHolder;
73 
74 import org.xmlpull.v1.XmlPullParser;
75 
76 import java.io.InputStream;
77 import java.io.StringReader;
78 
79 /**
80  * Utility class which maintains an instance of Launcher database and provides utility methods
81  * around it.
82  */
83 public class ModelDbController {
84     private static final String TAG = "LauncherProvider";
85 
86     private static final String EMPTY_DATABASE_CREATED = "EMPTY_DATABASE_CREATED";
87     public static final String EXTRA_DB_NAME = "db_name";
88 
89     protected DatabaseHelper mOpenHelper;
90 
91     private final Context mContext;
92 
ModelDbController(Context context)93     public ModelDbController(Context context) {
94         mContext = context;
95     }
96 
createDbIfNotExists()97     private synchronized void createDbIfNotExists() {
98         if (mOpenHelper == null) {
99             mOpenHelper = createDatabaseHelper(false /* forMigration */);
100             RestoreDbTask.restoreIfNeeded(mContext, this);
101         }
102     }
103 
createDatabaseHelper(boolean forMigration)104     protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
105         boolean isSandbox = mContext instanceof SandboxContext;
106         String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
107 
108         // Set the flag for empty DB
109         Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
110                 : () -> LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey(dbName).to(true));
111 
112         DatabaseHelper databaseHelper = new DatabaseHelper(mContext, dbName,
113                 this::getSerialNumberForUser, onEmptyDbCreateCallback);
114         // Table creation sometimes fails silently, which leads to a crash loop.
115         // This way, we will try to create a table every time after crash, so the device
116         // would eventually be able to recover.
117         if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
118             Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
119             // This operation is a no-op if the table already exists.
120             addTableToDb(databaseHelper.getWritableDatabase(),
121                     getSerialNumberForUser(Process.myUserHandle()),
122                     true /* optional */);
123         }
124         databaseHelper.mHotseatRestoreTableExists = tableExists(
125                 databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
126 
127         databaseHelper.initIds();
128         return databaseHelper;
129     }
130 
131     /**
132      * Refer {@link SQLiteDatabase#query}
133      */
134     @WorkerThread
query(String table, String[] projection, String selection, String[] selectionArgs, String sortOrder)135     public Cursor query(String table, String[] projection, String selection,
136             String[] selectionArgs, String sortOrder) {
137         createDbIfNotExists();
138         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
139         Cursor result = db.query(
140                 table, projection, selection, selectionArgs, null, null, sortOrder);
141 
142         final Bundle extra = new Bundle();
143         extra.putString(EXTRA_DB_NAME, mOpenHelper.getDatabaseName());
144         result.setExtras(extra);
145         return result;
146     }
147 
148     /**
149      * Refer {@link SQLiteDatabase#insert(String, String, ContentValues)}
150      */
151     @WorkerThread
insert(String table, ContentValues initialValues)152     public int insert(String table, ContentValues initialValues) {
153         createDbIfNotExists();
154 
155         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
156         addModifiedTime(initialValues);
157         int rowId = mOpenHelper.dbInsertAndCheck(db, table, initialValues);
158         if (rowId >= 0) {
159             onAddOrDeleteOp(db);
160         }
161         return rowId;
162     }
163 
164     /**
165      * Refer {@link SQLiteDatabase#delete(String, String, String[])}
166      */
167     @WorkerThread
delete(String table, String selection, String[] selectionArgs)168     public int delete(String table, String selection, String[] selectionArgs) {
169         createDbIfNotExists();
170         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
171 
172         int count = db.delete(table, selection, selectionArgs);
173         if (count > 0) {
174             onAddOrDeleteOp(db);
175         }
176         return count;
177     }
178 
179     /**
180      * Refer {@link SQLiteDatabase#update(String, ContentValues, String, String[])}
181      */
182     @WorkerThread
update(String table, ContentValues values, String selection, String[] selectionArgs)183     public int update(String table, ContentValues values,
184             String selection, String[] selectionArgs) {
185         createDbIfNotExists();
186 
187         addModifiedTime(values);
188         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
189         int count = db.update(table, values, selection, selectionArgs);
190         return count;
191     }
192 
193     /**
194      * Clears a previously set flag corresponding to empty db creation
195      */
196     @WorkerThread
clearEmptyDbFlag()197     public void clearEmptyDbFlag() {
198         createDbIfNotExists();
199         clearFlagEmptyDbCreated();
200     }
201 
202     /**
203      * Generates an id to be used for new item in the favorites table
204      */
205     @WorkerThread
generateNewItemId()206     public int generateNewItemId() {
207         createDbIfNotExists();
208         return mOpenHelper.generateNewItemId();
209     }
210 
211     /**
212      * Generates an id to be used for new workspace screen
213      */
214     @WorkerThread
getNewScreenId()215     public int getNewScreenId() {
216         createDbIfNotExists();
217         return mOpenHelper.getNewScreenId();
218     }
219 
220     /**
221      * Creates an empty DB clearing all existing data
222      */
223     @WorkerThread
createEmptyDB()224     public void createEmptyDB() {
225         createDbIfNotExists();
226         mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
227         LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey().to(true));
228     }
229 
230     /**
231      * Removes any widget which are present in the framework, but not in out internal DB
232      */
233     @WorkerThread
removeGhostWidgets()234     public void removeGhostWidgets() {
235         createDbIfNotExists();
236         mOpenHelper.removeGhostWidgets(mOpenHelper.getWritableDatabase());
237     }
238 
239     /**
240      * Returns a new {@link SQLiteTransaction}
241      */
242     @WorkerThread
newTransaction()243     public SQLiteTransaction newTransaction() {
244         createDbIfNotExists();
245         return new SQLiteTransaction(mOpenHelper.getWritableDatabase());
246     }
247 
248     /**
249      * Refreshes the internal state corresponding to presence of hotseat table
250      */
251     @WorkerThread
refreshHotseatRestoreTable()252     public void refreshHotseatRestoreTable() {
253         createDbIfNotExists();
254         mOpenHelper.mHotseatRestoreTableExists = tableExists(
255                 mOpenHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
256     }
257 
258 
259     /**
260      * Migrates the DB if needed. If the migration failed, it clears the DB.
261      */
tryMigrateDB()262     public void tryMigrateDB() {
263         if (!migrateGridIfNeeded()) {
264             Log.d(TAG, "Migration failed: resetting launcher database");
265             createEmptyDB();
266             LauncherPrefs.get(mContext).putSync(
267                     getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()).to(true));
268 
269             // Write the grid state to avoid another migration
270             new DeviceGridState(LauncherAppState.getIDP(mContext)).writeToPrefs(mContext);
271         }
272     }
273 
274     /**
275      * Migrates the DB if needed, and returns false if the migration failed
276      * and DB needs to be cleared.
277      * @return true if migration was success or ignored, false if migration failed
278      * and the DB should be reset.
279      */
migrateGridIfNeeded()280     private boolean migrateGridIfNeeded() {
281         createDbIfNotExists();
282         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
283             // If we have already create a new DB, ignore migration
284             return false;
285         }
286         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
287         if (!GridSizeMigrationUtil.needsToMigrate(mContext, idp)) {
288             return true;
289         }
290         String targetDbName = new DeviceGridState(idp).getDbFile();
291         if (TextUtils.equals(targetDbName, mOpenHelper.getDatabaseName())) {
292             Log.e(TAG, "migrateGridIfNeeded - target db is same as current: " + targetDbName);
293             return false;
294         }
295         DatabaseHelper oldHelper = mOpenHelper;
296         mOpenHelper = (mContext instanceof SandboxContext) ? oldHelper
297                 : createDatabaseHelper(true /* forMigration */);
298         try {
299             return GridSizeMigrationUtil.migrateGridIfNeeded(mContext, idp, mOpenHelper,
300                    oldHelper.getWritableDatabase());
301         } finally {
302             if (mOpenHelper != oldHelper) {
303                 oldHelper.close();
304             }
305         }
306     }
307 
308     /**
309      * Returns the underlying model database
310      */
getDb()311     public SQLiteDatabase getDb() {
312         createDbIfNotExists();
313         return mOpenHelper.getWritableDatabase();
314     }
315 
onAddOrDeleteOp(SQLiteDatabase db)316     private void onAddOrDeleteOp(SQLiteDatabase db) {
317         mOpenHelper.onAddOrDeleteOp(db);
318     }
319 
320     /**
321      * Deletes any empty folder from the DB.
322      * @return Ids of deleted folders.
323      */
324     @WorkerThread
deleteEmptyFolders()325     public IntArray deleteEmptyFolders() {
326         createDbIfNotExists();
327 
328         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
329         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
330             // Select folders whose id do not match any container value.
331             String selection = LauncherSettings.Favorites.ITEM_TYPE + " = "
332                     + LauncherSettings.Favorites.ITEM_TYPE_FOLDER + " AND "
333                     + LauncherSettings.Favorites._ID +  " NOT IN (SELECT "
334                     + LauncherSettings.Favorites.CONTAINER + " FROM "
335                     + Favorites.TABLE_NAME + ")";
336 
337             IntArray folderIds = LauncherDbUtils.queryIntArray(false, db, Favorites.TABLE_NAME,
338                     Favorites._ID, selection, null, null);
339             if (!folderIds.isEmpty()) {
340                 db.delete(Favorites.TABLE_NAME, Utilities.createDbSelectionQuery(
341                         LauncherSettings.Favorites._ID, folderIds), null);
342             }
343             t.commit();
344             return folderIds;
345         } catch (SQLException ex) {
346             Log.e(TAG, ex.getMessage(), ex);
347             return new IntArray();
348         }
349     }
350 
addModifiedTime(ContentValues values)351     private static void addModifiedTime(ContentValues values) {
352         values.put(LauncherSettings.Favorites.MODIFIED, System.currentTimeMillis());
353     }
354 
clearFlagEmptyDbCreated()355     private void clearFlagEmptyDbCreated() {
356         LauncherPrefs.get(mContext).removeSync(getEmptyDbCreatedKey());
357     }
358 
359     /**
360      * Loads the default workspace based on the following priority scheme:
361      *   1) From the app restrictions
362      *   2) From a package provided by play store
363      *   3) From a partner configuration APK, already in the system image
364      *   4) The default configuration for the particular device
365      */
366     @WorkerThread
loadDefaultFavoritesIfNecessary()367     public synchronized void loadDefaultFavoritesIfNecessary() {
368         createDbIfNotExists();
369 
370         if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey())) {
371             Log.d(TAG, "loading default workspace");
372 
373             LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
374             try {
375                 AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder);
376                 if (loader == null) {
377                     loader = AutoInstallsLayout.get(mContext, widgetHolder, mOpenHelper);
378                 }
379                 if (loader == null) {
380                     final Partner partner = Partner.get(mContext.getPackageManager());
381                     if (partner != null) {
382                         int workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT);
383                         if (workspaceResId != 0) {
384                             loader = new DefaultLayoutParser(mContext, widgetHolder,
385                                     mOpenHelper, partner.getResources(), workspaceResId);
386                         }
387                     }
388                 }
389 
390                 final boolean usingExternallyProvidedLayout = loader != null;
391                 if (loader == null) {
392                     loader = getDefaultLayoutParser(widgetHolder);
393                 }
394 
395                 // There might be some partially restored DB items, due to buggy restore logic in
396                 // previous versions of launcher.
397                 mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
398                 // Populate favorites table with initial favorites
399                 if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
400                         && usingExternallyProvidedLayout) {
401                     // Unable to load external layout. Cleanup and load the internal layout.
402                     mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
403                     mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
404                             getDefaultLayoutParser(widgetHolder));
405                 }
406                 clearFlagEmptyDbCreated();
407             } finally {
408                 widgetHolder.destroy();
409             }
410         }
411     }
412 
413     /**
414      * Creates workspace loader from an XML resource listed in the app restrictions.
415      *
416      * @return the loader if the restrictions are set and the resource exists; null otherwise.
417      */
createWorkspaceLoaderFromAppRestriction( LauncherWidgetHolder widgetHolder)418     private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction(
419             LauncherWidgetHolder widgetHolder) {
420         ContentResolver cr = mContext.getContentResolver();
421         String blobHandlerDigest = Settings.Secure.getString(cr, LAYOUT_DIGEST_KEY);
422         if (Utilities.ATLEAST_R && !TextUtils.isEmpty(blobHandlerDigest)) {
423             BlobStoreManager blobManager = mContext.getSystemService(BlobStoreManager.class);
424             try (InputStream in = new ParcelFileDescriptor.AutoCloseInputStream(
425                     blobManager.openBlob(BlobHandle.createWithSha256(
426                             Base64.decode(blobHandlerDigest, NO_WRAP | NO_PADDING),
427                             LAYOUT_DIGEST_LABEL, 0, LAYOUT_DIGEST_TAG)))) {
428                 return getAutoInstallsLayoutFromIS(in, widgetHolder, new SourceResources() { });
429             } catch (Exception e) {
430                 Log.e(TAG, "Error getting layout from blob handle" , e);
431                 return null;
432             }
433         }
434 
435         String authority = Settings.Secure.getString(cr, "launcher3.layout.provider");
436         if (TextUtils.isEmpty(authority)) {
437             return null;
438         }
439 
440         PackageManager pm = mContext.getPackageManager();
441         ProviderInfo pi = pm.resolveContentProvider(authority, 0);
442         if (pi == null) {
443             Log.e(TAG, "No provider found for authority " + authority);
444             return null;
445         }
446         Uri uri = getLayoutUri(authority, mContext);
447         try (InputStream in = cr.openInputStream(uri)) {
448             Log.d(TAG, "Loading layout from " + authority);
449 
450             Resources res = pm.getResourcesForApplication(pi.applicationInfo);
451             return getAutoInstallsLayoutFromIS(in, widgetHolder, SourceResources.wrap(res));
452         } catch (Exception e) {
453             Log.e(TAG, "Error getting layout stream from: " + authority , e);
454             return null;
455         }
456     }
457 
getAutoInstallsLayoutFromIS(InputStream in, LauncherWidgetHolder widgetHolder, SourceResources res)458     private AutoInstallsLayout getAutoInstallsLayoutFromIS(InputStream in,
459             LauncherWidgetHolder widgetHolder, SourceResources res) throws Exception {
460         // Read the full xml so that we fail early in case of any IO error.
461         String layout = new String(IOUtils.toByteArray(in));
462         XmlPullParser parser = Xml.newPullParser();
463         parser.setInput(new StringReader(layout));
464 
465         return new AutoInstallsLayout(mContext, widgetHolder, mOpenHelper, res,
466                 () -> parser, AutoInstallsLayout.TAG_WORKSPACE);
467     }
468 
getLayoutUri(String authority, Context ctx)469     public static Uri getLayoutUri(String authority, Context ctx) {
470         InvariantDeviceProfile grid = LauncherAppState.getIDP(ctx);
471         return new Uri.Builder().scheme("content").authority(authority).path("launcher_layout")
472                 .appendQueryParameter("version", "1")
473                 .appendQueryParameter("gridWidth", Integer.toString(grid.numColumns))
474                 .appendQueryParameter("gridHeight", Integer.toString(grid.numRows))
475                 .appendQueryParameter("hotseatSize", Integer.toString(grid.numDatabaseHotseatIcons))
476                 .build();
477     }
478 
getDefaultLayoutParser(LauncherWidgetHolder widgetHolder)479     private DefaultLayoutParser getDefaultLayoutParser(LauncherWidgetHolder widgetHolder) {
480         InvariantDeviceProfile idp = LauncherAppState.getIDP(mContext);
481         int defaultLayout = idp.demoModeLayoutId != 0
482                 && mContext.getSystemService(UserManager.class).isDemoUser()
483                 ? idp.demoModeLayoutId : idp.defaultLayoutId;
484 
485         return new DefaultLayoutParser(mContext, widgetHolder,
486                 mOpenHelper, mContext.getResources(), defaultLayout);
487     }
488 
getEmptyDbCreatedKey()489     private ConstantItem<Boolean> getEmptyDbCreatedKey() {
490         return getEmptyDbCreatedKey(mOpenHelper.getDatabaseName());
491     }
492 
493     /**
494      * Re-composite given key in respect to database. If the current db is
495      * {@link LauncherFiles#LAUNCHER_DB}, return the key as-is. Otherwise append the db name to
496      * given key. e.g. consider key="EMPTY_DATABASE_CREATED", dbName="minimal.db", the returning
497      * string will be "EMPTY_DATABASE_CREATED@minimal.db".
498      */
getEmptyDbCreatedKey(String dbName)499     private ConstantItem<Boolean> getEmptyDbCreatedKey(String dbName) {
500         if (mContext instanceof SandboxContext) {
501             return LauncherPrefs.nonRestorableItem(EMPTY_DATABASE_CREATED,
502                     false /* default value */, false /* boot aware */);
503         }
504         String key = TextUtils.equals(dbName, LauncherFiles.LAUNCHER_DB)
505                 ? EMPTY_DATABASE_CREATED : EMPTY_DATABASE_CREATED + "@" + dbName;
506         return LauncherPrefs.backedUpItem(key, false /* default value */, false /* boot aware */);
507     }
508 
509     /**
510      * Returns the serial number for the provided user
511      */
getSerialNumberForUser(UserHandle user)512     public long getSerialNumberForUser(UserHandle user) {
513         return UserCache.INSTANCE.get(mContext).getSerialNumberForUser(user);
514     }
515 }
516