| /* |
| * 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.content.SharedPreferences; |
| 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 com.google.android.collect.Maps; |
| |
| 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; |
| |
| 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; |
| |
| // Lock ordering rules: |
| // - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock |
| // - acquire mWritingToDiskLock before EditorImpl.mLock |
| |
| 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 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; |
| |
| SharedPreferencesImpl(File file, int mode) { |
| mFile = file; |
| mBackupFile = makeBackupFile(file); |
| mMode = mode; |
| mLoaded = false; |
| mMap = null; |
| startLoadFromDisk(); |
| } |
| |
| private void startLoadFromDisk() { |
| synchronized (mLock) { |
| mLoaded = false; |
| } |
| new Thread("SharedPreferencesImpl-load") { |
| public void run() { |
| loadFromDisk(); |
| } |
| }.start(); |
| } |
| |
| 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 map = null; |
| StructStat stat = null; |
| try { |
| stat = Os.stat(mFile.getPath()); |
| if (mFile.canRead()) { |
| BufferedInputStream str = null; |
| try { |
| str = new BufferedInputStream( |
| new FileInputStream(mFile), 16*1024); |
| map = XmlUtils.readMapXml(str); |
| } catch (Exception e) { |
| Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e); |
| } finally { |
| IoUtils.closeQuietly(str); |
| } |
| } |
| } catch (ErrnoException e) { |
| /* ignore */ |
| } |
| |
| synchronized (mLock) { |
| mLoaded = true; |
| if (map != null) { |
| mMap = map; |
| mStatTimestamp = stat.st_mtim; |
| mStatSize = stat.st_size; |
| } else { |
| mMap = new HashMap<>(); |
| } |
| mLock.notifyAll(); |
| } |
| } |
| |
| static File makeBackupFile(File prefsFile) { |
| return new File(prefsFile.getPath() + ".bak"); |
| } |
| |
| 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; |
| } |
| } |
| |
| public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { |
| synchronized(mLock) { |
| mListeners.put(listener, CONTENT); |
| } |
| } |
| |
| public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) { |
| synchronized(mLock) { |
| mListeners.remove(listener); |
| } |
| } |
| |
| 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) { |
| } |
| } |
| } |
| |
| public Map<String, ?> getAll() { |
| synchronized (mLock) { |
| awaitLoadedLocked(); |
| //noinspection unchecked |
| return new HashMap<String, Object>(mMap); |
| } |
| } |
| |
| @Nullable |
| public String getString(String key, @Nullable String defValue) { |
| synchronized (mLock) { |
| awaitLoadedLocked(); |
| String v = (String)mMap.get(key); |
| return v != null ? v : defValue; |
| } |
| } |
| |
| @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; |
| } |
| } |
| |
| public int getInt(String key, int defValue) { |
| synchronized (mLock) { |
| awaitLoadedLocked(); |
| Integer v = (Integer)mMap.get(key); |
| return v != null ? v : defValue; |
| } |
| } |
| public long getLong(String key, long defValue) { |
| synchronized (mLock) { |
| awaitLoadedLocked(); |
| Long v = (Long)mMap.get(key); |
| return v != null ? v : defValue; |
| } |
| } |
| public float getFloat(String key, float defValue) { |
| synchronized (mLock) { |
| awaitLoadedLocked(); |
| Float v = (Float)mMap.get(key); |
| return v != null ? v : defValue; |
| } |
| } |
| public boolean getBoolean(String key, boolean defValue) { |
| synchronized (mLock) { |
| awaitLoadedLocked(); |
| Boolean v = (Boolean)mMap.get(key); |
| return v != null ? v : defValue; |
| } |
| } |
| |
| public boolean contains(String key) { |
| synchronized (mLock) { |
| awaitLoadedLocked(); |
| return mMap.containsKey(key); |
| } |
| } |
| |
| 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; |
| @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, @Nullable List<String> keysModified, |
| @Nullable Set<OnSharedPreferenceChangeListener> listeners, |
| Map<String, Object> mapToWriteToDisk) { |
| this.memoryStateGeneration = memoryStateGeneration; |
| 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 mLock = new Object(); |
| |
| @GuardedBy("mLock") |
| private final Map<String, Object> mModified = Maps.newHashMap(); |
| |
| @GuardedBy("mLock") |
| private boolean mClear = false; |
| |
| public Editor putString(String key, @Nullable String value) { |
| synchronized (mLock) { |
| mModified.put(key, value); |
| return this; |
| } |
| } |
| public Editor putStringSet(String key, @Nullable Set<String> values) { |
| synchronized (mLock) { |
| mModified.put(key, |
| (values == null) ? null : new HashSet<String>(values)); |
| return this; |
| } |
| } |
| public Editor putInt(String key, int value) { |
| synchronized (mLock) { |
| mModified.put(key, value); |
| return this; |
| } |
| } |
| public Editor putLong(String key, long value) { |
| synchronized (mLock) { |
| mModified.put(key, value); |
| return this; |
| } |
| } |
| public Editor putFloat(String key, float value) { |
| synchronized (mLock) { |
| mModified.put(key, value); |
| return this; |
| } |
| } |
| public Editor putBoolean(String key, boolean value) { |
| synchronized (mLock) { |
| mModified.put(key, value); |
| return this; |
| } |
| } |
| |
| public Editor remove(String key) { |
| synchronized (mLock) { |
| mModified.put(key, this); |
| return this; |
| } |
| } |
| |
| public Editor clear() { |
| synchronized (mLock) { |
| mClear = true; |
| return this; |
| } |
| } |
| |
| public void apply() { |
| final long startTime = System.currentTimeMillis(); |
| |
| final MemoryCommitResult mcr = commitToMemory(); |
| final Runnable awaitCommit = new Runnable() { |
| 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() { |
| 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; |
| 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 (mLock) { |
| boolean changesMade = false; |
| |
| if (mClear) { |
| if (!mMap.isEmpty()) { |
| changesMade = true; |
| mMap.clear(); |
| } |
| 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 (!mMap.containsKey(k)) { |
| continue; |
| } |
| mMap.remove(k); |
| } else { |
| if (mMap.containsKey(k)) { |
| Object existingValue = mMap.get(k); |
| if (existingValue != null && existingValue.equals(v)) { |
| continue; |
| } |
| } |
| mMap.put(k, v); |
| } |
| |
| changesMade = true; |
| if (hasListeners) { |
| keysModified.add(k); |
| } |
| } |
| |
| mModified.clear(); |
| |
| if (changesMade) { |
| mCurrentMemoryStateGeneration++; |
| } |
| |
| memoryStateGeneration = mCurrentMemoryStateGeneration; |
| } |
| } |
| return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, |
| mapToWriteToDisk); |
| } |
| |
| 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.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 boolean isFromSyncCommit = (postWriteRunnable == null); |
| |
| final Runnable writeToDiskRunnable = new Runnable() { |
| 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; |
| } |
| |
| // Note: must hold 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); |
| } |
| } |