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