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