Merge "Make getSharedPreference() async, blocking on first access."
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index d2de382..f1842c6 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -532,10 +532,6 @@
         throw new RuntimeException("Not supported in system context");
     }
 
-    static File makeBackupFile(File prefsFile) {
-        return new File(prefsFile.getPath() + ".bak");
-    }
-
     public File getSharedPrefsFile(String name) {
         return makeFilename(getPreferencesDir(), name + ".xml");
     }
@@ -543,54 +539,19 @@
     @Override
     public SharedPreferences getSharedPreferences(String name, int mode) {
         SharedPreferencesImpl sp;
-        File prefsFile;
-        boolean needInitialLoad = false;
         synchronized (sSharedPrefs) {
             sp = sSharedPrefs.get(name);
-            if (sp != null && !sp.hasFileChangedUnexpectedly()) {
-                return sp;
-            }
-            prefsFile = getSharedPrefsFile(name);
             if (sp == null) {
-                sp = new SharedPreferencesImpl(prefsFile, mode, null);
+                File prefsFile = getSharedPrefsFile(name);
+                sp = new SharedPreferencesImpl(prefsFile, mode);
                 sSharedPrefs.put(name, sp);
-                needInitialLoad = true;
-            }
-        }
-
-        synchronized (sp) {
-            if (needInitialLoad && sp.isLoaded()) {
-                // lost the race to load; another thread handled it
                 return sp;
             }
-            File backup = makeBackupFile(prefsFile);
-            if (backup.exists()) {
-                prefsFile.delete();
-                backup.renameTo(prefsFile);
-            }
-
-            // Debugging
-            if (prefsFile.exists() && !prefsFile.canRead()) {
-                Log.w(TAG, "Attempt to read preferences file " + prefsFile + " without permission");
-            }
-
-            Map map = null;
-            FileStatus stat = new FileStatus();
-            if (FileUtils.getFileStatus(prefsFile.getPath(), stat) && prefsFile.canRead()) {
-                try {
-                    FileInputStream str = new FileInputStream(prefsFile);
-                    map = XmlUtils.readMapXml(str);
-                    str.close();
-                } catch (XmlPullParserException e) {
-                    Log.w(TAG, "getSharedPreferences", e);
-                } catch (FileNotFoundException e) {
-                    Log.w(TAG, "getSharedPreferences", e);
-                } catch (IOException e) {
-                    Log.w(TAG, "getSharedPreferences", e);
-                }
-            }
-            sp.replace(map, stat);
         }
+        // If somebody else (some other process) changed the prefs
+        // file behind our back, we reload it.  This has been the
+        // historical (if undocumented) behavior.
+        sp.startReloadIfChangedUnexpectedly();
         return sp;
     }
 
diff --git a/core/java/android/app/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java
index a807d3b..8aee65c 100644
--- a/core/java/android/app/SharedPreferencesImpl.java
+++ b/core/java/android/app/SharedPreferencesImpl.java
@@ -25,9 +25,12 @@
 import com.google.android.collect.Maps;
 import com.android.internal.util.XmlUtils;
 
+import dalvik.system.BlockGuard;
+
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.File;
+import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -61,32 +64,88 @@
 
     private final Object mWritingToDiskLock = new Object();
     private static final Object mContent = new Object();
-    private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners;
+    private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
+            new WeakHashMap<OnSharedPreferenceChangeListener, Object>();
 
