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
17package com.android.server.locksettings;
18
19import static android.content.Context.USER_SERVICE;
20
21import android.annotation.Nullable;
22import android.app.admin.DevicePolicyManager;
23import android.content.ContentValues;
24import android.content.Context;
25import android.content.pm.UserInfo;
26import android.database.Cursor;
27import android.database.sqlite.SQLiteDatabase;
28import android.database.sqlite.SQLiteOpenHelper;
29import android.os.Environment;
30import android.os.UserHandle;
31import android.os.UserManager;
32import android.os.storage.StorageManager;
33import android.util.ArrayMap;
34import android.util.Log;
35import android.util.Slog;
36
37import com.android.internal.annotations.VisibleForTesting;
38import com.android.internal.util.ArrayUtils;
39import com.android.internal.util.Preconditions;
40import com.android.internal.widget.LockPatternUtils;
41import com.android.server.LocalServices;
42import com.android.server.PersistentDataBlockManagerInternal;
43
44import java.io.ByteArrayInputStream;
45import java.io.ByteArrayOutputStream;
46import java.io.DataInputStream;
47import java.io.DataOutputStream;
48import java.io.File;
49import java.io.IOException;
50import java.io.RandomAccessFile;
51import java.util.ArrayList;
52import java.util.List;
53import java.util.Map;
54
55/**
56 * Storage for the lock settings service.
57 */
58class 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