1 /*
2  * Copyright (C) 2016 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.provider;
18 
19 import static com.android.launcher3.provider.LauncherDbUtils.dropTable;
20 
21 import android.app.backup.BackupManager;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.SharedPreferences;
25 import android.database.Cursor;
26 import android.database.sqlite.SQLiteDatabase;
27 import android.os.UserHandle;
28 import android.text.TextUtils;
29 import android.util.LongSparseArray;
30 import android.util.SparseLongArray;
31 
32 import androidx.annotation.NonNull;
33 
34 import com.android.launcher3.AppWidgetsRestoredReceiver;
35 import com.android.launcher3.InvariantDeviceProfile;
36 import com.android.launcher3.LauncherAppState;
37 import com.android.launcher3.LauncherProvider.DatabaseHelper;
38 import com.android.launcher3.LauncherSettings.Favorites;
39 import com.android.launcher3.Utilities;
40 import com.android.launcher3.logging.FileLog;
41 import com.android.launcher3.model.GridBackupTable;
42 import com.android.launcher3.model.data.LauncherAppWidgetInfo;
43 import com.android.launcher3.model.data.WorkspaceItemInfo;
44 import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction;
45 import com.android.launcher3.util.IntArray;
46 import com.android.launcher3.util.LogConfig;
47 
48 import java.io.InvalidObjectException;
49 import java.util.Arrays;
50 
51 /**
52  * Utility class to update DB schema after it has been restored.
53  *
54  * This task is executed when Launcher starts for the first time and not immediately after restore.
55  * This helps keep the model consistent if the launcher updates between restore and first startup.
56  */
57 public class RestoreDbTask {
58 
59     private static final String TAG = "RestoreDbTask";
60     private static final String RESTORE_TASK_PENDING = "restore_task_pending";
61 
62     private static final String INFO_COLUMN_NAME = "name";
63     private static final String INFO_COLUMN_DEFAULT_VALUE = "dflt_value";
64 
65     private static final String APPWIDGET_OLD_IDS = "appwidget_old_ids";
66     private static final String APPWIDGET_IDS = "appwidget_ids";
67 
performRestore(Context context, DatabaseHelper helper, BackupManager backupManager)68     public static boolean performRestore(Context context, DatabaseHelper helper,
69             BackupManager backupManager) {
70         SQLiteDatabase db = helper.getWritableDatabase();
71         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
72             RestoreDbTask task = new RestoreDbTask();
73             task.backupWorkspace(context, db);
74             task.sanitizeDB(helper, db, backupManager);
75             task.restoreAppWidgetIdsIfExists(context);
76             t.commit();
77             return true;
78         } catch (Exception e) {
79             FileLog.e(TAG, "Failed to verify db", e);
80             return false;
81         }
82     }
83 
84     /**
85      * Restore the workspace if backup is available.
86      */
restoreIfPossible(@onNull Context context, @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)87     public static boolean restoreIfPossible(@NonNull Context context,
88             @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager) {
89         final SQLiteDatabase db = helper.getWritableDatabase();
90         try (SQLiteTransaction t = new SQLiteTransaction(db)) {
91             RestoreDbTask task = new RestoreDbTask();
92             task.restoreWorkspace(context, db, helper, backupManager);
93             t.commit();
94             return true;
95         } catch (Exception e) {
96             FileLog.e(TAG, "Failed to restore db", e);
97             return false;
98         }
99     }
100 
101     /**
102      * Backup the workspace so that if things go south in restore, we can recover these entries.
103      */
backupWorkspace(Context context, SQLiteDatabase db)104     private void backupWorkspace(Context context, SQLiteDatabase db) throws Exception {
105         InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
106         new GridBackupTable(context, db, idp.numHotseatIcons, idp.numColumns, idp.numRows)
107                 .doBackup(getDefaultProfileId(db), GridBackupTable.OPTION_REQUIRES_SANITIZATION);
108     }
109 
restoreWorkspace(@onNull Context context, @NonNull SQLiteDatabase db, @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)110     private void restoreWorkspace(@NonNull Context context, @NonNull SQLiteDatabase db,
111             @NonNull DatabaseHelper helper, @NonNull BackupManager backupManager)
112             throws Exception {
113         final InvariantDeviceProfile idp = LauncherAppState.getIDP(context);
114         GridBackupTable backupTable = new GridBackupTable(context, db, idp.numHotseatIcons,
115                 idp.numColumns, idp.numRows);
116         if (backupTable.restoreFromRawBackupIfAvailable(getDefaultProfileId(db))) {
117             int itemsDeleted = sanitizeDB(helper, db, backupManager);
118             LauncherAppState.getInstance(context).getModel().forceReload();
119             restoreAppWidgetIdsIfExists(context);
120             if (itemsDeleted == 0) {
121                 // all the items are restored, we no longer need the backup table
122                 dropTable(db, Favorites.BACKUP_TABLE_NAME);
123             }
124         }
125     }
126 
127     /**
128      * Makes the following changes in the provider DB.
129      *   1. Removes all entries belonging to any profiles that were not restored.
130      *   2. Marks all entries as restored. The flags are updated during first load or as
131      *      the restored apps get installed.
132      *   3. If the user serial for any restored profile is different than that of the previous
133      *      device, update the entries to the new profile id.
134      *
135      * @return number of items deleted.
136      */
sanitizeDB(DatabaseHelper helper, SQLiteDatabase db, BackupManager backupManager)137     private int sanitizeDB(DatabaseHelper helper, SQLiteDatabase db, BackupManager backupManager)
138             throws Exception {
139         // Primary user ids
140         long myProfileId = helper.getDefaultUserSerial();
141         long oldProfileId = getDefaultProfileId(db);
142         LongSparseArray<Long> oldManagedProfileIds = getManagedProfileIds(db, oldProfileId);
143         LongSparseArray<Long> profileMapping = new LongSparseArray<>(oldManagedProfileIds.size()
144                 + 1);
145 
146         // Build mapping of restored profile ids to their new profile ids.
147         profileMapping.put(oldProfileId, myProfileId);
148         for (int i = oldManagedProfileIds.size() - 1; i >= 0; --i) {
149             long oldManagedProfileId = oldManagedProfileIds.keyAt(i);
150             UserHandle user = getUserForAncestralSerialNumber(backupManager, oldManagedProfileId);
151             if (user != null) {
152                 long newManagedProfileId = helper.getSerialNumberForUser(user);
153                 profileMapping.put(oldManagedProfileId, newManagedProfileId);
154             }
155         }
156 
157         // Delete all entries which do not belong to any restored profile(s).
158         int numProfiles = profileMapping.size();
159         String[] profileIds = new String[numProfiles];
160         profileIds[0] = Long.toString(oldProfileId);
161         for (int i = numProfiles - 1; i >= 1; --i) {
162             profileIds[i] = Long.toString(profileMapping.keyAt(i));
163         }
164         final String[] args = new String[profileIds.length];
165         Arrays.fill(args, "?");
166         final String where = "profileId NOT IN (" + TextUtils.join(", ", Arrays.asList(args)) + ")";
167         int itemsDeleted = db.delete(Favorites.TABLE_NAME, where, profileIds);
168         FileLog.d(TAG, itemsDeleted + " items from unrestored user(s) were deleted");
169 
170         // Mark all items as restored.
171         boolean keepAllIcons = Utilities.isPropertyEnabled(LogConfig.KEEP_ALL_ICONS);
172         ContentValues values = new ContentValues();
173         values.put(Favorites.RESTORED, WorkspaceItemInfo.FLAG_RESTORED_ICON
174                 | (keepAllIcons ? WorkspaceItemInfo.FLAG_RESTORE_STARTED : 0));
175         db.update(Favorites.TABLE_NAME, values, null, null);
176 
177         // Mark widgets with appropriate restore flag.
178         values.put(Favorites.RESTORED,  LauncherAppWidgetInfo.FLAG_ID_NOT_VALID
179                 | LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY
180                 | LauncherAppWidgetInfo.FLAG_UI_NOT_READY
181                 | (keepAllIcons ? LauncherAppWidgetInfo.FLAG_RESTORE_STARTED : 0));
182         db.update(Favorites.TABLE_NAME, values, "itemType = ?",
183                 new String[]{Integer.toString(Favorites.ITEM_TYPE_APPWIDGET)});
184 
185         // Migrate ids. To avoid any overlap, we initially move conflicting ids to a temp
186         // location. Using Long.MIN_VALUE since profile ids can not be negative, so there will
187         // be no overlap.
188         final long tempLocationOffset = Long.MIN_VALUE;
189         SparseLongArray tempMigratedIds = new SparseLongArray(profileMapping.size());
190         int numTempMigrations = 0;
191         for (int i = profileMapping.size() - 1; i >= 0; --i) {
192             long oldId = profileMapping.keyAt(i);
193             long newId = profileMapping.valueAt(i);
194 
195             if (oldId != newId) {
196                 if (profileMapping.indexOfKey(newId) >= 0) {
197                     tempMigratedIds.put(numTempMigrations, newId);
198                     numTempMigrations++;
199                     newId = tempLocationOffset + newId;
200                 }
201                 migrateProfileId(db, oldId, newId);
202             }
203         }
204 
205         // Migrate ids from their temporary id to their actual final id.
206         for (int i = tempMigratedIds.size() - 1; i >= 0; --i) {
207             long newId = tempMigratedIds.valueAt(i);
208             migrateProfileId(db, tempLocationOffset + newId, newId);
209         }
210 
211         if (myProfileId != oldProfileId) {
212             changeDefaultColumn(db, myProfileId);
213         }
214         return itemsDeleted;
215     }
216 
217     /**
218      * Updates profile id of all entries from {@param oldProfileId} to {@param newProfileId}.
219      */
migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId)220     protected void migrateProfileId(SQLiteDatabase db, long oldProfileId, long newProfileId) {
221         FileLog.d(TAG, "Changing profile user id from " + oldProfileId + " to " + newProfileId);
222         // Update existing entries.
223         ContentValues values = new ContentValues();
224         values.put(Favorites.PROFILE_ID, newProfileId);
225         db.update(Favorites.TABLE_NAME, values, "profileId = ?",
226                 new String[]{Long.toString(oldProfileId)});
227     }
228 
229 
230     /**
231      * Changes the default value for the column.
232      */
changeDefaultColumn(SQLiteDatabase db, long newProfileId)233     protected void changeDefaultColumn(SQLiteDatabase db, long newProfileId) {
234         db.execSQL("ALTER TABLE favorites RENAME TO favorites_old;");
235         Favorites.addTableToDb(db, newProfileId, false);
236         db.execSQL("INSERT INTO favorites SELECT * FROM favorites_old;");
237         dropTable(db, "favorites_old");
238     }
239 
240     /**
241      * Returns a list of the managed profile id(s) used in the favorites table of the provided db.
242      */
getManagedProfileIds(SQLiteDatabase db, long defaultProfileId)243     private LongSparseArray<Long> getManagedProfileIds(SQLiteDatabase db, long defaultProfileId) {
244         LongSparseArray<Long> ids = new LongSparseArray<>();
245         try (Cursor c = db.rawQuery("SELECT profileId from favorites WHERE profileId != ? "
246                 + "GROUP BY profileId", new String[] {Long.toString(defaultProfileId)})) {
247             while (c.moveToNext()) {
248                 ids.put(c.getLong(c.getColumnIndex(Favorites.PROFILE_ID)), null);
249             }
250         }
251         return ids;
252     }
253 
254     /**
255      * Returns a UserHandle of a restored managed profile with the given serial number, or null
256      * if none found.
257      */
getUserForAncestralSerialNumber(BackupManager backupManager, long ancestralSerialNumber)258     private UserHandle getUserForAncestralSerialNumber(BackupManager backupManager,
259             long ancestralSerialNumber) {
260         if (!Utilities.ATLEAST_Q) {
261             return null;
262         }
263         return backupManager.getUserForAncestralSerialNumber(ancestralSerialNumber);
264     }
265 
266     /**
267      * Returns the profile id used in the favorites table of the provided db.
268      */
getDefaultProfileId(SQLiteDatabase db)269     protected long getDefaultProfileId(SQLiteDatabase db) throws Exception {
270         try (Cursor c = db.rawQuery("PRAGMA table_info (favorites)", null)) {
271             int nameIndex = c.getColumnIndex(INFO_COLUMN_NAME);
272             while (c.moveToNext()) {
273                 if (Favorites.PROFILE_ID.equals(c.getString(nameIndex))) {
274                     return c.getLong(c.getColumnIndex(INFO_COLUMN_DEFAULT_VALUE));
275                 }
276             }
277             throw new InvalidObjectException("Table does not have a profile id column");
278         }
279     }
280 
isPending(Context context)281     public static boolean isPending(Context context) {
282         return Utilities.getPrefs(context).getBoolean(RESTORE_TASK_PENDING, false);
283     }
284 
setPending(Context context, boolean isPending)285     public static void setPending(Context context, boolean isPending) {
286         FileLog.d(TAG, "Restore data received through full backup " + isPending);
287         Utilities.getPrefs(context).edit().putBoolean(RESTORE_TASK_PENDING, isPending).commit();
288     }
289 
restoreAppWidgetIdsIfExists(Context context)290     private void restoreAppWidgetIdsIfExists(Context context) {
291         SharedPreferences prefs = Utilities.getPrefs(context);
292         if (prefs.contains(APPWIDGET_OLD_IDS) && prefs.contains(APPWIDGET_IDS)) {
293             AppWidgetsRestoredReceiver.restoreAppWidgetIds(context,
294                     IntArray.fromConcatString(prefs.getString(APPWIDGET_OLD_IDS, "")).toArray(),
295                     IntArray.fromConcatString(prefs.getString(APPWIDGET_IDS, "")).toArray());
296         } else {
297             FileLog.d(TAG, "No app widget ids to restore.");
298         }
299 
300         prefs.edit().remove(APPWIDGET_OLD_IDS)
301                 .remove(APPWIDGET_IDS).apply();
302     }
303 
setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds, @NonNull int[] newIds)304     public static void setRestoredAppWidgetIds(Context context, @NonNull int[] oldIds,
305             @NonNull int[] newIds) {
306         Utilities.getPrefs(context).edit()
307                 .putString(APPWIDGET_OLD_IDS, IntArray.wrap(oldIds).toConcatString())
308                 .putString(APPWIDGET_IDS, IntArray.wrap(newIds).toConcatString())
309                 .commit();
310     }
311 
312 }
313