OSDN Git Service

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