Add caching to LockSettingsStorage
Bug: 18163444
Change-Id: I4caa80ca55efec761e965807ae793db41864321f
diff --git a/services/core/java/com/android/server/LockSettingsStorage.java b/services/core/java/com/android/server/LockSettingsStorage.java
index acbf8ef..e92ea72d 100644
--- a/services/core/java/com/android/server/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/LockSettingsStorage.java
@@ -49,25 +49,31 @@
private static final String[] COLUMNS_FOR_QUERY = {
COLUMN_VALUE
};
+ private static final String[] COLUMNS_FOR_PREFETCH = {
+ COLUMN_KEY, COLUMN_VALUE
+ };
private static final String SYSTEM_DIRECTORY = "/system/";
private static final String LOCK_PATTERN_FILE = "gesture.key";
private static final String LOCK_PASSWORD_FILE = "password.key";
+ private static final Object DEFAULT = new Object();
+
private final DatabaseHelper mOpenHelper;
private final Context mContext;
+ private final Cache mCache = new Cache();
private final Object mFileWriteLock = new Object();
- LockSettingsStorage(Context context, Callback callback) {
+ public LockSettingsStorage(Context context, Callback callback) {
mContext = context;
mOpenHelper = new DatabaseHelper(context, callback);
}
- void writeKeyValue(String key, String value, int userId) {
+ public void writeKeyValue(String key, String value, int userId) {
writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
}
- void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
+ public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
ContentValues cv = new ContentValues();
cv.put(COLUMN_KEY, key);
cv.put(COLUMN_USERID, userId);
@@ -79,15 +85,24 @@
new String[] {key, Integer.toString(userId)});
db.insert(TABLE, null, cv);
db.setTransactionSuccessful();
+ mCache.putKeyValue(key, value, userId);
} finally {
db.endTransaction();
}
}
- String readKeyValue(String key, String defaultValue, int userId) {
+ public String readKeyValue(String key, String defaultValue, int userId) {
+ int version;
+ synchronized (mCache) {
+ if (mCache.hasKeyValue(key, userId)) {
+ return mCache.peekKeyValue(key, defaultValue, userId);
+ }
+ version = mCache.getVersion();
+ }
+
Cursor cursor;
- String result = defaultValue;
+ Object result = DEFAULT;
SQLiteDatabase db = mOpenHelper.getReadableDatabase();
if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
@@ -98,39 +113,77 @@
}
cursor.close();
}
- return result;
+ mCache.putKeyValueIfUnchanged(key, result, userId, version);
+ return result == DEFAULT ? defaultValue : (String) result;
}
- byte[] readPasswordHash(int userId) {
- final byte[] stored = readFile(getLockPasswordFilename(userId), userId);
+ public void prefetchUser(int userId) {
+ int version;
+ synchronized (mCache) {
+ if (mCache.isFetched(userId)) {
+ return;
+ }
+ mCache.setFetched(userId);
+ version = mCache.getVersion();
+ }
+
+ Cursor cursor;
+ SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+ if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
+ COLUMN_USERID + "=?",
+ new String[] { Integer.toString(userId) },
+ null, null, null)) != null) {
+ while (cursor.moveToNext()) {
+ String key = cursor.getString(0);
+ String value = cursor.getString(1);
+ mCache.putKeyValueIfUnchanged(key, value, userId, version);
+ }
+ cursor.close();
+ }
+
+ // Populate cache by reading the password and pattern files.
+ readPasswordHash(userId);
+ readPatternHash(userId);
+ }
+
+ public byte[] readPasswordHash(int userId) {
+ final byte[] stored = readFile(getLockPasswordFilename(userId));
if (stored != null && stored.length > 0) {
return stored;
}
return null;
}
- byte[] readPatternHash(int userId) {
- final byte[] stored = readFile(getLockPatternFilename(userId), userId);
+ public byte[] readPatternHash(int userId) {
+ final byte[] stored = readFile(getLockPatternFilename(userId));
if (stored != null && stored.length > 0) {
return stored;
}
return null;
}
- boolean hasPassword(int userId) {
- return hasFile(getLockPasswordFilename(userId), userId);
+ public boolean hasPassword(int userId) {
+ return hasFile(getLockPasswordFilename(userId));
}
- boolean hasPattern(int userId) {
- return hasFile(getLockPatternFilename(userId), userId);
+ public boolean hasPattern(int userId) {
+ return hasFile(getLockPatternFilename(userId));
}
- private boolean hasFile(String name, int userId) {
- byte[] contents = readFile(name, userId);
+ private boolean hasFile(String name) {
+ byte[] contents = readFile(name);
return contents != null && contents.length > 0;
}
- private byte[] readFile(String name, int userId) {
+ private byte[] readFile(String name) {
+ int version;
+ synchronized (mCache) {
+ if (mCache.hasFile(name)) {
+ return mCache.peekFile(name);
+ }
+ version = mCache.getVersion();
+ }
+
RandomAccessFile raf = null;
byte[] stored = null;
try {
@@ -149,10 +202,11 @@
}
}
}
+ mCache.putFileIfUnchanged(name, stored, version);
return stored;
}
- private void writeFile(String name, byte[] hash, int userId) {
+ private void writeFile(String name, byte[] hash) {
synchronized (mFileWriteLock) {
RandomAccessFile raf = null;
try {
@@ -176,43 +230,37 @@
}
}
}
+ mCache.putFile(name, hash);
}
}
public void writePatternHash(byte[] hash, int userId) {
- writeFile(getLockPatternFilename(userId), hash, userId);
+ writeFile(getLockPatternFilename(userId), hash);
}
public void writePasswordHash(byte[] hash, int userId) {
- writeFile(getLockPasswordFilename(userId), hash, userId);
+ writeFile(getLockPasswordFilename(userId), hash);
}
private String getLockPatternFilename(int userId) {
- String dataSystemDirectory =
- android.os.Environment.getDataDirectory().getAbsolutePath() +
- SYSTEM_DIRECTORY;
- userId = getUserParentOrSelfId(userId);
- if (userId == 0) {
- // Leave it in the same place for user 0
- return dataSystemDirectory + LOCK_PATTERN_FILE;
- } else {
- return new File(Environment.getUserSystemDirectory(userId), LOCK_PATTERN_FILE)
- .getAbsolutePath();
- }
+ return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
}
private String getLockPasswordFilename(int userId) {
+ return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
+ }
+
+ private String getLockCredentialFilePathForUser(int userId, String basename) {
userId = getUserParentOrSelfId(userId);
String dataSystemDirectory =
android.os.Environment.getDataDirectory().getAbsolutePath() +
SYSTEM_DIRECTORY;
if (userId == 0) {
// Leave it in the same place for user 0
- return dataSystemDirectory + LOCK_PASSWORD_FILE;
+ return dataSystemDirectory + basename;
} else {
- return new File(Environment.getUserSystemDirectory(userId), LOCK_PASSWORD_FILE)
- .getAbsolutePath();
+ return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
}
}
@@ -237,13 +285,17 @@
synchronized (mFileWriteLock) {
if (parentInfo == null) {
// This user owns its lock settings files - safe to delete them
- File file = new File(getLockPasswordFilename(userId));
+ String name = getLockPasswordFilename(userId);
+ File file = new File(name);
if (file.exists()) {
file.delete();
+ mCache.putFile(name, null);
}
- file = new File(getLockPatternFilename(userId));
+ name = getLockPatternFilename(userId);
+ file = new File(name);
if (file.exists()) {
file.delete();
+ mCache.putFile(name, null);
}
}
}
@@ -252,13 +304,14 @@
db.beginTransaction();
db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
db.setTransactionSuccessful();
+ mCache.removeUser(userId);
} finally {
db.endTransaction();
}
}
- interface Callback {
+ public interface Callback {
void initialize(SQLiteDatabase db);
}
@@ -304,4 +357,133 @@
}
}
}
+
+ /**
+ * Cache consistency model:
+ * - Writes to storage write directly to the cache, but this MUST happen within the atomic
+ * section either provided by the database transaction or mWriteLock, such that writes to the
+ * cache and writes to the backing storage are guaranteed to occur in the same order
+ *
+ * - Reads can populate the cache, but because they are no strong ordering guarantees with
+ * respect to writes this precaution is taken:
+ * - The cache is assigned a version number that increases every time the cache is modified.
+ * Reads from backing storage can only populate the cache if the backing storage
+ * has not changed since the load operation has begun.
+ * This guarantees that no read operation can shadow a write to the cache that happens
+ * after it had begun.
+ */
+ private static class Cache {
+ private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
+ private final CacheKey mCacheKey = new CacheKey();
+ private int mVersion = 0;
+
+ String peekKeyValue(String key, String defaultValue, int userId) {
+ Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
+ return cached == DEFAULT ? defaultValue : (String) cached;
+ }
+
+ boolean hasKeyValue(String key, int userId) {
+ return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
+ }
+
+ void putKeyValue(String key, String value, int userId) {
+ put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
+ }
+
+ void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
+ putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
+ }
+
+ byte[] peekFile(String fileName) {
+ return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
+ }
+
+ boolean hasFile(String fileName) {
+ return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
+ }
+
+ void putFile(String key, byte[] value) {
+ put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
+ }
+
+ void putFileIfUnchanged(String key, byte[] value, int version) {
+ putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
+ }
+
+ void setFetched(int userId) {
+ put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
+ }
+
+ boolean isFetched(int userId) {
+ return contains(CacheKey.TYPE_FETCHED, "", userId);
+ }
+
+
+ private synchronized void put(int type, String key, Object value, int userId) {
+ // Create a new CachKey here because it may be saved in the map if the key is absent.
+ mCache.put(new CacheKey().set(type, key, userId), value);
+ mVersion++;
+ }
+
+ private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
+ int version) {
+ if (!contains(type, key, userId) && mVersion == version) {
+ put(type, key, value, userId);
+ }
+ }
+
+ private synchronized boolean contains(int type, String key, int userId) {
+ return mCache.containsKey(mCacheKey.set(type, key, userId));
+ }
+
+ private synchronized Object peek(int type, String key, int userId) {
+ return mCache.get(mCacheKey.set(type, key, userId));
+ }
+
+ private synchronized int getVersion() {
+ return mVersion;
+ }
+
+ synchronized void removeUser(int userId) {
+ for (int i = mCache.size() - 1; i >= 0; i--) {
+ if (mCache.keyAt(i).userId == userId) {
+ mCache.removeAt(i);
+ }
+ }
+
+ // Make sure in-flight loads can't write to cache.
+ mVersion++;
+ }
+
+
+ private static final class CacheKey {
+ static final int TYPE_KEY_VALUE = 0;
+ static final int TYPE_FILE = 1;
+ static final int TYPE_FETCHED = 2;
+
+ String key;
+ int userId;
+ int type;
+
+ public CacheKey set(int type, String key, int userId) {
+ this.type = type;
+ this.key = key;
+ this.userId = userId;
+ return this;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CacheKey))
+ return false;
+ CacheKey o = (CacheKey) obj;
+ return userId == o.userId && type == o.type && key.equals(o.key);
+ }
+
+ @Override
+ public int hashCode() {
+ return key.hashCode() ^ userId ^ type;
+ }
+ }
+ }
}