Make a Singleton for ContactListFilterController

Better fix for issue 5165507 "Contacts to Display filter loses its
setting when going from People to Phone app". Now the whole app
has one instance and People and Phone UIs share it. All notification
will be delivered to both Activities, and thus no hack on onStart()
will be needed.

Also fixes issue 5299160 "Account filter header not updated when account
is removed from device". We need to update filter after
AccountTypeManager finishes its reload operation.

Now filter settings should be saved only from the controller, so this
change removes the code saving filter settings to SharedPreferences from
Activity/Fragment.

Bug: 5165507
Bug: 5299160
Change-Id: I4118271f1a78976af6cb3d432b1dd7b30c18eb7a
diff --git a/src/com/android/contacts/ContactsApplication.java b/src/com/android/contacts/ContactsApplication.java
index eb8ffa9..f806c1d 100644
--- a/src/com/android/contacts/ContactsApplication.java
+++ b/src/com/android/contacts/ContactsApplication.java
@@ -16,14 +16,13 @@
 
 package com.android.contacts;
 
+import com.android.contacts.list.ContactListFilterController;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.test.InjectedServices;
 import com.android.contacts.util.Constants;
 import com.google.common.annotations.VisibleForTesting;
 
 import android.app.Application;
-import android.app.FragmentManager;
-import android.app.LoaderManager;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
@@ -35,6 +34,7 @@
     private static InjectedServices sInjectedServices;
     private AccountTypeManager mAccountTypeManager;
     private ContactPhotoManager mContactPhotoManager;
+    private ContactListFilterController mContactListFilterController;
 
     /**
      * Overrides the system services with mocks for testing.
@@ -95,6 +95,14 @@
             return mContactPhotoManager;
         }
 
+        if (ContactListFilterController.CONTACT_LIST_FILTER_SERVICE.equals(name)) {
+            if (mContactListFilterController == null) {
+                mContactListFilterController =
+                        ContactListFilterController.createContactListFilterController(this);
+            }
+            return mContactListFilterController;
+        }
+
         return super.getSystemService(name);
     }
 
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index 0ce7309..571a988 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -214,6 +214,30 @@
     private CallLogFragment mCallLogFragment;
     private PhoneFavoriteFragment mPhoneFavoriteFragment;
 
+    private final ContactListFilterListener mContactListFilterListener =
+            new ContactListFilterListener() {
+        @Override
+        public void onContactListFilterChanged() {
+            boolean doInvalidateOptionsMenu = false;
+
+            if (mPhoneFavoriteFragment != null && mPhoneFavoriteFragment.isAdded()) {
+                mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter());
+                doInvalidateOptionsMenu = true;
+            }
+
+            if (mSearchFragment != null && mSearchFragment.isAdded()) {
+                mSearchFragment.setFilter(mContactListFilterController.getFilter());
+                doInvalidateOptionsMenu = true;
+            } else {
+                Log.w(TAG, "Search Fragment isn't available when ContactListFilter is changed");
+            }
+
+            if (doInvalidateOptionsMenu) {
+                invalidateOptionsMenu();
+            }
+        }
+    };
+
     private final TabListener mTabListener = new TabListener() {
         @Override
         public void onTabUnselected(Tab tab, FragmentTransaction ft) {
@@ -371,29 +395,8 @@
 
         setContentView(R.layout.dialtacts_activity);
 
-        mContactListFilterController = new ContactListFilterController(this);
-        mContactListFilterController.addListener(new ContactListFilterListener() {
-            @Override
-            public void onContactListFilterChanged() {
-                boolean doInvalidateOptionsMenu = false;
-
-                if (mPhoneFavoriteFragment != null && mPhoneFavoriteFragment.isAdded()) {
-                    mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter());
-                    doInvalidateOptionsMenu = true;
-                }
-
-                if (mSearchFragment != null && mSearchFragment.isAdded()) {
-                    mSearchFragment.setFilter(mContactListFilterController.getFilter());
-                    doInvalidateOptionsMenu = true;
-                } else {
-                    Log.w(TAG, "Search Fragment isn't available when ContactListFilter is changed");
-                }
-
-                if (doInvalidateOptionsMenu) {
-                    invalidateOptionsMenu();
-                }
-            }
-        });
+        mContactListFilterController = ContactListFilterController.getInstance(this);
+        mContactListFilterController.addListener(mContactListFilterListener);
 
         mViewPager = (ViewPager) findViewById(R.id.pager);
         mViewPager.setAdapter(new ViewPagerAdapter(getFragmentManager()));
@@ -429,12 +432,6 @@
     @Override
     public void onStart() {
         super.onStart();
-        // Force filter reload to reflect possible filter changes done via People UI.
-        //
-        // Ideally both (People/Phone) UI should share the same instance for
-        // ContactListFilterController and they should be able to receive filter change event
-        // from the same controller (Bug 5165507)
-        mContactListFilterController.onStart(true);
         if (mPhoneFavoriteFragment != null) {
             mPhoneFavoriteFragment.setFilter(mContactListFilterController.getFilter());
         }
@@ -443,6 +440,12 @@
         }
     }
 
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mContactListFilterController.removeListener(mContactListFilterListener);
+    }
+
     private void prepareSearchView() {
         final View searchViewLayout =
                 getLayoutInflater().inflate(R.layout.dialtacts_custom_action_bar, null);
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 24992cc..c14ab54 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -184,8 +184,6 @@
     public PeopleActivity() {
         mInstanceId = sNextInstanceId.getAndIncrement();
         mIntentResolver = new ContactsIntentResolver(this);
-        mContactListFilterController = new ContactListFilterController(this);
-        mContactListFilterController.addListener(this);
         mProviderStatusLoader = new ProviderStatusLoader(this);
     }
 
@@ -246,6 +244,9 @@
             return;
         }
 
+        mContactListFilterController = ContactListFilterController.getInstance(this);
+        mContactListFilterController.addListener(this);
+
         mIsRecreatedInstance = (savedState != null);
         createViewsAndFragments(savedState);
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
@@ -436,7 +437,6 @@
              */
             configureFragments(!mIsRecreatedInstance);
         }
