1 /* 2 * Copyright (C) 2018 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 com.android.launcher3.LauncherSettings.Favorites.BACKUP_TABLE_NAME; 19 import static com.android.launcher3.provider.LauncherDbUtils.dropTable; 20 import static com.android.launcher3.provider.LauncherDbUtils.tableExists; 21 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.database.Cursor; 25 import android.database.sqlite.SQLiteDatabase; 26 import android.graphics.Point; 27 import android.os.Process; 28 import android.util.Log; 29 30 import androidx.annotation.IntDef; 31 32 import com.android.launcher3.LauncherSettings.Favorites; 33 import com.android.launcher3.LauncherSettings.Settings; 34 import com.android.launcher3.pm.UserCache; 35 36 /** 37 * Helper class to backup and restore Favorites table into a separate table 38 * within the same data base. 39 */ 40 public class GridBackupTable { 41 private static final String TAG = "GridBackupTable"; 42 43 private static final int ID_PROPERTY = -1; 44 45 private static final String KEY_HOTSEAT_SIZE = Favorites.SCREEN; 46 private static final String KEY_GRID_X_SIZE = Favorites.SPANX; 47 private static final String KEY_GRID_Y_SIZE = Favorites.SPANY; 48 private static final String KEY_DB_VERSION = Favorites.RANK; 49 50 public static final int OPTION_REQUIRES_SANITIZATION = 1; 51 52 /** STATE_NOT_FOUND indicates backup doesn't exist in the db. */ 53 private static final int STATE_NOT_FOUND = 0; 54 /** 55 * STATE_RAW indicates the backup has not yet been sanitized. This implies it might still 56 * posses app info that doesn't exist in the workspace and needed to be sanitized before 57 * put into use. 58 */ 59 private static final int STATE_RAW = 1; 60 /** STATE_SANITIZED indicates the backup has already been sanitized, thus can be used as-is. */ 61 private static final int STATE_SANITIZED = 2; 62 63 private final Context mContext; 64 private final SQLiteDatabase mDb; 65 66 private final int mOldHotseatSize; 67 private final int mOldGridX; 68 private final int mOldGridY; 69 70 private int mRestoredHotseatSize; 71 private int mRestoredGridX; 72 private int mRestoredGridY; 73 74 @IntDef({STATE_NOT_FOUND, STATE_RAW, STATE_SANITIZED}) 75 private @interface BackupState { } 76 GridBackupTable(Context context, SQLiteDatabase db, int hotseatSize, int gridX, int gridY)77 public GridBackupTable(Context context, SQLiteDatabase db, int hotseatSize, int gridX, 78 int gridY) { 79 mContext = context; 80 mDb = db; 81 82 mOldHotseatSize = hotseatSize; 83 mOldGridX = gridX; 84 mOldGridY = gridY; 85 } 86 87 /** 88 * Create a backup from current workspace layout if one isn't created already (Note backup 89 * created this way is always sanitized). Otherwise restore from the backup instead. 90 */ backupOrRestoreAsNeeded()91 public boolean backupOrRestoreAsNeeded() { 92 // Check if backup table exists 93 if (!tableExists(mDb, BACKUP_TABLE_NAME)) { 94 if (Settings.call(mContext.getContentResolver(), Settings.METHOD_WAS_EMPTY_DB_CREATED) 95 .getBoolean(Settings.EXTRA_VALUE, false)) { 96 // No need to copy if empty DB was created. 97 return false; 98 } 99 doBackup(UserCache.INSTANCE.get(mContext).getSerialNumberForUser( 100 Process.myUserHandle()), 0); 101 return false; 102 } 103 return restoreIfBackupExists(Favorites.TABLE_NAME); 104 } 105 restoreToPreviewIfBackupExists()106 public boolean restoreToPreviewIfBackupExists() { 107 if (!tableExists(mDb, BACKUP_TABLE_NAME)) { 108 return false; 109 } 110 111 return restoreIfBackupExists(Favorites.PREVIEW_TABLE_NAME); 112 } 113 restoreIfBackupExists(String toTableName)114 private boolean restoreIfBackupExists(String toTableName) { 115 if (loadDBProperties() != STATE_SANITIZED) { 116 return false; 117 } 118 long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser( 119 Process.myUserHandle()); 120 copyTable(mDb, BACKUP_TABLE_NAME, toTableName, userSerial); 121 Log.d(TAG, "Backup table found"); 122 return true; 123 } 124 getRestoreHotseatAndGridSize(Point outGridSize)125 public int getRestoreHotseatAndGridSize(Point outGridSize) { 126 outGridSize.set(mRestoredGridX, mRestoredGridY); 127 return mRestoredHotseatSize; 128 } 129 130 /** 131 * Creates a new table and populates with copy of Favorites.TABLE_NAME 132 */ createCustomBackupTable(String tableName)133 public void createCustomBackupTable(String tableName) { 134 long profileId = UserCache.INSTANCE.get(mContext).getSerialNumberForUser( 135 Process.myUserHandle()); 136 copyTable(mDb, Favorites.TABLE_NAME, tableName, profileId); 137 encodeDBProperties(0); 138 } 139 140 /** 141 * 142 * Restores the contents of a custom table to Favorites.TABLE_NAME 143 */ 144 restoreFromCustomBackupTable(String tableName, boolean dropAfterUse)145 public void restoreFromCustomBackupTable(String tableName, boolean dropAfterUse) { 146 if (!tableExists(mDb, tableName)) { 147 return; 148 } 149 long userSerial = UserCache.INSTANCE.get(mContext).getSerialNumberForUser( 150 Process.myUserHandle()); 151 copyTable(mDb, tableName, Favorites.TABLE_NAME, userSerial); 152 if (dropAfterUse) { 153 dropTable(mDb, tableName); 154 } 155 } 156 /** 157 * Copy valid grid entries from one table to another. 158 */ copyTable(SQLiteDatabase db, String from, String to, long userSerial)159 private static void copyTable(SQLiteDatabase db, String from, String to, long userSerial) { 160 dropTable(db, to); 161 Favorites.addTableToDb(db, userSerial, false, to); 162 db.execSQL("INSERT INTO " + to + " SELECT * FROM " + from + " where _id > " + ID_PROPERTY); 163 } 164 encodeDBProperties(int options)165 private void encodeDBProperties(int options) { 166 ContentValues values = new ContentValues(); 167 values.put(Favorites._ID, ID_PROPERTY); 168 values.put(KEY_DB_VERSION, mDb.getVersion()); 169 values.put(KEY_GRID_X_SIZE, mOldGridX); 170 values.put(KEY_GRID_Y_SIZE, mOldGridY); 171 values.put(KEY_HOTSEAT_SIZE, mOldHotseatSize); 172 values.put(Favorites.OPTIONS, options); 173 mDb.insert(BACKUP_TABLE_NAME, null, values); 174 } 175 176 /** 177 * Load DB properties from grid backup table. 178 */ loadDBProperties()179 public @BackupState int loadDBProperties() { 180 try (Cursor c = mDb.query(BACKUP_TABLE_NAME, new String[] { 181 KEY_DB_VERSION, // 0 182 KEY_GRID_X_SIZE, // 1 183 KEY_GRID_Y_SIZE, // 2 184 KEY_HOTSEAT_SIZE, // 3 185 Favorites.OPTIONS}, // 4 186 "_id=" + ID_PROPERTY, null, null, null, null)) { 187 if (!c.moveToNext()) { 188 Log.e(TAG, "Meta data not found in backup table"); 189 return STATE_NOT_FOUND; 190 } 191 if (!validateDBVersion(mDb.getVersion(), c.getInt(0))) { 192 return STATE_NOT_FOUND; 193 } 194 195 mRestoredGridX = c.getInt(1); 196 mRestoredGridY = c.getInt(2); 197 mRestoredHotseatSize = c.getInt(3); 198 boolean isSanitized = (c.getInt(4) & OPTION_REQUIRES_SANITIZATION) == 0; 199 return isSanitized ? STATE_SANITIZED : STATE_RAW; 200 } 201 } 202 203 /** 204 * Restore workspace from raw backup if available. 205 */ restoreFromRawBackupIfAvailable(long oldProfileId)206 public boolean restoreFromRawBackupIfAvailable(long oldProfileId) { 207 if (!tableExists(mDb, Favorites.BACKUP_TABLE_NAME) 208 || loadDBProperties() != STATE_RAW 209 || mOldHotseatSize != mRestoredHotseatSize 210 || mOldGridX != mRestoredGridX 211 || mOldGridY != mRestoredGridY) { 212 // skip restore if dimensions in backup table differs from current setup. 213 return false; 214 } 215 copyTable(mDb, Favorites.BACKUP_TABLE_NAME, Favorites.TABLE_NAME, oldProfileId); 216 Log.d(TAG, "Backup restored"); 217 return true; 218 } 219 220 /** 221 * Performs a backup on the workspace layout. 222 */ doBackup(long profileId, int options)223 public void doBackup(long profileId, int options) { 224 copyTable(mDb, Favorites.TABLE_NAME, Favorites.BACKUP_TABLE_NAME, profileId); 225 encodeDBProperties(options); 226 } 227 validateDBVersion(int expected, int actual)228 private static boolean validateDBVersion(int expected, int actual) { 229 if (expected != actual) { 230 Log.e(TAG, String.format("Launcher.db version mismatch, expecting %d but %d was found", 231 expected, actual)); 232 return false; 233 } 234 return true; 235 } 236 } 237