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";
+ private static final String ATTRIBUTE_OPTIMISTIC_LOCK_ID = "optimisticLockId";
/**
- * Column that stores the current "check status" of the time zone update application packages.
+ * Attribute 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_CHECK_STATUS = "checkStatus";
/**
- * Column that stores the version of the time zone rules update 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_UPDATE_APP_VERSION = "update_app_package_version";
+ private static final String ATTRIBUTE_UPDATE_APP_VERSION = "updateAppPackageVersion";
/**
- * 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 data application being checked
+ * / last checked.
*/
- private static final String COLUMN_DATA_APP_VERSION = "data_app_package_version";
-
- /**
- * The ID of the one row.
- */
- 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();
+ }
}
/**
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();
- return getPackageStatusInternal();
+ 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;
+ 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 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;
}
- cursor.moveToFirst();
+ }
- // Determine check status.
- if (cursor.isNull(0)) {
- // This is normal the first time getPackageStatus() is called, or after
- // resetCheckState().
- 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;
}
- int checkStatus = cursor.getInt(0);
+ }
- // Determine package version.
- if (cursor.isNull(1) || cursor.isNull(2)) {
- Slog.e(TAG, "Package version information unexpectedly null");
- return null;
- }
- 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;
}
/**
}
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."
- + " synchronization failure?");
- return null;
+ try {
+ boolean statusUpdated = writePackageStatusWithOptimisticLockCheck(
+ optimisticLockId, newOptimisticLockId, CHECK_STARTED,
+ currentInstalledVersions);
+ if (!statusUpdated) {
+ throw new IllegalStateException("Unable to update status to CHECK_STARTED."
+ + " synchronization failure?");
+ }
+ return new CheckToken(newOptimisticLockId, currentInstalledVersions);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
}
- return new CheckToken(newOptimisticLockId, currentInstalledVersions);
}
}
*/
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);
}
}
}
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) };
-
- 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;
- }
- cursor.moveToFirst();
- return cursor.getInt(0);
+ // 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;
}
}
- // 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.");
+ /** 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;
+ }
+ }
+ 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;
}
+ }
- 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);
- }
+ // Caller should be synchronized(this).
+ private boolean writePackageStatusWithOptimisticLockCheck(int optimisticLockId,
+ int newOptimisticLockId, Integer status, PackageVersions packageVersions)
+ throws IOException {
- 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();
+ int currentOptimisticLockId;
+ try {
+ currentOptimisticLockId = getCurrentOptimisticLockId();
+ if (currentOptimisticLockId != optimisticLockId) {
+ return false;
+ }
+ } catch (ParseException e) {
+ recoverFromBadData(e);
+ return false;
}
- // 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;
- }
-
- /** Only used during tests to force an empty table. */
- void deleteRowForTests() {
- mDatabaseHelper.getWritableDatabase().delete(TABLE, null, null);
+ writePackageStatusInternal(status, newOptimisticLockId, packageVersions);
+ return true;
}
- /** Only used during tests to force a known table state. */
- public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
- 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;
- }
-
- @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);
+ // 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.");
}
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
- // no-op: nothing to upgrade
+ 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;
}
- /** 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);
+ /** Only used during tests to force a known table state. */
+ public void forceCheckStateForTests(int checkStatus, PackageVersions packageVersions) {
+ synchronized (this) {
+ try {
+ int optimisticLockId = getCurrentOptimisticLockId();
+ writePackageStatusWithOptimisticLockCheck(optimisticLockId, optimisticLockId,
+ checkStatus, packageVersions);
+ } catch (IOException | ParseException e) {
+ throw new IllegalStateException(e);
+ }
}
+ }
- /** 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;
+ 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;
}
- return initialOptimisticLockId;
+ return Integer.parseInt(attributeValue);
+ } catch (NumberFormatException e) {
+ throw new ParseException(
+ "Bad integer for attributeName=" + attributeName + ": " + attributeValue, 0);
}
+ }
- 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;
}
}