From 33d443897658e6ad8b76bd2e58e3598161fd3ead Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Thu, 16 Oct 2014 09:24:19 -0700 Subject: Updating backup restore logic > Adding DeviceProfile information in the backup > Removing SharedPreference backup > Adding helper methods to abort backup in the middle > Comparing keys against the backup journal during restore to avoid restoring corrupt/lost entries > Old backups are still compatible, but lost keys verification will be ignored in that case. Bug: 17937935 Bug: 17951775 Bug: 17260941 Change-Id: Iad48646cfdd69abaff5c163b2055f3b8a9b39b19 --- protos/backup.proto | 24 ++++++ src/com/android/launcher3/Launcher.java | 17 +---- src/com/android/launcher3/LauncherAppState.java | 32 ++++++-- .../launcher3/LauncherBackupAgentHelper.java | 34 +++------ .../android/launcher3/LauncherBackupHelper.java | 88 +++++++++++++++++++--- .../launcher3/LauncherPreferencesBackupHelper.java | 44 ----------- src/com/android/launcher3/LauncherProvider.java | 10 ++- 7 files changed, 147 insertions(+), 102 deletions(-) delete mode 100644 src/com/android/launcher3/LauncherPreferencesBackupHelper.java diff --git a/protos/backup.proto b/protos/backup.proto index 7ba293702..8ae175234 100644 --- a/protos/backup.proto +++ b/protos/backup.proto @@ -37,12 +37,36 @@ message CheckedMessage { required int64 checksum = 2; } +message DeviceProfieData { + required float desktop_rows = 1; + required float desktop_cols = 2; + required float hotseat_count = 3; + required int32 allapps_rank = 4; +} + message Journal { required int32 app_version = 1; + + // Time when the backup was created required int64 t = 2; + + // Total bytes written during the last backup + // OBSOLETE: A state may contain entries which are already present in the backup + // and were not written in the last backup optional int64 bytes = 3; + + // Total entries written during the last backup + // OBSOLETE: A state may contain entries which are already present in the backup + // and were not written in the last backup optional int32 rows = 4; + + // Valid keys for this state repeated Key key = 5; + + // Backup format version. + optional int32 backup_version = 6 [default = 1]; + + optional DeviceProfieData profile = 7; } message Favorite { diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java index b357e7a41..bb7265e44 100644 --- a/src/com/android/launcher3/Launcher.java +++ b/src/com/android/launcher3/Launcher.java @@ -53,7 +53,6 @@ import android.database.ContentObserver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.Drawable; @@ -71,7 +70,6 @@ import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.method.TextKeyListener; -import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; import android.view.Gravity; @@ -418,22 +416,9 @@ public class Launcher extends Activity LauncherAppState.setApplicationContext(getApplicationContext()); LauncherAppState app = LauncherAppState.getInstance(); LauncherAppState.getLauncherProvider().setLauncherProviderChangeListener(this); - // Determine the dynamic grid properties - Point smallestSize = new Point(); - Point largestSize = new Point(); - Point realSize = new Point(); - Display display = getWindowManager().getDefaultDisplay(); - display.getCurrentSizeRange(smallestSize, largestSize); - display.getRealSize(realSize); - DisplayMetrics dm = new DisplayMetrics(); - display.getMetrics(dm); // Lazy-initialize the dynamic grid - DeviceProfile grid = app.initDynamicGrid(this, - Math.min(smallestSize.x, smallestSize.y), - Math.min(largestSize.x, largestSize.y), - realSize.x, realSize.y, - dm.widthPixels, dm.heightPixels); + DeviceProfile grid = app.initDynamicGrid(this); // the LauncherApplication should call this, but in case of Instrumentation it might not be present yet mSharedPrefs = getSharedPreferences(LauncherAppState.getSharedPreferencesKey(), diff --git a/src/com/android/launcher3/LauncherAppState.java b/src/com/android/launcher3/LauncherAppState.java index 03ab94bab..48af216bb 100644 --- a/src/com/android/launcher3/LauncherAppState.java +++ b/src/com/android/launcher3/LauncherAppState.java @@ -25,8 +25,12 @@ import android.content.IntentFilter; import android.content.res.Configuration; import android.content.res.Resources; import android.database.ContentObserver; +import android.graphics.Point; import android.os.Handler; +import android.util.DisplayMetrics; import android.util.Log; +import android.view.Display; +import android.view.WindowManager; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat; @@ -190,21 +194,35 @@ public class LauncherAppState implements DeviceProfile.DeviceProfileCallbacks { return LauncherFiles.SHARED_PREFERENCES_KEY; } - DeviceProfile initDynamicGrid(Context context, int minWidth, int minHeight, - int width, int height, - int availableWidth, int availableHeight) { + DeviceProfile initDynamicGrid(Context context) { + // Determine the dynamic grid properties + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + + Point realSize = new Point(); + display.getRealSize(realSize); + DisplayMetrics dm = new DisplayMetrics(); + display.getMetrics(dm); + if (mDynamicGrid == null) { + Point smallestSize = new Point(); + Point largestSize = new Point(); + display.getCurrentSizeRange(smallestSize, largestSize); + mDynamicGrid = new DynamicGrid(context, context.getResources(), - minWidth, minHeight, width, height, - availableWidth, availableHeight); + Math.min(smallestSize.x, smallestSize.y), + Math.min(largestSize.x, largestSize.y), + realSize.x, realSize.y, + dm.widthPixels, dm.heightPixels); mDynamicGrid.getDeviceProfile().addCallback(this); } // Update the icon size DeviceProfile grid = mDynamicGrid.getDeviceProfile(); - grid.updateFromConfiguration(context, context.getResources(), width, height, - availableWidth, availableHeight); + grid.updateFromConfiguration(context, context.getResources(), + realSize.x, realSize.y, + dm.widthPixels, dm.heightPixels); return grid; } public DynamicGrid getDynamicGrid() { diff --git a/src/com/android/launcher3/LauncherBackupAgentHelper.java b/src/com/android/launcher3/LauncherBackupAgentHelper.java index 09ad22b47..b03b13ccc 100644 --- a/src/com/android/launcher3/LauncherBackupAgentHelper.java +++ b/src/com/android/launcher3/LauncherBackupAgentHelper.java @@ -22,7 +22,6 @@ import android.app.backup.BackupManager; import android.content.Context; import android.database.Cursor; import android.os.ParcelFileDescriptor; -import android.provider.Settings; import android.util.Log; import java.io.IOException; @@ -30,13 +29,14 @@ import java.io.IOException; public class LauncherBackupAgentHelper extends BackupAgentHelper { private static final String TAG = "LauncherBackupAgentHelper"; + + private static final String LAUNCHER_DATA_PREFIX = "L"; + static final boolean VERBOSE = true; static final boolean DEBUG = false; private static BackupManager sBackupManager; - protected static final String SETTING_RESTORE_ENABLED = "launcher_restore_enabled"; - /** * Notify the backup manager that out database is dirty. * @@ -51,28 +51,13 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { sBackupManager.dataChanged(); } - @Override - public void onDestroy() { - // There is only one process accessing this preference file, but the restore - // modifies the file outside the normal codepaths, so it looks like another - // process. This forces a reload of the file, in case this process persists. - String spKey = LauncherAppState.getSharedPreferencesKey(); - getSharedPreferences(spKey, Context.MODE_MULTI_PROCESS); - super.onDestroy(); - } + private LauncherBackupHelper mHelper; @Override public void onCreate() { - boolean restoreEnabled = 0 != Settings.Secure.getInt( - getContentResolver(), SETTING_RESTORE_ENABLED, 1); - if (VERBOSE) Log.v(TAG, "restore is " + (restoreEnabled ? "enabled" : "disabled")); - - addHelper(LauncherBackupHelper.LAUNCHER_PREFS_PREFIX, - new LauncherPreferencesBackupHelper(this, - LauncherAppState.getSharedPreferencesKey(), - restoreEnabled)); - addHelper(LauncherBackupHelper.LAUNCHER_PREFIX, - new LauncherBackupHelper(this, restoreEnabled)); + super.onCreate(); + mHelper = new LauncherBackupHelper(this); + addHelper(LAUNCHER_DATA_PREFIX, mHelper); } @Override @@ -92,7 +77,10 @@ public class LauncherBackupAgentHelper extends BackupAgentHelper { boolean hasData = c.moveToNext(); c.close(); - if (!hasData) { + if (hasData && mHelper.restoreSuccessful) { + LauncherAppState.getLauncherProvider().clearFlagEmptyDbCreated(); + LauncherClings.synchonouslyMarkFirstRunClingDismissed(this); + } else { if (VERBOSE) Log.v(TAG, "Nothing was restored, clearing DB"); LauncherAppState.getLauncherProvider().createEmptyDB(); } diff --git a/src/com/android/launcher3/LauncherBackupHelper.java b/src/com/android/launcher3/LauncherBackupHelper.java index b2ac577f4..8b9c5d96a 100644 --- a/src/com/android/launcher3/LauncherBackupHelper.java +++ b/src/com/android/launcher3/LauncherBackupHelper.java @@ -40,6 +40,7 @@ import com.android.launcher3.LauncherSettings.Favorites; import com.android.launcher3.LauncherSettings.WorkspaceScreens; import com.android.launcher3.backup.BackupProtos; import com.android.launcher3.backup.BackupProtos.CheckedMessage; +import com.android.launcher3.backup.BackupProtos.DeviceProfieData; import com.android.launcher3.backup.BackupProtos.Favorite; import com.android.launcher3.backup.BackupProtos.Journal; import com.android.launcher3.backup.BackupProtos.Key; @@ -66,24 +67,24 @@ import java.util.zip.CRC32; * Persist the launcher home state across calamities. */ public class LauncherBackupHelper implements BackupHelper { - private static final String TAG = "LauncherBackupHelper"; private static final boolean VERBOSE = LauncherBackupAgentHelper.VERBOSE; private static final boolean DEBUG = LauncherBackupAgentHelper.DEBUG; + private static final int BACKUP_VERSION = 2; private static final int MAX_JOURNAL_SIZE = 1000000; + // Journal key is such that it is always smaller than any dynamically generated + // key (any Base64 encoded string). + private static final String JOURNAL_KEY = "#"; + /** icons are large, dribble them out */ private static final int MAX_ICONS_PER_PASS = 10; /** widgets contain previews, which are very large, dribble them out */ private static final int MAX_WIDGETS_PER_PASS = 5; - public static final int IMAGE_COMPRESSION_QUALITY = 75; - - public static final String LAUNCHER_PREFIX = "L"; - - public static final String LAUNCHER_PREFS_PREFIX = "LP"; + private static final int IMAGE_COMPRESSION_QUALITY = 75; private static final Bitmap.CompressFormat IMAGE_FORMAT = android.graphics.Bitmap.CompressFormat.PNG; @@ -145,10 +146,13 @@ public class LauncherBackupHelper implements BackupHelper { private byte[] mBuffer = new byte[512]; private long mLastBackupRestoreTime; - public LauncherBackupHelper(Context context, boolean restoreEnabled) { + boolean restoreSuccessful; + + public LauncherBackupHelper(Context context) { mContext = context; mExistingKeys = new HashSet(); mKeys = new ArrayList(); + restoreSuccessful = true; } private void dataChanged() { @@ -178,7 +182,6 @@ public class LauncherBackupHelper implements BackupHelper { * @param oldState notes from the last backup * @param data incremental key/value pairs to persist off-device * @param newState notes for the next backup - * @throws IOException */ @Override public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, @@ -220,6 +223,12 @@ public class LauncherBackupHelper implements BackupHelper { mExistingKeys.clear(); mLastBackupRestoreTime = newBackupTime; + + // We store the journal at two places. + // 1) Storing it in newState allows us to do partial backups by comparing old state + // 2) Storing it in backup data allows us to validate keys during restore + Journal state = getCurrentStateJournal(); + writeRowToBackup(JOURNAL_KEY, state, data); } catch (IOException e) { Log.e(TAG, "launcher backup has failed", e); } @@ -227,15 +236,27 @@ public class LauncherBackupHelper implements BackupHelper { writeNewStateDescription(newState); } + /** + * @return true if the backup corresponding to oldstate can be successfully applied + * to this device. + */ + private boolean isBackupCompatible(Journal oldState) { + return true; + } + /** * Restore launcher configuration from the restored data stream. - * - *

Keys may arrive in any order. + * It assumes that the keys will arrive in lexical order. So if the journal was present in the + * backup, it should arrive first. * * @param data the key/value pair from the server */ @Override public void restoreEntity(BackupDataInputStream data) { + if (!restoreSuccessful) { + return; + } + int dataSize = data.size(); if (mBuffer.length < dataSize) { mBuffer = new byte[dataSize]; @@ -244,6 +265,27 @@ public class LauncherBackupHelper implements BackupHelper { int bytesRead = data.read(mBuffer, 0, dataSize); if (DEBUG) Log.d(TAG, "read " + bytesRead + " of " + dataSize + " available"); String backupKey = data.getKey(); + + if (JOURNAL_KEY.equals(backupKey)) { + if (VERBOSE) Log.v(TAG, "Journal entry restored"); + if (!mKeys.isEmpty()) { + // We received the journal key after a restore key. + Log.wtf(TAG, keyToBackupKey(mKeys.get(0)) + " received after " + JOURNAL_KEY); + restoreSuccessful = false; + return; + } + + Journal journal = new Journal(); + MessageNano.mergeFrom(journal, readCheckedBytes(mBuffer, dataSize)); + applyJournal(journal); + restoreSuccessful = isBackupCompatible(journal); + return; + } + + if (!mExistingKeys.isEmpty() && !mExistingKeys.contains(backupKey)) { + if (DEBUG) Log.e(TAG, "Ignoring key not present in the backup state " + backupKey); + return; + } Key key = backupKeyToKey(backupKey); mKeys.add(key); switch (key.type) { @@ -288,6 +330,8 @@ public class LauncherBackupHelper implements BackupHelper { journal.t = mLastBackupRestoreTime; journal.key = mKeys.toArray(new BackupProtos.Key[mKeys.size()]); journal.appVersion = getAppVersion(); + journal.backupVersion = BACKUP_VERSION; + journal.profile = getDeviceProfieData(); return journal; } @@ -300,6 +344,29 @@ public class LauncherBackupHelper implements BackupHelper { } } + /** + * @return the current device profile information. + */ + private DeviceProfieData getDeviceProfieData() { + LauncherAppState.setApplicationContext(mContext.getApplicationContext()); + LauncherAppState app = LauncherAppState.getInstance(); + + DeviceProfile profile; + if (app.getDynamicGrid() == null) { + // Initialize the grid + profile = app.initDynamicGrid(mContext); + } else { + profile = app.getDynamicGrid().getDeviceProfile(); + } + + DeviceProfieData data = new DeviceProfieData(); + data.desktopRows = profile.numRows; + data.desktopCols = profile.numColumns; + data.hotseatCount = profile.numHotseatIcons; + data.allappsRank = profile.hotseatAllAppsRank; + return data; + } + /** * Write all modified favorites to the data stream. * @@ -902,7 +969,6 @@ public class LauncherBackupHelper implements BackupHelper { return journal; } - private void writeRowToBackup(Key key, MessageNano proto, BackupDataOutput data) throws IOException { writeRowToBackup(keyToBackupKey(key), proto, data); diff --git a/src/com/android/launcher3/LauncherPreferencesBackupHelper.java b/src/com/android/launcher3/LauncherPreferencesBackupHelper.java deleted file mode 100644 index 6f9c05c88..000000000 --- a/src/com/android/launcher3/LauncherPreferencesBackupHelper.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.launcher3; - -import android.app.backup.BackupDataInputStream; -import android.app.backup.SharedPreferencesBackupHelper; -import android.content.Context; -import android.util.Log; - -public class LauncherPreferencesBackupHelper extends SharedPreferencesBackupHelper { - - private static final String TAG = "LauncherPreferencesBackupHelper"; - private static final boolean VERBOSE = LauncherBackupAgentHelper.VERBOSE; - - private final boolean mRestoreEnabled; - - public LauncherPreferencesBackupHelper(Context context, String sharedPreferencesKey, - boolean restoreEnabled) { - super(context, sharedPreferencesKey); - mRestoreEnabled = restoreEnabled; - } - - @Override - public void restoreEntity(BackupDataInputStream data) { - if (mRestoreEnabled) { - if (VERBOSE) Log.v(TAG, "restoring preferences"); - super.restoreEntity(data); - } - } -} diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index fe9bc171f..365d989bf 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -293,6 +293,14 @@ public class LauncherProvider extends ContentProvider { mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); } + public void clearFlagEmptyDbCreated() { + String spKey = LauncherAppState.getSharedPreferencesKey(); + getContext().getSharedPreferences(spKey, Context.MODE_PRIVATE) + .edit() + .remove(EMPTY_DATABASE_CREATED) + .commit(); + } + /** * Loads the default workspace based on the following priority scheme: * 1) From a package provided by play store @@ -334,7 +342,7 @@ public class LauncherProvider extends ContentProvider { mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), getDefaultLayoutParser()); } - sp.edit().remove(EMPTY_DATABASE_CREATED).commit(); + clearFlagEmptyDbCreated(); } } -- cgit v1.2.3