Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b82f04df authored by Brad Fitzpatrick's avatar Brad Fitzpatrick Committed by Android (Google) Code Review
Browse files

Merge "Move SharedPreferencesImpl out of ContextImpl.java"

parents 85a4ec0d d3da4403
Loading
Loading
Loading
Loading
+3 −484
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ package android.app;

import com.android.internal.policy.PolicyManager;
import com.android.internal.util.XmlUtils;
import com.google.android.collect.Maps;

import org.xmlpull.v1.XmlPullParserException;

@@ -114,11 +113,9 @@ import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
@@ -315,7 +312,7 @@ class ContextImpl extends Context {
        throw new RuntimeException("Not supported in system context");
    }

    private static File makeBackupFile(File prefsFile) {
    static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }

@@ -363,7 +360,7 @@ class ContextImpl extends Context {
                    FileInputStream str = new FileInputStream(prefsFile);
                    map = XmlUtils.readMapXml(str);
                    str.close();
                } catch (org.xmlpull.v1.XmlPullParserException e) {
                } catch (XmlPullParserException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } catch (FileNotFoundException e) {
                    Log.w(TAG, "getSharedPreferences", e);
@@ -1593,7 +1590,7 @@ class ContextImpl extends Context {
        return mActivityToken;
    }

    private static void setFilePermissionsFromMode(String name, int mode,
    static void setFilePermissionsFromMode(String name, int mode,
            int extraPermissions) {
        int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
            |FileUtils.S_IRGRP|FileUtils.S_IWGRP
@@ -2727,482 +2724,4 @@ class ContextImpl extends Context {
        private static HashMap<ResourceName, WeakReference<CharSequence> > sStringCache
                = new HashMap<ResourceName, WeakReference<CharSequence> >();
    }

    // ----------------------------------------------------------------------
    // ----------------------------------------------------------------------
    // ----------------------------------------------------------------------

    private static final class SharedPreferencesImpl implements SharedPreferences {

        // Lock ordering rules:
        //  - acquire SharedPreferencesImpl.this before EditorImpl.this
        //  - acquire mWritingToDiskLock before EditorImpl.this

        private final File mFile;
        private final File mBackupFile;
        private final int mMode;

        private Map<String, Object> mMap;     // guarded by 'this'
        private int mDiskWritesInFlight = 0;  // guarded by 'this'
        private boolean mLoaded = false;      // guarded by 'this'
        private long mStatTimestamp;          // guarded by 'this'
        private long mStatSize;               // guarded by 'this'

        private final Object mWritingToDiskLock = new Object();
        private static final Object mContent = new Object();
        private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners;

        SharedPreferencesImpl(
            File file, int mode, Map initialContents) {
            mFile = file;
            mBackupFile = makeBackupFile(file);
            mMode = mode;
            mLoaded = initialContents != null;
            mMap = initialContents != null ? initialContents : new HashMap<String, Object>();
            FileStatus stat = new FileStatus();
            if (FileUtils.getFileStatus(file.getPath(), stat)) {
                mStatTimestamp = stat.mtime;
            }
            mListeners = new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
        }

        // Has this SharedPreferences ever had values assigned to it?
        boolean isLoaded() {
            synchronized (this) {
                return mLoaded;
            }
        }

        // Has the file changed out from under us?  i.e. writes that
        // we didn't instigate.
        public boolean hasFileChangedUnexpectedly() {
            synchronized (this) {
                if (mDiskWritesInFlight > 0) {
                    // If we know we caused it, it's not unexpected.
                    if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
                    return false;
                }
            }
            FileStatus stat = new FileStatus();
            if (!FileUtils.getFileStatus(mFile.getPath(), stat)) {
                return true;
            }
            synchronized (this) {
                return mStatTimestamp != stat.mtime || mStatSize != stat.size;
            }
        }

        public void replace(Map newContents) {
            synchronized (this) {
                mLoaded = true;
                if (newContents != null) {
                    mMap = newContents;
                }
            }
        }

        public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
            synchronized(this) {
                mListeners.put(listener, mContent);
            }
        }

        public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
            synchronized(this) {
                mListeners.remove(listener);
            }
        }

        public Map<String, ?> getAll() {
            synchronized(this) {
                //noinspection unchecked
                return new HashMap<String, Object>(mMap);
            }
        }

        public String getString(String key, String defValue) {
            synchronized (this) {
                String v = (String)mMap.get(key);
                return v != null ? v : defValue;
            }
        }
        
        public Set<String> getStringSet(String key, Set<String> defValues) {
            synchronized (this) {
                Set<String> v = (Set<String>) mMap.get(key);
                return v != null ? v : defValues;
            }
        }

        public int getInt(String key, int defValue) {
            synchronized (this) {
                Integer v = (Integer)mMap.get(key);
                return v != null ? v : defValue;
            }
        }
        public long getLong(String key, long defValue) {
            synchronized (this) {
                Long v = (Long)mMap.get(key);
                return v != null ? v : defValue;
            }
        }
        public float getFloat(String key, float defValue) {
            synchronized (this) {
                Float v = (Float)mMap.get(key);
                return v != null ? v : defValue;
            }
        }
        public boolean getBoolean(String key, boolean defValue) {
            synchronized (this) {
                Boolean v = (Boolean)mMap.get(key);
                return v != null ? v : defValue;
            }
        }

        public boolean contains(String key) {
            synchronized (this) {
                return mMap.containsKey(key);
            }
        }

        public Editor edit() {
            return new EditorImpl();
        }

        // Return value from EditorImpl#commitToMemory()
        private static class MemoryCommitResult {
            public boolean changesMade;  // any keys different?
            public List<String> keysModified;  // may be null
            public Set<OnSharedPreferenceChangeListener> listeners;  // may be null
            public Map<?, ?> mapToWriteToDisk;
            public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
            public volatile boolean writeToDiskResult = false;

            public void setDiskWriteResult(boolean result) {
                writeToDiskResult = result;
                writtenToDiskLatch.countDown();
            }
        }

        public final class EditorImpl implements Editor {
            private final Map<String, Object> mModified = Maps.newHashMap();
            private boolean mClear = false;

            public Editor putString(String key, String value) {
                synchronized (this) {
                    mModified.put(key, value);
                    return this;
                }
            }
            public Editor putStringSet(String key, Set<String> values) {
                synchronized (this) {
                    mModified.put(key, values);
                    return this;
                }
            }
            public Editor putInt(String key, int value) {
                synchronized (this) {
                    mModified.put(key, value);
                    return this;
                }
            }
            public Editor putLong(String key, long value) {
                synchronized (this) {
                    mModified.put(key, value);
                    return this;
                }
            }
            public Editor putFloat(String key, float value) {
                synchronized (this) {
                    mModified.put(key, value);
                    return this;
                }
            }
            public Editor putBoolean(String key, boolean value) {
                synchronized (this) {
                    mModified.put(key, value);
                    return this;
                }
            }

            public Editor remove(String key) {
                synchronized (this) {
                    mModified.put(key, this);
                    return this;
                }
            }

            public Editor clear() {
                synchronized (this) {
                    mClear = true;
                    return this;
                }
            }

            public void apply() {
                final MemoryCommitResult mcr = commitToMemory();
                final Runnable awaitCommit = new Runnable() {
                        public void run() {
                            try {
                                mcr.writtenToDiskLatch.await();
                            } catch (InterruptedException ignored) {
                            }
                        }
                    };

                QueuedWork.add(awaitCommit);

                Runnable postWriteRunnable = new Runnable() {
                        public void run() {
                            awaitCommit.run();
                            QueuedWork.remove(awaitCommit);
                        }
                    };

                SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

                // Okay to notify the listeners before it's hit disk
                // because the listeners should always get the same
                // SharedPreferences instance back, which has the
                // changes reflected in memory.
                notifyListeners(mcr);
            }

            // Returns true if any changes were made
            private MemoryCommitResult commitToMemory() {
                MemoryCommitResult mcr = new MemoryCommitResult();
                synchronized (SharedPreferencesImpl.this) {
                    // We optimistically don't make a deep copy until
                    // a memory commit comes in when we're already
                    // writing to disk.
                    if (mDiskWritesInFlight > 0) {
                        // We can't modify our mMap as a currently
                        // in-flight write owns it.  Clone it before
                        // modifying it.
                        // noinspection unchecked
                        mMap = new HashMap<String, Object>(mMap);
                    }
                    mcr.mapToWriteToDisk = mMap;
                    mDiskWritesInFlight++;

                    boolean hasListeners = mListeners.size() > 0;
                    if (hasListeners) {
                        mcr.keysModified = new ArrayList<String>();
                        mcr.listeners =
                            new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                    }

                    synchronized (this) {
                        if (mClear) {
                            if (!mMap.isEmpty()) {
                                mcr.changesMade = true;
                                mMap.clear();
                            }
                            mClear = false;
                        }

                        for (Entry<String, Object> e : mModified.entrySet()) {
                            String k = e.getKey();
                            Object v = e.getValue();
                            if (v == this) {  // magic value for a removal mutation
                                if (!mMap.containsKey(k)) {
                                    continue;
                                }
                                mMap.remove(k);
                            } else {
                                boolean isSame = false;
                                if (mMap.containsKey(k)) {
                                    Object existingValue = mMap.get(k);
                                    if (existingValue != null && existingValue.equals(v)) {
                                        continue;
                                    }
                                }
                                mMap.put(k, v);
                            }

                            mcr.changesMade = true;
                            if (hasListeners) {
                                mcr.keysModified.add(k);
                            }
                        }

                        mModified.clear();
                    }
                }
                return mcr;
            }

            public boolean commit() {
                MemoryCommitResult mcr = commitToMemory();
                SharedPreferencesImpl.this.enqueueDiskWrite(
                    mcr, null /* sync write on this thread okay */);
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException e) {
                    return false;
                }
                notifyListeners(mcr);
                return mcr.writeToDiskResult;
            }

            private void notifyListeners(final MemoryCommitResult mcr) {
                if (mcr.listeners == null || mcr.keysModified == null ||
                    mcr.keysModified.size() == 0) {
                    return;
                }
                if (Looper.myLooper() == Looper.getMainLooper()) {
                    for (int i = mcr.keysModified.size() - 1; i >= 0; i--) {
                        final String key = mcr.keysModified.get(i);
                        for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
                            if (listener != null) {
                                listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, key);
                            }
                        }
                    }
                } else {
                    // Run this function on the main thread.
                    ActivityThread.sMainThreadHandler.post(new Runnable() {
                            public void run() {
                                notifyListeners(mcr);
                            }
                        });
                }
            }
        }

        /**
         * Enqueue an already-committed-to-memory result to be written
         * to disk.
         *
         * They will be written to disk one-at-a-time in the order
         * that they're enqueued.
         *
         * @param postWriteRunnable if non-null, we're being called
         *   from apply() and this is the runnable to run after
         *   the write proceeds.  if null (from a regular commit()),
         *   then we're allowed to do this disk write on the main
         *   thread (which in addition to reducing allocations and
         *   creating a background thread, this has the advantage that
         *   we catch them in userdebug StrictMode reports to convert
         *   them where possible to apply() ...)
         */
        private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                      final Runnable postWriteRunnable) {
            final Runnable writeToDiskRunnable = new Runnable() {
                    public void run() {
                        synchronized (mWritingToDiskLock) {
                            writeToFile(mcr);
                        }
                        synchronized (SharedPreferencesImpl.this) {
                            mDiskWritesInFlight--;
                        }
                        if (postWriteRunnable != null) {
                            postWriteRunnable.run();
                        }
                    }
                };

            final boolean isFromSyncCommit = (postWriteRunnable == null);

            // Typical #commit() path with fewer allocations, doing a write on
            // the current thread.
            if (isFromSyncCommit) {
                boolean wasEmpty = false;
                synchronized (SharedPreferencesImpl.this) {
                    wasEmpty = mDiskWritesInFlight == 1;
                }
                if (wasEmpty) {
                    writeToDiskRunnable.run();
                    return;
                }
            }

            QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
        }

        private static FileOutputStream createFileOutputStream(File file) {
            FileOutputStream str = null;
            try {
                str = new FileOutputStream(file);
            } catch (FileNotFoundException e) {
                File parent = file.getParentFile();
                if (!parent.mkdir()) {
                    Log.e(TAG, "Couldn't create directory for SharedPreferences file " + file);
                    return null;
                }
                FileUtils.setPermissions(
                    parent.getPath(),
                    FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
                    -1, -1);
                try {
                    str = new FileOutputStream(file);
                } catch (FileNotFoundException e2) {
                    Log.e(TAG, "Couldn't create SharedPreferences file " + file, e2);
                }
            }
            return str;
        }

        // Note: must hold mWritingToDiskLock
        private void writeToFile(MemoryCommitResult mcr) {
            // Rename the current file so it may be used as a backup during the next read
            if (mFile.exists()) {
                if (!mcr.changesMade) {
                    // If the file already exists, but no changes were
                    // made to the underlying map, it's wasteful to
                    // re-write the file.  Return as if we wrote it
                    // out.
                    mcr.setDiskWriteResult(true);
                    return;
                }
                if (!mBackupFile.exists()) {
                    if (!mFile.renameTo(mBackupFile)) {
                        Log.e(TAG, "Couldn't rename file " + mFile
                                + " to backup file " + mBackupFile);
                        mcr.setDiskWriteResult(false);
                        return;
                    }
                } else {
                    mFile.delete();
                }
            }

            // Attempt to write the file, delete the backup and return true as atomically as
            // possible.  If any exception occurs, delete the new file; next time we will restore
            // from the backup.
            try {
                FileOutputStream str = createFileOutputStream(mFile);
                if (str == null) {
                    mcr.setDiskWriteResult(false);
                    return;
                }
                XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
                FileUtils.sync(str);
                str.close();
                setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
                FileStatus stat = new FileStatus();
                if (FileUtils.getFileStatus(mFile.getPath(), stat)) {
                    synchronized (this) {
                        mStatTimestamp = stat.mtime;
                        mStatSize = stat.size;
                    }
                }
                // Writing was successful, delete the backup file if there is one.
                mBackupFile.delete();
                mcr.setDiskWriteResult(true);
                return;
            } catch (XmlPullParserException e) {
                Log.w(TAG, "writeToFile: Got exception:", e);
            } catch (IOException e) {
                Log.w(TAG, "writeToFile: Got exception:", e);
            }
            // Clean up an unsuccessfully written file
            if (mFile.exists()) {
                if (!mFile.delete()) {
                    Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
                }
            }
            mcr.setDiskWriteResult(false);
        }
    }
}
+518 −0

File added.

Preview size limit exceeded, changes collapsed.