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