1 /* 2 * Copyright (C) 2010 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 android.app; 18 19 import android.content.SharedPreferences; 20 import android.os.FileUtils; 21 import android.os.Looper; 22 import android.util.Log; 23 24 import com.google.android.collect.Maps; 25 import com.android.internal.util.XmlUtils; 26 27 import dalvik.system.BlockGuard; 28 29 import org.xmlpull.v1.XmlPullParserException; 30 31 import java.io.BufferedInputStream; 32 import java.io.File; 33 import java.io.FileInputStream; 34 import java.io.FileNotFoundException; 35 import java.io.FileOutputStream; 36 import java.io.IOException; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.Set; 43 import java.util.WeakHashMap; 44 import java.util.concurrent.CountDownLatch; 45 import java.util.concurrent.ExecutorService; 46 47 import libcore.io.ErrnoException; 48 import libcore.io.IoUtils; 49 import libcore.io.Libcore; 50 import libcore.io.StructStat; 51 52 final class SharedPreferencesImpl implements SharedPreferences { 53 private static final String TAG = "SharedPreferencesImpl"; 54 private static final boolean DEBUG = false; 55 56 // Lock ordering rules: 57 // - acquire SharedPreferencesImpl.this before EditorImpl.this 58 // - acquire mWritingToDiskLock before EditorImpl.this 59 60 private final File mFile; 61 private final File mBackupFile; 62 private final int mMode; 63 64 private Map<String, Object> mMap; // guarded by 'this' 65 private int mDiskWritesInFlight = 0; // guarded by 'this' 66 private boolean mLoaded = false; // guarded by 'this' 67 private long mStatTimestamp; // guarded by 'this' 68 private long mStatSize; // guarded by 'this' 69 70 private final Object mWritingToDiskLock = new Object(); 71 private static final Object mContent = new Object(); 72 private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners = 73 new WeakHashMap<OnSharedPreferenceChangeListener, Object>(); 74 75 SharedPreferencesImpl(File file, int mode) { 76 mFile = file; 77 mBackupFile = makeBackupFile(file); 78 mMode = mode; 79 mLoaded = false; 80 mMap = null; 81 startLoadFromDisk(); 82 } 83 84 private void startLoadFromDisk() { 85 synchronized (this) { 86 mLoaded = false; 87 } 88 new Thread("SharedPreferencesImpl-load") { 89 public void run() { 90 synchronized (SharedPreferencesImpl.this) { 91 loadFromDiskLocked(); 92 } 93 } 94 }.start(); 95 } 96 97 private void loadFromDiskLocked() { 98 if (mLoaded) { 99 return; 100 } 101 if (mBackupFile.exists()) { 102 mFile.delete(); 103 mBackupFile.renameTo(mFile); 104 } 105 106 // Debugging 107 if (mFile.exists() && !mFile.canRead()) { 108 Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission"); 109 } 110 111 Map map = null; 112 StructStat stat = null; 113 try { 114 stat = Libcore.os.stat(mFile.getPath()); 115 if (mFile.canRead()) { 116 BufferedInputStream str = null; 117 try { 118 str = new BufferedInputStream( 119 new FileInputStream(mFile), 16*1024); 120 map = XmlUtils.readMapXml(str); 121 } catch (XmlPullParserException e) { 122 Log.w(TAG, "getSharedPreferences", e); 123 } catch (FileNotFoundException e) { 124 Log.w(TAG, "getSharedPreferences", e); 125 } catch (IOException e) { 126 Log.w(TAG, "getSharedPreferences", e); 127 } finally { 128 IoUtils.closeQuietly(str); 129 } 130 } 131 } catch (ErrnoException e) { 132 } 133 mLoaded = true; 134 if (map != null) { 135 mMap = map; 136 mStatTimestamp = stat.st_mtime; 137 mStatSize = stat.st_size; 138 } else { 139 mMap = new HashMap<String, Object>(); 140 } 141 notifyAll(); 142 } 143 144 private static File makeBackupFile(File prefsFile) { 145 return new File(prefsFile.getPath() + ".bak"); 146 } 147 148 void startReloadIfChangedUnexpectedly() { 149 synchronized (this) { 150 // TODO: wait for any pending writes to disk? 151 if (!hasFileChangedUnexpectedly()) { 152 return; 153 } 154 startLoadFromDisk(); 155 } 156 } 157 158 // Has the file changed out from under us? i.e. writes that 159 // we didn't instigate. 160 private boolean hasFileChangedUnexpectedly() { 161 synchronized (this) { 162 if (mDiskWritesInFlight > 0) { 163 // If we know we caused it, it's not unexpected. 164 if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected."); 165 return false; 166 } 167 } 168 169 final StructStat stat; 170 try { 171 /* 172 * Metadata operations don't usually count as a block guard 173 * violation, but we explicitly want this one. 174 */ 175 BlockGuard.getThreadPolicy().onReadFromDisk(); 176 stat = Libcore.os.stat(mFile.getPath()); 177 } catch (ErrnoException e) { 178 return true; 179 } 180 181 synchronized (this) { 182 return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size; 183 } 184 } 185 186 public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 187 synchronized(this) { 188 mListeners.put(listener, mContent); 189 } 190 } 191 192 public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { 193 synchronized(this) { 194 mListeners.remove(listener); 195 } 196 } 197 198 private void awaitLoadedLocked() { 199 if (!mLoaded) { 200 // Raise an explicit StrictMode onReadFromDisk for this 201 // thread, since the real read will be in a different 202 // thread and otherwise ignored by StrictMode. 203 BlockGuard.getThreadPolicy().onReadFromDisk(); 204 } 205 while (!mLoaded) { 206 try { 207 wait(); 208 } catch (InterruptedException unused) { 209 } 210 } 211 } 212 213 public Map<String, ?> getAll() { 214 synchronized (this) { 215 awaitLoadedLocked(); 216 //noinspection unchecked 217 return new HashMap<String, Object>(mMap); 218 } 219 } 220 221 public String getString(String key, String defValue) { 222 synchronized (this) { 223 awaitLoadedLocked(); 224 String v = (String)mMap.get(key); 225 return v != null ? v : defValue; 226 } 227 } 228 229 public Set<String> getStringSet(String key, Set<String> defValues) { 230 synchronized (this) { 231 awaitLoadedLocked(); 232 Set<String> v = (Set<String>) mMap.get(key); 233 return v != null ? v : defValues; 234 } 235 } 236 237 public int getInt(String key, int defValue) { 238 synchronized (this) { 239 awaitLoadedLocked(); 240 Integer v = (Integer)mMap.get(key); 241 return v != null ? v : defValue; 242 } 243 } 244 public long getLong(String key, long defValue) { 245 synchronized (this) { 246 awaitLoadedLocked(); 247 Long v = (Long)mMap.get(key); 248 return v != null ? v : defValue; 249 } 250 } 251 public float getFloat(String key, float defValue) { 252 synchronized (this) { 253 awaitLoadedLocked(); 254 Float v = (Float)mMap.get(key); 255 return v != null ? v : defValue; 256 } 257 } 258 public boolean getBoolean(String key, boolean defValue) { 259 synchronized (this) { 260 awaitLoadedLocked(); 261 Boolean v = (Boolean)mMap.get(key); 262 return v != null ? v : defValue; 263 } 264 } 265 266 public boolean contains(String key) { 267 synchronized (this) { 268 awaitLoadedLocked(); 269 return mMap.containsKey(key); 270 } 271 } 272 273 public Editor edit() { 274 // TODO: remove the need to call awaitLoadedLocked() when 275 // requesting an editor. will require some work on the 276 // Editor, but then we should be able to do: 277 // 278 // context.getSharedPreferences(..).edit().putString(..).apply() 279 // 280 // ... all without blocking. 281 synchronized (this) { 282 awaitLoadedLocked(); 283 } 284 285 return new EditorImpl(); 286 } 287 288 // Return value from EditorImpl#commitToMemory() 289 private static class MemoryCommitResult { 290 public boolean changesMade; // any keys different? 291 public List<String> keysModified; // may be null 292 public Set<OnSharedPreferenceChangeListener> listeners; // may be null 293 public Map<?, ?> mapToWriteToDisk; 294 public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1); 295 public volatile boolean writeToDiskResult = false; 296 297 public void setDiskWriteResult(boolean result) { 298 writeToDiskResult = result; 299 writtenToDiskLatch.countDown(); 300 } 301 } 302 303 public final class EditorImpl implements Editor { 304 private final Map<String, Object> mModified = Maps.newHashMap(); 305 private boolean mClear = false; 306 307 public Editor putString(String key, String value) { 308 synchronized (this) { 309 mModified.put(key, value); 310 return this; 311 } 312 } 313 public Editor putStringSet(String key, Set<String> values) { 314 synchronized (this) { 315 mModified.put(key, 316 (values == null) ? null : new HashSet<String>(values)); 317 return this; 318 } 319 } 320 public Editor putInt(String key, int value) { 321 synchronized (this) { 322 mModified.put(key, value); 323 return this; 324 } 325 } 326 public Editor putLong(String key, long value) { 327 synchronized (this) { 328 mModified.put(key, value); 329 return this; 330 } 331 } 332 public Editor putFloat(String key, float value) { 333 synchronized (this) { 334 mModified.put(key, value); 335 return this; 336 } 337 } 338 public Editor putBoolean(String key, boolean value) { 339 synchronized (this) { 340 mModified.put(key, value); 341 return this; 342 } 343 } 344 345 public Editor remove(String key) { 346 synchronized (this) { 347 mModified.put(key, this); 348 return this; 349 } 350 } 351 352 public Editor clear() { 353 synchronized (this) { 354 mClear = true; 355 return this; 356 } 357 } 358 359 public void apply() { 360 final MemoryCommitResult mcr = commitToMemory(); 361 final Runnable awaitCommit = new Runnable() { 362 public void run() { 363 try { 364 mcr.writtenToDiskLatch.await(); 365 } catch (InterruptedException ignored) { 366 } 367 } 368 }; 369 370 QueuedWork.add(awaitCommit); 371 372 Runnable postWriteRunnable = new Runnable() { 373 public void run() { 374 awaitCommit.run(); 375 QueuedWork.remove(awaitCommit); 376 } 377 }; 378 379 SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); 380 381 // Okay to notify the listeners before it's hit disk 382 // because the listeners should always get the same 383 // SharedPreferences instance back, which has the 384 // changes reflected in memory. 385 notifyListeners(mcr); 386 } 387 388 // Returns true if any changes were made 389 private MemoryCommitResult commitToMemory() { 390 MemoryCommitResult mcr = new MemoryCommitResult(); 391 synchronized (SharedPreferencesImpl.this) { 392 // We optimistically don't make a deep copy until 393 // a memory commit comes in when we're already 394 // writing to disk. 395 if (mDiskWritesInFlight > 0) { 396 // We can't modify our mMap as a currently 397 // in-flight write owns it. Clone it before 398 // modifying it. 399 // noinspection unchecked 400 mMap = new HashMap<String, Object>(mMap); 401 } 402 mcr.mapToWriteToDisk = mMap; 403 mDiskWritesInFlight++; 404 405 boolean hasListeners = mListeners.size() > 0; 406 if (hasListeners) { 407 mcr.keysModified = new ArrayList<String>(); 408 mcr.listeners = 409 new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); 410 } 411 412 synchronized (this) { 413 if (mClear) { 414 if (!mMap.isEmpty()) { 415 mcr.changesMade = true; 416 mMap.clear(); 417 } 418 mClear = false; 419 } 420 421 for (Map.Entry<String, Object> e : mModified.entrySet()) { 422 String k = e.getKey(); 423 Object v = e.getValue(); 424 if (v == this) { // magic value for a removal mutation 425 if (!mMap.containsKey(k)) { 426 continue; 427 } 428 mMap.remove(k); 429 } else { 430 boolean isSame = false; 431 if (mMap.containsKey(k)) { 432 Object existingValue = mMap.get(k); 433 if (existingValue != null && existingValue.equals(v)) { 434 continue; 435 } 436 } 437 mMap.put(k, v); 438 } 439 440 mcr.changesMade = true; 441 if (hasListeners) { 442 mcr.keysModified.add(k); 443 } 444 } 445 446 mModified.clear(); 447 } 448 } 449 return mcr; 450 } 451 452 public boolean commit() { 453 MemoryCommitResult mcr = commitToMemory(); 454 SharedPreferencesImpl.this.enqueueDiskWrite( 455 mcr, null /* sync write on this thread okay */); 456 try { 457 mcr.writtenToDiskLatch.await(); 458 } catch (InterruptedException e) { 459 return false; 460 } 461 notifyListeners(mcr); 462 return mcr.writeToDiskResult; 463 } 464 465 private void notifyListeners(final MemoryCommitResult mcr) { 466 if (mcr.listeners == null || mcr.keysModified == null || 467 mcr.keysModified.size() == 0) { 468 return; 469 } 470 if (Looper.myLooper() == Looper.getMainLooper()) { 471 for (int i = mcr.keysModified.size() - 1; i >= 0; i--) { 472 final String key = mcr.keysModified.get(i); 473 for (OnSharedPreferenceChangeListener listener : mcr.listeners) { 474 if (listener != null) { 475 listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key); 476 } 477 } 478 } 479 } else { 480 // Run this function on the main thread. 481 ActivityThread.sMainThreadHandler.post(new Runnable() { 482 public void run() { 483 notifyListeners(mcr); 484 } 485 }); 486 } 487 } 488 } 489 490 /** 491 * Enqueue an already-committed-to-memory result to be written 492 * to disk. 493 * 494 * They will be written to disk one-at-a-time in the order 495 * that they're enqueued. 496 * 497 * @param postWriteRunnable if non-null, we're being called 498 * from apply() and this is the runnable to run after 499 * the write proceeds. if null (from a regular commit()), 500 * then we're allowed to do this disk write on the main 501 * thread (which in addition to reducing allocations and 502 * creating a background thread, this has the advantage that 503 * we catch them in userdebug StrictMode reports to convert 504 * them where possible to apply() ...) 505 */ 506 private void enqueueDiskWrite(final MemoryCommitResult mcr, 507 final Runnable postWriteRunnable) { 508 final Runnable writeToDiskRunnable = new Runnable() { 509 public void run() { 510 synchronized (mWritingToDiskLock) { 511 writeToFile(mcr); 512 } 513 synchronized (SharedPreferencesImpl.this) { 514 mDiskWritesInFlight--; 515 } 516 if (postWriteRunnable != null) { 517 postWriteRunnable.run(); 518 } 519 } 520 }; 521 522 final boolean isFromSyncCommit = (postWriteRunnable == null); 523 524 // Typical #commit() path with fewer allocations, doing a write on 525 // the current thread. 526 if (isFromSyncCommit) { 527 boolean wasEmpty = false; 528 synchronized (SharedPreferencesImpl.this) { 529 wasEmpty = mDiskWritesInFlight == 1; 530 } 531 if (wasEmpty) { 532 writeToDiskRunnable.run(); 533 return; 534 } 535 } 536 537 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); 538 } 539 540 private static FileOutputStream createFileOutputStream(File file) { 541 FileOutputStream str = null; 542 try { 543 str = new FileOutputStream(file); 544 } catch (FileNotFoundException e) { 545 File parent = file.getParentFile(); 546 if (!parent.mkdir()) { 547 Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file); 548 return null; 549 } 550 FileUtils.setPermissions( 551 parent.getPath(), 552 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, 553 -1, -1); 554 try { 555 str = new FileOutputStream(file); 556 } catch (FileNotFoundException e2) { 557 Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2); 558 } 559 } 560 return str; 561 } 562 563 // Note: must hold mWritingToDiskLock 564 private void writeToFile(MemoryCommitResult mcr) { 565 // Rename the current file so it may be used as a backup during the next read 566 if (mFile.exists()) { 567 if (!mcr.changesMade) { 568 // If the file already exists, but no changes were 569 // made to the underlying map, it's wasteful to 570 // re-write the file. Return as if we wrote it 571 // out. 572 mcr.setDiskWriteResult(true); 573 return; 574 } 575 if (!mBackupFile.exists()) { 576 if (!mFile.renameTo(mBackupFile)) { 577 Log.e(TAG, "Couldn't rename file " + mFile 578 + " to backup file " + mBackupFile); 579 mcr.setDiskWriteResult(false); 580 return; 581 } 582 } else { 583 mFile.delete(); 584 } 585 } 586 587 // Attempt to write the file, delete the backup and return true as atomically as 588 // possible. If any exception occurs, delete the new file; next time we will restore 589 // from the backup. 590 try { 591 FileOutputStream str = createFileOutputStream(mFile); 592 if (str == null) { 593 mcr.setDiskWriteResult(false); 594 return; 595 } 596 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); 597 FileUtils.sync(str); 598 str.close(); 599 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); 600 try { 601 final StructStat stat = Libcore.os.stat(mFile.getPath()); 602 synchronized (this) { 603 mStatTimestamp = stat.st_mtime; 604 mStatSize = stat.st_size; 605 } 606 } catch (ErrnoException e) { 607 // Do nothing 608 } 609 // Writing was successful, delete the backup file if there is one. 610 mBackupFile.delete(); 611 mcr.setDiskWriteResult(true); 612 return; 613 } catch (XmlPullParserException e) { 614 Log.w(TAG, "writeToFile: Got exception:", e); 615 } catch (IOException e) { 616 Log.w(TAG, "writeToFile: Got exception:", e); 617 } 618 // Clean up an unsuccessfully written file 619 if (mFile.exists()) { 620 if (!mFile.delete()) { 621 Log.e(TAG, "Couldn't clean up partially-written file " + mFile); 622 } 623 } 624 mcr.setDiskWriteResult(false); 625 } 626 } 627