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