LockSettingsStorage.java revision 3dcae68501a1fc1c433d12a9d55a31c7eaab016c
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; 18 19import android.content.ContentValues; 20import android.content.Context; 21import android.content.pm.UserInfo; 22import android.database.Cursor; 23import android.database.sqlite.SQLiteDatabase; 24import android.database.sqlite.SQLiteOpenHelper; 25import android.os.Environment; 26import android.os.UserManager; 27import android.util.ArrayMap; 28import android.util.Log; 29import android.util.Slog; 30 31import java.io.File; 32import java.io.IOException; 33import java.io.RandomAccessFile; 34 35import static android.content.Context.USER_SERVICE; 36 37/** 38 * Storage for the lock settings service. 39 */ 40class LockSettingsStorage { 41 42 private static final String TAG = "LockSettingsStorage"; 43 private static final String TABLE = "locksettings"; 44 45 private static final String COLUMN_KEY = "name"; 46 private static final String COLUMN_USERID = "user"; 47 private static final String COLUMN_VALUE = "value"; 48 49 private static final String[] COLUMNS_FOR_QUERY = { 50 COLUMN_VALUE 51 }; 52 private static final String[] COLUMNS_FOR_PREFETCH = { 53 COLUMN_KEY, COLUMN_VALUE 54 }; 55 56 private static final String SYSTEM_DIRECTORY = "/system/"; 57 private static final String LOCK_PATTERN_FILE = "gesture.key"; 58 private static final String LOCK_PASSWORD_FILE = "password.key"; 59 60 private static final Object DEFAULT = new Object(); 61 62 private final DatabaseHelper mOpenHelper; 63 private final Context mContext; 64 private final Cache mCache = new Cache(); 65 private final Object mFileWriteLock = new Object(); 66 67 public LockSettingsStorage(Context context, Callback callback) { 68 mContext = context; 69 mOpenHelper = new DatabaseHelper(context, callback); 70 } 71 72 public void writeKeyValue(String key, String value, int userId) { 73 writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId); 74 } 75 76 public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) { 77 ContentValues cv = new ContentValues(); 78 cv.put(COLUMN_KEY, key); 79 cv.put(COLUMN_USERID, userId); 80 cv.put(COLUMN_VALUE, value); 81 82 db.beginTransaction(); 83 try { 84 db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", 85 new String[] {key, Integer.toString(userId)}); 86 db.insert(TABLE, null, cv); 87 db.setTransactionSuccessful(); 88 mCache.putKeyValue(key, value, userId); 89 } finally { 90 db.endTransaction(); 91 } 92 93 } 94 95 public String readKeyValue(String key, String defaultValue, int userId) { 96 int version; 97 synchronized (mCache) { 98 if (mCache.hasKeyValue(key, userId)) { 99 return mCache.peekKeyValue(key, defaultValue, userId); 100 } 101 version = mCache.getVersion(); 102 } 103 104 Cursor cursor; 105 Object result = DEFAULT; 106 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 107 if ((cursor = db.query(TABLE, COLUMNS_FOR_QUERY, 108 COLUMN_USERID + "=? AND " + COLUMN_KEY + "=?", 109 new String[] { Integer.toString(userId), key }, 110 null, null, null)) != null) { 111 if (cursor.moveToFirst()) { 112 result = cursor.getString(0); 113 } 114 cursor.close(); 115 } 116 mCache.putKeyValueIfUnchanged(key, result, userId, version); 117 return result == DEFAULT ? defaultValue : (String) result; 118 } 119 120 public void prefetchUser(int userId) { 121 int version; 122 synchronized (mCache) { 123 if (mCache.isFetched(userId)) { 124 return; 125 } 126 mCache.setFetched(userId); 127 version = mCache.getVersion(); 128 } 129 130 Cursor cursor; 131 SQLiteDatabase db = mOpenHelper.getReadableDatabase(); 132 if ((cursor = db.query(TABLE, COLUMNS_FOR_PREFETCH, 133 COLUMN_USERID + "=?", 134 new String[] { Integer.toString(userId) }, 135 null, null, null)) != null) { 136 while (cursor.moveToNext()) { 137 String key = cursor.getString(0); 138 String value = cursor.getString(1); 139 mCache.putKeyValueIfUnchanged(key, value, userId, version); 140 } 141 cursor.close(); 142 } 143 144 // Populate cache by reading the password and pattern files. 145 readPasswordHash(userId); 146 readPatternHash(userId); 147 } 148 149 public byte[] readPasswordHash(int userId) { 150 final byte[] stored = readFile(getLockPasswordFilename(userId)); 151 if (stored != null && stored.length > 0) { 152 return stored; 153 } 154 return null; 155 } 156 157 public byte[] readPatternHash(int userId) { 158 final byte[] stored = readFile(getLockPatternFilename(userId)); 159 if (stored != null && stored.length > 0) { 160 return stored; 161 } 162 return null; 163 } 164 165 public boolean hasPassword(int userId) { 166 return hasFile(getLockPasswordFilename(userId)); 167 } 168 169 public boolean hasPattern(int userId) { 170 return hasFile(getLockPatternFilename(userId)); 171 } 172 173 private boolean hasFile(String name) { 174 byte[] contents = readFile(name); 175 return contents != null && contents.length > 0; 176 } 177 178 private byte[] readFile(String name) { 179 int version; 180 synchronized (mCache) { 181 if (mCache.hasFile(name)) { 182 return mCache.peekFile(name); 183 } 184 version = mCache.getVersion(); 185 } 186 187 RandomAccessFile raf = null; 188 byte[] stored = null; 189 try { 190 raf = new RandomAccessFile(name, "r"); 191 stored = new byte[(int) raf.length()]; 192 raf.readFully(stored, 0, stored.length); 193 raf.close(); 194 } catch (IOException e) { 195 Slog.e(TAG, "Cannot read file " + e); 196 } finally { 197 if (raf != null) { 198 try { 199 raf.close(); 200 } catch (IOException e) { 201 Slog.e(TAG, "Error closing file " + e); 202 } 203 } 204 } 205 mCache.putFileIfUnchanged(name, stored, version); 206 return stored; 207 } 208 209 private void writeFile(String name, byte[] hash) { 210 synchronized (mFileWriteLock) { 211 RandomAccessFile raf = null; 212 try { 213 // Write the hash to file 214 raf = new RandomAccessFile(name, "rw"); 215 // Truncate the file if pattern is null, to clear the lock 216 if (hash == null || hash.length == 0) { 217 raf.setLength(0); 218 } else { 219 raf.write(hash, 0, hash.length); 220 } 221 raf.close(); 222 } catch (IOException e) { 223 Slog.e(TAG, "Error writing to file " + e); 224 } finally { 225 if (raf != null) { 226 try { 227 raf.close(); 228 } catch (IOException e) { 229 Slog.e(TAG, "Error closing file " + e); 230 } 231 } 232 } 233 mCache.putFile(name, hash); 234 } 235 } 236 237 public void writePatternHash(byte[] hash, int userId) { 238 writeFile(getLockPatternFilename(userId), hash); 239 } 240 241 public void writePasswordHash(byte[] hash, int userId) { 242 writeFile(getLockPasswordFilename(userId), hash); 243 } 244 245 246 private String getLockPatternFilename(int userId) { 247 return getLockCredentialFilePathForUser(userId, LOCK_PATTERN_FILE); 248 } 249 250 private String getLockPasswordFilename(int userId) { 251 return getLockCredentialFilePathForUser(userId, LOCK_PASSWORD_FILE); 252 } 253 254 private String getLockCredentialFilePathForUser(int userId, String basename) { 255 userId = getUserParentOrSelfId(userId); 256 String dataSystemDirectory = 257 android.os.Environment.getDataDirectory().getAbsolutePath() + 258 SYSTEM_DIRECTORY; 259 if (userId == 0) { 260 // Leave it in the same place for user 0 261 return dataSystemDirectory + basename; 262 } else { 263 return new File(Environment.getUserSystemDirectory(userId), basename).getAbsolutePath(); 264 } 265 } 266 267 private int getUserParentOrSelfId(int userId) { 268 if (userId != 0) { 269 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); 270 final UserInfo pi = um.getProfileParent(userId); 271 if (pi != null) { 272 return pi.id; 273 } 274 } 275 return userId; 276 } 277 278 279 public void removeUser(int userId) { 280 SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 281 282 final UserManager um = (UserManager) mContext.getSystemService(USER_SERVICE); 283 final UserInfo parentInfo = um.getProfileParent(userId); 284 285 synchronized (mFileWriteLock) { 286 if (parentInfo == null) { 287 // This user owns its lock settings files - safe to delete them 288 String name = getLockPasswordFilename(userId); 289 File file = new File(name); 290 if (file.exists()) { 291 file.delete(); 292 mCache.putFile(name, null); 293 } 294 name = getLockPatternFilename(userId); 295 file = new File(name); 296 if (file.exists()) { 297 file.delete(); 298 mCache.putFile(name, null); 299 } 300 } 301 } 302 303 try { 304 db.beginTransaction(); 305 db.delete(TABLE, COLUMN_USERID + "='" + userId + "'", null); 306 db.setTransactionSuccessful(); 307 mCache.removeUser(userId); 308 } finally { 309 db.endTransaction(); 310 } 311 } 312 313 314 public interface Callback { 315 void initialize(SQLiteDatabase db); 316 } 317 318 class DatabaseHelper extends SQLiteOpenHelper { 319 private static final String TAG = "LockSettingsDB"; 320 private static final String DATABASE_NAME = "locksettings.db"; 321 322 private static final int DATABASE_VERSION = 2; 323 324 private final Callback mCallback; 325 326 public DatabaseHelper(Context context, Callback callback) { 327 super(context, DATABASE_NAME, null, DATABASE_VERSION); 328 setWriteAheadLoggingEnabled(true); 329 mCallback = callback; 330 } 331 332 private void createTable(SQLiteDatabase db) { 333 db.execSQL("CREATE TABLE " + TABLE + " (" + 334 "_id INTEGER PRIMARY KEY AUTOINCREMENT," + 335 COLUMN_KEY + " TEXT," + 336 COLUMN_USERID + " INTEGER," + 337 COLUMN_VALUE + " TEXT" + 338 ");"); 339 } 340 341 @Override 342 public void onCreate(SQLiteDatabase db) { 343 createTable(db); 344 mCallback.initialize(db); 345 } 346 347 @Override 348 public void onUpgrade(SQLiteDatabase db, int oldVersion, int currentVersion) { 349 int upgradeVersion = oldVersion; 350 if (upgradeVersion == 1) { 351 // Previously migrated lock screen widget settings. Now defunct. 352 upgradeVersion = 2; 353 } 354 355 if (upgradeVersion != DATABASE_VERSION) { 356 Log.w(TAG, "Failed to upgrade database!"); 357 } 358 } 359 } 360 361 /** 362 * Cache consistency model: 363 * - Writes to storage write directly to the cache, but this MUST happen within the atomic 364 * section either provided by the database transaction or mWriteLock, such that writes to the 365 * cache and writes to the backing storage are guaranteed to occur in the same order 366 * 367 * - Reads can populate the cache, but because they are no strong ordering guarantees with 368 * respect to writes this precaution is taken: 369 * - The cache is assigned a version number that increases every time the cache is modified. 370 * Reads from backing storage can only populate the cache if the backing storage 371 * has not changed since the load operation has begun. 372 * This guarantees that no read operation can shadow a write to the cache that happens 373 * after it had begun. 374 */ 375 private static class Cache { 376 private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>(); 377 private final CacheKey mCacheKey = new CacheKey(); 378 private int mVersion = 0; 379 380 String peekKeyValue(String key, String defaultValue, int userId) { 381 Object cached = peek(CacheKey.TYPE_KEY_VALUE, key, userId); 382 return cached == DEFAULT ? defaultValue : (String) cached; 383 } 384 385 boolean hasKeyValue(String key, int userId) { 386 return contains(CacheKey.TYPE_KEY_VALUE, key, userId); 387 } 388 389 void putKeyValue(String key, String value, int userId) { 390 put(CacheKey.TYPE_KEY_VALUE, key, value, userId); 391 } 392 393 void putKeyValueIfUnchanged(String key, Object value, int userId, int version) { 394 putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version); 395 } 396 397 byte[] peekFile(String fileName) { 398 return (byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */); 399 } 400 401 boolean hasFile(String fileName) { 402 return contains(CacheKey.TYPE_FILE, fileName, -1 /* userId */); 403 } 404 405 void putFile(String key, byte[] value) { 406 put(CacheKey.TYPE_FILE, key, value, -1 /* userId */); 407 } 408 409 void putFileIfUnchanged(String key, byte[] value, int version) { 410 putIfUnchanged(CacheKey.TYPE_FILE, key, value, -1 /* userId */, version); 411 } 412 413 void setFetched(int userId) { 414 put(CacheKey.TYPE_FETCHED, "isFetched", "true", userId); 415 } 416 417 boolean isFetched(int userId) { 418 return contains(CacheKey.TYPE_FETCHED, "", userId); 419 } 420 421 422 private synchronized void put(int type, String key, Object value, int userId) { 423 // Create a new CachKey here because it may be saved in the map if the key is absent. 424 mCache.put(new CacheKey().set(type, key, userId), value); 425 mVersion++; 426 } 427 428 private synchronized void putIfUnchanged(int type, String key, Object value, int userId, 429 int version) { 430 if (!contains(type, key, userId) && mVersion == version) { 431 put(type, key, value, userId); 432 } 433 } 434 435 private synchronized boolean contains(int type, String key, int userId) { 436 return mCache.containsKey(mCacheKey.set(type, key, userId)); 437 } 438 439 private synchronized Object peek(int type, String key, int userId) { 440 return mCache.get(mCacheKey.set(type, key, userId)); 441 } 442 443 private synchronized int getVersion() { 444 return mVersion; 445 } 446 447 synchronized void removeUser(int userId) { 448 for (int i = mCache.size() - 1; i >= 0; i--) { 449 if (mCache.keyAt(i).userId == userId) { 450 mCache.removeAt(i); 451 } 452 } 453 454 // Make sure in-flight loads can't write to cache. 455 mVersion++; 456 } 457 458 459 private static final class CacheKey { 460 static final int TYPE_KEY_VALUE = 0; 461 static final int TYPE_FILE = 1; 462 static final int TYPE_FETCHED = 2; 463 464 String key; 465 int userId; 466 int type; 467 468 public CacheKey set(int type, String key, int userId) { 469 this.type = type; 470 this.key = key; 471 this.userId = userId; 472 return this; 473 } 474 475 @Override 476 public boolean equals(Object obj) { 477 if (!(obj instanceof CacheKey)) 478 return false; 479 CacheKey o = (CacheKey) obj; 480 return userId == o.userId && type == o.type && key.equals(o.key); 481 } 482 483 @Override 484 public int hashCode() { 485 return key.hashCode() ^ userId ^ type; 486 } 487 } 488 } 489} 490