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