-        mContactListFilterController.onStart(false);
         super.onStart();
     }
 
@@ -473,6 +473,7 @@
         if (mActionBarAdapter != null) {
             mActionBarAdapter.setListener(null);
         }
+        mContactListFilterController.removeListener(this);
         super.onDestroy();
     }
 
@@ -859,6 +860,7 @@
             mAllFragment.setSelectedContactUri(contactUri);
         }
 
+        mAllFragment.setFilter(mContactListFilterController.getFilter());
         mAllFragment.setSearchMode(mActionBarAdapter.isSearchMode());
         mAllFragment.setQueryString(mActionBarAdapter.getQueryString(), false);
 
@@ -867,13 +869,12 @@
         } else {
             mAllFragment.setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE);
         }
-
-        if (mContactListFilterController.isInitialized()) {
-            mAllFragment.setFilter(mContactListFilterController.getFilter());
-        }
     }
 
     private void configureContactListFragment() {
+        // Filter may be changed when this Activity is in background.
+        mAllFragment.setFilter(mContactListFilterController.getFilter());
+
         final boolean showSearchResult = mActionBarAdapter.shouldShowSearchResult();
         mAllFragment.setSearchMode(showSearchResult);
 
diff --git a/src/com/android/contacts/list/ContactListFilter.java b/src/com/android/contacts/list/ContactListFilter.java
index 01d76a2..152b152 100644
--- a/src/com/android/contacts/list/ContactListFilter.java
+++ b/src/com/android/contacts/list/ContactListFilter.java
@@ -20,7 +20,6 @@
 import android.graphics.drawable.Drawable;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.provider.ContactsContract.Contacts;
 import android.text.TextUtils;
 
 /**
@@ -296,4 +295,45 @@
         }
         return mId;
     }
+
+    public String toDebugString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("[filter type: " + filterType + " (" + filterTypeToString(filterType) + ")");
+        if (filterType == FILTER_TYPE_ACCOUNT) {
+            builder.append(", accountType: " + accountType)
+                    .append(", accountName: " + accountName)
+                    .append(", dataSet: " + dataSet);
+        }
+        if (filterType == FILTER_TYPE_GROUP) {
+            builder.append(", groupId: " + groupId)
+                    .append(", groupSourceId: " + groupSourceId)
+                    .append(", groupReadOnly: " + groupReadOnly)
+                    .append("title: " + title);
+        }
+        builder.append(", icon: " + icon + "]");
+        return builder.toString();
+    }
+
+    public static final String filterTypeToString(int filterType) {
+        switch (filterType) {
+            case FILTER_TYPE_DEFAULT:
+                return "FILTER_TYPE_DEFAULT";
+            case FILTER_TYPE_ALL_ACCOUNTS:
+                return "FILTER_TYPE_ALL_ACCOUNTS";
+            case FILTER_TYPE_CUSTOM:
+                return "FILTER_TYPE_CUSTOM";
+            case FILTER_TYPE_STARRED:
+                return "FILTER_TYPE_STARRED";
+            case FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
+                return "FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY";
+            case FILTER_TYPE_SINGLE_CONTACT:
+                return "FILTER_TYPE_SINGLE_CONTACT";
+            case FILTER_TYPE_ACCOUNT:
+                return "FILTER_TYPE_ACCOUNT";
+            case FILTER_TYPE_GROUP:
+                return "FILTER_TYPE_GROUP";
+            default:
+                return "(unknown)";
+        }
+    }
 }
diff --git a/src/com/android/contacts/list/ContactListFilterController.java b/src/com/android/contacts/list/ContactListFilterController.java
index ead3a60..bf52188 100644
--- a/src/com/android/contacts/list/ContactListFilterController.java
+++ b/src/com/android/contacts/list/ContactListFilterController.java
@@ -15,7 +15,9 @@
  */
 package com.android.contacts.list;
 
-import android.app.Activity;
+import com.android.contacts.model.AccountTypeManager;
+import com.android.contacts.model.AccountWithDataSet;
+
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.preference.PreferenceManager;
@@ -24,48 +26,77 @@
 import java.util.List;
 
 /**
- * Stores the {@link ContactListFilter} selected by the user and saves it to
- * {@link SharedPreferences} if necessary.
+ * Manages {@link ContactListFilter}. All methods must be called from UI thread.
  */
-public class ContactListFilterController {
+public abstract class ContactListFilterController {
+
+    public static final String CONTACT_LIST_FILTER_SERVICE = "contactListFilter";
 
     public interface ContactListFilterListener {
         void onContactListFilterChanged();
     }
 
-    private Context mContext;
-    private List<ContactListFilterListener> mListeners = new ArrayList<ContactListFilterListener>();
-    private ContactListFilter mFilter;
-
-    private boolean mIsInitialized;
-
-    public ContactListFilterController(Activity activity) {
-        mContext = activity;
+    public static ContactListFilterController getInstance(Context context) {
+        return (ContactListFilterController)
+                context.getApplicationContext().getSystemService(CONTACT_LIST_FILTER_SERVICE);
     }
 
+    public static ContactListFilterController
+            createContactListFilterController(Context context) {
+        return new ContactListFilterControllerImpl(context);
+    }
+
+    public abstract void addListener(ContactListFilterListener listener);
+
+    public abstract void removeListener(ContactListFilterListener listener);
+
+    public abstract ContactListFilter getFilter();
+
     /**
-     * @param forceFilterReload when true filter is reloaded even when there's already a cache
-     * for it.
+     * @param filter the filter
+     * @param persistent True when the given filter should be saved soon. False when the filter
+     * should not be saved. The latter case may happen when some Intent requires a certain type of
+     * UI (e.g. single contact) temporarily.
      */
-    public void onStart(boolean forceFilterReload) {
-        if (mFilter == null || forceFilterReload) {
-            mFilter = ContactListFilter.restoreDefaultPreferences(getSharedPreferences());
-        }
-        mIsInitialized = true;
+    public abstract void setContactListFilter(ContactListFilter filter, boolean persistent);
+
+    public abstract void selectCustomFilter();
+
+    /**
+     * Checks if the current filter is valid and reset the filter if not. It may happen when
+     * an account is removed while the filter points to the account with
+     * {@link ContactListFilter#FILTER_TYPE_ACCOUNT} type, for example.
+     */
+    public abstract void checkFilterValidity();
+}
+
+/**
+ * Stores the {@link ContactListFilter} selected by the user and saves it to
+ * {@link SharedPreferences} if necessary.
+ */
+class ContactListFilterControllerImpl extends ContactListFilterController {
+    private final Context mContext;
+    private final List<ContactListFilterListener> mListeners =
+            new ArrayList<ContactListFilterListener>();
+    private ContactListFilter mFilter;
+
+    public ContactListFilterControllerImpl(Context context) {
+        mContext = context;
+        mFilter = ContactListFilter.restoreDefaultPreferences(getSharedPreferences());
+        checkFilterValidity();
     }
 
-    public boolean isInitialized() {
-        return mIsInitialized;
-    }
-
+    @Override
     public void addListener(ContactListFilterListener listener) {
         mListeners.add(listener);
     }
 
+    @Override
     public void removeListener(ContactListFilterListener listener) {
         mListeners.remove(listener);
     }
 
+    @Override
     public ContactListFilter getFilter() {
         return mFilter;
     }
@@ -74,18 +105,20 @@
         return PreferenceManager.getDefaultSharedPreferences(mContext);
     }
 
+    @Override
     public void setContactListFilter(ContactListFilter filter, boolean persistent) {
         if (!filter.equals(mFilter)) {
             mFilter = filter;
             if (persistent) {
                 ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter);
             }
-            if (mListeners != null) {
-               notifyContactListFilterChanged();
+            if (!mListeners.isEmpty()) {
+                notifyContactListFilterChanged();
             }
         }
     }
 
+    @Override
     public void selectCustomFilter() {
         setContactListFilter(ContactListFilter.createFilterWithType(
                 ContactListFilter.FILTER_TYPE_CUSTOM), true);
@@ -97,4 +130,27 @@
         }
     }
 
+    @Override
+    public void checkFilterValidity() {
+        if (mFilter == null || mFilter.filterType != ContactListFilter.FILTER_TYPE_ACCOUNT) {
+            return;
+        }
+
+        if (!filterAccountExists()) {
+            // The current account filter points to invalid account. Use "all" filter instead.
+            setContactListFilter(
+                    ContactListFilter.createFilterWithType(
+                            ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), true);
+        }
+    }
+
+    /**
+     * @return true if the Account for the current filter exists.
+     */
+    private boolean filterAccountExists() {
+        final AccountTypeManager accountTypeManager = AccountTypeManager.getInstance(mContext);
+        final AccountWithDataSet filterAccount = new AccountWithDataSet(
+                mFilter.accountName, mFilter.accountType, mFilter.dataSet);
+        return accountTypeManager.contains(filterAccount, false);
+    }
 }
diff --git a/src/com/android/contacts/list/PhoneFavoriteFragment.java b/src/com/android/contacts/list/PhoneFavoriteFragment.java
index 5e53eb8..17247a4 100644
--- a/src/com/android/contacts/list/PhoneFavoriteFragment.java
+++ b/src/com/android/contacts/list/PhoneFavoriteFragment.java
@@ -27,11 +27,9 @@
 import android.content.CursorLoader;
 import android.content.Intent;
 import android.content.Loader;
-import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.preference.PreferenceManager;
 import android.provider.ContactsContract.Directory;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -193,7 +191,6 @@
      */
     private boolean mAllContactsForceReload;
 
-    private SharedPreferences mPrefs;
     private ContactsPreferences mContactsPrefs;
     private ContactListFilter mFilter;
 
@@ -231,7 +228,6 @@
     public void onAttach(Activity activity) {
         super.onAttach(activity);
 
-        mPrefs = PreferenceManager.getDefaultSharedPreferences(activity);
         mContactsPrefs = new ContactsPreferences(activity);
     }
 
@@ -316,12 +312,6 @@
     }
 
     @Override
-    public void onDetach() {
-        super.onDetach();
-        mPrefs = null;
-    }
-
-    @Override
     public void onStart() {
         super.onStart();
 
@@ -448,10 +438,6 @@
         }
 
         mFilter = filter;
-        if (mPrefs != null) {
-            // Save the preference now.
-            ContactListFilter.storeToPreferences(mPrefs, mFilter);
-        }
 
         if (mAllContactsAdapter != null) {
             mAllContactsAdapter.setFilter(mFilter);
diff --git a/src/com/android/contacts/list/PhoneNumberPickerFragment.java b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
index dd53b4b..64d5f13 100644
--- a/src/com/android/contacts/list/PhoneNumberPickerFragment.java
+++ b/src/com/android/contacts/list/PhoneNumberPickerFragment.java
@@ -20,10 +20,8 @@
 
 import android.app.Activity;
 import android.content.Intent;
-import android.content.SharedPreferences;
 import android.net.Uri;
 import android.os.Bundle;
-import android.preference.PreferenceManager;
 import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.MenuItem;
@@ -42,7 +40,6 @@
     private OnPhoneNumberPickerActionListener mListener;
     private String mShortcutAction;
 
-    private SharedPreferences mPrefs;
     private ContactListFilter mFilter;
 
     private TextView mAccountFilterHeaderView;
@@ -142,18 +139,6 @@
     }
 
     @Override
-    public void onAttach(Activity activity) {
-        super.onAttach(activity);
-        mPrefs = PreferenceManager.getDefaultSharedPreferences(activity);
-    }
-
-    @Override
-    public void onDetach() {
-        super.onDetach();
-        mPrefs = null;
-    }
-
-    @Override
     public void restoreSavedState(Bundle savedState) {
         super.restoreSavedState(savedState);
 
@@ -283,13 +268,6 @@
         }
 
         mFilter = filter;
-        if (mPrefs != null) {
-            // Save the preference now.
-            ContactListFilter.storeToPreferences(mPrefs, mFilter);
-        }
-
-        // This method can be called before {@link #onStart} where we start the loader.  In that
-        // case we shouldn't start the loader yet, as we haven't done all initialization yet.
         if (mLoaderStarted) {
             reloadData();
         }
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index 6a438c6..cb4e9f5 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -17,6 +17,7 @@
 package com.android.contacts.model;
 
 import com.android.contacts.ContactsUtils;
+import com.android.contacts.list.ContactListFilterController;
 import com.android.contacts.util.Constants;
 import com.android.i18n.phonenumbers.PhoneNumberUtil;
 import com.android.internal.util.Objects;
@@ -43,6 +44,7 @@
 import android.os.AsyncTask;
 import android.os.Handler;
 import android.os.HandlerThread;
+import android.os.Looper;
 import android.os.Message;
 import android.os.RemoteException;
 import android.os.SystemClock;
@@ -137,12 +139,25 @@
         return type == null ? null : type.getKindForMimetype(mimeType);
     }
 
