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