Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 05f30889 authored by Sunny Goyal's avatar Sunny Goyal
Browse files

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
parent 07557e81
Loading
Loading
Loading
Loading
+20 −0
Original line number Diff line number Diff line
{
  // Note: Comments are not supported in JSON schema, but android parser is lenient.

  // Maximum DB version supported by this schema
  "version" : 27,

  // Downgrade from 27 to 26. Empty array indicates, the DB is compatible
  "downgrade_to_26" : [],
  "downgrade_to_25" : [],
  "downgrade_to_24" : [],
  "downgrade_to_23" : [],
  "downgrade_to_22" : [
    "ALTER TABLE favorites RENAME TO temp_favorites;",
    "CREATE TABLE favorites(_id INTEGER PRIMARY KEY, title TEXT, intent TEXT, container INTEGER, screen INTEGER, cellX INTEGER, cellY INTEGER, spanX INTEGER, spanY INTEGER, itemType INTEGER, appWidgetId INTEGER NOT NULL DEFAULT - 1, iconPackage TEXT, iconResource TEXT, icon BLOB, appWidgetProvider TEXT, modified INTEGER NOT NULL DEFAULT 0, restored INTEGER NOT NULL DEFAULT 0, profileId INTEGER DEFAULT 0, rank INTEGER NOT NULL DEFAULT 0);",
    "INSERT INTO favorites SELECT _id, title, intent, container, screen, cellX, cellY, spanX, spanY, itemType, appWidgetId, iconPackage, iconResource, icon, appWidgetProvider, modified, restored, profileId, rank FROM temp_favorites;",
    "DROP TABLE temp_favorites;"
  ]

  // Missing values indicate the DB is not compatible
}
 No newline at end of file
+1 −0
Original line number Diff line number Diff line
@@ -3,5 +3,6 @@

    <include domain="database" path="launcher.db" />
    <include domain="sharedpref" path="com.android.launcher3.prefs.xml" />
    <include domain="file" path="downgrade_schema.json" />

</full-backup-content>
 No newline at end of file
+31 −53
Original line number Diff line number Diff line
@@ -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,34 +699,23 @@ 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: {
        protected void handleOneTimeDataUpgrade(SQLiteDatabase db) {
            // Remove "profile extra"
            UserManagerCompat um = UserManagerCompat.getInstance(mContext);
            for (UserHandle user : um.getUserProfiles()) {
@@ -740,12 +725,6 @@ public class LauncherProvider extends ContentProvider {
                db.execSQL(sql);
            }
        }
                case 2:
                case 3:
                    // data updated
                    return;
            }
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
@@ -850,16 +829,15 @@ 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;
            }
            // 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.");
            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);
            }
        }

        /**
         * Clears all the data for a fresh start.
+108 −0
Original line number Diff line number Diff line
/*
 * 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<String[]> mStatements = new SparseArray<>();
    public final int version;

    private DbDowngradeHelper(int version) {
        this.version = version;
    }

    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        ArrayList<String> 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);
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -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;
            }

Loading