blob: 31f0e3145f8a27819b0f31ce47411d42c5e758e9 [file] [log] [blame]
/*
* 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.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 android.util.Slog;
import java.io.File;
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;
/**
* 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.
*/
final class PackageStatusStorage {
private static final String 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";
/**
* Column 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";
/**
* Column 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";
/**
* Column 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 int UNKNOWN_PACKAGE_VERSION = -1;
private final DatabaseHelper mDatabaseHelper;
PackageStatusStorage(Context context) {
mDatabaseHelper = new DatabaseHelper(context);
}
void deleteDatabaseForTests() {
SQLiteDatabase.deleteDatabase(mDatabaseHelper.getDatabaseFile());
}
/**
* Obtain the current check status of the application packages. Returns {@code null} the first
* time it is called, or after {@link #resetCheckState()}.
*/
PackageStatus getPackageStatus() {
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);
// Reset the storage so it is in a good state again.
mDatabaseHelper.recoverFromBadData();
return getPackageStatusInternal();
}
}
}
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().
return null;
}
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));
return new PackageStatus(checkStatus, packageVersions);
}
/**
* Generate a new {@link CheckToken} that can be passed to the time zone rules update
* application.
*/
CheckToken generateCheckToken(PackageVersions currentInstalledVersions) {
if (currentInstalledVersions == null) {
throw new NullPointerException("currentInstalledVersions == null");
}
synchronized (this) {
Integer optimisticLockId = getCurrentOptimisticLockId();
if (optimisticLockId == null) {
Slog.w(TAG, "Unable to find optimistic lock ID from package status row");
// Recover.
optimisticLockId = mDatabaseHelper.recoverFromBadData();
}
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;
}
return new CheckToken(newOptimisticLockId, currentInstalledVersions);
}
}
/**
* Reset the current device state to "unknown".
*/
void resetCheckState() {
synchronized(this) {
Integer optimisticLockId = getCurrentOptimisticLockId();
if (optimisticLockId == null) {
Slog.w(TAG, "resetCheckState: Unable to find optimistic lock ID from package"
+ " status row");
// Attempt to recover the storage state.
optimisticLockId = mDatabaseHelper.recoverFromBadData();
}
int newOptimisticLockId = optimisticLockId + 1;
if (!writeStatusRow(optimisticLockId, newOptimisticLockId,
null /* status */, null /* packageVersions */)) {
Slog.e(TAG, "resetCheckState: Unable to reset package status row,"
+ " newOptimisticLockId=" + newOptimisticLockId);
}
}
}
/**
* Update the current device state if possible. Returns true if the update was successful.
* {@code false} indicates the storage has been changed since the {@link CheckToken} was
* generated and the update was discarded.
*/
boolean markChecked(CheckToken checkToken, boolean succeeded) {
synchronized (this) {
int optimisticLockId = checkToken.mOptimisticLockId;
int newOptimisticLockId = optimisticLockId + 1;
int status = succeeded ? CHECK_COMPLETED_SUCCESS : CHECK_COMPLETED_FAILURE;
return writeStatusRow(optimisticLockId, newOptimisticLockId,
status, checkToken.mPackageVersions);
}
}
// 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 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.");
}
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);
}
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();
}
// 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);
}
/** 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);
}
@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);
}
/** 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 initialOptimisticLockId;
}
File getDatabaseFile() {
return mContext.getDatabasePath(DATABASE_NAME);
}
}
}