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

Commit 4e77ae6d authored by Neil Fuller's avatar Neil Fuller Committed by Gerrit Code Review
Browse files

Merge "Switch PackageStatusStorage to XML-based storage"

parents ceaccbfe 5f6750f9
Loading
Loading
Loading
Loading
+236 −194
Original line number Diff line number Diff line
@@ -16,73 +16,83 @@

package com.android.server.timezone;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import com.android.internal.util.FastXmlSerializer;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;

import android.util.AtomicFile;
import android.util.Slog;
import android.util.Xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;

import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_FAILURE;
import static com.android.server.timezone.PackageStatus.CHECK_COMPLETED_SUCCESS;
import static com.android.server.timezone.PackageStatus.CHECK_STARTED;
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;

/**
 * Storage logic for accessing/mutating the Android system's persistent state related to time zone
 * update checking. There is expected to be a single instance and all methods synchronized on
 * {@code this} for thread safety.
 * update checking. There is expected to be a single instance. All non-private methods are thread
 * safe.
 */
final class PackageStatusStorage {

    private static final String TAG = "timezone.PackageStatusStorage";
    private static final String LOG_TAG = "timezone.PackageStatusStorage";

    private static final String DATABASE_NAME = "timezonepackagestatus.db";
    private static final int DATABASE_VERSION = 1;

    /** The table name. It will have a single row with _id == {@link #SINGLETON_ID} */
    private static final String TABLE = "status";
    private static final String COLUMN_ID = "_id";
    private static final String TAG_PACKAGE_STATUS = "PackageStatus";

    /**
     * Column that stores a monotonically increasing lock ID, used to detect concurrent update
     * Attribute that stores a monotonically increasing lock ID, used to detect concurrent update
     * issues without on-line locks. Incremented on every write.
     */
    private static final String COLUMN_OPTIMISTIC_LOCK_ID = "optimistic_lock_id";

    /**
     * Column that stores the current "check status" of the time zone update application packages.
     */
    private static final String COLUMN_CHECK_STATUS = "check_status";
    private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId";

    /**
     * Column that stores the version of the time zone rules update application being checked / last
     * checked.
     * Attribute that stores the current "check status" of the time zone update application
     * packages.
     */
    private static final String COLUMN_UPDATE_APP_VERSION = "update_app_package_version";
    private static final String ATTRIBUTE_CHECK_STATUS = "checkStatus";

    /**
     * Column that stores the version of the time zone rules data application being checked / last
     * checked.
     * Attribute that stores the version of the time zone rules update application being checked
     * / last checked.
     */
    private static final String COLUMN_DATA_APP_VERSION = "data_app_package_version";
    private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion";

    /**
     * The ID of the one row.
     * Attribute that stores the version of the time zone rules data application being checked
     * / last checked.
     */
    private static final int SINGLETON_ID = 1;
    private static final String ATTRIBUTE_DATA_APP_VERSION = "dataAppPackageVersion";

    private static final int UNKNOWN_PACKAGE_VERSION = -1;

    private final DatabaseHelper mDatabaseHelper;
    private final AtomicFile mPackageStatusFile;

