blob: 3d973a04c212415b179676431b31ce4f7c2d1da3 [file] [log] [blame]
Adrian Roos261d5ab2014-10-29 14:42:38 +01001/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.server;
18
Rubin Xu1de89b32016-11-30 20:03:13 +000019import static android.content.Context.USER_SERVICE;
Adrian Roose5424992014-11-07 21:47:17 +010020
Adrian Roos261d5ab2014-10-29 14:42:38 +010021import android.content.ContentValues;
22import android.content.Context;
23import android.content.pm.UserInfo;
24import android.database.Cursor;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.os.Environment;
28import android.os.UserManager;
29import android.util.ArrayMap;
30import android.util.Log;
31import android.util.Slog;
Rubin Xu1de89b32016-11-30 20:03:13 +000032
33import com.android.internal.annotations.VisibleForTesting;
34import com.android.internal.util.ArrayUtils;
35import com.android.internal.widget.LockPatternUtils;
Adrian Roos261d5ab2014-10-29 14:42:38 +010036
37import java.io.File;
38import java.io.IOException;
39import java.io.RandomAccessFile;
40
Adrian Roos261d5ab2014-10-29 14:42:38 +010041/**
42 * Storage for the lock settings service.
43 */
44class LockSettingsStorage {
45
46 private static final String TAG = "LockSettingsStorage";
47 private static final String TABLE = "locksettings";
Ricky Waidc283a82016-03-24 19:55:08 +000048 private static final boolean DEBUG = false;
Adrian Roos261d5ab2014-10-29 14:42:38 +010049
50 private static final String COLUMN_KEY = "name";
51 private static final String COLUMN_USERID = "user";
52 private static final String COLUMN_VALUE = "value";
53
54 private static final String[] COLUMNS_FOR_QUERY = {
55 COLUMN_VALUE
56 };
Adrian Roos3dcae682014-10-29 14:43:56 +010057 private static final String[] COLUMNS_FOR_PREFETCH = {
58 COLUMN_KEY, COLUMN_VALUE
59 };
Adrian Roos261d5ab2014-10-29 14:42:38 +010060
61 private static final String SYSTEM_DIRECTORY = "/system/";
Andres Moralese40bad82015-05-28 14:21:36 -070062 private static final String LOCK_PATTERN_FILE = "gatekeeper.pattern.key";
63 private static final String BASE_ZERO_LOCK_PATTERN_FILE = "gatekeeper.gesture.key";
Andres Morales8fa56652015-03-31 09:19:50 -070064 private static final String LEGACY_LOCK_PATTERN_FILE = "gesture.key";
65 private static final String LOCK_PASSWORD_FILE = "gatekeeper.password.key";
66 private static final String LEGACY_LOCK_PASSWORD_FILE = "password.key";
Ricky Waidc283a82016-03-24 19:55:08 +000067 private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
Adrian Roos261d5ab2014-10-29 14:42:38 +010068
Adrian Roos3dcae682014-10-29 14:43:56 +010069 private static final Object DEFAULT = new Object();
70
Adrian Roos261d5ab2014-10-29 14:42:38 +010071 private final DatabaseHelper mOpenHelper;
72 private final Context mContext;
Adrian Roos3dcae682014-10-29 14:43:56 +010073 private final Cache mCache = new Cache();
Adrian Roos261d5ab2014-10-29 14:42:38 +010074 private final Object mFileWriteLock = new Object();
75
Rubin Xu1de89b32016-11-30 20:03:13 +000076 @VisibleForTesting
77 public static class CredentialHash {
Andres Morales8fa56652015-03-31 09:19:50 -070078 static final int VERSION_LEGACY = 0;
79 static final int VERSION_GATEKEEPER = 1;
80
Rubin Xu1de89b32016-11-30 20:03:13 +000081 private CredentialHash(byte[] hash, int type, int version) {
82 if (type != LockPatternUtils.CREDENTIAL_TYPE_NONE) {
83 if (hash == null) {
84 throw new RuntimeException("Empty hash for CredentialHash");
85 }
86 } else /* type == LockPatternUtils.CREDENTIAL_TYPE_NONE */ {
87 if (hash != null) {
88 throw new RuntimeException("None type CredentialHash should not have hash");
89 }
90 }
Andres Morales8fa56652015-03-31 09:19:50 -070091 this.hash = hash;
Rubin Xu1de89b32016-11-30 20:03:13 +000092 this.type = type;
Andres Morales8fa56652015-03-31 09:19:50 -070093 this.version = version;
Andres Moralese40bad82015-05-28 14:21:36 -070094 this.isBaseZeroPattern = false;
95 }
96
Rubin Xu1de89b32016-11-30 20:03:13 +000097 private CredentialHash(byte[] hash, boolean isBaseZeroPattern) {
Andres Moralese40bad82015-05-28 14:21:36 -070098 this.hash = hash;
Rubin Xu1de89b32016-11-30 20:03:13 +000099 this.type = LockPatternUtils.CREDENTIAL_TYPE_PATTERN;
Andres Moralese40bad82015-05-28 14:21:36 -0700100 this.version = VERSION_GATEKEEPER;
101 this.isBaseZeroPattern = isBaseZeroPattern;
Andres Morales8fa56652015-03-31 09:19:50 -0700102 }
103
Rubin Xu1de89b32016-11-30 20:03:13 +0000104 static CredentialHash create(byte[] hash, int type) {
105 if (type == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
106 throw new RuntimeException("Bad type for CredentialHash");
107 }
108 return new CredentialHash(hash, type, VERSION_GATEKEEPER);
109 }
110
111 static CredentialHash createEmptyHash() {
112 return new CredentialHash(null, LockPatternUtils.CREDENTIAL_TYPE_NONE,
113 VERSION_GATEKEEPER);
114 }
115
Andres Morales8fa56652015-03-31 09:19:50 -0700116 byte[] hash;
Rubin Xu1de89b32016-11-30 20:03:13 +0000117 int type;
Andres Morales8fa56652015-03-31 09:19:50 -0700118 int version;
Andres Moralese40bad82015-05-28 14:21:36 -0700119 boolean isBaseZeroPattern;
Andres Morales8fa56652015-03-31 09:19:50 -0700120 }
121
Adrian Roos3dcae682014-10-29 14:43:56 +0100122 public LockSettingsStorage(Context context, Callback callback) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100123 mContext = context;
124 mOpenHelper = new DatabaseHelper(context, callback);
125 }
126
Adrian Roos3dcae682014-10-29 14:43:56 +0100127 public void writeKeyValue(String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100128 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
129 }
130
Adrian Roos3dcae682014-10-29 14:43:56 +0100131 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100132 ContentValues cv = new ContentValues();
133 cv.put(COLUMN_KEY, key);
134 cv.put(COLUMN_USERID, userId);
135 cv.put(COLUMN_VALUE, value);
136
137 db.beginTransaction();
138 try {
139 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
140 new String[] {key, Integer.toString(userId)});
141 db.insert(TABLE, null, cv);
142 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100143 mCache.putKeyValue(key, value, userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100144 } finally {
145 db.endTransaction();
146 }
147
148 }
149
Adrian Roos3dcae682014-10-29 14:43:56 +0100150 public String readKeyValue(String key, String defaultValue, int userId) {
151 int version;
152 synchronized (mCache) {
153 if (mCache.hasKeyValue(key, userId)) {
154 return mCache.peekKeyValue(key, defaultValue, userId);
155 }
156 version = mCache.getVersion();
157 }
158
Adrian Roos261d5ab2014-10-29 14:42:38 +0100159 Cursor cursor;
Adrian Roos3dcae682014-10-29 14:43:56 +0100160 Object result = DEFAULT;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100161 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
162 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
163 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
164 new String[] { Integer.toString(userId), key },
165 null, null, null)) != null) {
166 if (cursor.moveToFirst()) {
167 result = cursor.getString(0);
168 }
169 cursor.close();
170 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100171 mCache.putKeyValueIfUnchanged(key, result, userId, version);
172 return result == DEFAULT ? defaultValue : (String) result;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100173 }
174
Adrian Roos3dcae682014-10-29 14:43:56 +0100175 public void prefetchUser(int userId) {
176 int version;
177 synchronized (mCache) {
178 if (mCache.isFetched(userId)) {
179 return;
180 }
181 mCache.setFetched(userId);
182 version = mCache.getVersion();
183 }
184
185 Cursor cursor;
186 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
187 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
188 COLUMN_USERID + "=?",
189 new String[] { Integer.toString(userId) },
190 null, null, null)) != null) {
191 while (cursor.moveToNext()) {
192 String key = cursor.getString(0);
193 String value = cursor.getString(1);
194 mCache.putKeyValueIfUnchanged(key, value, userId, version);
195 }
196 cursor.close();
197 }
198
199 // Populate cache by reading the password and pattern files.
Rubin Xu1de89b32016-11-30 20:03:13 +0000200 readCredentialHash(userId);
Adrian Roos3dcae682014-10-29 14:43:56 +0100201 }
202
Rubin Xu1de89b32016-11-30 20:03:13 +0000203 private CredentialHash readPasswordHashIfExists(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700204 byte[] stored = readFile(getLockPasswordFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000205 if (!ArrayUtils.isEmpty(stored)) {
206 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
207 CredentialHash.VERSION_GATEKEEPER);
Andres Morales8fa56652015-03-31 09:19:50 -0700208 }
209
210 stored = readFile(getLegacyLockPasswordFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000211 if (!ArrayUtils.isEmpty(stored)) {
212 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
213 CredentialHash.VERSION_LEGACY);
Andres Morales8fa56652015-03-31 09:19:50 -0700214 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100215 return null;
216 }
217
Rubin Xu1de89b32016-11-30 20:03:13 +0000218 private CredentialHash readPatternHashIfExists(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700219 byte[] stored = readFile(getLockPatternFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000220 if (!ArrayUtils.isEmpty(stored)) {
221 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
222 CredentialHash.VERSION_GATEKEEPER);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100223 }
Andres Morales8fa56652015-03-31 09:19:50 -0700224
Andres Moralese40bad82015-05-28 14:21:36 -0700225 stored = readFile(getBaseZeroLockPatternFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000226 if (!ArrayUtils.isEmpty(stored)) {
Andres Moralese40bad82015-05-28 14:21:36 -0700227 return new CredentialHash(stored, true);
228 }
229
Andres Morales8fa56652015-03-31 09:19:50 -0700230 stored = readFile(getLegacyLockPatternFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000231 if (!ArrayUtils.isEmpty(stored)) {
232 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
233 CredentialHash.VERSION_LEGACY);
Andres Morales8fa56652015-03-31 09:19:50 -0700234 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100235 return null;
236 }
237
Rubin Xu1de89b32016-11-30 20:03:13 +0000238 public CredentialHash readCredentialHash(int userId) {
239 CredentialHash passwordHash = readPasswordHashIfExists(userId);
240 CredentialHash patternHash = readPatternHashIfExists(userId);
241 if (passwordHash != null && patternHash != null) {
242 if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) {
243 return passwordHash;
244 } else {
245 return patternHash;
246 }
247 } else if (passwordHash != null) {
248 return passwordHash;
249 } else if (patternHash != null) {
250 return patternHash;
251 } else {
252 return CredentialHash.createEmptyHash();
253 }
254 }
255
Ricky Waidc283a82016-03-24 19:55:08 +0000256 public void removeChildProfileLock(int userId) {
257 if (DEBUG)
258 Slog.e(TAG, "Remove child profile lock for user: " + userId);
259 try {
260 deleteFile(getChildProfileLockFile(userId));
261 } catch (Exception e) {
262 e.printStackTrace();
263 }
264 }
265
266 public void writeChildProfileLock(int userId, byte[] lock) {
267 writeFile(getChildProfileLockFile(userId), lock);
268 }
269
270 public byte[] readChildProfileLock(int userId) {
271 return readFile(getChildProfileLockFile(userId));
272 }
273
274 public boolean hasChildProfileLock(int userId) {
275 return hasFile(getChildProfileLockFile(userId));
276 }
Andres Moralese40bad82015-05-28 14:21:36 -0700277
Adrian Roos3dcae682014-10-29 14:43:56 +0100278 public boolean hasPassword(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700279 return hasFile(getLockPasswordFilename(userId)) ||
280 hasFile(getLegacyLockPasswordFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100281 }
282
Adrian Roos3dcae682014-10-29 14:43:56 +0100283 public boolean hasPattern(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700284 return hasFile(getLockPatternFilename(userId)) ||
Andres Moralese40bad82015-05-28 14:21:36 -0700285 hasFile(getBaseZeroLockPatternFilename(userId)) ||
Andres Morales8fa56652015-03-31 09:19:50 -0700286 hasFile(getLegacyLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100287 }
288
Adrian Roos3dcae682014-10-29 14:43:56 +0100289 private boolean hasFile(String name) {
290 byte[] contents = readFile(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100291 return contents != null && contents.length > 0;
292 }
293
Adrian Roos3dcae682014-10-29 14:43:56 +0100294 private byte[] readFile(String name) {
295 int version;
296 synchronized (mCache) {
297 if (mCache.hasFile(name)) {
298 return mCache.peekFile(name);
299 }
300 version = mCache.getVersion();
301 }
302
Adrian Roos261d5ab2014-10-29 14:42:38 +0100303 RandomAccessFile raf = null;
304 byte[] stored = null;
305 try {
306 raf = new RandomAccessFile(name, "r");
307 stored = new byte[(int) raf.length()];
308 raf.readFully(stored, 0, stored.length);
309 raf.close();
310 } catch (IOException e) {
311 Slog.e(TAG, "Cannot read file " + e);
312 } finally {
313 if (raf != null) {
314 try {
315 raf.close();
316 } catch (IOException e) {
317 Slog.e(TAG, "Error closing file " + e);
318 }
319 }
320 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100321 mCache.putFileIfUnchanged(name, stored, version);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100322 return stored;
323 }
324
Adrian Roos3dcae682014-10-29 14:43:56 +0100325 private void writeFile(String name, byte[] hash) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100326 synchronized (mFileWriteLock) {
327 RandomAccessFile raf = null;
328 try {
329 // Write the hash to file
330 raf = new RandomAccessFile(name, "rw");
331 // Truncate the file if pattern is null, to clear the lock
332 if (hash == null || hash.length == 0) {
333 raf.setLength(0);
334 } else {
335 raf.write(hash, 0, hash.length);
336 }
337 raf.close();
338 } catch (IOException e) {
339 Slog.e(TAG, "Error writing to file " + e);
340 } finally {
341 if (raf != null) {
342 try {
343 raf.close();
344 } catch (IOException e) {
345 Slog.e(TAG, "Error closing file " + e);
346 }
347 }
348 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100349 mCache.putFile(name, hash);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100350 }
351 }
352
Andres Moralese40bad82015-05-28 14:21:36 -0700353 private void deleteFile(String name) {
Ricky Waidc283a82016-03-24 19:55:08 +0000354 if (DEBUG) Slog.e(TAG, "Delete file " + name);
355 synchronized (mFileWriteLock) {
356 File file = new File(name);
357 if (file.exists()) {
358 file.delete();
359 mCache.putFile(name, null);
360 }
Andres Moralese40bad82015-05-28 14:21:36 -0700361 }
362 }
363
Rubin Xu1de89b32016-11-30 20:03:13 +0000364 public void writeCredentialHash(CredentialHash hash, int userId) {
365 byte[] patternHash = null;
366 byte[] passwordHash = null;
Robin Lee68e4ba42015-03-10 12:34:28 +0000367
Rubin Xu1de89b32016-11-30 20:03:13 +0000368 if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) {
369 passwordHash = hash.hash;
370 } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
371 patternHash = hash.hash;
372 }
373 writeFile(getLockPasswordFilename(userId), passwordHash);
374 writeFile(getLockPatternFilename(userId), patternHash);
Robin Lee68e4ba42015-03-10 12:34:28 +0000375 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100376
Adrian Roose5424992014-11-07 21:47:17 +0100377 @VisibleForTesting
378 String getLockPatternFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100379 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100380 }
381
Adrian Roose5424992014-11-07 21:47:17 +0100382 @VisibleForTesting
383 String getLockPasswordFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100384 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
385 }
386
Andres Morales8fa56652015-03-31 09:19:50 -0700387 @VisibleForTesting
388 String getLegacyLockPatternFilename(int userId) {
389 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
390 }
391
392 @VisibleForTesting
393 String getLegacyLockPasswordFilename(int userId) {
394 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
395 }
396
Andres Moralese40bad82015-05-28 14:21:36 -0700397 private String getBaseZeroLockPatternFilename(int userId) {
398 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
399 }
400
Ricky Waia46b40f2016-03-31 16:48:29 +0100401 @VisibleForTesting
402 String getChildProfileLockFile(int userId) {
Ricky Waidc283a82016-03-24 19:55:08 +0000403 return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE);
404 }
405
Adrian Roos3dcae682014-10-29 14:43:56 +0100406 private String getLockCredentialFilePathForUser(int userId, String basename) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100407 String dataSystemDirectory =
408 android.os.Environment.getDataDirectory().getAbsolutePath() +
409 SYSTEM_DIRECTORY;
410 if (userId == 0) {
411 // Leave it in the same place for user 0
Adrian Roos3dcae682014-10-29 14:43:56 +0100412 return dataSystemDirectory + basename;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100413 } else {
Adrian Roos3dcae682014-10-29 14:43:56 +0100414 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
Adrian Roos261d5ab2014-10-29 14:42:38 +0100415 }
416 }
417
Adrian Roos261d5ab2014-10-29 14:42:38 +0100418 public void removeUser(int userId) {
419 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
420
421 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
422 final UserInfo parentInfo = um.getProfileParent(userId);
423
Robin Lee68e4ba42015-03-10 12:34:28 +0000424 if (parentInfo == null) {
425 // This user owns its lock settings files - safe to delete them
426 synchronized (mFileWriteLock) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100427 String name = getLockPasswordFilename(userId);
428 File file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100429 if (file.exists()) {
430 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100431 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100432 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100433 name = getLockPatternFilename(userId);
434 file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100435 if (file.exists()) {
436 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100437 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100438 }
439 }
Ricky Waidc283a82016-03-24 19:55:08 +0000440 } else {
441 // Manged profile
442 removeChildProfileLock(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100443 }
444
445 try {
446 db.beginTransaction();
447 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
448 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100449 mCache.removeUser(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100450 } finally {
451 db.endTransaction();
452 }
453 }
454
Adrian Roose5424992014-11-07 21:47:17 +0100455 @VisibleForTesting
456 void closeDatabase() {
457 mOpenHelper.close();
458 }
459
460 @VisibleForTesting
461 void clearCache() {
462 mCache.clear();
463 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100464
Adrian Roos3dcae682014-10-29 14:43:56 +0100465 public interface Callback {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100466 void initialize(SQLiteDatabase db);
467 }
468
469 class DatabaseHelper extends SQLiteOpenHelper {
470 private static final String TAG = "LockSettingsDB";
471 private static final String DATABASE_NAME = "locksettings.db";
472
473 private static final int DATABASE_VERSION = 2;
474
475 private final Callback mCallback;
476
477 public DatabaseHelper(Context context, Callback callback) {
478 super(context, DATABASE_NAME, null, DATABASE_VERSION);
479 setWriteAheadLoggingEnabled(true);
480 mCallback = callback;
481 }
482
483 private void createTable(SQLiteDatabase db) {
484 db.execSQL("CREATE TABLE " + TABLE + " (" +
485 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
486 COLUMN_KEY + " TEXT," +
487 COLUMN_USERID + " INTEGER," +
488 COLUMN_VALUE + " TEXT" +
489 ");");
490 }
491
492 @Override
493 public void onCreate(SQLiteDatabase db) {
494 createTable(db);
495 mCallback.initialize(db);
496 }
497
498 @Override
499 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
500 int upgradeVersion = oldVersion;
501 if (upgradeVersion == 1) {
502 // Previously migrated lock screen widget settings. Now defunct.
503 upgradeVersion = 2;
504 }
505
506 if (upgradeVersion != DATABASE_VERSION) {
507 Log.w(TAG, "Failed to upgrade database!");
508 }
509 }
510 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100511
512 /**
513 * Cache consistency model:
514 * - Writes to storage write directly to the cache, but this MUST happen within the atomic
515 * section either provided by the database transaction or mWriteLock, such that writes to the
516 * cache and writes to the backing storage are guaranteed to occur in the same order
517 *
518 * - Reads can populate the cache, but because they are no strong ordering guarantees with
519 * respect to writes this precaution is taken:
520 * - The cache is assigned a version number that increases every time the cache is modified.
521 * Reads from backing storage can only populate the cache if the backing storage
522 * has not changed since the load operation has begun.
523 * This guarantees that no read operation can shadow a write to the cache that happens
524 * after it had begun.
525 */
526 private static class Cache {
527 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
528 private final CacheKey mCacheKey = new CacheKey();
529 private int mVersion = 0;
530
531 String peekKeyValue(String key, String defaultValue, int userId) {
532 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
533 return cached == DEFAULT ? defaultValue : (String) cached;
534 }
535
536 boolean hasKeyValue(String key, int userId) {
537 return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
538 }
539
540 void putKeyValue(String key, String value, int userId) {
541 put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
542 }
543
544 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
545 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
546 }
547
548 byte[] peekFile(String fileName) {
549 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
550 }
551
552 boolean hasFile(String fileName) {
553 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
554 }
555
556 void putFile(String key, byte[] value) {
557 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
558 }
559
560 void putFileIfUnchanged(String key, byte[] value, int version) {
561 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
562 }
563
564 void setFetched(int userId) {
565 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
566 }
567
568 boolean isFetched(int userId) {
569 return contains(CacheKey.TYPE_FETCHED, "", userId);
570 }
571
572
573 private synchronized void put(int type, String key, Object value, int userId) {
574 // Create a new CachKey here because it may be saved in the map if the key is absent.
575 mCache.put(new CacheKey().set(type, key, userId), value);
576 mVersion++;
577 }
578
579 private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
580 int version) {
581 if (!contains(type, key, userId) && mVersion == version) {
582 put(type, key, value, userId);
583 }
584 }
585
586 private synchronized boolean contains(int type, String key, int userId) {
587 return mCache.containsKey(mCacheKey.set(type, key, userId));
588 }
589
590 private synchronized Object peek(int type, String key, int userId) {
591 return mCache.get(mCacheKey.set(type, key, userId));
592 }
593
594 private synchronized int getVersion() {
595 return mVersion;
596 }
597
598 synchronized void removeUser(int userId) {
599 for (int i = mCache.size() - 1; i >= 0; i--) {
600 if (mCache.keyAt(i).userId == userId) {
601 mCache.removeAt(i);
602 }
603 }
604
605 // Make sure in-flight loads can't write to cache.
606 mVersion++;
607 }
608
Adrian Roose5424992014-11-07 21:47:17 +0100609 synchronized void clear() {
610 mCache.clear();
611 mVersion++;
612 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100613
614 private static final class CacheKey {
615 static final int TYPE_KEY_VALUE = 0;
616 static final int TYPE_FILE = 1;
617 static final int TYPE_FETCHED = 2;
618
619 String key;
620 int userId;
621 int type;
622
623 public CacheKey set(int type, String key, int userId) {
624 this.type = type;
625 this.key = key;
626 this.userId = userId;
627 return this;
628 }
629
630 @Override
631 public boolean equals(Object obj) {
632 if (!(obj instanceof CacheKey))
633 return false;
634 CacheKey o = (CacheKey) obj;
635 return userId == o.userId && type == o.type && key.equals(o.key);
636 }
637
638 @Override
639 public int hashCode() {
640 return key.hashCode() ^ userId ^ type;
641 }
642 }
643 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100644}