Home | History | Annotate | Download | only in server
      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 
     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 
    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 
    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 
    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 
    125     public LockSettingsStorage(Context context) {
    126         mContext = context;
    127         mOpenHelper = new DatabaseHelper(context);
    128     }
    129 
    130     public void setDatabaseOnCreateCallback(Callback callback) {
    131         mOpenHelper.setCallback(callback);
    132     }
    133 
    134     public void writeKeyValue(String key, String value, int userId) {
    135         writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
    136     }
    137 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    273     public void writeChildProfileLock(int userId, byte[] lock) {
    274         writeFile(getChildProfileLockFile(userId), lock);
    275     }
    276 
    277     public byte[] readChildProfileLock(int userId) {
    278         return readFile(getChildProfileLockFile(userId));
    279     }
    280 
    281     public boolean hasChildProfileLock(int userId) {
    282         return hasFile(getChildProfileLockFile(userId));
    283     }
    284 
    285     public boolean hasPassword(int userId) {
    286         return hasFile(getLockPasswordFilename(userId)) ||
    287             hasFile(getLegacyLockPasswordFilename(userId));
    288     }
    289 
    290     public boolean hasPattern(int userId) {
    291         return hasFile(getLockPatternFilename(userId)) ||
    292             hasFile(getBaseZeroLockPatternFilename(userId)) ||
    293             hasFile(getLegacyLockPatternFilename(userId));
    294     }
    295 
    296     public boolean hasCredential(int userId) {
    297         return hasPassword(userId) || hasPattern(userId);
    298     }
    299 
    300     private boolean hasFile(String name) {
    301         byte[] contents = readFile(name);
    302         return contents != null && contents.length > 0;
    303     }
    304 
    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 
    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 
    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 
    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
    392     String getLockPatternFilename(int userId) {
    393         return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
    394     }
    395 
    396     @VisibleForTesting
    397     String getLockPasswordFilename(int userId) {
    398         return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
    399     }
    400 
    401     @VisibleForTesting
    402     String getLegacyLockPatternFilename(int userId) {
    403         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
    404     }
    405 
    406     @VisibleForTesting
    407     String getLegacyLockPasswordFilename(int userId) {
    408         return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
    409     }
    410 
    411     private String getBaseZeroLockPatternFilename(int userId) {
    412         return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
    413     }
    414 
    415     @VisibleForTesting
    416     String getChildProfileLockFile(int userId) {
    417         return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE);
    418     }
    419 
    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 
    431     public void writeSyntheticPasswordState(int userId, long handle, String name, byte[] data) {
    432         writeFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name), data);
    433     }
    434 
    435     public byte[] readSyntheticPasswordState(int userId, long handle, String name) {
    436         return readFile(getSynthenticPasswordStateFilePathForUser(userId, handle, name));
    437     }
    438 
    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
    455     protected File getSyntheticPasswordDirectoryForUser(int userId) {
    456         return new File(Environment.getDataSystemDeDirectory(userId) ,SYNTHETIC_PASSWORD_DIRECTORY);
    457     }
    458 
    459     @VisibleForTesting
    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 
    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
    513     void closeDatabase() {
    514         mOpenHelper.close();
    515     }
    516 
    517     @VisibleForTesting
    518     void clearCache() {
    519         mCache.clear();
    520     }
    521 
    522     public interface Callback {
    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 
    534         public DatabaseHelper(Context context) {
    535             super(context, DATABASE_NAME, null, DATABASE_VERSION);
    536             setWriteAheadLoggingEnabled(true);
    537         }
    538 
    539         public void setCallback(Callback callback) {
    540             mCallback = callback;
    541         }
    542 
    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
    553         public void onCreate(SQLiteDatabase db) {
    554             createTable(db);
    555             if (mCallback != null) {
    556                 mCallback.initialize(db);
    557             }
    558         }
    559 
    560         @Override
    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 
    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 
    598         boolean hasKeyValue(String key, int userId) {
    599             return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
    600         }
    601 
    602         void putKeyValue(String key, String value, int userId) {
    603             put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
    604         }
    605 
    606         void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
    607             putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
    608         }
    609 
    610         byte[] peekFile(String fileName) {
    611             return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
    612         }
    613 
    614         boolean hasFile(String fileName) {
    615             return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
    616         }
    617 
    618         void putFile(String key, byte[] value) {
    619             put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
    620         }
    621 
    622         void putFileIfUnchanged(String key, byte[] value, int version) {
    623             putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
    624         }
    625 
    626         void setFetched(int userId) {
    627             put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
    628         }
    629 
    630         boolean isFetched(int userId) {
    631             return contains(CacheKey.TYPE_FETCHED, "", userId);
    632         }
    633 
    634 
    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 
    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 
    648         private synchronized boolean contains(int type, String key, int userId) {
    649             return mCache.containsKey(mCacheKey.set(type, key, userId));
    650         }
    651 
    652         private synchronized Object peek(int type, String key, int userId) {
    653             return mCache.get(mCacheKey.set(type, key, userId));
    654         }
    655 
    656         private synchronized int getVersion() {
    657             return mVersion;
    658         }
    659 
    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 
    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 
    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 
    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
    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
    711             public int hashCode() {
    712                 return key.hashCode() ^ userId ^ type;
    713             }
    714         }
    715     }
    716 
    717 }
    718