-    SharedPreferencesImpl(
-        File file, int mode, Map initialContents) {
+    SharedPreferencesImpl(File file, int mode) {
         mFile = file;
-        mBackupFile = ContextImpl.makeBackupFile(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>();
+        mLoaded = false;
+        mMap = null;
+        startLoadFromDisk();
     }
 
-    // Has this SharedPreferences ever had values assigned to it?
-    boolean isLoaded() {
+    private void startLoadFromDisk() {
         synchronized (this) {
-            return mLoaded;
+            mLoaded = false;
+        }
+        new Thread("SharedPreferencesImpl-load") {
+            public void run() {
+                synchronized (SharedPreferencesImpl.this) {
+                    loadFromDiskLocked();
+                }
+            }
+        }.start();
+    }
+
+    private void loadFromDiskLocked() {
+        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;
+        FileStatus stat = new FileStatus();
+        if (FileUtils.getFileStatus(mFile.getPath(), stat) && mFile.canRead()) {
+            try {
+                FileInputStream str = new FileInputStream(mFile);
+                map = XmlUtils.readMapXml(str);
+                str.close();
+            } catch (XmlPullParserException e) {
+                Log.w(TAG, "getSharedPreferences", e);
+            } catch (FileNotFoundException e) {
+                Log.w(TAG, "getSharedPreferences", e);
+            } catch (IOException e) {
+                Log.w(TAG, "getSharedPreferences", e);
+            }
+        }
+        mLoaded = true;
+        if (map != null) {
+            mMap = map;
+            mStatTimestamp = stat.mtime;
+            mStatSize = stat.size;
+        } else {
+            mMap = new HashMap<String, Object>();
+        }
+        notifyAll();
+    }
+
+    private static File makeBackupFile(File prefsFile) {
+        return new File(prefsFile.getPath() + ".bak");
+    }
+
+    void startReloadIfChangedUnexpectedly() {
+        synchronized (this) {
+            // 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.
-    public boolean hasFileChangedUnexpectedly() {
+    private boolean hasFileChangedUnexpectedly() {
         synchronized (this) {
             if (mDiskWritesInFlight > 0) {
                 // If we know we caused it, it's not unexpected.
@@ -103,19 +162,6 @@
         }
     }
 
-    /*package*/ void replace(Map newContents, FileStatus stat) {
-        synchronized (this) {
-            mLoaded = true;
-            if (newContents != null) {
-                mMap = newContents;
-            }
-            if (stat != null) {
-                mStatTimestamp = stat.mtime;
-                mStatSize = stat.size;
-            }
-        }
-    }
-
     public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
         synchronized(this) {
             mListeners.put(listener, mContent);
@@ -128,8 +174,24 @@
         }
     }
 
+    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 {
+                wait();
+            } catch (InterruptedException unused) {
+            }
+        }
+    }
+
     public Map<String, ?> getAll() {
-        synchronized(this) {
+        synchronized (this) {
+            awaitLoadedLocked();
             //noinspection unchecked
             return new HashMap<String, Object>(mMap);
         }
@@ -137,6 +199,7 @@
 
     public String getString(String key, String defValue) {
         synchronized (this) {
+            awaitLoadedLocked();
             String v = (String)mMap.get(key);
             return v != null ? v : defValue;
         }
@@ -144,6 +207,7 @@
 
     public Set<String> getStringSet(String key, Set<String> defValues) {
         synchronized (this) {
+            awaitLoadedLocked();
             Set<String> v = (Set<String>) mMap.get(key);
             return v != null ? v : defValues;
         }
@@ -151,24 +215,28 @@
 
     public int getInt(String key, int defValue) {
         synchronized (this) {
+            awaitLoadedLocked();
             Integer v = (Integer)mMap.get(key);
             return v != null ? v : defValue;
         }
     }
     public long getLong(String key, long defValue) {
         synchronized (this) {
+            awaitLoadedLocked();
             Long v = (Long)mMap.get(key);
             return v != null ? v : defValue;
         }
     }
     public float getFloat(String key, float defValue) {
         synchronized (this) {
+            awaitLoadedLocked();
             Float v = (Float)mMap.get(key);
             return v != null ? v : defValue;
         }
     }
     public boolean getBoolean(String key, boolean defValue) {
         synchronized (this) {
+            awaitLoadedLocked();
             Boolean v = (Boolean)mMap.get(key);
             return v != null ? v : defValue;
         }
@@ -176,11 +244,23 @@
 
     public boolean contains(String key) {
         synchronized (this) {
+            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 (this) {
+            awaitLoadedLocked();
+        }
+
         return new EditorImpl();
     }