896 lines
30 KiB
Java
896 lines
30 KiB
Java
/*
|
|
* Copyright (C) 2010 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package android.app;
|
|
|
|
import android.annotation.Nullable;
|
|
import android.compat.Compatibility;
|
|
import android.compat.annotation.ChangeId;
|
|
import android.compat.annotation.EnabledSince;
|
|
import android.compat.annotation.UnsupportedAppUsage;
|
|
import android.content.SharedPreferences;
|
|
import android.os.Build;
|
|
import android.os.FileUtils;
|
|
import android.os.Looper;
|
|
import android.system.ErrnoException;
|
|
import android.system.Os;
|
|
import android.system.StructStat;
|
|
import android.system.StructTimespec;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.util.ExponentiallyBucketedHistogram;
|
|
import com.android.internal.util.XmlUtils;
|
|
|
|
import dalvik.system.BlockGuard;
|
|
|
|
import libcore.io.IoUtils;
|
|
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
|
|
import java.io.BufferedInputStream;
|
|
import java.io.File;
|
|
import java.io.FileInputStream;
|
|
import java.io.FileNotFoundException;
|
|
import java.io.FileOutputStream;
|
|
import java.io.IOException;
|
|
import java.util.ArrayList;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
import java.util.WeakHashMap;
|
|
import java.util.concurrent.CountDownLatch;
|
|
import java.util.concurrent.Executors;
|
|
import java.util.concurrent.LinkedBlockingQueue;
|
|
import java.util.concurrent.ThreadFactory;
|
|
import java.util.concurrent.ThreadPoolExecutor;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
final class SharedPreferencesImpl implements SharedPreferences {
|
|
private static final String TAG = "SharedPreferencesImpl";
|
|
private static final boolean DEBUG = false;
|
|
private static final Object CONTENT = new Object();
|
|
|
|
/** If a fsync takes more than {@value #MAX_FSYNC_DURATION_MILLIS} ms, warn */
|
|
private static final long MAX_FSYNC_DURATION_MILLIS = 256;
|
|
|
|
/**
|
|
* There will now be a callback to {@link
|
|
* android.content.SharedPreferences.OnSharedPreferenceChangeListener#onSharedPreferenceChanged
|
|
* OnSharedPreferenceChangeListener.onSharedPreferenceChanged} with a {@code null} key on
|
|
* {@link android.content.SharedPreferences.Editor#clear Editor.clear}.
|
|
*/
|
|
@ChangeId
|
|
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.R)
|
|
private static final long CALLBACK_ON_CLEAR_CHANGE = 119147584L;
|
|
|
|
// Lock ordering rules:
|
|
// - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
|
|
// - acquire mWritingToDiskLock before EditorImpl.mLock
|
|
|
|
@UnsupportedAppUsage
|
|
private final File mFile;
|
|
private final File mBackupFile;
|
|
private final int mMode;
|
|
private final Object mLock = new Object();
|
|
private final Object mWritingToDiskLock = new Object();
|
|
|
|
@GuardedBy("mLock")
|
|
private Map<String, Object> mMap;
|
|
@GuardedBy("mLock")
|
|
private Throwable mThrowable;
|
|
|
|
@GuardedBy("mLock")
|
|
private int mDiskWritesInFlight = 0;
|
|
|
|
@GuardedBy("mLock")
|
|
private boolean mLoaded = false;
|
|
|
|
@GuardedBy("mLock")
|
|
private StructTimespec mStatTimestamp;
|
|
|
|
@GuardedBy("mLock")
|
|
private long mStatSize;
|
|
|
|
@GuardedBy("mLock")
|
|
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
|
|
new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
|
|
|
|
/** Current memory state (always increasing) */
|
|
@GuardedBy("this")
|
|
private long mCurrentMemoryStateGeneration;
|
|
|
|
/** Latest memory state that was committed to disk */
|
|
@GuardedBy("mWritingToDiskLock")
|
|
private long mDiskStateGeneration;
|
|
|
|
/** Time (and number of instances) of file-system sync requests */
|
|
@GuardedBy("mWritingToDiskLock")
|
|
private final ExponentiallyBucketedHistogram mSyncTimes = new ExponentiallyBucketedHistogram(16);
|
|
private int mNumSync = 0;
|
|
|
|
private static final ThreadPoolExecutor sLoadExecutor = new ThreadPoolExecutor(0, 1, 10L,
|
|
TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
|
|
new SharedPreferencesThreadFactory());
|
|
|
|
@UnsupportedAppUsage
|
|
SharedPreferencesImpl(File file, int mode) {
|
|
mFile = file;
|
|
mBackupFile = makeBackupFile(file);
|
|
mMode = mode;
|
|
mLoaded = false;
|
|
mMap = null;
|
|
mThrowable = null;
|
|
startLoadFromDisk();
|
|
}
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
private void startLoadFromDisk() {
|
|
synchronized (mLock) {
|
|
mLoaded = false;
|
|
}
|
|
|
|
sLoadExecutor.execute(() -> {
|
|
loadFromDisk();
|
|
});
|
|
}
|
|
|
|
private void loadFromDisk() {
|
|
synchronized (mLock) {
|
|
if (mLoaded) {
|
|
return;
|
|
}
|
|
if (mBackupFile.exists()) {
|
|
mFile.delete();
|
|
mBackupFile.renameTo(mFile);
|
|
}
|
|
}
|
|
|
|
// Debugging
|
|
if (mFile.exists() && !mFile.canRead()) {
|
|
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
|
|
}
|
|
|
|
Map<String, Object> map = null;
|
|
StructStat stat = null;
|
|
Throwable thrown = null;
|
|
try {
|
|
stat = Os.stat(mFile.getPath());
|
|
if (mFile.canRead()) {
|
|
BufferedInputStream str = null;
|
|
try {
|
|
str = new BufferedInputStream(
|
|
new FileInputStream(mFile), 16 * 1024);
|
|
map = (Map<String, Object>) XmlUtils.readMapXml(str);
|
|
} catch (Exception e) {
|
|
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
|
|
} finally {
|
|
IoUtils.closeQuietly(str);
|
|
}
|
|
}
|
|
} catch (ErrnoException e) {
|
|
// An errno exception means the stat failed. Treat as empty/non-existing by
|
|
// ignoring.
|
|
} catch (Throwable t) {
|
|
thrown = t;
|
|
}
|
|
|
|
synchronized (mLock) {
|
|
mLoaded = true;
|
|
mThrowable = thrown;
|
|
|
|
// It's important that we always signal waiters, even if we'll make
|
|
// them fail with an exception. The try-finally is pretty wide, but
|
|
// better safe than sorry.
|
|
try {
|
|
if (thrown == null) {
|
|
if (map != null) {
|
|
mMap = map;
|
|
mStatTimestamp = stat.st_mtim;
|
|
mStatSize = stat.st_size;
|
|
} else {
|
|
mMap = new HashMap<>();
|
|
}
|
|
}
|
|
// In case of a thrown exception, we retain the old map. That allows
|
|
// any open editors to commit and store updates.
|
|
} catch (Throwable t) {
|
|
mThrowable = t;
|
|
} finally {
|
|
mLock.notifyAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
static File makeBackupFile(File prefsFile) {
|
|
return new File(prefsFile.getPath() + ".bak");
|
|
}
|
|
|
|
@UnsupportedAppUsage
|
|
void startReloadIfChangedUnexpectedly() {
|
|
synchronized (mLock) {
|
|
// TODO: wait for any pending writes to disk?
|
|
if (!hasFileChangedUnexpectedly()) {
|
|
return;
|
|
}
|
|
startLoadFromDisk();
|
|
}
|
|
}
|
|
|
|
// Has the file changed out from under us? i.e. writes that
|
|
// we didn't instigate.
|
|
private boolean hasFileChangedUnexpectedly() {
|
|
synchronized (mLock) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
final StructStat stat;
|
|
try {
|
|
/*
|
|
* Metadata operations don't usually count as a block guard
|
|
* violation, but we explicitly want this one.
|
|
*/
|
|
BlockGuard.getThreadPolicy().onReadFromDisk();
|
|
stat = Os.stat(mFile.getPath());
|
|
} catch (ErrnoException e) {
|
|
return true;
|
|
}
|
|
|
|
synchronized (mLock) {
|
|
return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
|
synchronized(mLock) {
|
|
mListeners.put(listener, CONTENT);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
|
synchronized(mLock) {
|
|
mListeners.remove(listener);
|
|
}
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private void awaitLoadedLocked() {
|
|
if (!mLoaded) {
|
|
// Raise an explicit StrictMode onReadFromDisk for this
|
|
// thread, since the real read will be in a different
|
|
// thread and otherwise ignored by StrictMode.
|
|
BlockGuard.getThreadPolicy().onReadFromDisk();
|
|
}
|
|
while (!mLoaded) {
|
|
try {
|
|
mLock.wait();
|
|
} catch (InterruptedException unused) {
|
|
}
|
|
}
|
|
if (mThrowable != null) {
|
|
throw new IllegalStateException(mThrowable);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Map<String, ?> getAll() {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
//noinspection unchecked
|
|
return new HashMap<String, Object>(mMap);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public String getString(String key, @Nullable String defValue) {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
String v = (String)mMap.get(key);
|
|
return v != null ? v : defValue;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
Set<String> v = (Set<String>) mMap.get(key);
|
|
return v != null ? v : defValues;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public int getInt(String key, int defValue) {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
Integer v = (Integer)mMap.get(key);
|
|
return v != null ? v : defValue;
|
|
}
|
|
}
|
|
@Override
|
|
public long getLong(String key, long defValue) {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
Long v = (Long)mMap.get(key);
|
|
return v != null ? v : defValue;
|
|
}
|
|
}
|
|
@Override
|
|
public float getFloat(String key, float defValue) {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
Float v = (Float)mMap.get(key);
|
|
return v != null ? v : defValue;
|
|
}
|
|
}
|
|
@Override
|
|
public boolean getBoolean(String key, boolean defValue) {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
Boolean v = (Boolean)mMap.get(key);
|
|
return v != null ? v : defValue;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean contains(String key) {
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
return mMap.containsKey(key);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Editor edit() {
|
|
// TODO: remove the need to call awaitLoadedLocked() when
|
|
// requesting an editor. will require some work on the
|
|
// Editor, but then we should be able to do:
|
|
//
|
|
// context.getSharedPreferences(..).edit().putString(..).apply()
|
|
//
|
|
// ... all without blocking.
|
|
synchronized (mLock) {
|
|
awaitLoadedLocked();
|
|
}
|
|
|
|
return new EditorImpl();
|
|
}
|
|
|
|
// Return value from EditorImpl#commitToMemory()
|
|
private static class MemoryCommitResult {
|
|
final long memoryStateGeneration;
|
|
final boolean keysCleared;
|
|
@Nullable final List<String> keysModified;
|
|
@Nullable final Set<OnSharedPreferenceChangeListener> listeners;
|
|
final Map<String, Object> mapToWriteToDisk;
|
|
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
|
|
|
|
@GuardedBy("mWritingToDiskLock")
|
|
volatile boolean writeToDiskResult = false;
|
|
boolean wasWritten = false;
|
|
|
|
private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,
|
|
@Nullable List<String> keysModified,
|
|
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
|
|
Map<String, Object> mapToWriteToDisk) {
|
|
this.memoryStateGeneration = memoryStateGeneration;
|
|
this.keysCleared = keysCleared;
|
|
this.keysModified = keysModified;
|
|
this.listeners = listeners;
|
|
this.mapToWriteToDisk = mapToWriteToDisk;
|
|
}
|
|
|
|
void setDiskWriteResult(boolean wasWritten, boolean result) {
|
|
this.wasWritten = wasWritten;
|
|
writeToDiskResult = result;
|
|
writtenToDiskLatch.countDown();
|
|
}
|
|
}
|
|
|
|
public final class EditorImpl implements Editor {
|
|
private final Object mEditorLock = new Object();
|
|
|
|
@GuardedBy("mEditorLock")
|
|
private final Map<String, Object> mModified = new HashMap<>();
|
|
|
|
@GuardedBy("mEditorLock")
|
|
private boolean mClear = false;
|
|
|
|
@Override
|
|
public Editor putString(String key, @Nullable String value) {
|
|
synchronized (mEditorLock) {
|
|
mModified.put(key, value);
|
|
return this;
|
|
}
|
|
}
|
|
@Override
|
|
public Editor putStringSet(String key, @Nullable Set<String> values) {
|
|
synchronized (mEditorLock) {
|
|
mModified.put(key,
|
|
(values == null) ? null : new HashSet<String>(values));
|
|
return this;
|
|
}
|
|
}
|
|
@Override
|
|
public Editor putInt(String key, int value) {
|
|
synchronized (mEditorLock) {
|
|
mModified.put(key, value);
|
|
return this;
|
|
}
|
|
}
|
|
@Override
|
|
public Editor putLong(String key, long value) {
|
|
synchronized (mEditorLock) {
|
|
mModified.put(key, value);
|
|
return this;
|
|
}
|
|
}
|
|
@Override
|
|
public Editor putFloat(String key, float value) {
|
|
synchronized (mEditorLock) {
|
|
mModified.put(key, value);
|
|
return this;
|
|
}
|
|
}
|
|
@Override
|
|
public Editor putBoolean(String key, boolean value) {
|
|
synchronized (mEditorLock) {
|
|
mModified.put(key, value);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Editor remove(String key) {
|
|
synchronized (mEditorLock) {
|
|
mModified.put(key, this);
|
|
return this;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public Editor clear() {
|
|
synchronized (mEditorLock) {
|
|
mClear = true;
|
|
return this;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void apply() {
|
|
final long startTime = System.currentTimeMillis();
|
|
|
|
final MemoryCommitResult mcr = commitToMemory();
|
|
final Runnable awaitCommit = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
try {
|
|
mcr.writtenToDiskLatch.await();
|
|
} catch (InterruptedException ignored) {
|
|
}
|
|
|
|
if (DEBUG && mcr.wasWritten) {
|
|
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
|
|
+ " applied after " + (System.currentTimeMillis() - startTime)
|
|
+ " ms");
|
|
}
|
|
}
|
|
};
|
|
|
|
QueuedWork.addFinisher(awaitCommit);
|
|
|
|
Runnable postWriteRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
awaitCommit.run();
|
|
QueuedWork.removeFinisher(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() {
|
|
long memoryStateGeneration;
|
|
boolean keysCleared = false;
|
|
List<String> keysModified = null;
|
|
Set<OnSharedPreferenceChangeListener> listeners = null;
|
|
Map<String, Object> mapToWriteToDisk;
|
|
|
|
synchronized (SharedPreferencesImpl.this.mLock) {
|
|
// 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);
|
|
}
|
|
mapToWriteToDisk = mMap;
|
|
mDiskWritesInFlight++;
|
|
|
|
boolean hasListeners = mListeners.size() > 0;
|
|
if (hasListeners) {
|
|
keysModified = new ArrayList<String>();
|
|
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
|
|
}
|
|
|
|
synchronized (mEditorLock) {
|
|
boolean changesMade = false;
|
|
|
|
if (mClear) {
|
|
if (!mapToWriteToDisk.isEmpty()) {
|
|
changesMade = true;
|
|
mapToWriteToDisk.clear();
|
|
}
|
|
keysCleared = true;
|
|
mClear = false;
|
|
}
|
|
|
|
for (Map.Entry<String, Object> e : mModified.entrySet()) {
|
|
String k = e.getKey();
|
|
Object v = e.getValue();
|
|
// "this" is the magic value for a removal mutation. In addition,
|
|
// setting a value to "null" for a given key is specified to be
|
|
// equivalent to calling remove on that key.
|
|
if (v == this || v == null) {
|
|
if (!mapToWriteToDisk.containsKey(k)) {
|
|
continue;
|
|
}
|
|
mapToWriteToDisk.remove(k);
|
|
} else {
|
|
if (mapToWriteToDisk.containsKey(k)) {
|
|
Object existingValue = mapToWriteToDisk.get(k);
|
|
if (existingValue != null && existingValue.equals(v)) {
|
|
continue;
|
|
}
|
|
}
|
|
mapToWriteToDisk.put(k, v);
|
|
}
|
|
|
|
changesMade = true;
|
|
if (hasListeners) {
|
|
keysModified.add(k);
|
|
}
|
|
}
|
|
|
|
mModified.clear();
|
|
|
|
if (changesMade) {
|
|
mCurrentMemoryStateGeneration++;
|
|
}
|
|
|
|
memoryStateGeneration = mCurrentMemoryStateGeneration;
|
|
}
|
|
}
|
|
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
|
|
listeners, mapToWriteToDisk);
|
|
}
|
|
|
|
@Override
|
|
public boolean commit() {
|
|
long startTime = 0;
|
|
|
|
if (DEBUG) {
|
|
startTime = System.currentTimeMillis();
|
|
}
|
|
|
|
MemoryCommitResult mcr = commitToMemory();
|
|
|
|
SharedPreferencesImpl.this.enqueueDiskWrite(
|
|
mcr, null /* sync write on this thread okay */);
|
|
try {
|
|
mcr.writtenToDiskLatch.await();
|
|
} catch (InterruptedException e) {
|
|
return false;
|
|
} finally {
|
|
if (DEBUG) {
|
|
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
|
|
+ " committed after " + (System.currentTimeMillis() - startTime)
|
|
+ " ms");
|
|
}
|
|
}
|
|
notifyListeners(mcr);
|
|
return mcr.writeToDiskResult;
|
|
}
|
|
|
|
private void notifyListeners(final MemoryCommitResult mcr) {
|
|
if (mcr.listeners == null || (mcr.keysModified == null && !mcr.keysCleared)) {
|
|
return;
|
|
}
|
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
if (mcr.keysCleared && Compatibility.isChangeEnabled(CALLBACK_ON_CLEAR_CHANGE)) {
|
|
for (OnSharedPreferenceChangeListener listener : mcr.listeners) {
|
|
if (listener != null) {
|
|
listener.onSharedPreferenceChanged(SharedPreferencesImpl.this, null);
|
|
}
|
|
}
|
|
}
|
|
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(() -> 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 boolean isFromSyncCommit = (postWriteRunnable == null);
|
|
|
|
final Runnable writeToDiskRunnable = new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
synchronized (mWritingToDiskLock) {
|
|
writeToFile(mcr, isFromSyncCommit);
|
|
}
|
|
synchronized (mLock) {
|
|
mDiskWritesInFlight--;
|
|
}
|
|
if (postWriteRunnable != null) {
|
|
postWriteRunnable.run();
|
|
}
|
|
}
|
|
};
|
|
|
|
// Typical #commit() path with fewer allocations, doing a write on
|
|
// the current thread.
|
|
if (isFromSyncCommit) {
|
|
boolean wasEmpty = false;
|
|
synchronized (mLock) {
|
|
wasEmpty = mDiskWritesInFlight == 1;
|
|
}
|
|
if (wasEmpty) {
|
|
writeToDiskRunnable.run();
|
|
return;
|
|
}
|
|
}
|
|
|
|
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
@GuardedBy("mWritingToDiskLock")
|
|
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
|
|
long startTime = 0;
|
|
long existsTime = 0;
|
|
long backupExistsTime = 0;
|
|
long outputStreamCreateTime = 0;
|
|
long writeTime = 0;
|
|
long fsyncTime = 0;
|
|
long setPermTime = 0;
|
|
long fstatTime = 0;
|
|
long deleteTime = 0;
|
|
|
|
if (DEBUG) {
|
|
startTime = System.currentTimeMillis();
|
|
}
|
|
|
|
boolean fileExists = mFile.exists();
|
|
|
|
if (DEBUG) {
|
|
existsTime = System.currentTimeMillis();
|
|
|
|
// Might not be set, hence init them to a default value
|
|
backupExistsTime = existsTime;
|
|
}
|
|
|
|
// Rename the current file so it may be used as a backup during the next read
|
|
if (fileExists) {
|
|
boolean needsWrite = false;
|
|
|
|
// Only need to write if the disk state is older than this commit
|
|
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
|
|
if (isFromSyncCommit) {
|
|
needsWrite = true;
|
|
} else {
|
|
synchronized (mLock) {
|
|
// No need to persist intermediate states. Just wait for the latest state to
|
|
// be persisted.
|
|
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
|
|
needsWrite = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!needsWrite) {
|
|
mcr.setDiskWriteResult(false, true);
|
|
return;
|
|
}
|
|
|
|
boolean backupFileExists = mBackupFile.exists();
|
|
|
|
if (DEBUG) {
|
|
backupExistsTime = System.currentTimeMillis();
|
|
}
|
|
|
|
if (!backupFileExists) {
|
|
if (!mFile.renameTo(mBackupFile)) {
|
|
Log.e(TAG, "Couldn't rename file " + mFile
|
|
+ " to backup file " + mBackupFile);
|
|
mcr.setDiskWriteResult(false, 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 (DEBUG) {
|
|
outputStreamCreateTime = System.currentTimeMillis();
|
|
}
|
|
|
|
if (str == null) {
|
|
mcr.setDiskWriteResult(false, false);
|
|
return;
|
|
}
|
|
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
|
|
|
|
writeTime = System.currentTimeMillis();
|
|
|
|
FileUtils.sync(str);
|
|
|
|
fsyncTime = System.currentTimeMillis();
|
|
|
|
str.close();
|
|
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
|
|
|
|
if (DEBUG) {
|
|
setPermTime = System.currentTimeMillis();
|
|
}
|
|
|
|
try {
|
|
final StructStat stat = Os.stat(mFile.getPath());
|
|
synchronized (mLock) {
|
|
mStatTimestamp = stat.st_mtim;
|
|
mStatSize = stat.st_size;
|
|
}
|
|
} catch (ErrnoException e) {
|
|
// Do nothing
|
|
}
|
|
|
|
if (DEBUG) {
|
|
fstatTime = System.currentTimeMillis();
|
|
}
|
|
|
|
// Writing was successful, delete the backup file if there is one.
|
|
mBackupFile.delete();
|
|
|
|
if (DEBUG) {
|
|
deleteTime = System.currentTimeMillis();
|
|
}
|
|
|
|
mDiskStateGeneration = mcr.memoryStateGeneration;
|
|
|
|
mcr.setDiskWriteResult(true, true);
|
|
|
|
if (DEBUG) {
|
|
Log.d(TAG, "write: " + (existsTime - startTime) + "/"
|
|
+ (backupExistsTime - startTime) + "/"
|
|
+ (outputStreamCreateTime - startTime) + "/"
|
|
+ (writeTime - startTime) + "/"
|
|
+ (fsyncTime - startTime) + "/"
|
|
+ (setPermTime - startTime) + "/"
|
|
+ (fstatTime - startTime) + "/"
|
|
+ (deleteTime - startTime));
|
|
}
|
|
|
|
long fsyncDuration = fsyncTime - writeTime;
|
|
mSyncTimes.add((int) fsyncDuration);
|
|
mNumSync++;
|
|
|
|
if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
|
|
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
|
|
}
|
|
|
|
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, false);
|
|
}
|
|
|
|
|
|
private static final class SharedPreferencesThreadFactory implements ThreadFactory {
|
|
@Override
|
|
public Thread newThread(Runnable runnable) {
|
|
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
|
|
thread.setName("SharedPreferences");
|
|
return thread;
|
|
}
|
|
}
|
|
}
|