Merge "Don't load provider status on the ui thread" into jb-dev
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 2d6e4c8..7e691ea 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -48,8 +48,8 @@
 import com.android.contacts.list.DirectoryListLoader;
 import com.android.contacts.list.OnContactBrowserActionListener;
 import com.android.contacts.list.OnContactsUnavailableActionListener;
-import com.android.contacts.list.ProviderStatusLoader;
-import com.android.contacts.list.ProviderStatusLoader.ProviderStatusListener;
+import com.android.contacts.list.ProviderStatusWatcher;
+import com.android.contacts.list.ProviderStatusWatcher.ProviderStatusListener;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.AccountWithDataSet;
 import com.android.contacts.preference.ContactsPreferenceActivity;
@@ -149,7 +149,7 @@
     private ContactListFilterController mContactListFilterController;
 
     private ContactsUnavailableFragment mContactsUnavailableFragment;
-    private ProviderStatusLoader mProviderStatusLoader;
+    private ProviderStatusWatcher mProviderStatusWatcher;
     private int mProviderStatus = -1;
 
     private boolean mOptionsMenuContactsAvailable;
@@ -207,7 +207,7 @@
     public PeopleActivity() {
         mInstanceId = sNextInstanceId.getAndIncrement();
         mIntentResolver = new ContactsIntentResolver(this);
-        mProviderStatusLoader = new ProviderStatusLoader(this);
+        mProviderStatusWatcher = ProviderStatusWatcher.getInstance(this);
     }
 
     @Override
@@ -247,7 +247,6 @@
             mContactDetailFragment = (ContactDetailFragment) fragment;
         } else if (fragment instanceof ContactsUnavailableFragment) {
             mContactsUnavailableFragment = (ContactsUnavailableFragment)fragment;
-            mContactsUnavailableFragment.setProviderStatusLoader(mProviderStatusLoader);
             mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
                     new ContactsUnavailableFragmentListener());
         }
@@ -269,6 +268,8 @@
         mContactListFilterController.checkFilterValidity(false);
         mContactListFilterController.addListener(this);
 
+        mProviderStatusWatcher.addListener(this);
+
         mIsRecreatedInstance = (savedState != null);
         createViewsAndFragments(savedState);
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -486,14 +487,14 @@
         mOptionsMenuContactsAvailable = false;
 
         mProviderStatus = -1;
