blob: a292ecbf4df0d7e17726c3f25d3a31ef35c89436 [file] [log] [blame]
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -08001/*
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
17package android.app;
18
19import android.content.SharedPreferences;
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080020import android.os.FileUtils;
21import android.os.Looper;
22import android.util.Log;
23
24import com.google.android.collect.Maps;
25import com.android.internal.util.XmlUtils;
26
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080027import dalvik.system.BlockGuard;
28
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080029import org.xmlpull.v1.XmlPullParserException;
30
Dianne Hackborn2e8fb732011-10-10 18:47:00 -070031import java.io.BufferedInputStream;
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080032import java.io.File;
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080033import java.io.FileInputStream;
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080034import java.io.FileNotFoundException;
35import java.io.FileOutputStream;
36import java.io.IOException;
37import java.util.ArrayList;
38import java.util.HashMap;
39import java.util.HashSet;
40import java.util.List;
41import java.util.Map;
42import java.util.Set;
43import java.util.WeakHashMap;
44import java.util.concurrent.CountDownLatch;
45import java.util.concurrent.ExecutorService;
46
Kenny Root98e15e72012-08-16 11:38:04 -070047import libcore.io.ErrnoException;
48import libcore.io.IoUtils;
49import libcore.io.Libcore;
50import libcore.io.StructStat;
51
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080052final 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();
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080072 private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
73 new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080074
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080075 SharedPreferencesImpl(File file, int mode) {
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080076 mFile = file;
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080077 mBackupFile = makeBackupFile(file);
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080078 mMode = mode;
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080079 mLoaded = false;
80 mMap = null;
81 startLoadFromDisk();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080082 }
83
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080084 private void startLoadFromDisk() {
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -080085 synchronized (this) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -080086 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;
Kenny Root98e15e72012-08-16 11:38:04 -0700112 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 }
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800130 }
Kenny Root98e15e72012-08-16 11:38:04 -0700131 } catch (ErrnoException e) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800132 }
133 mLoaded = true;
134 if (map != null) {
135 mMap = map;
Kenny Root98e15e72012-08-16 11:38:04 -0700136 mStatTimestamp = stat.st_mtime;
137 mStatSize = stat.st_size;
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800138 } 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();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800155 }
156 }
157
158 // Has the file changed out from under us? i.e. writes that
159 // we didn't instigate.
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800160 private boolean hasFileChangedUnexpectedly() {
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800161 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 }
Kenny Root98e15e72012-08-16 11:38:04 -0700168
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) {
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800178 return true;
179 }
Kenny Root98e15e72012-08-16 11:38:04 -0700180
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800181 synchronized (this) {
Kenny Root98e15e72012-08-16 11:38:04 -0700182 return mStatTimestamp != stat.st_mtime || mStatSize != stat.st_size;
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800183 }
184 }
185
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800186 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
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800198 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
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800213 public Map<String, ?> getAll() {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800214 synchronized (this) {
215 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800216 //noinspection unchecked
217 return new HashMap<String, Object>(mMap);
218 }
219 }
220
221 public String getString(String key, String defValue) {
222 synchronized (this) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800223 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800224 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) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800231 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800232 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) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800239 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800240 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) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800246 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800247 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) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800253 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800254 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) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800260 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800261 Boolean v = (Boolean)mMap.get(key);
262 return v != null ? v : defValue;
263 }
264 }
265
266 public boolean contains(String key) {
267 synchronized (this) {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800268 awaitLoadedLocked();
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800269 return mMap.containsKey(key);
270 }
271 }
272
273 public Editor edit() {
Brad Fitzpatrick4cd50b82010-12-01 17:31:45 -0800274 // 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
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800285 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) {
Christopher Tate01ed79c2012-10-18 19:01:01 -0700315 mModified.put(key,
316 (values == null) ? null : new HashSet<String>(values));
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800317 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();
Narayan Kamathc6f42902014-01-08 11:58:32 +0000424 // "this" is the magic value for a removal mutation. In addition,
425 // setting a value to "null" for a given key is specified to be
426 // equivalent to calling remove on that key.
427 if (v == this || v == null) {
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800428 if (!mMap.containsKey(k)) {
429 continue;
430 }
431 mMap.remove(k);
432 } else {
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800433 if (mMap.containsKey(k)) {
434 Object existingValue = mMap.get(k);
435 if (existingValue != null && existingValue.equals(v)) {
436 continue;
437 }
438 }
439 mMap.put(k, v);
440 }
441
442 mcr.changesMade = true;
443 if (hasListeners) {
444 mcr.keysModified.add(k);
445 }
446 }
447
448 mModified.clear();
449 }
450 }
451 return mcr;
452 }
453
454 public boolean commit() {
455 MemoryCommitResult mcr = commitToMemory();
456 SharedPreferencesImpl.this.enqueueDiskWrite(
457 mcr, null /* sync write on this thread okay */);
458 try {
459 mcr.writtenToDiskLatch.await();
460 } catch (InterruptedException e) {
461 return false;
462 }
463 notifyListeners(mcr);
464 return mcr.writeToDiskResult;
465 }
466
467 private void notifyListeners(final MemoryCommitResult mcr) {
468 if (mcr.listeners == null || mcr.keysModified == null ||
469 mcr.keysModified.size() == 0) {
470 return;
471 }
472 if (Looper.myLooper() == Looper.getMainLooper()) {
473 for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
474 final String key = mcr.keysModified.get(i);
475 for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
476 if (listener != null) {
477 listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
478 }
479 }
480 }
481 } else {
482 // Run this function on the main thread.
483 ActivityThread.sMainThreadHandler.post(new Runnable() {
484 public void run() {
485 notifyListeners(mcr);
486 }
487 });
488 }
489 }
490 }
491
492 /**
493 * Enqueue an already-committed-to-memory result to be written
494 * to disk.
495 *
496 * They will be written to disk one-at-a-time in the order
497 * that they're enqueued.
498 *
499 * @param postWriteRunnable if non-null, we're being called
500 * from apply() and this is the runnable to run after
501 * the write proceeds. if null (from a regular commit()),
502 * then we're allowed to do this disk write on the main
503 * thread (which in addition to reducing allocations and
504 * creating a background thread, this has the advantage that
505 * we catch them in userdebug StrictMode reports to convert
506 * them where possible to apply() ...)
507 */
508 private void enqueueDiskWrite(final MemoryCommitResult mcr,
509 final Runnable postWriteRunnable) {
510 final Runnable writeToDiskRunnable = new Runnable() {
511 public void run() {
512 synchronized (mWritingToDiskLock) {
513 writeToFile(mcr);
514 }
515 synchronized (SharedPreferencesImpl.this) {
516 mDiskWritesInFlight--;
517 }
518 if (postWriteRunnable != null) {
519 postWriteRunnable.run();
520 }
521 }
522 };
523
524 final boolean isFromSyncCommit = (postWriteRunnable == null);
525
526 // Typical #commit() path with fewer allocations, doing a write on
527 // the current thread.
528 if (isFromSyncCommit) {
529 boolean wasEmpty = false;
530 synchronized (SharedPreferencesImpl.this) {
531 wasEmpty = mDiskWritesInFlight == 1;
532 }
533 if (wasEmpty) {
534 writeToDiskRunnable.run();
535 return;
536 }
537 }
538
539 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
540 }
541
542 private static FileOutputStream createFileOutputStream(File file) {
543 FileOutputStream str = null;
544 try {
545 str = new FileOutputStream(file);
546 } catch (FileNotFoundException e) {
547 File parent = file.getParentFile();
548 if (!parent.mkdir()) {
549 Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
550 return null;
551 }
552 FileUtils.setPermissions(
553 parent.getPath(),
554 FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
555 -1, -1);
556 try {
557 str = new FileOutputStream(file);
558 } catch (FileNotFoundException e2) {
559 Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
560 }
561 }
562 return str;
563 }
564
565 // Note: must hold mWritingToDiskLock
566 private void writeToFile(MemoryCommitResult mcr) {
567 // Rename the current file so it may be used as a backup during the next read
568 if (mFile.exists()) {
569 if (!mcr.changesMade) {
570 // If the file already exists, but no changes were
571 // made to the underlying map, it's wasteful to
572 // re-write the file. Return as if we wrote it
573 // out.
574 mcr.setDiskWriteResult(true);
575 return;
576 }
577 if (!mBackupFile.exists()) {
578 if (!mFile.renameTo(mBackupFile)) {
579 Log.e(TAG, "Couldn't rename file " + mFile
580 + " to backup file " + mBackupFile);
581 mcr.setDiskWriteResult(false);
582 return;
583 }
584 } else {
585 mFile.delete();
586 }
587 }
588
589 // Attempt to write the file, delete the backup and return true as atomically as
590 // possible. If any exception occurs, delete the new file; next time we will restore
591 // from the backup.
592 try {
593 FileOutputStream str = createFileOutputStream(mFile);
594 if (str == null) {
595 mcr.setDiskWriteResult(false);
596 return;
597 }
598 XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
599 FileUtils.sync(str);
600 str.close();
601 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
Kenny Root98e15e72012-08-16 11:38:04 -0700602 try {
603 final StructStat stat = Libcore.os.stat(mFile.getPath());
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800604 synchronized (this) {
Kenny Root98e15e72012-08-16 11:38:04 -0700605 mStatTimestamp = stat.st_mtime;
606 mStatSize = stat.st_size;
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800607 }
Kenny Root98e15e72012-08-16 11:38:04 -0700608 } catch (ErrnoException e) {
609 // Do nothing
Brad Fitzpatrickd3da4402010-11-10 08:27:11 -0800610 }
611 // Writing was successful, delete the backup file if there is one.
612 mBackupFile.delete();
613 mcr.setDiskWriteResult(true);
614 return;
615 } catch (XmlPullParserException e) {
616 Log.w(TAG, "writeToFile: Got exception:", e);
617 } catch (IOException e) {
618 Log.w(TAG, "writeToFile: Got exception:", e);
619 }
620 // Clean up an unsuccessfully written file
621 if (mFile.exists()) {
622 if (!mFile.delete()) {
623 Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
624 }
625 }
626 mcr.setDiskWriteResult(false);
627 }
628}