blob: c858036eb0249011ca1c5c5869f2744cc233faa3 [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
Rubin Xu0cbc19e2016-12-09 14:00:21 +0000122 public LockSettingsStorage(Context context) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100123 mContext = context;
Rubin Xu0cbc19e2016-12-09 14:00:21 +0000124 mOpenHelper = new DatabaseHelper(context);
125 }
126
127 public void setDatabaseOnCreateCallback(Callback callback) {
128 mOpenHelper.setCallback(callback);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100129 }
130
Adrian Roos3dcae682014-10-29 14:43:56 +0100131 public void writeKeyValue(String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100132 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId);
133 }
134
Adrian Roos3dcae682014-10-29 14:43:56 +0100135 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100136 ContentValues cv = new ContentValues();
137 cv.put(COLUMN_KEY, key);
138 cv.put(COLUMN_USERID, userId);
139 cv.put(COLUMN_VALUE, value);
140
141 db.beginTransaction();
142 try {
143 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?",
144 new String[] {key, Integer.toString(userId)});
145 db.insert(TABLE, null, cv);
146 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100147 mCache.putKeyValue(key, value, userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100148 } finally {
149 db.endTransaction();
150 }
151
152 }
153
Adrian Roos3dcae682014-10-29 14:43:56 +0100154 public String readKeyValue(String key, String defaultValue, int userId) {
155 int version;
156 synchronized (mCache) {
157 if (mCache.hasKeyValue(key, userId)) {
158 return mCache.peekKeyValue(key, defaultValue, userId);
159 }
160 version = mCache.getVersion();
161 }
162
Adrian Roos261d5ab2014-10-29 14:42:38 +0100163 Cursor cursor;
Adrian Roos3dcae682014-10-29 14:43:56 +0100164 Object result = DEFAULT;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100165 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
166 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY,
167 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?",
168 new String[] { Integer.toString(userId), key },
169 null, null, null)) != null) {
170 if (cursor.moveToFirst()) {
171 result = cursor.getString(0);
172 }
173 cursor.close();
174 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100175 mCache.putKeyValueIfUnchanged(key, result, userId, version);
176 return result == DEFAULT ? defaultValue : (String) result;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100177 }
178
Adrian Roos3dcae682014-10-29 14:43:56 +0100179 public void prefetchUser(int userId) {
180 int version;
181 synchronized (mCache) {
182 if (mCache.isFetched(userId)) {
183 return;
184 }
185 mCache.setFetched(userId);
186 version = mCache.getVersion();
187 }
188
189 Cursor cursor;
190 SQLiteDatabase db = mOpenHelper.getReadableDatabase();
191 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH,
192 COLUMN_USERID + "=?",
193 new String[] { Integer.toString(userId) },
194 null, null, null)) != null) {
195 while (cursor.moveToNext()) {
196 String key = cursor.getString(0);
197 String value = cursor.getString(1);
198 mCache.putKeyValueIfUnchanged(key, value, userId, version);
199 }
200 cursor.close();
201 }
202
203 // Populate cache by reading the password and pattern files.
Rubin Xu1de89b32016-11-30 20:03:13 +0000204 readCredentialHash(userId);
Adrian Roos3dcae682014-10-29 14:43:56 +0100205 }
206
Rubin Xu1de89b32016-11-30 20:03:13 +0000207 private CredentialHash readPasswordHashIfExists(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700208 byte[] stored = readFile(getLockPasswordFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000209 if (!ArrayUtils.isEmpty(stored)) {
210 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
211 CredentialHash.VERSION_GATEKEEPER);
Andres Morales8fa56652015-03-31 09:19:50 -0700212 }
213
214 stored = readFile(getLegacyLockPasswordFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000215 if (!ArrayUtils.isEmpty(stored)) {
216 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
217 CredentialHash.VERSION_LEGACY);
Andres Morales8fa56652015-03-31 09:19:50 -0700218 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100219 return null;
220 }
221
Rubin Xu1de89b32016-11-30 20:03:13 +0000222 private CredentialHash readPatternHashIfExists(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700223 byte[] stored = readFile(getLockPatternFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000224 if (!ArrayUtils.isEmpty(stored)) {
225 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
226 CredentialHash.VERSION_GATEKEEPER);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100227 }
Andres Morales8fa56652015-03-31 09:19:50 -0700228
Andres Moralese40bad82015-05-28 14:21:36 -0700229 stored = readFile(getBaseZeroLockPatternFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000230 if (!ArrayUtils.isEmpty(stored)) {
Andres Moralese40bad82015-05-28 14:21:36 -0700231 return new CredentialHash(stored, true);
232 }
233
Andres Morales8fa56652015-03-31 09:19:50 -0700234 stored = readFile(getLegacyLockPatternFilename(userId));
Rubin Xu1de89b32016-11-30 20:03:13 +0000235 if (!ArrayUtils.isEmpty(stored)) {
236 return new CredentialHash(stored, LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
237 CredentialHash.VERSION_LEGACY);
Andres Morales8fa56652015-03-31 09:19:50 -0700238 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100239 return null;
240 }
241
Rubin Xu1de89b32016-11-30 20:03:13 +0000242 public CredentialHash readCredentialHash(int userId) {
243 CredentialHash passwordHash = readPasswordHashIfExists(userId);
244 CredentialHash patternHash = readPatternHashIfExists(userId);
245 if (passwordHash != null && patternHash != null) {
246 if (passwordHash.version == CredentialHash.VERSION_GATEKEEPER) {
247 return passwordHash;
248 } else {
249 return patternHash;
250 }
251 } else if (passwordHash != null) {
252 return passwordHash;
253 } else if (patternHash != null) {
254 return patternHash;
255 } else {
256 return CredentialHash.createEmptyHash();
257 }
258 }
259
Ricky Waidc283a82016-03-24 19:55:08 +0000260 public void removeChildProfileLock(int userId) {
261 if (DEBUG)
262 Slog.e(TAG, "Remove child profile lock for user: " + userId);
263 try {
264 deleteFile(getChildProfileLockFile(userId));
265 } catch (Exception e) {
266 e.printStackTrace();
267 }
268 }
269
270 public void writeChildProfileLock(int userId, byte[] lock) {
271 writeFile(getChildProfileLockFile(userId), lock);
272 }
273
274 public byte[] readChildProfileLock(int userId) {
275 return readFile(getChildProfileLockFile(userId));
276 }
277
278 public boolean hasChildProfileLock(int userId) {
279 return hasFile(getChildProfileLockFile(userId));
280 }
Andres Moralese40bad82015-05-28 14:21:36 -0700281
Adrian Roos3dcae682014-10-29 14:43:56 +0100282 public boolean hasPassword(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700283 return hasFile(getLockPasswordFilename(userId)) ||
284 hasFile(getLegacyLockPasswordFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100285 }
286
Adrian Roos3dcae682014-10-29 14:43:56 +0100287 public boolean hasPattern(int userId) {
Andres Morales8fa56652015-03-31 09:19:50 -0700288 return hasFile(getLockPatternFilename(userId)) ||
Andres Moralese40bad82015-05-28 14:21:36 -0700289 hasFile(getBaseZeroLockPatternFilename(userId)) ||
Andres Morales8fa56652015-03-31 09:19:50 -0700290 hasFile(getLegacyLockPatternFilename(userId));
Adrian Roos261d5ab2014-10-29 14:42:38 +0100291 }
292
Adrian Roos3dcae682014-10-29 14:43:56 +0100293 private boolean hasFile(String name) {
294 byte[] contents = readFile(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100295 return contents != null && contents.length > 0;
296 }
297
Adrian Roos3dcae682014-10-29 14:43:56 +0100298 private byte[] readFile(String name) {
299 int version;
300 synchronized (mCache) {
301 if (mCache.hasFile(name)) {
302 return mCache.peekFile(name);
303 }
304 version = mCache.getVersion();
305 }
306
Adrian Roos261d5ab2014-10-29 14:42:38 +0100307 RandomAccessFile raf = null;
308 byte[] stored = null;
309 try {
310 raf = new RandomAccessFile(name, "r");
311 stored = new byte[(int) raf.length()];
312 raf.readFully(stored, 0, stored.length);
313 raf.close();
314 } catch (IOException e) {
315 Slog.e(TAG, "Cannot read file " + e);
316 } finally {
317 if (raf != null) {
318 try {
319 raf.close();
320 } catch (IOException e) {
321 Slog.e(TAG, "Error closing file " + e);
322 }
323 }
324 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100325 mCache.putFileIfUnchanged(name, stored, version);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100326 return stored;
327 }
328
Adrian Roos3dcae682014-10-29 14:43:56 +0100329 private void writeFile(String name, byte[] hash) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100330 synchronized (mFileWriteLock) {
331 RandomAccessFile raf = null;
332 try {
333 // Write the hash to file
334 raf = new RandomAccessFile(name, "rw");
335 // Truncate the file if pattern is null, to clear the lock
336 if (hash == null || hash.length == 0) {
337 raf.setLength(0);
338 } else {
339 raf.write(hash, 0, hash.length);
340 }
341 raf.close();
342 } catch (IOException e) {
343 Slog.e(TAG, "Error writing to file " + e);
344 } finally {
345 if (raf != null) {
346 try {
347 raf.close();
348 } catch (IOException e) {
349 Slog.e(TAG, "Error closing file " + e);
350 }
351 }
352 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100353 mCache.putFile(name, hash);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100354 }
355 }
356
Andres Moralese40bad82015-05-28 14:21:36 -0700357 private void deleteFile(String name) {
Ricky Waidc283a82016-03-24 19:55:08 +0000358 if (DEBUG) Slog.e(TAG, "Delete file " + name);
359 synchronized (mFileWriteLock) {
360 File file = new File(name);
361 if (file.exists()) {
362 file.delete();
363 mCache.putFile(name, null);
364 }
Andres Moralese40bad82015-05-28 14:21:36 -0700365 }
366 }
367
Rubin Xu1de89b32016-11-30 20:03:13 +0000368 public void writeCredentialHash(CredentialHash hash, int userId) {
369 byte[] patternHash = null;
370 byte[] passwordHash = null;
Robin Lee68e4ba42015-03-10 12:34:28 +0000371
Rubin Xu1de89b32016-11-30 20:03:13 +0000372 if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD) {
373 passwordHash = hash.hash;
374 } else if (hash.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
375 patternHash = hash.hash;
376 }
377 writeFile(getLockPasswordFilename(userId), passwordHash);
378 writeFile(getLockPatternFilename(userId), patternHash);
Robin Lee68e4ba42015-03-10 12:34:28 +0000379 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100380
Adrian Roose5424992014-11-07 21:47:17 +0100381 @VisibleForTesting
382 String getLockPatternFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100383 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100384 }
385
Adrian Roose5424992014-11-07 21:47:17 +0100386 @VisibleForTesting
387 String getLockPasswordFilename(int userId) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100388 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE);
389 }
390
Andres Morales8fa56652015-03-31 09:19:50 -0700391 @VisibleForTesting
392 String getLegacyLockPatternFilename(int userId) {
393 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PATTERN_FILE);
394 }
395
396 @VisibleForTesting
397 String getLegacyLockPasswordFilename(int userId) {
398 return getLockCredentialFilePathForUser(userId, LEGACY_LOCK_PASSWORD_FILE);
399 }
400
Andres Moralese40bad82015-05-28 14:21:36 -0700401 private String getBaseZeroLockPatternFilename(int userId) {
402 return getLockCredentialFilePathForUser(userId, BASE_ZERO_LOCK_PATTERN_FILE);
403 }
404
Ricky Waia46b40f2016-03-31 16:48:29 +0100405 @VisibleForTesting
406 String getChildProfileLockFile(int userId) {
Ricky Waidc283a82016-03-24 19:55:08 +0000407 return getLockCredentialFilePathForUser(userId, CHILD_PROFILE_LOCK_FILE);
408 }
409
Adrian Roos3dcae682014-10-29 14:43:56 +0100410 private String getLockCredentialFilePathForUser(int userId, String basename) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100411 String dataSystemDirectory =
412 android.os.Environment.getDataDirectory().getAbsolutePath() +
413 SYSTEM_DIRECTORY;
414 if (userId == 0) {
415 // Leave it in the same place for user 0
Adrian Roos3dcae682014-10-29 14:43:56 +0100416 return dataSystemDirectory + basename;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100417 } else {
Adrian Roos3dcae682014-10-29 14:43:56 +0100418 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath();
Adrian Roos261d5ab2014-10-29 14:42:38 +0100419 }
420 }
421
Adrian Roos261d5ab2014-10-29 14:42:38 +0100422 public void removeUser(int userId) {
423 SQLiteDatabase db = mOpenHelper.getWritableDatabase();
424
425 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE);
426 final UserInfo parentInfo = um.getProfileParent(userId);
427
Robin Lee68e4ba42015-03-10 12:34:28 +0000428 if (parentInfo == null) {
429 // This user owns its lock settings files - safe to delete them
430 synchronized (mFileWriteLock) {
Adrian Roos3dcae682014-10-29 14:43:56 +0100431 String name = getLockPasswordFilename(userId);
432 File file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100433 if (file.exists()) {
434 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100435 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100436 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100437 name = getLockPatternFilename(userId);
438 file = new File(name);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100439 if (file.exists()) {
440 file.delete();
Adrian Roos3dcae682014-10-29 14:43:56 +0100441 mCache.putFile(name, null);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100442 }
443 }
Ricky Waidc283a82016-03-24 19:55:08 +0000444 } else {
445 // Manged profile
446 removeChildProfileLock(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100447 }
448
449 try {
450 db.beginTransaction();
451 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null);
452 db.setTransactionSuccessful();
Adrian Roos3dcae682014-10-29 14:43:56 +0100453 mCache.removeUser(userId);
Adrian Roos261d5ab2014-10-29 14:42:38 +0100454 } finally {
455 db.endTransaction();
456 }
457 }
458
Adrian Roose5424992014-11-07 21:47:17 +0100459 @VisibleForTesting
460 void closeDatabase() {
461 mOpenHelper.close();
462 }
463
464 @VisibleForTesting
465 void clearCache() {
466 mCache.clear();
467 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100468
Adrian Roos3dcae682014-10-29 14:43:56 +0100469 public interface Callback {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100470 void initialize(SQLiteDatabase db);
471 }
472
473 class DatabaseHelper extends SQLiteOpenHelper {
474 private static final String TAG = "LockSettingsDB";
475 private static final String DATABASE_NAME = "locksettings.db";
476
477 private static final int DATABASE_VERSION = 2;
478
Rubin Xu0cbc19e2016-12-09 14:00:21 +0000479 private Callback mCallback;
Adrian Roos261d5ab2014-10-29 14:42:38 +0100480
Rubin Xu0cbc19e2016-12-09 14:00:21 +0000481 public DatabaseHelper(Context context) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100482 super(context, DATABASE_NAME, null, DATABASE_VERSION);
483 setWriteAheadLoggingEnabled(true);
Rubin Xu0cbc19e2016-12-09 14:00:21 +0000484 }
485
486 public void setCallback(Callback callback) {
Adrian Roos261d5ab2014-10-29 14:42:38 +0100487 mCallback = callback;
488 }
489
490 private void createTable(SQLiteDatabase db) {
491 db.execSQL("CREATE TABLE " + TABLE + " (" +
492 "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
493 COLUMN_KEY + " TEXT," +
494 COLUMN_USERID + " INTEGER," +
495 COLUMN_VALUE + " TEXT" +
496 ");");
497 }
498
499 @Override
500 public void onCreate(SQLiteDatabase db) {
501 createTable(db);
Rubin Xu0cbc19e2016-12-09 14:00:21 +0000502 if (mCallback != null) {
503 mCallback.initialize(db);
504 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100505 }
506
507 @Override
508 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) {
509 int upgradeVersion = oldVersion;
510 if (upgradeVersion == 1) {
511 // Previously migrated lock screen widget settings. Now defunct.
512 upgradeVersion = 2;
513 }
514
515 if (upgradeVersion != DATABASE_VERSION) {
516 Log.w(TAG, "Failed to upgrade database!");
517 }
518 }
519 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100520
521 /**
522 * Cache consistency model:
523 * - Writes to storage write directly to the cache, but this MUST happen within the atomic
524 * section either provided by the database transaction or mWriteLock, such that writes to the
525 * cache and writes to the backing storage are guaranteed to occur in the same order
526 *
527 * - Reads can populate the cache, but because they are no strong ordering guarantees with
528 * respect to writes this precaution is taken:
529 * - The cache is assigned a version number that increases every time the cache is modified.
530 * Reads from backing storage can only populate the cache if the backing storage
531 * has not changed since the load operation has begun.
532 * This guarantees that no read operation can shadow a write to the cache that happens
533 * after it had begun.
534 */
535 private static class Cache {
536 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>();
537 private final CacheKey mCacheKey = new CacheKey();
538 private int mVersion = 0;
539
540 String peekKeyValue(String key, String defaultValue, int userId) {
541 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId);
542 return cached == DEFAULT ? defaultValue : (String) cached;
543 }
544
545 boolean hasKeyValue(String key, int userId) {
546 return contains(CacheKey.TYPE_KEY_VALUE, key, userId);
547 }
548
549 void putKeyValue(String key, String value, int userId) {
550 put(CacheKey.TYPE_KEY_VALUE, key, value, userId);
551 }
552
553 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) {
554 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version);
555 }
556
557 byte[] peekFile(String fileName) {
558 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
559 }
560
561 boolean hasFile(String fileName) {
562 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */);
563 }
564
565 void putFile(String key, byte[] value) {
566 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */);
567 }
568
569 void putFileIfUnchanged(String key, byte[] value, int version) {
570 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version);
571 }
572
573 void setFetched(int userId) {
574 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId);
575 }
576
577 boolean isFetched(int userId) {
578 return contains(CacheKey.TYPE_FETCHED, "", userId);
579 }
580
581
582 private synchronized void put(int type, String key, Object value, int userId) {
583 // Create a new CachKey here because it may be saved in the map if the key is absent.
584 mCache.put(new CacheKey().set(type, key, userId), value);
585 mVersion++;
586 }
587
588 private synchronized void putIfUnchanged(int type, String key, Object value, int userId,
589 int version) {
590 if (!contains(type, key, userId) && mVersion == version) {
591 put(type, key, value, userId);
592 }
593 }
594
595 private synchronized boolean contains(int type, String key, int userId) {
596 return mCache.containsKey(mCacheKey.set(type, key, userId));
597 }
598
599 private synchronized Object peek(int type, String key, int userId) {
600 return mCache.get(mCacheKey.set(type, key, userId));
601 }
602
603 private synchronized int getVersion() {
604 return mVersion;
605 }
606
607 synchronized void removeUser(int userId) {
608 for (int i = mCache.size() - 1; i >= 0; i--) {
609 if (mCache.keyAt(i).userId == userId) {
610 mCache.removeAt(i);
611 }
612 }
613
614 // Make sure in-flight loads can't write to cache.
615 mVersion++;
616 }
617
Adrian Roose5424992014-11-07 21:47:17 +0100618 synchronized void clear() {
619 mCache.clear();
620 mVersion++;
621 }
Adrian Roos3dcae682014-10-29 14:43:56 +0100622
623 private static final class CacheKey {
624 static final int TYPE_KEY_VALUE = 0;
625 static final int TYPE_FILE = 1;
626 static final int TYPE_FETCHED = 2;
627
628 String key;
629 int userId;
630 int type;
631
632 public CacheKey set(int type, String key, int userId) {
633 this.type = type;
634 this.key = key;
635 this.userId = userId;
636 return this;
637 }
638
639 @Override
640 public boolean equals(Object obj) {
641 if (!(obj instanceof CacheKey))
642 return false;
643 CacheKey o = (CacheKey) obj;
644 return userId == o.userId && type == o.type && key.equals(o.key);
645 }
646
647 @Override
648 public int hashCode() {
649 return key.hashCode() ^ userId ^ type;
650 }
651 }
652 }
Adrian Roos261d5ab2014-10-29 14:42:38 +0100653}