-    /*
+    /**
      * Returns all registered {@link AccountType}s, including extension ones.
      *
      * @param contactWritableOnly if true, it only returns ones that support writing contacts.
      */
     public abstract List<AccountType> getAccountTypes(boolean contactWritableOnly);
+
+    /**
+     * @param contactWritableOnly if true, it only returns ones that support writing contacts.
+     * @return true when this instance contains the given account.
+     */
+    public boolean contains(AccountWithDataSet account, boolean contactWritableOnly) {
+        for (AccountWithDataSet account_2 : getAccounts(false)) {
+            if (account.equals(account_2)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
 
 class AccountTypeManagerImpl extends AccountTypeManager
@@ -193,6 +208,14 @@
     private HandlerThread mListenerThread;
     private Handler mListenerHandler;
 
+    private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
+    private final Runnable mCheckFilterValidityRunnable = new Runnable () {
+        @Override
+        public void run() {
+            ContactListFilterController.getInstance(mContext).checkFilterValidity();
+        }
+    };
+
     private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
 
         @Override
@@ -498,6 +521,10 @@
         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
             Log.d(Constants.PERFORMANCE_TAG, "AccountTypeManager.loadAccountsInBackground finish");
         }
+
+        // Check filter validity since filter may become obsolete after account update. It must be
+        // done from UI thread.
+        mMainThreadHandler.post(mCheckFilterValidityRunnable);
     }
 
     // Bookkeeping method for tracking the known account types in the given maps.