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