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