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