Home | History | Annotate | Download | only in app
      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 
     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                 loadFromDisk();
     91             }
     92         }.start();
     93     }
     94 
     95     private void loadFromDisk() {
     96         synchronized (SharedPreferencesImpl.this) {
     97             if (mLoaded) {
     98                 return;
     99             }
    100             if (mBackupFile.exists()) {
    101                 mFile.delete();
    102                 mBackupFile.renameTo(mFile);
    103             }
    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 | IOException e) {
    122                     Log.w(TAG, "getSharedPreferences", e);
    123                 } finally {
    124                     IoUtils.closeQuietly(str);
    125                 }
    126             }
    127         } catch (ErrnoException e) {
    128             /* ignore */
    129         }
    130 
    131         synchronized (SharedPreferencesImpl.this) {
    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<>();
    139             }
    140             notifyAll();
    141         }
    142     }
    143 
    144     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 = 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     @Nullable
    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
    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 
    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     }
    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     }
    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     }
    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 
    268     public boolean contains(String key) {
    269         synchronized (this) {
    270             awaitLoadedLocked();
    271             return mMap.containsKey(key);
    272         }
    273     }
    274 
    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 
    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 
    309         public Editor putString(String key, @Nullable String value) {
    310             synchronized (this) {
    311                 mModified.put(key, value);
    312                 return this;
    313             }
    314         }
    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         }
    322         public Editor putInt(String key, int value) {
    323             synchronized (this) {
    324                 mModified.put(key, value);
    325                 return this;
    326             }
    327         }
    328         public Editor putLong(String key, long value) {
    329             synchronized (this) {
    330                 mModified.put(key, value);
    331                 return this;
    332             }
    333         }
    334         public Editor putFloat(String key, float value) {
    335             synchronized (this) {
    336                 mModified.put(key, value);
    337                 return this;
    338             }
    339         }
    340         public Editor putBoolean(String key, boolean value) {
    341             synchronized (this) {
    342                 mModified.put(key, value);
    343                 return this;
    344             }
    345         }
    346 
    347         public Editor remove(String key) {
    348             synchronized (this) {
    349                 mModified.put(key, this);
    350                 return this;
    351             }
    352         }
    353 
    354         public Editor clear() {
    355             synchronized (this) {
    356                 mClear = true;
    357                 return this;
    358             }
    359         }
    360 
    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
    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 
    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 
    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      */
    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 
    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
    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