-        mProviderStatusLoader.setProviderStatusListener(null);
+        mProviderStatusWatcher.stop();
         super.onPause();
     }
 
     @Override
     protected void onResume() {
         super.onResume();
-        mProviderStatusLoader.setProviderStatusListener(this);
+        mProviderStatusWatcher.start();
         showContactsUnavailableFragmentIfNecessary();
 
         // Re-register the listener, which may have been cleared when onSaveInstanceState was
@@ -515,6 +516,8 @@
 
     @Override
     protected void onDestroy() {
+        mProviderStatusWatcher.removeListener(this);
+
         // Some of variables will be null if this Activity redirects Intent.
         // See also onCreate() or other methods called during the Activity's initialization.
         if (mActionBarAdapter != null) {
@@ -975,7 +978,7 @@
     }
 
     private void showContactsUnavailableFragmentIfNecessary() {
-        int providerStatus = mProviderStatusLoader.getProviderStatus();
+        int providerStatus = mProviderStatusWatcher.getProviderStatus();
         if (providerStatus == mProviderStatus) {
             return;
         }
@@ -1015,7 +1018,6 @@
             }
             if (mContactsUnavailableFragment == null) {
                 mContactsUnavailableFragment = new ContactsUnavailableFragment();
-                mContactsUnavailableFragment.setProviderStatusLoader(mProviderStatusLoader);
                 mContactsUnavailableFragment.setOnContactsUnavailableActionListener(
                         new ContactsUnavailableFragmentListener());
                 getFragmentManager().beginTransaction()
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
index 5ee25a2..27fdada 100644
--- a/src/com/android/contacts/list/ContactEntryListFragment.java
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -128,7 +128,6 @@
     private ContextMenuAdapter mContextMenuAdapter;
     private ContactPhotoManager mPhotoManager;
     private ContactListEmptyView mEmptyView;
-    private ProviderStatusLoader mProviderStatusLoader;
     private ContactsPreferences mContactsPrefs;
 
     private boolean mForceLoad;
@@ -291,10 +290,6 @@
 
         mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
 
-        if (mProviderStatusLoader == null) {
-            mProviderStatusLoader = new ProviderStatusLoader(mContext);
-        }
-
         mForceLoad = loadPreferences();
 
         mDirectoryListStatus = STATUS_NOT_LOADED;
diff --git a/src/com/android/contacts/list/ContactsUnavailableFragment.java b/src/com/android/contacts/list/ContactsUnavailableFragment.java
index 74a578f..becc704 100644
--- a/src/com/android/contacts/list/ContactsUnavailableFragment.java
+++ b/src/com/android/contacts/list/ContactsUnavailableFragment.java
@@ -35,7 +35,7 @@
  */
 public class ContactsUnavailableFragment extends Fragment implements OnClickListener {
 
-    private ProviderStatusLoader mProviderStatusLoader;
+    private ProviderStatusWatcher mProviderStatusWatcher;
 
     private View mView;
     private TextView mMessageView;
@@ -52,6 +52,12 @@
     private OnContactsUnavailableActionListener mListener;
 
     @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mProviderStatusWatcher = ProviderStatusWatcher.getInstance(getActivity());
+    }
+
+    @Override
     public View onCreateView(
             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         mView = inflater.inflate(R.layout.contacts_unavailable_fragment, null);
@@ -77,12 +83,8 @@
         mListener = listener;
     }
 
-    public void setProviderStatusLoader(ProviderStatusLoader loader) {
-        mProviderStatusLoader = loader;
-    }
-
     public void update() {
-        int providerStatus = mProviderStatusLoader.getProviderStatus();
+        int providerStatus = mProviderStatusWatcher.getProviderStatus();
         switch (providerStatus) {
             case ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS:
                 setMessageText(mNoContactsMsgResId, mNSecNoContactsMsgResId);
@@ -120,7 +122,7 @@
 
             case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY:
                 String message = getResources().getString(R.string.upgrade_out_of_memory,
-                        new Object[] { mProviderStatusLoader.getProviderStatusData() });
+                        new Object[] { mProviderStatusWatcher.getProviderStatusData() });
                 mMessageView.setText(message);
                 mMessageView.setGravity(Gravity.LEFT);
                 mMessageView.setVisibility(View.VISIBLE);
@@ -153,7 +155,7 @@
                 mListener.onFreeInternalStorageAction();
                 break;
             case R.id.import_failure_retry_button:
-                mProviderStatusLoader.retryUpgrade();
+                mProviderStatusWatcher.retryUpgrade();
                 break;
         }
     }
@@ -166,7 +168,7 @@
         mNoContactsMsgResId = resId;
         mNSecNoContactsMsgResId = secResId;
         if (mMessageView != null &&
-                mProviderStatusLoader.getProviderStatus() ==
+                mProviderStatusWatcher.getProviderStatus() ==
                     ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
             if (resId != -1) {
                 mMessageView.setText(mNoContactsMsgResId);
diff --git a/src/com/android/contacts/list/ProviderStatusLoader.java b/src/com/android/contacts/list/ProviderStatusLoader.java
deleted file mode 100644
index a6afa9f..0000000
--- a/src/com/android/contacts/list/ProviderStatusLoader.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * 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 com.android.contacts.list;
-
-import android.content.ContentResolver;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.os.Handler;
-import android.provider.ContactsContract.ProviderStatus;
-
-/**
- * Checks provider status and configures a list adapter accordingly.
- */
-public class ProviderStatusLoader extends ContentObserver {
-
-    /**
-     * Callback interface invoked when the provider status changes.
-     */
-    public interface ProviderStatusListener {
-        public void onProviderStatusChange();
-    }
-
-    private static final String[] PROJECTION = new String[] {
-        ProviderStatus.STATUS,
-        ProviderStatus.DATA1
-    };
-
-    private static final int UNKNOWN = -1;
-
-    private final Context mContext;
-    private int mProviderStatus = UNKNOWN;
-    private String mProviderData;
-    private ProviderStatusListener mListener;
-    private Handler mHandler = new Handler();
-
-    public ProviderStatusLoader(Context context) {
-        super(null);
-        this.mContext = context;
-    }
-
-    public int getProviderStatus() {
-        if (mProviderStatus == UNKNOWN) {
-            loadProviderStatus();
-        }
-
-        return mProviderStatus;
-    }
-
-    public String getProviderStatusData() {
-        if (mProviderStatus == UNKNOWN) {
-            loadProviderStatus();
-        }
-
-        return mProviderData;
-    }
-
-    protected void loadProviderStatus() {
-
-        // Default to normal status
-        mProviderStatus = ProviderStatus.STATUS_NORMAL;
-
-        // This query can be performed on the UI thread because
-        // the API explicitly allows such use.
-        Cursor cursor = mContext.getContentResolver().query(ProviderStatus.CONTENT_URI,
-                PROJECTION, null, null, null);
-        if (cursor != null) {
-            try {
-                if (cursor.moveToFirst()) {
-                    mProviderStatus = cursor.getInt(0);
-                    mProviderData = cursor.getString(1);
-                }
-            } finally {
-                cursor.close();
-            }
-        }
-    }
-
-    public void setProviderStatusListener(ProviderStatusListener listener) {
-        mListener = listener;
-
-        ContentResolver resolver = mContext.getContentResolver();
-        if (listener != null) {
-            mProviderStatus = UNKNOWN;
-            resolver.registerContentObserver(ProviderStatus.CONTENT_URI, false, this);
-        } else {
-            resolver.unregisterContentObserver(this);
-        }
-    }
-
-    @Override
-    public void onChange(boolean selfChange) {
-        // Deliver a notification on the UI thread
-        mHandler.post(new Runnable() {
-            @Override
-            public void run() {
-                if (mListener != null) {
-                    mProviderStatus = UNKNOWN;
-                    mListener.onProviderStatusChange();
-                }
-            }
-        });
-    }
-
-    /**
-     * Sends a provider status update, which will trigger a retry of database upgrade
-     */
-    public void retryUpgrade() {
-        ContentValues values = new ContentValues();
-        values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
-        mContext.getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null);
-    }
-}
diff --git a/src/com/android/contacts/list/ProviderStatusWatcher.java b/src/com/android/contacts/list/ProviderStatusWatcher.java
new file mode 100644
index 0000000..3ce4b78
--- /dev/null
+++ b/src/com/android/contacts/list/ProviderStatusWatcher.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2012 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 com.android.contacts.list;
+
+import com.google.common.collect.Lists;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.provider.ContactsContract.ProviderStatus;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * A singleton that keeps track of the last known provider status.
+ *
+ * All methods must be called on the UI thread unless noted otherwise.
+ *
+ * All members must be set on the UI thread unless noted otherwise.
+ */
+public class ProviderStatusWatcher extends ContentObserver {
+    private static final String TAG = "ProviderStatusWatcher";
+    private static final boolean DEBUG = false;
+
+    /**
+     * Callback interface invoked when the provider status changes.
+     */
+    public interface ProviderStatusListener {
+        public void onProviderStatusChange();
+    }
+
+    private static final String[] PROJECTION = new String[] {
+        ProviderStatus.STATUS,
+        ProviderStatus.DATA1
+    };
+
+    /**
+     * We'll wait for this amount of time on the UI thread if the load hasn't finished.
+     */
+    private static final int LOAD_WAIT_TIMEOUT_MS = 1000;
+
+    private static final int STATUS_UNKNOWN = -1;
+
+    private static ProviderStatusWatcher sInstance;
+
+    private final Context mContext;
+    private final Handler mHandler = new Handler();
+
+    private final Object mSignal = new Object();
+
+    private int mStartRequestedCount;
+
+    private LoaderTask mLoaderTask;
+
+    /** Last known provider status.  This can be changed on a worker thread. */
+    private int mProviderStatus = STATUS_UNKNOWN;
+
+    /** Last known provider status data.  This can be changed on a worker thread. */
+    private String mProviderData;
+
+    private final ArrayList<ProviderStatusListener> mListeners = Lists.newArrayList();
+
+    private final Runnable mStartLoadingRunnable = new Runnable() {
+        @Override
+        public void run() {
+            startLoading();
+        }
+    };
+
+    /**
+     * Returns the singleton instance.
+     */
+    public synchronized static ProviderStatusWatcher getInstance(Context context) {
+        if (sInstance == null) {
+            sInstance = new ProviderStatusWatcher(context);
+        }
+        return sInstance;
+    }
+
+    private ProviderStatusWatcher(Context context) {
+        super(null);
+        mContext = context;
+    }
+
+    /** Add a listener. */
+    public void addListener(ProviderStatusListener listener) {
+        mListeners.add(listener);
+    }
+
+    /** Remove a listener */
+    public void removeListener(ProviderStatusListener listener) {
+        mListeners.remove(listener);
+    }
+
+    private void notifyListeners() {
+        if (DEBUG) {
+            Log.d(TAG, "notifyListeners: " + mListeners.size());
+        }
+        if (isStarted()) {
+            for (ProviderStatusListener listener : mListeners) {
+                listener.onProviderStatusChange();
+            }
+        }
+    }
+
+    private boolean isStarted() {
+        return mStartRequestedCount > 0;
+    }
+
+    /**
+     * Starts watching the provider status.  {@link #start()} and {@link #stop()} calls can be
+     * nested.
+     */
+    public void start() {
+        if (++mStartRequestedCount == 1) {
+            mContext.getContentResolver()
+                .registerContentObserver(ProviderStatus.CONTENT_URI, false, this);
+            startLoading();
+
+            if (DEBUG) {
+                Log.d(TAG, "Start observing");
+            }
+        }
+    }
+
+    /**
+     * Stops watching the provider status.
+     */
+    public void stop() {
+        if (!isStarted()) {
+            Log.e(TAG, "Already stopped");
+            return;
+        }
+        if (--mStartRequestedCount == 0) {
+
+            mHandler.removeCallbacks(mStartLoadingRunnable);
+
+            mContext.getContentResolver().unregisterContentObserver(this);
+            if (DEBUG) {
+                Log.d(TAG, "Stop observing");
+            }
+        }
+    }
+
+    /**
+     * @return last known provider status.
+     *
+     * If this method is called when we haven't started the status query or the query is still in
+     * progress, it will start a query in a worker thread if necessary, and *wait for the result*.
+     *
+     * This means this method is essentially a blocking {@link ProviderStatus#CONTENT_URI} query.
+     * This URI is not backed by the file system, so is usually fast enough to perform on the main
+     * thread, but in extreme cases (when the system takes a while to bring up the contacts
+     * provider?) this may still cause ANRs.
+     *
+     * In order to avoid that, if we can't load the status within {@link #LOAD_WAIT_TIMEOUT_MS},
+     * we'll give up and just returns {@link ProviderStatus#STATUS_UPGRADING} in order to unblock
+     * the UI thread.  The actual result will be delivered later via {@link ProviderStatusListener}.
+     * (If {@link ProviderStatus#STATUS_UPGRADING} is returned, the app (should) shows an according
+     * message, like "contacts are being updated".)
+     */
+    public int getProviderStatus() {
+        waitForLoaded();
+
+        if (mProviderStatus == STATUS_UNKNOWN) {
+            return ProviderStatus.STATUS_UPGRADING;
+        }
+
+        return mProviderStatus;
+    }
+
+    /**
+     * @return last known provider status data.  See also {@link #getProviderStatus()}.
+     */
+    public String getProviderStatusData() {
+        waitForLoaded();
+
+        if (mProviderStatus == STATUS_UNKNOWN) {
+            // STATUS_UPGRADING has no data.
+            return "";
+        }
+
+        return mProviderData;
+    }
+
+    private void waitForLoaded() {
+        if (mProviderStatus == STATUS_UNKNOWN) {
+            if (mLoaderTask == null) {
+                // For some reason the loader couldn't load the status.  Let's start it again.
+                startLoading();
+            }
+            synchronized (mSignal) {
+                try {
+                    mSignal.wait(LOAD_WAIT_TIMEOUT_MS);
+                } catch (InterruptedException ignore) {
+                }
+            }
+        }
+    }
+
+    private void startLoading() {
+        if (mLoaderTask != null) {
+            return; // Task already running.
+        }
+
+        if (DEBUG) {
+            Log.d(TAG, "Start loading");
+        }
+
+        mLoaderTask = new LoaderTask();
+        mLoaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    private class LoaderTask extends AsyncTask<Void, Void, Boolean> {
+        @Override
+        protected Boolean doInBackground(Void... params) {
+            try {
+                Cursor cursor = mContext.getContentResolver().query(ProviderStatus.CONTENT_URI,
+                        PROJECTION, null, null, null);
+                if (cursor != null) {
+                    try {
+                        if (cursor.moveToFirst()) {
+                            mProviderStatus = cursor.getInt(0);
+                            mProviderData = cursor.getString(1);
+                            return true;
+                        }
+                    } finally {
+                        cursor.close();
+                    }
+                }
+                return false;
+            } finally {
+                synchronized (mSignal) {
+                    mSignal.notifyAll();
+                }
+            }
+        }
+
+        @Override
+        protected void onCancelled(Boolean result) {
+            cleanUp();
+        }
+
+        @Override
+        protected void onPostExecute(Boolean loaded) {
+            cleanUp();
+            if (loaded != null && loaded) {
+                notifyListeners();
+            }
+        }
+
+        private void cleanUp() {
+            mLoaderTask = null;
+        }
+    }
+
+    /**
+     * Called when provider status may has changed.
+     *
+     * This method will be called on a worker thread by the framework.
+     */
+    @Override
+    public void onChange(boolean selfChange, Uri uri) {
+        if (!ProviderStatus.CONTENT_URI.equals(uri)) return;
+
+        // Provider status change is rare, so okay to log.
+        Log.i(TAG, "Provider status changed.");
+
+        mHandler.removeCallbacks(mStartLoadingRunnable); // Remove one in the queue, if any.
+        mHandler.post(mStartLoadingRunnable);
+    }
+
+    /**
+     * Sends a provider status update, which will trigger a retry of database upgrade
+     */
+    public void retryUpgrade() {
+        Log.i(TAG, "retryUpgrade");
+        final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                ContentValues values = new ContentValues();
+                values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING);
+                mContext.getContentResolver().update(ProviderStatus.CONTENT_URI, values,
+                        null, null);
+                return null;
+            }
+        };
+        task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+}