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.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 
     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 
     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 
     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 
    134     private static File makeBackupFile(File prefsFile) {
    135         return new File(prefsFile.getPath() + ".bak");
    136     }
    137 
    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.
    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 
    167     public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
    168         synchronized(this) {
    169             mListeners.put(listener, mContent);
    170         }
    171     }
    172 
    173     public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
    174         synchronized(this) {
    175             mListeners.remove(listener);
    176         }
    177     }
    178 
    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 
    194     public Map<String, ?> getAll() {
    195         synchronized (this) {
    196             awaitLoadedLocked();
    197             //noinspection unchecked
    198             return new HashMap<String, Object>(mMap);
    199         }
    200     }
    201 
    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 
    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 
    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     }
    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     }
    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     }
    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 
    247     public boolean contains(String key) {
    248         synchronized (this) {
    249             awaitLoadedLocked();
    250             return mMap.containsKey(key);
    251         }
    252     }
    253 
    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 
    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 
    288         public Editor putString(String key, String value) {
    289             synchronized (this) {
    290                 mModified.put(key, value);
    291                 return this;
    292             }
    293         }
    294         public Editor putStringSet(String key, Set<String> values) {
    295             synchronized (this) {
    296                 mModified.put(key, values);
    297                 return this;
    298             }
    299         }
    300         public Editor putInt(String key, int value) {
    301             synchronized (this) {
    302                 mModified.put(key, value);
    303                 return this;
    304             }
    305         }
    306         public Editor putLong(String key, long value) {
    307             synchronized (this) {
    308                 mModified.put(key, value);
    309                 return this;
    310             }
    311         }
    312         public Editor putFloat(String key, float value) {
    313             synchronized (this) {
    314                 mModified.put(key, value);
    315                 return this;
    316             }
    317         }
    318         public Editor putBoolean(String key, boolean value) {
    319             synchronized (this) {
    320                 mModified.put(key, value);
    321                 return this;
    322             }
    323         }
    324 
    325         public Editor remove(String key) {
    326             synchronized (this) {
    327                 mModified.put(key, this);
    328                 return this;
    329             }
    330         }
    331 
    332         public Editor clear() {
    333             synchronized (this) {
    334                 mClear = true;
    335                 return this;
    336             }
    337         }
    338 
    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
    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 
    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 
    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      */
    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 
    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
    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