    PackageStatusStorage(Context context) {
        mDatabaseHelper = new DatabaseHelper(context);
    PackageStatusStorage(File storageDir) {
        mPackageStatusFile = new AtomicFile(new File(storageDir, "packageStatus.xml"));
        if (!mPackageStatusFile.getBaseFile().exists()) {
            try {
                insertInitialPackageStatus();
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    void deleteDatabaseForTests() {
        SQLiteDatabase.deleteDatabase(mDatabaseHelper.getDatabaseFile());
    void deleteFileForTests() {
        synchronized(this) {
            mPackageStatusFile.delete();
        }
    }

    /**
@@ -93,48 +103,60 @@ final class PackageStatusStorage {
        synchronized (this) {
            try {
                return getPackageStatusInternal();
            } catch (IllegalArgumentException e) {
                // This means that data exists in the table but it was bad.
                Slog.e(TAG, "Package status invalid, resetting and retrying", e);
            } catch (ParseException e) {
                // This means that data exists in the file but it was bad.
                Slog.e(LOG_TAG, "Package status invalid, resetting and retrying", e);

                // Reset the storage so it is in a good state again.
                mDatabaseHelper.recoverFromBadData();
                recoverFromBadData(e);
                try {
                    return getPackageStatusInternal();
                } catch (ParseException e2) {
                    throw new IllegalStateException("Recovery from bad file failed", e2);
                }
            }
        }

    private PackageStatus getPackageStatusInternal() {
        String[] columns = {
                COLUMN_CHECK_STATUS, COLUMN_UPDATE_APP_VERSION, COLUMN_DATA_APP_VERSION
        };
        Cursor cursor = mDatabaseHelper.getReadableDatabase()
                .query(TABLE, columns, COLUMN_ID + " = ?",
                        new String[] { Integer.toString(SINGLETON_ID) },
                        null /* groupBy */, null /* having */, null /* orderBy */);
        if (cursor.getCount() != 1) {
            Slog.e(TAG, "Unable to find package status from package status row. Rows returned: "
                    + cursor.getCount());
            return null;
    }
        cursor.moveToFirst();

        // Determine check status.
        if (cursor.isNull(0)) {
            // This is normal the first time getPackageStatus() is called, or after
            // resetCheckState().
    private PackageStatus getPackageStatusInternal() throws ParseException {
        try (FileInputStream fis = mPackageStatusFile.openRead()) {
            XmlPullParser parser = parseToPackageStatusTag(fis);
            Integer checkStatus = getNullableIntAttribute(parser, ATTRIBUTE_CHECK_STATUS);
            if (checkStatus == null) {
                return null;
            }
        int checkStatus = cursor.getInt(0);
            int updateAppVersion = getIntAttribute(parser, ATTRIBUTE_UPDATE_APP_VERSION);
            int dataAppVersion = getIntAttribute(parser, ATTRIBUTE_DATA_APP_VERSION);
            return new PackageStatus(checkStatus,
                    new PackageVersions(updateAppVersion, dataAppVersion));
        } catch (IOException e) {
            ParseException e2 = new ParseException("Error reading package status", 0);
            e2.initCause(e);
            throw e2;
        }
    }

        // Determine package version.
        if (cursor.isNull(1) || cursor.isNull(2)) {
            Slog.e(TAG, "Package version information unexpectedly null");
            return null;
    // Callers should be synchronized(this).
    private int recoverFromBadData(Exception cause) {
        mPackageStatusFile.delete();
        try {
            return insertInitialPackageStatus();
        } catch (IOException e) {
            IllegalStateException fatal = new IllegalStateException(e);
            fatal.addSuppressed(cause);
            throw fatal;
        }
        PackageVersions packageVersions = new PackageVersions(cursor.getInt(1), cursor.getInt(2));
    }

    /** Insert the initial data, returning the optimistic lock ID */
    private int insertInitialPackageStatus() throws IOException {
        // Doesn't matter what it is, but we avoid the obvious starting value each time the data
        // is reset to ensure that old tokens are unlikely to work.
        final int initialOptimisticLockId = (int) System.currentTimeMillis();

        return new PackageStatus(checkStatus, packageVersions);
        writePackageStatusInternal(null /* status */, initialOptimisticLockId,
                null /* packageVersions */);
        return initialOptimisticLockId;
    }

    /**
@@ -147,23 +169,29 @@ final class PackageStatusStorage {
        }

        synchronized (this) {
            Integer optimisticLockId = getCurrentOptimisticLockId();
            if (optimisticLockId == null) {
                Slog.w(TAG, "Unable to find optimistic lock ID from package status row");
            int optimisticLockId;
            try {
                optimisticLockId = getCurrentOptimisticLockId();
            } catch (ParseException e) {
                Slog.w(LOG_TAG, "Unable to find optimistic lock ID from package status");

                // Recover.
                optimisticLockId = mDatabaseHelper.recoverFromBadData();
                optimisticLockId = recoverFromBadData(e);
            }

            int newOptimisticLockId = optimisticLockId + 1;
            boolean statusRowUpdated = writeStatusRow(
                    optimisticLockId, newOptimisticLockId, CHECK_STARTED, currentInstalledVersions);
            if (!statusRowUpdated) {
                Slog.e(TAG, "Unable to update status to CHECK_STARTED in package status row."
            try {
                boolean statusUpdated = writePackageStatusWithOptimisticLockCheck(
                        optimisticLockId, newOptimisticLockId, CHECK_STARTED,
                        currentInstalledVersions);
                if (!statusUpdated) {
                    throw new IllegalStateException("Unable to update status to CHECK_STARTED."
                            + " synchronization failure?");
                return null;
                }
                return new CheckToken(newOptimisticLockId, currentInstalledVersions);
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
        }
    }

@@ -172,19 +200,25 @@ final class PackageStatusStorage {
     */
    void resetCheckState() {
        synchronized(this) {
            Integer optimisticLockId = getCurrentOptimisticLockId();
            if (optimisticLockId == null) {
                Slog.w(TAG, "resetCheckState: Unable to find optimistic lock ID from package"
                        + " status row");
            int optimisticLockId;
            try {
                optimisticLockId = getCurrentOptimisticLockId();
            } catch (ParseException e) {
                Slog.w(LOG_TAG, "resetCheckState: Unable to find optimistic lock ID from package"
                        + " status");
                // Attempt to recover the storage state.
                optimisticLockId = mDatabaseHelper.recoverFromBadData();
                optimisticLockId = recoverFromBadData(e);
            }

            int newOptimisticLockId = optimisticLockId + 1;
            if (!writeStatusRow(optimisticLockId, newOptimisticLockId,
                    null /* status */, null /* packageVersions */)) {
                Slog.e(TAG, "resetCheckState: Unable to reset package status row,"
                        + " newOptimisticLockId=" + newOptimisticLockId);
            try {
                if (!writePackageStatusWithOptimisticLockCheck(optimisticLockId,
                        newOptimisticLockId, null /* status */, null /* packageVersions */)) {
                    throw new IllegalStateException("resetCheckState: Unable to reset package"
                            + " status, newOptimisticLockId=" + newOptimisticLockId);
                }
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
        }
    }
@@ -199,138 +233,146 @@ final class PackageStatusStorage {
            int optimisticLockId = checkToken.mOptimisticLockId;
            int newOptimisticLockId = optimisticLockId + 1;
            int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE;
            return writeStatusRow(optimisticLockId, newOptimisticLockId,
                    status, checkToken.mPackageVersions);
            try {
                return writePackageStatusWithOptimisticLockCheck(optimisticLockId,
                        newOptimisticLockId, status, checkToken.mPackageVersions);
            } catch (IOException e) {
                throw new IllegalStateException(e);
            }
        }
    }

    // Caller should be synchronized(this)
    private Integer getCurrentOptimisticLockId() {
        final String[] columns = { COLUMN_OPTIMISTIC_LOCK_ID };
        final String querySelection = COLUMN_ID + " = ?";
        final String[] querySelectionArgs = { Integer.toString(SINGLETON_ID) };
    // Caller should be synchronized(this).
    private int getCurrentOptimisticLockId() throws ParseException {
        try (FileInputStream fis = mPackageStatusFile.openRead()) {
            XmlPullParser parser = parseToPackageStatusTag(fis);
            return getIntAttribute(parser, ATTRIBUTE_OPTIMISTIC_LOCK_ID);
        } catch (IOException e) {
            ParseException e2 = new ParseException("Unable to read file", 0);
            e2.initCause(e);
            throw e2;
        }
    }

        SQLiteDatabase database = mDatabaseHelper.getReadableDatabase();
        try (Cursor cursor = database.query(TABLE, columns, querySelection, querySelectionArgs,
                null /* groupBy */, null /* having */, null /* orderBy */)) {
            if (cursor.getCount() != 1) {
                Slog.w(TAG, cursor.getCount() + " rows returned, expected exactly one.");
                return null;
    /** Returns a parser or throws ParseException, never returns null. */
    private static XmlPullParser parseToPackageStatusTag(FileInputStream fis)
            throws ParseException {
        try {
            XmlPullParser parser = Xml.newPullParser();
            parser.setInput(fis, StandardCharsets.UTF_8.name());
            int type;
            while ((type = parser.next()) != END_DOCUMENT) {
                final String tag = parser.getName();
                if (type == START_TAG && TAG_PACKAGE_STATUS.equals(tag)) {
                    return parser;
                }
            cursor.moveToFirst();
            return cursor.getInt(0);
            }
            throw new ParseException("Unable to find " + TAG_PACKAGE_STATUS + " tag", 0);
        } catch (XmlPullParserException e) {
            throw new IllegalStateException("Unable to configure parser", e);
        } catch (IOException e) {
            ParseException e2 = new ParseException("Error reading XML", 0);
            e.initCause(e);
            throw e2;
        }
    }

    // Caller should be synchronized(this)
    private boolean writeStatusRow(int optimisticLockId, int newOptimisticLockId, Integer status,
            PackageVersions packageVersions) {
        if ((status == null) != (packageVersions == null)) {
            throw new IllegalArgumentException(
                    "Provide both status and packageVersions, or neither.");
    // Caller should be synchronized(this).
    private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId,
            int newOptimisticLockId, Integer status, PackageVersions packageVersions)
            throws IOException {

        int currentOptimisticLockId;
        try {
            currentOptimisticLockId = getCurrentOptimisticLockId();
            if (currentOptimisticLockId != optimisticLockId) {
                return false;
            }
        } catch (ParseException e) {
            recoverFromBadData(e);
            return false;
        }

        SQLiteDatabase database = mDatabaseHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(COLUMN_OPTIMISTIC_LOCK_ID, newOptimisticLockId);
        if (status == null) {
            values.putNull(COLUMN_CHECK_STATUS);
            values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
            values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
        } else {
            values.put(COLUMN_CHECK_STATUS, status);
            values.put(COLUMN_UPDATE_APP_VERSION, packageVersions.mUpdateAppVersion);
            values.put(COLUMN_DATA_APP_VERSION, packageVersions.mDataAppVersion);
        writePackageStatusInternal(status, newOptimisticLockId, packageVersions);
        return true;
    }

        String updateSelection = COLUMN_ID + " = ? AND " + COLUMN_OPTIMISTIC_LOCK_ID + " = ?";
        String[] updateSelectionArgs = {
                Integer.toString(SINGLETON_ID), Integer.toString(optimisticLockId)
        };
        int count = database.update(TABLE, values, updateSelection, updateSelectionArgs);
        if (count > 1) {
            // This has to be because of corruption: there should only ever be one row.
            Slog.w(TAG, "writeStatusRow: " + count + " rows updated, expected exactly one.");
            // Reset the table.
            mDatabaseHelper.recoverFromBadData();
    // Caller should be synchronized(this).
    private void writePackageStatusInternal(Integer status, int optimisticLockId,
            PackageVersions packageVersions) throws IOException {
        if ((status == null) != (packageVersions == null)) {
            throw new IllegalArgumentException(
                    "Provide both status and packageVersions, or neither.");
        }

        // 1 is the success case. 0 rows updated means the row is missing or the optimistic lock ID
        // was not as expected, this could be because of corruption but is most likely due to an
        // optimistic lock failure. Callers can decide on a case-by-case basis.
        return count == 1;
        FileOutputStream fos = null;
        try {
            fos = mPackageStatusFile.startWrite();
            XmlSerializer serializer = new FastXmlSerializer();
            serializer.setOutput(fos, StandardCharsets.UTF_8.name());
            serializer.startDocument(null /* encoding */, true /* standalone */);
            final String namespace = null;
            serializer.startTag(namespace, TAG_PACKAGE_STATUS);
            String statusAttributeValue = status == null ? "" : Integer.toString(status);
            serializer.attribute(namespace, ATTRIBUTE_CHECK_STATUS, statusAttributeValue);
            serializer.attribute(namespace, ATTRIBUTE_OPTIMISTIC_LOCK_ID,
                    Integer.toString(optimisticLockId));
            int updateAppVersion = status == null
                    ? UNKNOWN_PACKAGE_VERSION : packageVersions.mUpdateAppVersion;
            serializer.attribute(namespace, ATTRIBUTE_UPDATE_APP_VERSION,
                    Integer.toString(updateAppVersion));
            int dataAppVersion = status == null
                    ? UNKNOWN_PACKAGE_VERSION : packageVersions.mDataAppVersion;
            serializer.attribute(namespace, ATTRIBUTE_DATA_APP_VERSION,
                    Integer.toString(dataAppVersion));
            serializer.endTag(namespace, TAG_PACKAGE_STATUS);
            serializer.endDocument();
            serializer.flush();
            mPackageStatusFile.finishWrite(fos);
        } catch (IOException e) {
            if (fos != null) {
                mPackageStatusFile.failWrite(fos);
            }
            throw e;
        }

    /** Only used during tests to force an empty table. */
    void deleteRowForTests() {
        mDatabaseHelper.getWritableDatabase().delete(TABLE, null, null);
    }

    /** Only used during tests to force a known table state. */
    public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
        synchronized (this) {
            try {
                int optimisticLockId = getCurrentOptimisticLockId();
        writeStatusRow(optimisticLockId, optimisticLockId, checkStatus, packageVersions);
    }

    static class DatabaseHelper extends SQLiteOpenHelper {

        private final Context mContext;

        public DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
            mContext = context;
                writePackageStatusWithOptimisticLockCheck(optimisticLockId, optimisticLockId,
                        checkStatus, packageVersions);
            } catch (IOException | ParseException e) {
                throw new IllegalStateException(e);
            }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL("CREATE TABLE " + TABLE + " (" +
                    "_id INTEGER PRIMARY KEY," +
                    COLUMN_OPTIMISTIC_LOCK_ID + " INTEGER NOT NULL," +
                    COLUMN_CHECK_STATUS + " INTEGER," +
                    COLUMN_UPDATE_APP_VERSION + " INTEGER NOT NULL," +
                    COLUMN_DATA_APP_VERSION + " INTEGER NOT NULL" +
                    ");");
            insertInitialRowState(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
            // no-op: nothing to upgrade
    }

        /** Recover the initial data row state, returning the new current optimistic lock ID */
        int recoverFromBadData() {
            // Delete the table content.
            SQLiteDatabase writableDatabase = getWritableDatabase();
            writableDatabase.delete(TABLE, null /* whereClause */, null /* whereArgs */);

            // Insert the initial content.
            return insertInitialRowState(writableDatabase);
    private static Integer getNullableIntAttribute(XmlPullParser parser, String attributeName)
            throws ParseException {
        String attributeValue = parser.getAttributeValue(null, attributeName);
        try {
            if (attributeValue == null) {
                throw new ParseException("Attribute " + attributeName + " missing", 0);
            } else if (attributeValue.isEmpty()) {
                return null;
            }

        /** Insert the initial data row, returning the optimistic lock ID */
        private static int insertInitialRowState(SQLiteDatabase db) {
            // Doesn't matter what it is, but we avoid the obvious starting value each time the row
            // is reset to ensure that old tokens are unlikely to work.
           final int initialOptimisticLockId = (int) System.currentTimeMillis();

            // Insert the one row.
            ContentValues values = new ContentValues();
            values.put(COLUMN_ID, SINGLETON_ID);
            values.put(COLUMN_OPTIMISTIC_LOCK_ID, initialOptimisticLockId);
            values.putNull(COLUMN_CHECK_STATUS);
            values.put(COLUMN_UPDATE_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
            values.put(COLUMN_DATA_APP_VERSION, UNKNOWN_PACKAGE_VERSION);
            long id = db.insert(TABLE, null, values);
            if (id == -1) {
                Slog.w(TAG, "insertInitialRow: could not insert initial row, id=" + id);
                return -1;
            return Integer.parseInt(attributeValue);
        } catch (NumberFormatException e) {
            throw new ParseException(
                    "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0);
        }
            return initialOptimisticLockId;
    }

        File getDatabaseFile() {
            return mContext.getDatabasePath(DATABASE_NAME);
    private static int getIntAttribute(XmlPullParser parser, String attributeName)
            throws ParseException {
        Integer value = getNullableIntAttribute(parser, attributeName);
        if (value == null) {
            throw new ParseException("Missing attribute " + attributeName, 0);
        }
        return value;
    }
}
+10 −1

File changed.

Preview size limit exceeded, changes collapsed.

+15 −13

File changed.

Preview size limit exceeded, changes collapsed.

+2 −2
Original line number Diff line number Diff line
@@ -71,7 +71,7 @@ public class PackageTrackerTest {

        // Using the instrumentation context means the database is created in a test app-specific
        // directory. We can use the real thing for this test.
        mPackageStatusStorage = new PackageStatusStorage(context);
        mPackageStatusStorage = new PackageStatusStorage(context.getFilesDir());

        // For other interactions with the Android framework we create a fake object.
        mFakeIntentHelper = new FakeIntentHelper();
@@ -88,7 +88,7 @@ public class PackageTrackerTest {
    @After
    public void tearDown() throws Exception {
        if (mPackageStatusStorage != null) {
            mPackageStatusStorage.deleteDatabaseForTests();
            mPackageStatusStorage.deleteFileForTests();
        }
    }