• 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.FileStatus;
21 import android.os.FileUtils;
22 import android.os.Looper;
23 import android.util.Log;
24 
25 import com.google.android.collect.Maps;
26 import com.android.internal.util.XmlUtils;
27 
28 import dalvik.system.BlockGuard;
29 
30 import org.xmlpull.v1.XmlPullParserException;
31 
32 import java.io.BufferedInputStream;
33 import java.io.File;
34 import java.io.FileInputStream;
35 import java.io.FileNotFoundException;
36 import java.io.FileOutputStream;
37 import java.io.IOException;
38 import java.util.ArrayList;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Set;
44 import java.util.WeakHashMap;
45 import java.util.concurrent.CountDownLatch;
46 import java.util.concurrent.ExecutorService;
47 
48 final 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 
SharedPreferencesImpl(File file, int mode)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 
startLoadFromDisk()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 
loadFromDiskLocked()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 
makeBackupFile(File prefsFile)134     private static File makeBackupFile(File prefsFile) {
135         return new File(prefsFile.getPath() + ".bak");
136     }
137 
startReloadIfChangedUnexpectedly()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.
hasFileChangedUnexpectedly()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 
registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener)167     public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
168         synchronized(this) {
169             mListeners.put(listener, mContent);
170         }
171     }
172 
unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener)173     public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
174         synchronized(this) {
175             mListeners.remove(listener);
176         }
177     }
178 
awaitLoadedLocked()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 
getAll()194     public Map<String, ?> getAll() {
195         synchronized (this) {
196             awaitLoadedLocked();
197             //noinspection unchecked
198             return new HashMap<String, Object>(mMap);
199         }
200     }
201 
getString(String key, String defValue)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 
getStringSet(String key, Set<String> defValues)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 
getInt(String key, int defValue)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     }
getLong(String key, long defValue)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     }
getFloat(String key, float defValue)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     }
getBoolean(String key, boolean defValue)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 
contains(String key)247     public boolean contains(String key) {
248         synchronized (this) {
249             awaitLoadedLocked();
250             return mMap.containsKey(key);
251         }
252     }
253 
edit()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 
setDiskWriteResult(boolean result)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 
putString(String key, String value)288         public Editor putString(String key, String value) {
289             synchronized (this) {
290                 mModified.put(key, value);
291                 return this;
292             }
293         }
putStringSet(String key, Set<String> values)294         public Editor putStringSet(String key, Set<String> values) {
295             synchronized (this) {
296                 mModified.put(key, values);
297                 return this;
298             }
299         }
putInt(String key, int value)300         public Editor putInt(String key, int value) {
301             synchronized (this) {
302                 mModified.put(key, value);
303                 return this;
304             }
305         }
putLong(String key, long value)306         public Editor putLong(String key, long value) {
307             synchronized (this) {
308                 mModified.put(key, value);
309                 return this;
310             }
311         }
putFloat(String key, float value)312         public Editor putFloat(String key, float value) {
313             synchronized (this) {
314                 mModified.put(key, value);
315                 return this;
316             }
317         }
putBoolean(String key, boolean value)318         public Editor putBoolean(String key, boolean value) {
319             synchronized (this) {
320                 mModified.put(key, value);
321                 return this;
322             }
323         }
324 
remove(String key)325         public Editor remove(String key) {
326             synchronized (this) {
327                 mModified.put(key, this);
328                 return this;
329             }
330         }
331 
clear()332         public Editor clear() {
333             synchronized (this) {
334                 mClear = true;
335                 return this;
336             }
337         }
338 
apply()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
commitToMemory()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 
commit()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 
notifyListeners(final MemoryCommitResult mcr)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      */
enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable)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 
createFileOutputStream(File file)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
writeToFile(MemoryCommitResult mcr)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