blob: 816c791d47faa9321304332ec48bea8cc3f71895 [file] [log] [blame]
Adrian Roos261d5ab2014-10-29 14:42:38 +01001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.server;
18
Adrian Roose5424992014-11-07 21:47:17 +010019import com.android.internal.annotations.VisibleForTesting;
Clara Bayarri10ad84a2015-12-01 17:38:05 +000020import com.android.internal.widget.LockPatternUtils;
Adrian Roose5424992014-11-07 21:47:17 +010021
Adrian Roos261d5ab2014-10-29 14:42:38 +010022import android.content.ContentValues;
23import android.content.Context;
24import android.content.pm.UserInfo;
25import android.database.Cursor;
26import android.database.sqlite.SQLiteDatabase;
27import android.database.sqlite.SQLiteOpenHelper;
28import android.os.Environment;
29import android.os.UserManager;
30import android.util.ArrayMap;
31import android.util.Log;
32import android.util.Slog;
33
34import java.io.File;
35import java.io.IOException;
36import java.io.RandomAccessFile;
37
38import static android.content.Context.USER_SERVICE;
39
40/**
41 * Storage for the lock settings service.
42 */
43class LockSettingsStorage {
44
45 private static final String TAG = "LockSettingsStorage";
46 private static final String TABLE = "locksettings";
47
48 private static final String COLUMN_KEY = "name";
49 private static final String COLUMN_USERID = "user";
50 private static final String COLUMN_VALUE = "value";
51
52 private static final String[] COLUMNS_FOR_QUERY = {
53 COLUMN_VALUE
54 };
Adrian Roos3dcae682014-10-29 14:43:56 +010055 private static final String[] COLUMNS_FOR_PREFETCH = {
56 COLUMN_KEY, COLUMN_VALUE
57 };
Adrian Roos261d5ab2014-10-29 14:42:38 +010058
59 private static final String SYSTEM_DIRECTORY = "/system/";
Andres Moralese40bad82015-05-28 14:21:36 -070060 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key";
61 private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key";
Andres Morales8fa56652015-03-31 09:19:50 -070062 private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key";
63 private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key";
64 private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
Adrian Roos261d5ab2014-10-29 14:42:38 +010065
Adrian Roos3dcae682014-10-29 14:43:56 +010066 private static final Object DEFAULT = new Object();
67
Adrian Roos261d5ab2014-10-29 14:42:38 +010068 private final DatabaseHelper mOpenHelper;
69 private final Context mContext;
Adrian Roos3dcae682014-10-29 14:43:56 +010070 private final Cache mCache = new Cache();
Adrian Roos261d5ab2014-10-29 14:42:38 +010071 private final Object mFileWriteLock = new Object();
72
Andres Morales8fa56652015-03-31 09:19:50 -070073 private int mStoredCredentialType;
Clara Bayarria1771112015-12-18 16:29:18 +000074 private LockPatternUtils mLockPatternUtils;
Andres Morales8fa56652015-03-31 09:19:50 -070075
76 class CredentialHash {
77 static final int TYPE_NONE = -1;
78 static final int TYPE_PATTERN = 1;
79 static final int TYPE_PASSWORD = 2;
80
81 static final int VERSION_LEGACY = 0;
82 static final int VERSION_GATEKEEPER = 1;
83
84 CredentialHash(byte[] hash, int version) {
85 this.hash = hash;
86 this.version = version;
Andres Moralese40bad82015-05-28 14:21:36 -070087 this.isBaseZeroPattern = false;
88 }
89
90 CredentialHash(byte[] hash, boolean isBaseZeroPattern) {
91 this.hash = hash;
92 this.version = VERSION_GATEKEEPER;
93 this.isBaseZeroPattern = isBaseZeroPattern;
Andres Morales8fa56652015-03-31 09:19:50 -070094 }
95
96 byte[] hash;
97 int version;
Andres Moralese40bad82015-05-28 14:21:36 -070098 boolean isBaseZeroPattern;
Andres Morales8fa56652015-03-31 09:19:50 -070099 }
100
Adrian Roos3dcae682014-10-29 14:43:56 +0100101 public LockSettingsStorage(Context context, Callback callback) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100102 mContext = context;
103 mOpenHelper = new DatabaseHelper(context, callback);
Clara Bayarria1771112015-12-18 16:29:18 +0000104 mLockPatternUtils = new LockPatternUtils(context);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100105 }
106
Adrian Roos3dcae682014-10-29 14:43:56 +0100107 public void writeKeyValue(String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100108 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
109 }
110
Adrian Roos3dcae682014-10-29 14:43:56 +0100111 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100112 ContentValues cv = new ContentValues();
113 cv.put(COLUMN_KEY, key);
114 cv.put(COLUMN_USERID, userId);
115 cv.put(COLUMN_VALUE, value);
116
117 db.beginTransaction();
118 try {
119 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
120 new String[] {key, Integer.toString(userId)});
121 db.insert(TABLE, null, cv);
122 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100123 mCache.putKeyValue(key, value, userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100124 } finally {
125 db.endTransaction();
126 }
127
128 }
129
Adrian Roos3dcae682014-10-29 14:43:56 +0100130 public String readKeyValue(String key, String defaultValue, int userId) {
131 int version;
132 synchronized (mCache) {
133 if (mCache.hasKeyValue(key, userId)) {
134 return mCache.peekKeyValue(key, defaultValue, userId);
135 }
136 version = mCache.getVersion();
137 }
138
Adrian Roos261d5ab2014-10-29 14:42:38 +0100139 Cursor cursor;
Adrian Roos3dcae682014-10-29 14:43:56 +0100140 Object result = DEFAULT;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100141 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
142 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
143 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
144 new String[] { Integer.toString(userId), key },
145 null, null, null)) != null) {
146 if (cursor.moveToFirst()) {
147 result = cursor.getString(0);
148 }
149 cursor.close();
150 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100151 mCache.putKeyValueIfUnchanged(key, result, userId, version);
152 return result == DEFAULT ? defaultValue : (String) result;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100153 }
154
Adrian Roos3dcae682014-10-29 14:43:56 +0100155 public void prefetchUser(int userId) {
156 int version;
157 synchronized (mCache) {
158 if (mCache.isFetched(userId)) {
159 return;
160 }
161 mCache.setFetched(userId);
162 version = mCache.getVersion();
163 }
164
165 Cursor cursor;
166 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
167 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
168 COLUMN_USERID + "=?",
169 new String[] { Integer.toString(userId) },
170 null, null, null)) != null) {
171 while (cursor.moveToNext()) {
172 String key = cursor.getString(0);
173 String value = cursor.getString(1);
174 mCache.putKeyValueIfUnchanged(key, value, userId, version);
175 }
176 cursor.close();
177 }
178
179 // Populate cache by reading the password and pattern files.
180 readPasswordHash(userId);
181 readPatternHash(userId);
182 }
183
Andres Morales8fa56652015-03-31 09:19:50 -0700184 public int getStoredCredentialType(int userId) {
185 if (mStoredCredentialType != 0) {
186 return mStoredCredentialType;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100187 }
Andres Morales8fa56652015-03-31 09:19:50 -0700188
189 CredentialHash pattern = readPatternHash(userId);
190 if (pattern == null) {
191 if (readPasswordHash(userId) != null) {
192 mStoredCredentialType = CredentialHash.TYPE_PASSWORD;
193 } else {
194 mStoredCredentialType = CredentialHash.TYPE_NONE;
195 }
196 } else {
197 CredentialHash password = readPasswordHash(userId);
198 if (password != null) {
199 // Both will never be GateKeeper
200 if (password.version == CredentialHash.VERSION_GATEKEEPER) {
201 mStoredCredentialType = CredentialHash.TYPE_PASSWORD;
202 } else {
203 mStoredCredentialType = CredentialHash.TYPE_PATTERN;
204 }
205 } else {
206 mStoredCredentialType = CredentialHash.TYPE_PATTERN;
207 }
208 }
209
210 return mStoredCredentialType;
211 }
212
213
214 public CredentialHash readPasswordHash(int userId) {
215 byte[] stored = readFile(getLockPasswordFilename(userId));
216 if (stored != null && stored.length > 0) {
217 return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER);
218 }
219
220 stored = readFile(getLegacyLockPasswordFilename(userId));
221 if (stored != null && stored.length > 0) {
222 return new CredentialHash(stored, CredentialHash.VERSION_LEGACY);
223 }
224
Adrian Roos261d5ab2014-10-29 14:42:38 +0100225 return null;
226 }
227
Andres Morales8fa56652015-03-31 09:19:50 -0700228 public CredentialHash readPatternHash(int userId) {
229 byte[] stored = readFile(getLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100230 if (stored != null && stored.length > 0) {
Andres Morales8fa56652015-03-31 09:19:50 -0700231 return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100232 }
Andres Morales8fa56652015-03-31 09:19:50 -0700233
Andres Moralese40bad82015-05-28 14:21:36 -0700234 stored = readFile(getBaseZeroLockPatternFilename(userId));
235 if (stored != null && stored.length > 0) {
236 return new CredentialHash(stored, true);
237 }
238
Andres Morales8fa56652015-03-31 09:19:50 -0700239 stored = readFile(getLegacyLockPatternFilename(userId));
240 if (stored != null && stored.length > 0) {
241 return new CredentialHash(stored, CredentialHash.VERSION_LEGACY);
242 }
243
Adrian Roos261d5ab2014-10-29 14:42:38 +0100244 return null;
245 }
246
Andres Moralese40bad82015-05-28 14:21:36 -0700247
Adrian Roos3dcae682014-10-29 14:43:56 +0100248 public boolean hasPassword(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700249 return hasFile(getLockPasswordFilename(userId)) ||
250 hasFile(getLegacyLockPasswordFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100251 }
252
Adrian Roos3dcae682014-10-29 14:43:56 +0100253 public boolean hasPattern(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700254 return hasFile(getLockPatternFilename(userId)) ||
Andres Moralese40bad82015-05-28 14:21:36 -0700255 hasFile(getBaseZeroLockPatternFilename(userId)) ||
Andres Morales8fa56652015-03-31 09:19:50 -0700256 hasFile(getLegacyLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100257 }
258
Adrian Roos3dcae682014-10-29 14:43:56 +0100259 private boolean hasFile(String name) {
260 byte[] contents = readFile(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100261 return contents != null && contents.length > 0;
262 }
263
Adrian Roos3dcae682014-10-29 14:43:56 +0100264 private byte[] readFile(String name) {
265 int version;
266 synchronized (mCache) {
267 if (mCache.hasFile(name)) {
268 return mCache.peekFile(name);
269 }
270 version = mCache.getVersion();
271 }
272
Adrian Roos261d5ab2014-10-29 14:42:38 +0100273 RandomAccessFile raf = null;
274 byte[] stored = null;
275 try {
276 raf = new RandomAccessFile(name, "r");
277 stored = new byte[(int) raf.length()];
278 raf.readFully(stored, 0, stored.length);
279 raf.close();
280 } catch (IOException e) {
281 Slog.e(TAG, "Cannot read file " + e);
282 } finally {
283 if (raf != null) {
284 try {
285 raf.close();
286 } catch (IOException e) {
287 Slog.e(TAG, "Error closing file " + e);
288 }
289 }
290 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100291 mCache.putFileIfUnchanged(name, stored, version);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100292 return stored;
293 }
294
Adrian Roos3dcae682014-10-29 14:43:56 +0100295 private void writeFile(String name, byte[] hash) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100296 synchronized (mFileWriteLock) {
297 RandomAccessFile raf = null;
298 try {
299 // Write the hash to file
300 raf = new RandomAccessFile(name, "rw");
301 // Truncate the file if pattern is null, to clear the lock
302 if (hash == null || hash.length == 0) {
303 raf.setLength(0);
304 } else {
305 raf.write(hash, 0, hash.length);
306 }
307 raf.close();
308 } catch (IOException e) {
309 Slog.e(TAG, "Error writing to file " + e);
310 } finally {
311 if (raf != null) {
312 try {
313 raf.close();
314 } catch (IOException e) {
315 Slog.e(TAG, "Error closing file " + e);
316 }
317 }
318 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100319 mCache.putFile(name, hash);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100320 }
321 }
322
Andres Moralese40bad82015-05-28 14:21:36 -0700323 private void deleteFile(String name) {
324 File f = new File(name);
325 if (f != null) {
326 f.delete();
327 }
328 }
329
Adrian Roos261d5ab2014-10-29 14:42:38 +0100330 public void writePatternHash(byte[] hash, int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700331 mStoredCredentialType = hash == null
332 ? CredentialHash.TYPE_NONE
333 : CredentialHash.TYPE_PATTERN;
Adrian Roos3dcae682014-10-29 14:43:56 +0100334 writeFile(getLockPatternFilename(userId), hash);
Robin Lee68e4ba42015-03-10 12:34:28 +0000335 clearPasswordHash(userId);
336 }
337
338 private void clearPatternHash(int userId) {
339 writeFile(getLockPatternFilename(userId), null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100340 }
341
342 public void writePasswordHash(byte[] hash, int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700343 mStoredCredentialType = hash == null
344 ? CredentialHash.TYPE_NONE
345 : CredentialHash.TYPE_PASSWORD;
Adrian Roos3dcae682014-10-29 14:43:56 +0100346 writeFile(getLockPasswordFilename(userId), hash);
Robin Lee68e4ba42015-03-10 12:34:28 +0000347 clearPatternHash(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100348 }
349
Robin Lee68e4ba42015-03-10 12:34:28 +0000350 private void clearPasswordHash(int userId) {
351 writeFile(getLockPasswordFilename(userId), null);
352 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100353
Adrian Roose5424992014-11-07 21:47:17 +0100354 @VisibleForTesting
355 String getLockPatternFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100356 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100357 }
358
Adrian Roose5424992014-11-07 21:47:17 +0100359 @VisibleForTesting
360 String getLockPasswordFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100361 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
362 }
363
Andres Morales8fa56652015-03-31 09:19:50 -0700364 @VisibleForTesting
365 String getLegacyLockPatternFilename(int userId) {
366 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
367 }
368
369 @VisibleForTesting
370 String getLegacyLockPasswordFilename(int userId) {
371 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
372 }
373
Andres Moralese40bad82015-05-28 14:21:36 -0700374 private String getBaseZeroLockPatternFilename(int userId) {
375 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
376 }
377
Adrian Roos3dcae682014-10-29 14:43:56 +0100378 private String getLockCredentialFilePathForUser(int userId, String basename) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100379 userId = getUserParentOrSelfId(userId);
380 String dataSystemDirectory =
381 android.os.Environment.getDataDirectory().getAbsolutePath() +
382 SYSTEM_DIRECTORY;
383 if (userId == 0) {
384 // Leave it in the same place for user 0
Adrian Roos3dcae682014-10-29 14:43:56 +0100385 return dataSystemDirectory + basename;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100386 } else {
Adrian Roos3dcae682014-10-29 14:43:56 +0100387 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
Adrian Roos261d5ab2014-10-29 14:42:38 +0100388 }
389 }
390
391 private int getUserParentOrSelfId(int userId) {
Clara Bayarri965da392015-10-28 17:53:53 +0000392 // Device supports per user encryption, so lock is applied to the given user.
Clara Bayarria1771112015-12-18 16:29:18 +0000393 if (mLockPatternUtils.isSeparateProfileChallengeEnabled(userId)) {
Clara Bayarri07b668e2015-10-13 12:28:26 +0100394 return userId;
395 }
396 // Device uses Block Based Encryption, and the parent user's lock is used for the whole
397 // device.
Adrian Roos261d5ab2014-10-29 14:42:38 +0100398 if (userId != 0) {
399 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
400 final UserInfo pi = um.getProfileParent(userId);
401 if (pi != null) {
402 return pi.id;
403 }
404 }
405 return userId;
406 }
407
Adrian Roos261d5ab2014-10-29 14:42:38 +0100408 public void removeUser(int userId) {
409 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
410
411 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
412 final UserInfo parentInfo = um.getProfileParent(userId);
413
Robin Lee68e4ba42015-03-10 12:34:28 +0000414 if (parentInfo == null) {
415 // This user owns its lock settings files - safe to delete them
416 synchronized (mFileWriteLock) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100417 String name = getLockPasswordFilename(userId);
418 File file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100419 if (file.exists()) {
420 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100421 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100422 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100423 name = getLockPatternFilename(userId);
424 file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100425 if (file.exists()) {
426 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100427 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100428 }
429 }
430 }
431
432 try {
433 db.beginTransaction();
434 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
435 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100436 mCache.removeUser(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100437 } finally {
438 db.endTransaction();
439 }
440 }
441
Adrian Roose5424992014-11-07 21:47:17 +0100442 @VisibleForTesting
443 void closeDatabase() {
444 mOpenHelper.close();
445 }
446
447 @VisibleForTesting
448 void clearCache() {
449 mCache.clear();
450 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100451
Adrian Roos3dcae682014-10-29 14:43:56 +0100452 public interface Callback {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100453 void initialize(SQLiteDatabase db);
454 }
455
456 class DatabaseHelper extends SQLiteOpenHelper {
457 private static final String TAG = "LockSettingsDB";
458 private static final String DATABASE_NAME = "locksettings.db";
459
460 private static final int DATABASE_VERSION = 2;
461
462 private final Callback mCallback;
463
464 public DatabaseHelper(Context context, Callback callback) {
465 super(context, DATABASE_NAME, null, DATABASE_VERSION);
466 setWriteAheadLoggingEnabled(true);
467 mCallback = callback;
468 }
469
470 private void createTable(SQLiteDatabase db) {
471 db.execSQL("CREATE TABLE " + TABLE + " (" +
472 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
473 COLUMN_KEY + " TEXT," +
474 COLUMN_USERID + " INTEGER," +
475 COLUMN_VALUE + " TEXT" +
476 ");");
477 }
478
479 @Override
480 public void onCreate(SQLiteDatabase db) {
481 createTable(db);
482 mCallback.initialize(db);
483 }
484
485 @Override
486 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
487 int upgradeVersion = oldVersion;
488 if (upgradeVersion == 1) {
489 // Previously migrated lock screen widget settings. Now defunct.
490 upgradeVersion = 2;
491 }
492
493 if (upgradeVersion != DATABASE_VERSION) {
494 Log.w(TAG, "Failed to upgrade database!");
495 }
496 }
497 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100498
499 /**
500 * Cache consistency model:
501 * - Writes to storage write directly to the cache, but this MUST happen within the atomic
502 * section either provided by the database transaction or mWriteLock, such that writes to the
503 * cache and writes to the backing storage are guaranteed to occur in the same order
504 *
505 * - Reads can populate the cache, but because they are no strong ordering guarantees with
506 * respect to writes this precaution is taken:
507 * - The cache is assigned a version number that increases every time the cache is modified.
508 * Reads from backing storage can only populate the cache if the backing storage
509 * has not changed since the load operation has begun.
510 * This guarantees that no read operation can shadow a write to the cache that happens
511 * after it had begun.
512 */
513 private static class Cache {
514 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
515 private final CacheKey mCacheKey = new CacheKey();
516 private int mVersion = 0;
517
518 String peekKeyValue(String key, String defaultValue, int userId) {
519 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
520 return cached == DEFAULT ? defaultValue : (String) cached;
521 }
522
523 boolean hasKeyValue(String key, int userId) {
524 return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
525 }
526
527 void putKeyValue(String key, String value, int userId) {
528 put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
529 }
530
531 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
532 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
533 }
534
535 byte[] peekFile(String fileName) {
536 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
537 }
538
539 boolean hasFile(String fileName) {
540 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
541 }
542
543 void putFile(String key, byte[] value) {
544 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
545 }
546
547 void putFileIfUnchanged(String key, byte[] value, int version) {
548 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
549 }
550
551 void setFetched(int userId) {
552 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
553 }
554
555 boolean isFetched(int userId) {
556 return contains(CacheKey.TYPE_FETCHED, "", userId);
557 }
558
559
560 private synchronized void put(int type, String key, Object value, int userId) {
561 // Create a new CachKey here because it may be saved in the map if the key is absent.
562 mCache.put(new CacheKey().set(type, key, userId), value);
563 mVersion++;
564 }
565
566 private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
567 int version) {
568 if (!contains(type, key, userId) && mVersion == version) {
569 put(type, key, value, userId);
570 }
571 }
572
573 private synchronized boolean contains(int type, String key, int userId) {
574 return mCache.containsKey(mCacheKey.set(type, key, userId));
575 }
576
577 private synchronized Object peek(int type, String key, int userId) {
578 return mCache.get(mCacheKey.set(type, key, userId));
579 }
580
581 private synchronized int getVersion() {
582 return mVersion;
583 }
584
585 synchronized void removeUser(int userId) {
586 for (int i = mCache.size() - 1; i >= 0; i--) {
587 if (mCache.keyAt(i).userId == userId) {
588 mCache.removeAt(i);
589 }
590 }
591
592 // Make sure in-flight loads can't write to cache.
593 mVersion++;
594 }
595
Adrian Roose5424992014-11-07 21:47:17 +0100596 synchronized void clear() {
597 mCache.clear();
598 mVersion++;
599 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100600
601 private static final class CacheKey {
602 static final int TYPE_KEY_VALUE = 0;
603 static final int TYPE_FILE = 1;
604 static final int TYPE_FETCHED = 2;
605
606 String key;
607 int userId;
608 int type;
609
610 public CacheKey set(int type, String key, int userId) {
611 this.type = type;
612 this.key = key;
613 this.userId = userId;
614 return this;
615 }
616
617 @Override
618 public boolean equals(Object obj) {
619 if (!(obj instanceof CacheKey))
620 return false;
621 CacheKey o = (CacheKey) obj;
622 return userId == o.userId && type == o.type && key.equals(o.key);
623 }
624
625 @Override
626 public int hashCode() {
627 return key.hashCode() ^ userId ^ type;
628 }
629 }
630 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100631}