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