blob: de48e714b4c555025e0dc9fa0e93c9897eb1355f [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;
20
Adrian Roos261d5ab2014-10-29 14:42:38 +010021import android.content.ContentValues;
22import android.content.Context;
23import android.content.pm.UserInfo;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.os.Environment;
28import android.os.UserManager;
29import android.util.ArrayMap;
30import android.util.Log;
31import android.util.Slog;
32
33import java.io.File;
34import java.io.IOException;
35import java.io.RandomAccessFile;
36
37import static android.content.Context.USER_SERVICE;
38
39/**
40 * Storage for the lock settings service.
41 */
42class LockSettingsStorage {
43
44 private static final String TAG = "LockSettingsStorage";
45 private static final String TABLE = "locksettings";
46
47 private static final String COLUMN_KEY = "name";
48 private static final String COLUMN_USERID = "user";
49 private static final String COLUMN_VALUE = "value";
50
51 private static final String[] COLUMNS_FOR_QUERY = {
52 COLUMN_VALUE
53 };
Adrian Roos3dcae682014-10-29 14:43:56 +010054 private static final String[] COLUMNS_FOR_PREFETCH = {
55 COLUMN_KEY, COLUMN_VALUE
56 };
Adrian Roos261d5ab2014-10-29 14:42:38 +010057
58 private static final String SYSTEM_DIRECTORY = "/system/";
Andres Moralese40bad82015-05-28 14:21:36 -070059 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key";
60 private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key";
Andres Morales8fa56652015-03-31 09:19:50 -070061 private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key";
62 private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key";
63 private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
Adrian Roos261d5ab2014-10-29 14:42:38 +010064
Adrian Roos3dcae682014-10-29 14:43:56 +010065 private static final Object DEFAULT = new Object();
66
Adrian Roos261d5ab2014-10-29 14:42:38 +010067 private final DatabaseHelper mOpenHelper;
68 private final Context mContext;
Adrian Roos3dcae682014-10-29 14:43:56 +010069 private final Cache mCache = new Cache();
Adrian Roos261d5ab2014-10-29 14:42:38 +010070 private final Object mFileWriteLock = new Object();
71
Andres Morales8fa56652015-03-31 09:19:50 -070072 private int mStoredCredentialType;
73
74 class CredentialHash {
75 static final int TYPE_NONE = -1;
76 static final int TYPE_PATTERN = 1;
77 static final int TYPE_PASSWORD = 2;
78
79 static final int VERSION_LEGACY = 0;
80 static final int VERSION_GATEKEEPER = 1;
81
82 CredentialHash(byte[] hash, int version) {
83 this.hash = hash;
84 this.version = version;
Andres Moralese40bad82015-05-28 14:21:36 -070085 this.isBaseZeroPattern = false;
86 }
87
88 CredentialHash(byte[] hash, boolean isBaseZeroPattern) {
89 this.hash = hash;
90 this.version = VERSION_GATEKEEPER;
91 this.isBaseZeroPattern = isBaseZeroPattern;
Andres Morales8fa56652015-03-31 09:19:50 -070092 }
93
94 byte[] hash;
95 int version;
Andres Moralese40bad82015-05-28 14:21:36 -070096 boolean isBaseZeroPattern;
Andres Morales8fa56652015-03-31 09:19:50 -070097 }
98
Adrian Roos3dcae682014-10-29 14:43:56 +010099 public LockSettingsStorage(Context context, Callback callback) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100100 mContext = context;
101 mOpenHelper = new DatabaseHelper(context, callback);
102 }
103
Adrian Roos3dcae682014-10-29 14:43:56 +0100104 public void writeKeyValue(String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100105 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
106 }
107
Adrian Roos3dcae682014-10-29 14:43:56 +0100108 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100109 ContentValues cv = new ContentValues();
110 cv.put(COLUMN_KEY, key);
111 cv.put(COLUMN_USERID, userId);
112 cv.put(COLUMN_VALUE, value);
113
114 db.beginTransaction();
115 try {
116 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
117 new String[] {key, Integer.toString(userId)});
118 db.insert(TABLE, null, cv);
119 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100120 mCache.putKeyValue(key, value, userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100121 } finally {
122 db.endTransaction();
123 }
124
125 }
126
Adrian Roos3dcae682014-10-29 14:43:56 +0100127 public String readKeyValue(String key, String defaultValue, int userId) {
128 int version;
129 synchronized (mCache) {
130 if (mCache.hasKeyValue(key, userId)) {
131 return mCache.peekKeyValue(key, defaultValue, userId);
132 }
133 version = mCache.getVersion();
134 }
135
Adrian Roos261d5ab2014-10-29 14:42:38 +0100136 Cursor cursor;
Adrian Roos3dcae682014-10-29 14:43:56 +0100137 Object result = DEFAULT;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100138 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
139 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
140 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
141 new String[] { Integer.toString(userId), key },
142 null, null, null)) != null) {
143 if (cursor.moveToFirst()) {
144 result = cursor.getString(0);
145 }
146 cursor.close();
147 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100148 mCache.putKeyValueIfUnchanged(key, result, userId, version);
149 return result == DEFAULT ? defaultValue : (String) result;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100150 }
151
Adrian Roos3dcae682014-10-29 14:43:56 +0100152 public void prefetchUser(int userId) {
153 int version;
154 synchronized (mCache) {
155 if (mCache.isFetched(userId)) {
156 return;
157 }
158 mCache.setFetched(userId);
159 version = mCache.getVersion();
160 }
161
162 Cursor cursor;
163 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
164 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
165 COLUMN_USERID + "=?",
166 new String[] { Integer.toString(userId) },
167 null, null, null)) != null) {
168 while (cursor.moveToNext()) {
169 String key = cursor.getString(0);
170 String value = cursor.getString(1);
171 mCache.putKeyValueIfUnchanged(key, value, userId, version);
172 }
173 cursor.close();
174 }
175
176 // Populate cache by reading the password and pattern files.
177 readPasswordHash(userId);
178 readPatternHash(userId);
179 }
180
Andres Morales8fa56652015-03-31 09:19:50 -0700181 public int getStoredCredentialType(int userId) {
182 if (mStoredCredentialType != 0) {
183 return mStoredCredentialType;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100184 }
Andres Morales8fa56652015-03-31 09:19:50 -0700185
186 CredentialHash pattern = readPatternHash(userId);
187 if (pattern == null) {
188 if (readPasswordHash(userId) != null) {
189 mStoredCredentialType = CredentialHash.TYPE_PASSWORD;
190 } else {
191 mStoredCredentialType = CredentialHash.TYPE_NONE;
192 }
193 } else {
194 CredentialHash password = readPasswordHash(userId);
195 if (password != null) {
196 // Both will never be GateKeeper
197 if (password.version == CredentialHash.VERSION_GATEKEEPER) {
198 mStoredCredentialType = CredentialHash.TYPE_PASSWORD;
199 } else {
200 mStoredCredentialType = CredentialHash.TYPE_PATTERN;
201 }
202 } else {
203 mStoredCredentialType = CredentialHash.TYPE_PATTERN;
204 }
205 }
206
207 return mStoredCredentialType;
208 }
209
210
211 public CredentialHash readPasswordHash(int userId) {
212 byte[] stored = readFile(getLockPasswordFilename(userId));
213 if (stored != null && stored.length > 0) {
214 return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER);
215 }
216
217 stored = readFile(getLegacyLockPasswordFilename(userId));
218 if (stored != null && stored.length > 0) {
219 return new CredentialHash(stored, CredentialHash.VERSION_LEGACY);
220 }
221
Adrian Roos261d5ab2014-10-29 14:42:38 +0100222 return null;
223 }
224
Andres Morales8fa56652015-03-31 09:19:50 -0700225 public CredentialHash readPatternHash(int userId) {
226 byte[] stored = readFile(getLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100227 if (stored != null && stored.length > 0) {
Andres Morales8fa56652015-03-31 09:19:50 -0700228 return new CredentialHash(stored, CredentialHash.VERSION_GATEKEEPER);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100229 }
Andres Morales8fa56652015-03-31 09:19:50 -0700230
Andres Moralese40bad82015-05-28 14:21:36 -0700231 stored = readFile(getBaseZeroLockPatternFilename(userId));
232 if (stored != null && stored.length > 0) {
233 return new CredentialHash(stored, true);
234 }
235
Andres Morales8fa56652015-03-31 09:19:50 -0700236 stored = readFile(getLegacyLockPatternFilename(userId));
237 if (stored != null && stored.length > 0) {
238 return new CredentialHash(stored, CredentialHash.VERSION_LEGACY);
239 }
240
Adrian Roos261d5ab2014-10-29 14:42:38 +0100241 return null;
242 }
243
Andres Moralese40bad82015-05-28 14:21:36 -0700244
Adrian Roos3dcae682014-10-29 14:43:56 +0100245 public boolean hasPassword(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700246 return hasFile(getLockPasswordFilename(userId)) ||
247 hasFile(getLegacyLockPasswordFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100248 }
249
Adrian Roos3dcae682014-10-29 14:43:56 +0100250 public boolean hasPattern(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700251 return hasFile(getLockPatternFilename(userId)) ||
Andres Moralese40bad82015-05-28 14:21:36 -0700252 hasFile(getBaseZeroLockPatternFilename(userId)) ||
Andres Morales8fa56652015-03-31 09:19:50 -0700253 hasFile(getLegacyLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100254 }
255
Adrian Roos3dcae682014-10-29 14:43:56 +0100256 private boolean hasFile(String name) {
257 byte[] contents = readFile(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100258 return contents != null && contents.length > 0;
259 }
260
Adrian Roos3dcae682014-10-29 14:43:56 +0100261 private byte[] readFile(String name) {
262 int version;
263 synchronized (mCache) {
264 if (mCache.hasFile(name)) {
265 return mCache.peekFile(name);
266 }
267 version = mCache.getVersion();
268 }
269
Adrian Roos261d5ab2014-10-29 14:42:38 +0100270 RandomAccessFile raf = null;
271 byte[] stored = null;
272 try {
273 raf = new RandomAccessFile(name, "r");
274 stored = new byte[(int) raf.length()];
275 raf.readFully(stored, 0, stored.length);
276 raf.close();
277 } catch (IOException e) {
278 Slog.e(TAG, "Cannot read file " + e);
279 } finally {
280 if (raf != null) {
281 try {
282 raf.close();
283 } catch (IOException e) {
284 Slog.e(TAG, "Error closing file " + e);
285 }
286 }
287 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100288 mCache.putFileIfUnchanged(name, stored, version);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100289 return stored;
290 }
291
Adrian Roos3dcae682014-10-29 14:43:56 +0100292 private void writeFile(String name, byte[] hash) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100293 synchronized (mFileWriteLock) {
294 RandomAccessFile raf = null;
295 try {
296 // Write the hash to file
297 raf = new RandomAccessFile(name, "rw");
298 // Truncate the file if pattern is null, to clear the lock
299 if (hash == null || hash.length == 0) {
300 raf.setLength(0);
301 } else {
302 raf.write(hash, 0, hash.length);
303 }
304 raf.close();
305 } catch (IOException e) {
306 Slog.e(TAG, "Error writing to file " + e);
307 } finally {
308 if (raf != null) {
309 try {
310 raf.close();
311 } catch (IOException e) {
312 Slog.e(TAG, "Error closing file " + e);
313 }
314 }
315 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100316 mCache.putFile(name, hash);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100317 }
318 }
319
Andres Moralese40bad82015-05-28 14:21:36 -0700320 private void deleteFile(String name) {
321 File f = new File(name);
322 if (f != null) {
323 f.delete();
324 }
325 }
326
Adrian Roos261d5ab2014-10-29 14:42:38 +0100327 public void writePatternHash(byte[] hash, int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700328 mStoredCredentialType = hash == null
329 ? CredentialHash.TYPE_NONE
330 : CredentialHash.TYPE_PATTERN;
Adrian Roos3dcae682014-10-29 14:43:56 +0100331 writeFile(getLockPatternFilename(userId), hash);
Robin Lee68e4ba42015-03-10 12:34:28 +0000332 clearPasswordHash(userId);
333 }
334
335 private void clearPatternHash(int userId) {
336 writeFile(getLockPatternFilename(userId), null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100337 }
338
339 public void writePasswordHash(byte[] hash, int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700340 mStoredCredentialType = hash == null
341 ? CredentialHash.TYPE_NONE
342 : CredentialHash.TYPE_PASSWORD;
Adrian Roos3dcae682014-10-29 14:43:56 +0100343 writeFile(getLockPasswordFilename(userId), hash);
Robin Lee68e4ba42015-03-10 12:34:28 +0000344 clearPatternHash(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100345 }
346
Robin Lee68e4ba42015-03-10 12:34:28 +0000347 private void clearPasswordHash(int userId) {
348 writeFile(getLockPasswordFilename(userId), null);
349 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100350
Adrian Roose5424992014-11-07 21:47:17 +0100351 @VisibleForTesting
352 String getLockPatternFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100353 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100354 }
355
Adrian Roose5424992014-11-07 21:47:17 +0100356 @VisibleForTesting
357 String getLockPasswordFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100358 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
359 }
360
Andres Morales8fa56652015-03-31 09:19:50 -0700361 @VisibleForTesting
362 String getLegacyLockPatternFilename(int userId) {
363 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
364 }
365
366 @VisibleForTesting
367 String getLegacyLockPasswordFilename(int userId) {
368 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
369 }
370
Andres Moralese40bad82015-05-28 14:21:36 -0700371 private String getBaseZeroLockPatternFilename(int userId) {
372 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
373 }
374
Adrian Roos3dcae682014-10-29 14:43:56 +0100375 private String getLockCredentialFilePathForUser(int userId, String basename) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100376 userId = getUserParentOrSelfId(userId);
377 String dataSystemDirectory =
378 android.os.Environment.getDataDirectory().getAbsolutePath() +
379 SYSTEM_DIRECTORY;
380 if (userId == 0) {
381 // Leave it in the same place for user 0
Adrian Roos3dcae682014-10-29 14:43:56 +0100382 return dataSystemDirectory + basename;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100383 } else {
Adrian Roos3dcae682014-10-29 14:43:56 +0100384 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
Adrian Roos261d5ab2014-10-29 14:42:38 +0100385 }
386 }
387
388 private int getUserParentOrSelfId(int userId) {
389 if (userId != 0) {
390 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
391 final UserInfo pi = um.getProfileParent(userId);
392 if (pi != null) {
393 return pi.id;
394 }
395 }
396 return userId;
397 }
398
Adrian Roos261d5ab2014-10-29 14:42:38 +0100399 public void removeUser(int userId) {
400 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
401
402 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
403 final UserInfo parentInfo = um.getProfileParent(userId);
404
Robin Lee68e4ba42015-03-10 12:34:28 +0000405 if (parentInfo == null) {
406 // This user owns its lock settings files - safe to delete them
407 synchronized (mFileWriteLock) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100408 String name = getLockPasswordFilename(userId);
409 File file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100410 if (file.exists()) {
411 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100412 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100413 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100414 name = getLockPatternFilename(userId);
415 file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100416 if (file.exists()) {
417 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100418 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100419 }
420 }
421 }
422
423 try {
424 db.beginTransaction();
425 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
426 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100427 mCache.removeUser(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100428 } finally {
429 db.endTransaction();
430 }
431 }
432
Adrian Roose5424992014-11-07 21:47:17 +0100433 @VisibleForTesting
434 void closeDatabase() {
435 mOpenHelper.close();
436 }
437
438 @VisibleForTesting
439 void clearCache() {
440 mCache.clear();
441 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100442
Adrian Roos3dcae682014-10-29 14:43:56 +0100443 public interface Callback {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100444 void initialize(SQLiteDatabase db);
445 }
446
447 class DatabaseHelper extends SQLiteOpenHelper {
448 private static final String TAG = "LockSettingsDB";
449 private static final String DATABASE_NAME = "locksettings.db";
450
451 private static final int DATABASE_VERSION = 2;
452
453 private final Callback mCallback;
454
455 public DatabaseHelper(Context context, Callback callback) {
456 super(context, DATABASE_NAME, null, DATABASE_VERSION);
457 setWriteAheadLoggingEnabled(true);
458 mCallback = callback;
459 }
460
461 private void createTable(SQLiteDatabase db) {
462 db.execSQL("CREATE TABLE " + TABLE + " (" +
463 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
464 COLUMN_KEY + " TEXT," +
465 COLUMN_USERID + " INTEGER," +
466 COLUMN_VALUE + " TEXT" +
467 ");");
468 }
469
470 @Override
471 public void onCreate(SQLiteDatabase db) {
472 createTable(db);
473 mCallback.initialize(db);
474 }
475
476 @Override
477 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
478 int upgradeVersion = oldVersion;
479 if (upgradeVersion == 1) {
480 // Previously migrated lock screen widget settings. Now defunct.
481 upgradeVersion = 2;
482 }
483
484 if (upgradeVersion != DATABASE_VERSION) {
485 Log.w(TAG, "Failed to upgrade database!");
486 }
487 }
488 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100489
490 /**
491 * Cache consistency model:
492 * - Writes to storage write directly to the cache, but this MUST happen within the atomic
493 * section either provided by the database transaction or mWriteLock, such that writes to the
494 * cache and writes to the backing storage are guaranteed to occur in the same order
495 *
496 * - Reads can populate the cache, but because they are no strong ordering guarantees with
497 * respect to writes this precaution is taken:
498 * - The cache is assigned a version number that increases every time the cache is modified.
499 * Reads from backing storage can only populate the cache if the backing storage
500 * has not changed since the load operation has begun.
501 * This guarantees that no read operation can shadow a write to the cache that happens
502 * after it had begun.
503 */
504 private static class Cache {
505 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
506 private final CacheKey mCacheKey = new CacheKey();
507 private int mVersion = 0;
508
509 String peekKeyValue(String key, String defaultValue, int userId) {
510 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
511 return cached == DEFAULT ? defaultValue : (String) cached;
512 }
513
514 boolean hasKeyValue(String key, int userId) {
515 return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
516 }
517
518 void putKeyValue(String key, String value, int userId) {
519 put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
520 }
521
522 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
523 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
524 }
525
526 byte[] peekFile(String fileName) {
527 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
528 }
529
530 boolean hasFile(String fileName) {
531 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
532 }
533
534 void putFile(String key, byte[] value) {
535 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
536 }
537
538 void putFileIfUnchanged(String key, byte[] value, int version) {
539 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
540 }
541
542 void setFetched(int userId) {
543 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
544 }
545
546 boolean isFetched(int userId) {
547 return contains(CacheKey.TYPE_FETCHED, "", userId);
548 }
549
550
551 private synchronized void put(int type, String key, Object value, int userId) {
552 // Create a new CachKey here because it may be saved in the map if the key is absent.
553 mCache.put(new CacheKey().set(type, key, userId), value);
554 mVersion++;
555 }
556
557 private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
558 int version) {
559 if (!contains(type, key, userId) && mVersion == version) {
560 put(type, key, value, userId);
561 }
562 }
563
564 private synchronized boolean contains(int type, String key, int userId) {
565 return mCache.containsKey(mCacheKey.set(type, key, userId));
566 }
567
568 private synchronized Object peek(int type, String key, int userId) {
569 return mCache.get(mCacheKey.set(type, key, userId));
570 }
571
572 private synchronized int getVersion() {
573 return mVersion;
574 }
575
576 synchronized void removeUser(int userId) {
577 for (int i = mCache.size() - 1; i >= 0; i--) {
578 if (mCache.keyAt(i).userId == userId) {
579 mCache.removeAt(i);
580 }
581 }
582
583 // Make sure in-flight loads can't write to cache.
584 mVersion++;
585 }
586
Adrian Roose5424992014-11-07 21:47:17 +0100587 synchronized void clear() {
588 mCache.clear();
589 mVersion++;
590 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100591
592 private static final class CacheKey {
593 static final int TYPE_KEY_VALUE = 0;
594 static final int TYPE_FILE = 1;
595 static final int TYPE_FETCHED = 2;
596
597 String key;
598 int userId;
599 int type;
600
601 public CacheKey set(int type, String key, int userId) {
602 this.type = type;
603 this.key = key;
604 this.userId = userId;
605 return this;
606 }
607
608 @Override
609 public boolean equals(Object obj) {
610 if (!(obj instanceof CacheKey))
611 return false;
612 CacheKey o = (CacheKey) obj;
613 return userId == o.userId && type == o.type && key.equals(o.key);
614 }
615
616 @Override
617 public int hashCode() {
618 return key.hashCode() ^ userId ^ type;
619 }
620 }
621 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100622}