1 /*
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 
17 package com.android.server;
18 
19 import static android.content.Context.USER_SERVICE;
20 
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.content.pm.UserInfo;
24 import android.database.Cursor;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.sqlite.SQLiteOpenHelper;
27 import android.os.Environment;
28 import android.os.UserManager;
29 import android.os.storage.StorageManager;
30 import android.util.ArrayMap;
31 import android.util.Log;
32 import android.util.Slog;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.util.ArrayUtils;
36 import com.android.internal.widget.LockPatternUtils;
37 
38 import java.io.File;
39 import java.io.IOException;
40 import java.io.RandomAccessFile;
41 
42 /**
43  * Storage for the lock settings service.
44  */
45 class LockSettingsStorage {
46 
47     private static final String TAG = "LockSettingsStorage";
48     private static final String TABLE = "locksettings";
49     private static final boolean DEBUG = false;
50 
51     private static final String COLUMN_KEY = "name";
52     private static final String COLUMN_USERID = "user";
53     private static final String COLUMN_VALUE = "value";
54 
55     private static final String[] COLUMNS_FOR_QUERY = {
56             COLUMN_VALUE
57     };
58     private static final String[] COLUMNS_FOR_PREFETCH = {
59             COLUMN_KEY, COLUMN_VALUE
60     };
61 
62     private static final String SYSTEM_DIRECTORY = "/system/";
63     private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key";
64     private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key";
65     private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key";
66     private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key";
67     private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
68     private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
69 
70     private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
71 
72     private static final Object DEFAULT = new Object();
73 
74     private final DatabaseHelper mOpenHelper;
75     private final Context mContext;
76     private final Cache mCache = new Cache();
77     private final Object mFileWriteLock = new Object();
78 
79     @VisibleForTesting
80     public static class CredentialHash {
81         static final int VERSION_LEGACY = 0;
82         static final int VERSION_GATEKEEPER = 1;
83 
CredentialHash(byte[] hash, int type, int version)84         private CredentialHash(byte[] hash, int type, int version) {
85             if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
86                 if (hash == null) {
87                     throw new RuntimeException("Empty hash for CredentialHash");
88                 }
89             } else /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ {
90                 if (hash != null) {
91                     throw new RuntimeException("None type CredentialHash should not have hash");
92                 }
93             }
94             this.hash = hash;
95             this.type = type;
96             this.version = version;
97             this.isBaseZeroPattern = false;
98         }
99 
CredentialHash(byte[] hash, boolean isBaseZeroPattern)100         private CredentialHash(byte[] hash, boolean isBaseZeroPattern) {
101             this.hash = hash;
102             this.type = LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
103             this.version = VERSION_GATEKEEPER;
104             this.isBaseZeroPattern = isBaseZeroPattern;
105         }
106 
create(byte[] hash, int type)107         static CredentialHash create(byte[] hash, int type) {
108             if (type == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
109                 throw new RuntimeException("Bad type for CredentialHash");
110             }
111             return new CredentialHash(hash, type, VERSION_GATEKEEPER);
112         }
113 
createEmptyHash()114         static CredentialHash createEmptyHash() {
115             return new CredentialHash(null, LockPatternUtils.CREDENTIAL_TYPE_NONE,
116                     VERSION_GATEKEEPER);
117         }
118 
119         byte[] hash;
120         int type;
121         int version;
122         boolean isBaseZeroPattern;
123     }
124 
LockSettingsStorage(Context context)125     public LockSettingsStorage(Context context) {
126         mContext = context;
127         mOpenHelper = new DatabaseHelper(context);
128     }
129 
setDatabaseOnCreateCallback(Callback callback)130     public void setDatabaseOnCreateCallback(Callback callback) {
131         mOpenHelper.setCallback(callback);
132     }
133 
writeKeyValue(String key, String value, int userId)134     public void writeKeyValue(String key, String value, int userId) {
135         writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
136     }
137 
writeKeyValue(SQLiteDatabase db, String key, String value, int userId)138     public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
139         ContentValues cv = new ContentValues();
140         cv.put(COLUMN_KEY, key);
141         cv.put(COLUMN_USERID, userId);
142         cv.put(COLUMN_VALUE, value);
143 
144         db.beginTransaction();
145         try {
146             db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
147                     new String[] {key, Integer.toString(userId)});
148             db.insert(TABLE, null, cv);
149             db.setTransactionSuccessful();
150             mCache.putKeyValue(key, value, userId);
151         } finally {
152             db.endTransaction();
153         }
154 
155     }
156 
readKeyValue(String key, String defaultValue, int userId)157     public String readKeyValue(String key, String defaultValue, int userId) {
158         int version;
159         synchronized (mCache) {
160             if (mCache.hasKeyValue(key, userId)) {
161                 return mCache.peekKeyValue(key, defaultValue, userId);
162             }
163             version = mCache.getVersion();
164         }
165 
166         Cursor cursor;
167         Object result = DEFAULT;
168         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
169         if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
170                 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
171                 new String[] { Integer.toString(userId), key },
172                 null, null, null)) != null) {
173             if (cursor.moveToFirst()) {
174                 result = cursor.getString(0);
175             }
176             cursor.close();
177         }
178         mCache.putKeyValueIfUnchanged(key, result, userId, version);
179         return result == DEFAULT ? defaultValue : (String) result;
180     }
181 
prefetchUser(int userId)182     public void prefetchUser(int userId) {
183         int version;
184         synchronized (mCache) {
185             if (mCache.isFetched(userId)) {
186                 return;
187             }
188             mCache.setFetched(userId);
189             version = mCache.getVersion();
190         }
191 
192         Cursor cursor;
193         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
194         if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
195                 COLUMN_USERID + "=?",
196                 new String[] { Integer.toString(userId) },
197                 null, null, null)) != null) {
198             while (cursor.moveToNext()) {
199                 String key = cursor.getString(0);
200                 String value = cursor.getString(1);
201                 mCache.putKeyValueIfUnchanged(key, value, userId, version);
202             }
203             cursor.close();
204         }
205 
206         // Populate cache by reading the password and pattern files.
207         readCredentialHash(userId);
208     }
209 
readPasswordHashIfExists(int userId)210     private CredentialHash readPasswordHashIfExists(int userId) {
211         byte[] stored = readFile(getLockPasswordFilename(userId));
212         if (!ArrayUtils.isEmpty(stored)) {
213             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
214                     CredentialHash.VERSION_GATEKEEPER);
215         }
216 
217         stored = readFile(getLegacyLockPasswordFilename(userId));
218         if (!ArrayUtils.isEmpty(stored)) {
219             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
220                     CredentialHash.VERSION_LEGACY);
221         }
222         return null;
223     }
224 
readPatternHashIfExists(int userId)225     private CredentialHash readPatternHashIfExists(int userId) {
226         byte[] stored = readFile(getLockPatternFilename(userId));
227         if (!ArrayUtils.isEmpty(stored)) {
228             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
229                     CredentialHash.VERSION_GATEKEEPER);
230         }
231 
232         stored = readFile(getBaseZeroLockPatternFilename(userId));
233         if (!ArrayUtils.isEmpty(stored)) {
234             return new CredentialHash(stored, true);
235         }
236 
237         stored = readFile(getLegacyLockPatternFilename(userId));
238         if (!ArrayUtils.isEmpty(stored)) {
239             return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
240                     CredentialHash.VERSION_LEGACY);
241         }
242         return null;
243     }
244 
readCredentialHash(int userId)245     public CredentialHash readCredentialHash(int userId) {
246         CredentialHash passwordHash = readPasswordHashIfExists(userId);
247         CredentialHash patternHash = readPatternHashIfExists(userId);
248         if (passwordHash != null && patternHash != null) {
249             if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) {
250                 return passwordHash;
251             } else {
252                 return patternHash;
253             }
254         } else if (passwordHash != null) {
255             return passwordHash;
256         } else if (patternHash != null) {
257             return patternHash;
258         } else {
259             return CredentialHash.createEmptyHash();
260         }
261     }
262 
removeChildProfileLock(int userId)263     public void removeChildProfileLock(int userId) {
264         if (DEBUG)
265             Slog.e(TAG, "Remove child profile lock for user: " + userId);
266         try {
267             deleteFile(getChildProfileLockFile(userId));
268         } catch (Exception e) {
269             e.printStackTrace();
270         }
271     }
272 
writeChildProfileLock(int userId, byte[] lock)273     public void writeChildProfileLock(int userId, byte[] lock) {
274         writeFile(getChildProfileLockFile(userId), lock);
275     }
276 
readChildProfileLock(int userId)277     public byte[] readChildProfileLock(int userId) {
278         return readFile(getChildProfileLockFile(userId));
279     }
280 
hasChildProfileLock(int userId)281     public boolean hasChildProfileLock(int userId) {
282         return hasFile(getChildProfileLockFile(userId));
283     }
284 
hasPassword(int userId)285     public boolean hasPassword(int userId) {
286         return hasFile(getLockPasswordFilename(userId)) ||
287             hasFile(getLegacyLockPasswordFilename(userId));
288     }
289 
hasPattern(int userId)290     public boolean hasPattern(int userId) {
291         return hasFile(getLockPatternFilename(userId)) ||
292             hasFile(getBaseZeroLockPatternFilename(userId)) ||
293             hasFile(getLegacyLockPatternFilename(userId));
294     }
295 
hasCredential(int userId)296     public boolean hasCredential(int userId) {
297         return hasPassword(userId) || hasPattern(userId);
298     }
299 
hasFile(String name)300     private boolean hasFile(String name) {
301         byte[] contents = readFile(name);
302         return contents != null && contents.length > 0;
303     }
304 
readFile(String name)305     private byte[] readFile(String name) {
306         int version;
307         synchronized (mCache) {
308             if (mCache.hasFile(name)) {
309                 return mCache.peekFile(name);
310             }
311             version = mCache.getVersion();
312         }
313 
314         RandomAccessFile raf = null;
315         byte[] stored = null;
316         try {
317             raf = new RandomAccessFile(name, "r");
318             stored = new byte[(int) raf.length()];
319             raf.readFully(stored, 0, stored.length);
320             raf.close();
321         } catch (IOException e) {
322             Slog.e(TAG, "Cannot read file " + e);
323         } finally {
324             if (raf != null) {
325                 try {
326                     raf.close();
327                 } catch (IOException e) {
328                     Slog.e(TAG, "Error closing file " + e);
329                 }
330             }
331         }
332         mCache.putFileIfUnchanged(name, stored, version);
333         return stored;
334     }
335 
writeFile(String name, byte[] hash)336     private void writeFile(String name, byte[] hash) {
337         synchronized (mFileWriteLock) {
338             RandomAccessFile raf = null;
339             try {
340                 // Write the hash to file, requiring each write to be synchronized to the
341                 // underlying storage device immediately to avoid data loss in case of power loss.
342                 // This also ensures future secdiscard operation on the file succeeds since the
343                 // file would have been allocated on flash.
344                 raf = new RandomAccessFile(name, "rws");
345                 // Truncate the file if pattern is null, to clear the lock
346                 if (hash == null || hash.length == 0) {
347                     raf.setLength(0);
348                 } else {
349                     raf.write(hash, 0, hash.length);
350                 }
351                 raf.close();
352             } catch (IOException e) {
353                 Slog.e(TAG, "Error writing to file " + e);
354             } finally {
355                 if (raf != null) {
356                     try {
357                         raf.close();
358                     } catch (IOException e) {
359                         Slog.e(TAG, "Error closing file " + e);
360                     }
361                 }
362             }
363             mCache.putFile(name, hash);
364         }
365     }
366 
deleteFile(String name)367     private void deleteFile(String name) {
368         if (DEBUG) Slog.e(TAG, "Delete file " + name);
369         synchronized (mFileWriteLock) {
370             File file = new File(name);
371             if (file.exists()) {
372                 file.delete();
373                 mCache.putFile(name, null);
374             }
375         }
376     }
377 
writeCredentialHash(CredentialHash hash, int userId)378     public void writeCredentialHash(CredentialHash hash, int userId) {
379         byte[] patternHash = null;
380         byte[] passwordHash = null;
381 
382         if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) {
383             passwordHash = hash.hash;
384         } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
385             patternHash = hash.hash;
386         }
387         writeFile(getLockPasswordFilename(userId), passwordHash);
388         writeFile(getLockPatternFilename(userId), patternHash);
389     }
390 
391     @VisibleForTesting
getLockPatternFilename(int userId)392     String getLockPatternFilename(int userId) {
393         return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
394     }
395 
396     @VisibleForTesting
getLockPasswordFilename(int userId)397     String getLockPasswordFilename(int userId) {
398         return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
399     }
400 
401     @VisibleForTesting
getLegacyLockPatternFilename(int userId)402     String getLegacyLockPatternFilename(int userId) {
403         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
404     }
405 
406     @VisibleForTesting
getLegacyLockPasswordFilename(int userId)407     String getLegacyLockPasswordFilename(int userId) {
408         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
409     }
410 
getBaseZeroLockPatternFilename(int userId)411     private String getBaseZeroLockPatternFilename(int userId) {
412         return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
413     }
414 
415     @VisibleForTesting
getChildProfileLockFile(int userId)416     String getChildProfileLockFile(int userId) {
417         return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE);
418     }
419 
getLockCredentialFilePathForUser(int userId, String basename)420     private String getLockCredentialFilePathForUser(int userId, String basename) {
421         String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
422                         SYSTEM_DIRECTORY;
423         if (userId == 0) {
424             // Leave it in the same place for user 0
425             return dataSystemDirectory + basename;
426         } else {
427             return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
428         }
429     }
430 
writeSyntheticPasswordState(int userId, long handle, String name, byte[] data)431     public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) {
432         writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data);
433     }
434 
readSyntheticPasswordState(int userId, long handle, String name)435     public byte[] readSyntheticPasswordState(int userId, long handle, String name) {
436         return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name));
437     }
438 
deleteSyntheticPasswordState(int userId, long handle, String name)439     public void deleteSyntheticPasswordState(int userId, long handle, String name) {
440         String path = getSynthenticPasswordStateFilePathForUser(userId, handle, name);
441         File file = new File(path);
442         if (file.exists()) {
443             try {
444                 mContext.getSystemService(StorageManager.class).secdiscard(file.getAbsolutePath());
445             } catch (Exception e) {
446                 Slog.w(TAG, "Failed to secdiscard " + path, e);
447             } finally {
448                 file.delete();
449             }
450             mCache.putFile(path, null);
451         }
452     }
453 
454     @VisibleForTesting
getSyntheticPasswordDirectoryForUser(int userId)455     protected File getSyntheticPasswordDirectoryForUser(int userId) {
456         return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY);
457     }
458 
459     @VisibleForTesting
getSynthenticPasswordStateFilePathForUser(int userId, long handle, String name)460     protected String getSynthenticPasswordStateFilePathForUser(int userId, long handle,
461             String name) {
462         File baseDir = getSyntheticPasswordDirectoryForUser(userId);
463         String baseName = String.format("%016x.%s", handle, name);
464         if (!baseDir.exists()) {
465             baseDir.mkdir();
466         }
467         return new File(baseDir, baseName).getAbsolutePath();
468     }
469 
removeUser(int userId)470     public void removeUser(int userId) {
471         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
472 
473         final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
474         final UserInfo parentInfo = um.getProfileParent(userId);
475 
476         if (parentInfo == null) {
477             // This user owns its lock settings files - safe to delete them
478             synchronized (mFileWriteLock) {
479                 String name = getLockPasswordFilename(userId);
480                 File file = new File(name);
481                 if (file.exists()) {
482                     file.delete();
483                     mCache.putFile(name, null);
484                 }
485                 name = getLockPatternFilename(userId);
486                 file = new File(name);
487                 if (file.exists()) {
488                     file.delete();
489                     mCache.putFile(name, null);
490                 }
491             }
492         } else {
493             // Managed profile
494             removeChildProfileLock(userId);
495         }
496 
497         File spStateDir = getSyntheticPasswordDirectoryForUser(userId);
498         try {
499             db.beginTransaction();
500             db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
501             db.setTransactionSuccessful();
502             mCache.removeUser(userId);
503             // The directory itself will be deleted as part of user deletion operation by the
504             // framework, so only need to purge cache here.
505             //TODO: (b/34600579) invoke secdiscardable
506             mCache.purgePath(spStateDir.getAbsolutePath());
507         } finally {
508             db.endTransaction();
509         }
510     }
511 
512     @VisibleForTesting
closeDatabase()513     void closeDatabase() {
514         mOpenHelper.close();
515     }
516 
517     @VisibleForTesting
clearCache()518     void clearCache() {
519         mCache.clear();
520     }
521 
522     public interface Callback {
initialize(SQLiteDatabase db)523         void initialize(SQLiteDatabase db);
524     }
525 
526     class DatabaseHelper extends SQLiteOpenHelper {
527         private static final String TAG = "LockSettingsDB";
528         private static final String DATABASE_NAME = "locksettings.db";
529 
530         private static final int DATABASE_VERSION = 2;
531 
532         private Callback mCallback;
533 
DatabaseHelper(Context context)534         public DatabaseHelper(Context context) {
535             super(context, DATABASE_NAME, null, DATABASE_VERSION);
536             setWriteAheadLoggingEnabled(true);
537         }
538 
setCallback(Callback callback)539         public void setCallback(Callback callback) {
540             mCallback = callback;
541         }
542 
createTable(SQLiteDatabase db)543         private void createTable(SQLiteDatabase db) {
544             db.execSQL("CREATE TABLE " + TABLE + " (" +
545                     "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
546                     COLUMN_KEY + " TEXT," +
547                     COLUMN_USERID + " INTEGER," +
548                     COLUMN_VALUE + " TEXT" +
549                     ");");
550         }
551 
552         @Override
onCreate(SQLiteDatabase db)553         public void onCreate(SQLiteDatabase db) {
554             createTable(db);
555             if (mCallback != null) {
556                 mCallback.initialize(db);
557             }
558         }
559 
560         @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion)561         public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
562             int upgradeVersion = oldVersion;
563             if (upgradeVersion == 1) {
564                 // Previously migrated lock screen widget settings. Now defunct.
565                 upgradeVersion = 2;
566             }
567 
568             if (upgradeVersion != DATABASE_VERSION) {
569                 Log.w(TAG, "Failed to upgrade database!");
570             }
571         }
572     }
573 
574     /**
575      * Cache consistency model:
576      * - Writes to storage write directly to the cache, but this MUST happen within the atomic
577      *   section either provided by the database transaction or mWriteLock, such that writes to the
578      *   cache and writes to the backing storage are guaranteed to occur in the same order
579      *
580      * - Reads can populate the cache, but because they are no strong ordering guarantees with
581      *   respect to writes this precaution is taken:
582      *   - The cache is assigned a version number that increases every time the cache is modified.
583      *     Reads from backing storage can only populate the cache if the backing storage
584      *     has not changed since the load operation has begun.
585      *     This guarantees that no read operation can shadow a write to the cache that happens
586      *     after it had begun.
587      */
588     private static class Cache {
589         private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
590         private final CacheKey mCacheKey = new CacheKey();
591         private int mVersion = 0;
592 
peekKeyValue(String key, String defaultValue, int userId)593         String peekKeyValue(String key, String defaultValue, int userId) {
594             Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
595             return cached == DEFAULT ? defaultValue : (String) cached;
596         }
597 
hasKeyValue(String key, int userId)598         boolean hasKeyValue(String key, int userId) {
599             return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
600         }
601 
putKeyValue(String key, String value, int userId)602         void putKeyValue(String key, String value, int userId) {
603             put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
604         }
605 
putKeyValueIfUnchanged(String key, Object value, int userId, int version)606         void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
607             putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
608         }
609 
peekFile(String fileName)610         byte[] peekFile(String fileName) {
611             return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
612         }
613 
hasFile(String fileName)614         boolean hasFile(String fileName) {
615             return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
616         }
617 
putFile(String key, byte[] value)618         void putFile(String key, byte[] value) {
619             put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
620         }
621 
putFileIfUnchanged(String key, byte[] value, int version)622         void putFileIfUnchanged(String key, byte[] value, int version) {
623             putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
624         }
625 
setFetched(int userId)626         void setFetched(int userId) {
627             put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
628         }
629 
isFetched(int userId)630         boolean isFetched(int userId) {
631             return contains(CacheKey.TYPE_FETCHED, "", userId);
632         }
633 
634 
put(int type, String key, Object value, int userId)635         private synchronized void put(int type, String key, Object value, int userId) {
636             // Create a new CachKey here because it may be saved in the map if the key is absent.
637             mCache.put(new CacheKey().set(type, key, userId), value);
638             mVersion++;
639         }
640 
putIfUnchanged(int type, String key, Object value, int userId, int version)641         private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
642                 int version) {
643             if (!contains(type, key, userId) && mVersion == version) {
644                 put(type, key, value, userId);
645             }
646         }
647 
contains(int type, String key, int userId)648         private synchronized boolean contains(int type, String key, int userId) {
649             return mCache.containsKey(mCacheKey.set(type, key, userId));
650         }
651 
peek(int type, String key, int userId)652         private synchronized Object peek(int type, String key, int userId) {
653             return mCache.get(mCacheKey.set(type, key, userId));
654         }
655 
getVersion()656         private synchronized int getVersion() {
657             return mVersion;
658         }
659 
removeUser(int userId)660         synchronized void removeUser(int userId) {
661             for (int i = mCache.size() - 1; i >= 0; i--) {
662                 if (mCache.keyAt(i).userId == userId) {
663                     mCache.removeAt(i);
664                 }
665             }
666 
667             // Make sure in-flight loads can't write to cache.
668             mVersion++;
669         }
670 
purgePath(String path)671         synchronized void purgePath(String path) {
672             for (int i = mCache.size() - 1; i >= 0; i--) {
673                 CacheKey entry = mCache.keyAt(i);
674                 if (entry.type == CacheKey.TYPE_FILE && entry.key.startsWith(path)) {
675                     mCache.removeAt(i);
676                 }
677             }
678             mVersion++;
679         }
680 
clear()681         synchronized void clear() {
682             mCache.clear();
683             mVersion++;
684         }
685 
686         private static final class CacheKey {
687             static final int TYPE_KEY_VALUE = 0;
688             static final int TYPE_FILE = 1;
689             static final int TYPE_FETCHED = 2;
690 
691             String key;
692             int userId;
693             int type;
694 
set(int type, String key, int userId)695             public CacheKey set(int type, String key, int userId) {
696                 this.type = type;
697                 this.key = key;
698                 this.userId = userId;
699                 return this;
700             }
701 
702             @Override
equals(Object obj)703             public boolean equals(Object obj) {
704                 if (!(obj instanceof CacheKey))
705                     return false;
706                 CacheKey o = (CacheKey) obj;
707                 return userId == o.userId && type == o.type && key.equals(o.key);
708             }
709 
710             @Override
hashCode()711             public int hashCode() {
712                 return key.hashCode() ^ userId ^ type;
713             }
714         }
715     }
716 
717 }
718