From 05f30889d6ba0f803c720813067b28c9c4cf2bfd Mon Sep 17 00:00:00 2001 From: Sunny Goyal Date: Wed, 3 May 2017 12:42:18 -0700 Subject: Adding support for DB downgrade Adding a schema file for handling DB downgrade. This schema file is part of the backup/restore set, and hence is available on a device with lower app version. Bug: 37257575 Change-Id: I69c8ef5f28d5209be6e6679412c7459d4eeda5d0 --- src/com/android/launcher3/LauncherProvider.java | 84 ++++++---------- .../android/launcher3/model/DbDowngradeHelper.java | 108 +++++++++++++++++++++ .../launcher3/provider/LauncherDbUtils.java | 2 + src/com/android/launcher3/util/IOUtils.java | 55 +++++++++++ 4 files changed, 196 insertions(+), 53 deletions(-) create mode 100644 src/com/android/launcher3/model/DbDowngradeHelper.java create mode 100644 src/com/android/launcher3/util/IOUtils.java (limited to 'src') diff --git a/src/com/android/launcher3/LauncherProvider.java b/src/com/android/launcher3/LauncherProvider.java index bd53b4d72..c84a4312d 100644 --- a/src/com/android/launcher3/LauncherProvider.java +++ b/src/com/android/launcher3/LauncherProvider.java @@ -56,6 +56,7 @@ import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dynamicui.ExtractionUtils; import com.android.launcher3.graphics.IconShapeOverride; import com.android.launcher3.logging.FileLog; +import com.android.launcher3.model.DbDowngradeHelper; import com.android.launcher3.provider.LauncherDbUtils; import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; import com.android.launcher3.provider.RestoreDbTask; @@ -64,6 +65,7 @@ import com.android.launcher3.util.NoLocaleSqliteContext; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Thunk; +import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.reflect.Method; @@ -77,18 +79,12 @@ public class LauncherProvider extends ContentProvider { private static final String TAG = "LauncherProvider"; private static final boolean LOGD = false; + private static final String DOWNGRADE_SCHEMA_FILE = "downgrade_schema.json"; + /** * Represents the schema of the database. Changes in scheme need not be backwards compatible. */ - private static final int SCHEMA_VERSION = 27; - /** - * Represents the actual data. It could include additional validations and normalizations added - * overtime. These must be backwards compatible, else we risk breaking old devices during - * restore or binary version downgrade. - */ - private static final int DATA_VERSION = 3; - - private static final String PREF_KEY_DATA_VERISON = "provider_data_version"; + public static final int SCHEMA_VERSION = 27; public static final String AUTHORITY = (BuildConfig.APPLICATION_ID + ".settings").intern(); @@ -703,47 +699,30 @@ public class LauncherProvider extends ContentProvider { @Override public void onOpen(SQLiteDatabase db) { super.onOpen(db); - SharedPreferences prefs = mContext - .getSharedPreferences(LauncherFiles.DEVICE_PREFERENCES_KEY, 0); - int oldVersion = prefs.getInt(PREF_KEY_DATA_VERISON, 0); - if (oldVersion != DATA_VERSION) { - // Only run the data upgrade path for an existing db. - if (!Utilities.getPrefs(mContext).getBoolean(EMPTY_DATABASE_CREATED, false)) { - try (SQLiteTransaction t = new SQLiteTransaction(db)) { - onDataUpgrade(db, oldVersion); - t.commit(); - } catch (Exception e) { - Log.d(TAG, "Error updating data version, ignoring", e); - return; - } - } - prefs.edit().putInt(PREF_KEY_DATA_VERISON, DATA_VERSION).apply(); + + File schemaFile = mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE); + if (!schemaFile.exists()) { + handleOneTimeDataUpgrade(db); } + DbDowngradeHelper.updateSchemaFile(schemaFile, SCHEMA_VERSION, mContext, + R.raw.downgrade_schema); } /** - * Called when the data is updated as part of app update. It can be called multiple times - * with old version, even though it had been run before. The changes made here must be - * backwards compatible, else we risk breaking old devices during restore or binary - * version downgrade. + * One-time data updated before support of onDowngrade was added. This update is backwards + * compatible and can safely be run multiple times. + * Note: No new logic should be added here after release, as the new logic might not get + * executed on an existing device. + * TODO: Move this to db upgrade path, once the downgrade path is released. */ - protected void onDataUpgrade(SQLiteDatabase db, int oldVersion) { - switch (oldVersion) { - case 0: - case 1: { - // Remove "profile extra" - UserManagerCompat um = UserManagerCompat.getInstance(mContext); - for (UserHandle user : um.getUserProfiles()) { - long serial = um.getSerialNumberForUser(user); - String sql = "update favorites set intent = replace(intent, " - + "';l.profile=" + serial + ";', ';') where itemType = 0;"; - db.execSQL(sql); - } - } - case 2: - case 3: - // data updated - return; + protected void handleOneTimeDataUpgrade(SQLiteDatabase db) { + // Remove "profile extra" + UserManagerCompat um = UserManagerCompat.getInstance(mContext); + for (UserHandle user : um.getUserProfiles()) { + long serial = um.getSerialNumberForUser(user); + String sql = "update favorites set intent = replace(intent, " + + "';l.profile=" + serial + ";', ';') where itemType = 0;"; + db.execSQL(sql); } } @@ -850,15 +829,14 @@ public class LauncherProvider extends ContentProvider { @Override public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { - if (oldVersion == 28 && newVersion == 27) { - // TODO: remove this check. This is only applicable for internal development/testing - // and for any released version of Launcher. - return; + try { + DbDowngradeHelper.parse(mContext.getFileStreamPath(DOWNGRADE_SCHEMA_FILE)) + .onDowngrade(db, oldVersion, newVersion); + } catch (Exception e) { + Log.d(TAG, "Unable to downgrade from: " + oldVersion + " to " + newVersion + + ". Wiping databse.", e); + createEmptyDB(db); } - // This shouldn't happen -- throw our hands up in the air and start over. - Log.w(TAG, "Database version downgrade from: " + oldVersion + " to " + newVersion + - ". Wiping databse."); - createEmptyDB(db); } /** diff --git a/src/com/android/launcher3/model/DbDowngradeHelper.java b/src/com/android/launcher3/model/DbDowngradeHelper.java new file mode 100644 index 000000000..cd86b728b --- /dev/null +++ b/src/com/android/launcher3/model/DbDowngradeHelper.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 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.model; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.util.Log; +import android.util.SparseArray; + +import com.android.launcher3.provider.LauncherDbUtils.SQLiteTransaction; +import com.android.launcher3.util.IOUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; + +/** + * Utility class to handle DB downgrade + */ +public class DbDowngradeHelper { + + private static final String TAG = "DbDowngradeHelper"; + + private static final String KEY_VERSION = "version"; + private static final String KEY_DOWNGRADE_TO = "downgrade_to_"; + + private final SparseArray mStatements = new SparseArray<>(); + public final int version; + + private DbDowngradeHelper(int version) { + this.version = version; + } + + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + ArrayList allCommands = new ArrayList<>(); + + for (int i = oldVersion - 1; i >= newVersion; i--) { + String[] commands = mStatements.get(i); + if (commands == null) { + throw new SQLiteException("Downgrade path not supported to version " + i); + } + Collections.addAll(allCommands, commands); + } + + try (SQLiteTransaction t = new SQLiteTransaction(db)) { + for (String sql : allCommands) { + db.execSQL(sql); + } + t.commit(); + } + } + + public static DbDowngradeHelper parse(File file) throws JSONException, IOException { + JSONObject obj = new JSONObject(new String(IOUtils.toByteArray(file))); + DbDowngradeHelper helper = new DbDowngradeHelper(obj.getInt(KEY_VERSION)); + for (int version = helper.version - 1; version > 0; version--) { + if (obj.has(KEY_DOWNGRADE_TO + version)) { + JSONArray statements = obj.getJSONArray(KEY_DOWNGRADE_TO + version); + String[] parsed = new String[statements.length()]; + for (int i = 0; i < parsed.length; i++) { + parsed[i] = statements.getString(i); + } + helper.mStatements.put(version, parsed); + } + } + return helper; + } + + public static void updateSchemaFile(File schemaFile, int expectedVersion, + Context context, int schemaResId) { + try { + if (DbDowngradeHelper.parse(schemaFile).version >= expectedVersion) { + return; + } + } catch (Exception e) { + // Schema error + } + + // Write the updated schema + try (FileOutputStream fos = new FileOutputStream(schemaFile); + InputStream in = context.getResources().openRawResource(schemaResId)) { + IOUtils.copy(in, fos); + } catch (IOException e) { + Log.e(TAG, "Error writing schema file", e); + } + } +} diff --git a/src/com/android/launcher3/provider/LauncherDbUtils.java b/src/com/android/launcher3/provider/LauncherDbUtils.java index 632504060..74373d307 100644 --- a/src/com/android/launcher3/provider/LauncherDbUtils.java +++ b/src/com/android/launcher3/provider/LauncherDbUtils.java @@ -52,6 +52,7 @@ public class LauncherDbUtils { if (screenIds.isEmpty()) { // No update needed + t.commit(); return true; } if (screenIds.get(0) != 0) { @@ -71,6 +72,7 @@ public class LauncherDbUtils { if (DatabaseUtils.queryNumEntries(db, Favorites.TABLE_NAME, "container = -100 and screen = 0 and cellY = 0") == 0) { // First row is empty, no need to migrate. + t.commit(); return true; } diff --git a/src/com/android/launcher3/util/IOUtils.java b/src/com/android/launcher3/util/IOUtils.java new file mode 100644 index 000000000..77c21fe92 --- /dev/null +++ b/src/com/android/launcher3/util/IOUtils.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2017 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.util; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Supports various IO utility functions + */ +public class IOUtils { + + private static final int BUF_SIZE = 0x1000; // 4K + + public static byte[] toByteArray(File file) throws IOException { + try (InputStream in = new FileInputStream(file)) { + return toByteArray(in); + } + } + + public static byte[] toByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + copy(in, out); + return out.toByteArray(); + } + + public static long copy(InputStream from, OutputStream to) throws IOException { + byte[] buf = new byte[BUF_SIZE]; + long total = 0; + int r; + while ((r = from.read(buf)) != -1) { + to.write(buf, 0, r); + total += r; + } + return total; + } +} -- cgit v1.2.3