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.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 
     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 
     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 
     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 
    143     private static File makeBackupFile(File prefsFile) {
    144         return new File(prefsFile.getPath() + ".bak");
    145     }
    146 
    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.
    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 
    185     public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
    186         synchronized(this) {
    187             mListeners.put(listener, mContent);
    188         }
    189     }
    190 
    191     public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
    192         synchronized(this) {
    193             mListeners.remove(listener);
    194         }
    195     }
    196 
    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 
    212     public Map<String, ?> getAll() {
    213         synchronized (this) {
    214             awaitLoadedLocked();
    215             //noinspection unchecked
    216             return new HashMap<String, Object>(mMap);
    217         }
    218     }
    219 
    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 
    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 
    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     }
    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     }
    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     }
    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 
    265     public boolean contains(String key) {
    266         synchronized (this) {
    267             awaitLoadedLocked();
    268             return mMap.containsKey(key);
    269         }
    270     }
    271 
    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 
    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 
    306         public Editor putString(String key, String value) {
    307             synchronized (this) {
    308                 mModified.put(key, value);
    309                 return this;
    310             }
    311         }
    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         }
    319         public Editor putInt(String key, int value) {
    320             synchronized (this) {
    321                 mModified.put(key, value);
    322                 return this;
    323             }
    324         }
    325         public Editor putLong(String key, long value) {
    326             synchronized (this) {
    327                 mModified.put(key, value);
    328                 return this;
    329             }
    330         }
    331         public Editor putFloat(String key, float value) {
    332             synchronized (this) {
    333                 mModified.put(key, value);
    334                 return this;
    335             }
    336         }
    337         public Editor putBoolean(String key, boolean value) {
    338             synchronized (this) {
    339                 mModified.put(key, value);
    340                 return this;
    341             }
    342         }
    343 
    344         public Editor remove(String key) {
    345             synchronized (this) {
    346                 mModified.put(key, this);
    347                 return this;
    348             }
    349         }
    350 
    351         public Editor clear() {
    352             synchronized (this) {
    353                 mClear = true;
    354                 return this;
    355             }
    356         }
    357 
    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
    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 
    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 
    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      */
    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 
    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
    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