Implementing new selection policies in Contacts

1. Selection is now persistent per filter
2. If there is no selection or selection not in the list,
   display the first item on the list
3. After creating a new contact or per user request
   select the newly created contact in the list.  If
   it is not in the list, change filter to "Contact"
   and display that contact by itself.

Change-Id: I9343fe9d25c86c5d041954d2386f66da2a1bc41f
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 5cff078..da4856c 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1361,6 +1361,9 @@
     <!-- Contact list filter selection indicating that the list shows all contacts with phone numbers [CHAR LIMIT=64] -->
     <string name="list_filter_phones">Contacts with phone numbers</string>
 
+    <!-- Contact list filter selection indicating that the list shows only the selected contact [CHAR LIMIT=64] -->
+    <string name="list_filter_single">Contact</string>
+
     <!-- Title of the activity that allows the user to customize filtering of contact list [CHAR LIMIT=128] -->
     <string name="custom_list_filter">Define custom view</string>
 
diff --git a/src/com/android/contacts/activities/ContactBrowserActivity.java b/src/com/android/contacts/activities/ContactBrowserActivity.java
index ce484f0..054c50f 100644
--- a/src/com/android/contacts/activities/ContactBrowserActivity.java
+++ b/src/com/android/contacts/activities/ContactBrowserActivity.java
@@ -27,6 +27,7 @@
 import com.android.contacts.list.ContactListFilterController;
 import com.android.contacts.list.ContactsIntentResolver;
 import com.android.contacts.list.ContactsRequest;
+import com.android.contacts.list.CustomContactListFilterActivity;
 import com.android.contacts.list.DefaultContactBrowseListFragment;
 import com.android.contacts.list.DirectoryListLoader;
 import com.android.contacts.list.OnContactBrowserActionListener;
@@ -48,7 +49,6 @@
 import android.content.ContentValues;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.SharedPreferences.Editor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -57,7 +57,6 @@
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Intents;
 import android.provider.Settings;
-import android.text.TextUtils;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.Menu;
@@ -80,15 +79,15 @@
  */
 public class ContactBrowserActivity extends Activity
         implements View.OnCreateContextMenuListener, ActionBarAdapter.Listener,
-        DialogManager.DialogShowingViewActivity {
+        DialogManager.DialogShowingViewActivity,
+        ContactListFilterController.ContactListFilterListener {
 
     private static final String TAG = "ContactBrowserActivity";
 
     private static final int SUBACTIVITY_NEW_CONTACT = 2;
     private static final int SUBACTIVITY_SETTINGS = 3;
     private static final int SUBACTIVITY_EDIT_CONTACT = 4;
-
-    private static final String KEY_DEFAULT_CONTACT_URI = "defaultSelectedContactUri";
+    private static final int SUBACTIVITY_CUSTOMIZE_FILTER = 5;
 
     private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
 
@@ -129,6 +128,7 @@
     public ContactBrowserActivity() {
         mIntentResolver = new ContactsIntentResolver(this);
         mContactListFilterController = new ContactListFilterController(this);
+        mContactListFilterController.addListener(this);
     }
 
     @Override
@@ -136,9 +136,11 @@
         if (fragment instanceof ContactBrowseListFragment) {
             mListFragment = (ContactBrowseListFragment)fragment;
             mListFragment.setOnContactListActionListener(new ContactBrowserActionListener());
-            if (mListFragment instanceof DefaultContactBrowseListFragment) {
-                ((DefaultContactBrowseListFragment) mListFragment).setContactListFilterController(
-                        mContactListFilterController);
+            if (mListFragment instanceof DefaultContactBrowseListFragment
+                    && mContactListFilterController != null
+                    && mContactListFilterController.isLoaded()) {
+                ((DefaultContactBrowseListFragment) mListFragment).setFilter(
+                        mContactListFilterController.getFilter());
             }
         } else if (fragment instanceof ContactNoneFragment) {
             mEmptyFragment = (ContactNoneFragment)fragment;
@@ -216,7 +218,9 @@
     protected void onNewIntent(Intent intent) {
         super.onNewIntent(intent);
         if (Intent.ACTION_VIEW.equals(intent.getAction())) {
-            Uri uri = intent.getData();
+            mRequest = mIntentResolver.resolveIntent(getIntent());
+
+            Uri uri = mRequest.getContactUri();
             if (uri == null) {
                 return;
             }
@@ -227,27 +231,20 @@
                     mActionBarAdapter.setSearchMode(false);
                 }
             }
-            setSelectedContactUri(uri);
-            setupContactDetailFragment(uri);
+            mListFragment.setSelectedContactUri(uri);
             mListFragment.requestSelectionOnScreen(true);
+            if (mContactContentDisplayed) {
+                setupContactDetailFragment(uri);
+            }
         }
     }
 
-    public void setSelectedContactUri(Uri contactLookupUri) {
-        mListFragment.setSelectedContactUri(contactLookupUri);
-
-        Editor editor = mPrefs.edit();
-        if (contactLookupUri == null) {
-            editor.remove(KEY_DEFAULT_CONTACT_URI);
-        } else {
-            editor.putString(KEY_DEFAULT_CONTACT_URI, contactLookupUri.toString());
+    @Override
+    protected void onStart() {
+        if (mContactListFilterController != null) {
+            mContactListFilterController.startLoading();
         }
-        editor.apply();
-    }
-
-    public Uri getDefaultSelectedContactUri() {
-        String uriString = mPrefs.getString(KEY_DEFAULT_CONTACT_URI, null);
-        return TextUtils.isEmpty(uriString) ? null : Uri.parse(uriString);
+        super.onStart();
     }
 
     private void configureListFragment(boolean fromRequest) {
@@ -315,23 +312,14 @@
             }
         }
 
-        Uri selectUri = null;
         if (fromRequest) {
-            selectUri = mRequest.getContactUri();
+            Uri selectUri = mRequest.getContactUri();
             if (selectUri != null) {
-                setSelectedContactUri(selectUri);
+                mListFragment.setSelectedContactUri(selectUri);
+                mListFragment.requestSelectionOnScreen(false);
             }
         }
 
-        if (selectUri == null && mListFragment.getSelectedContactUri() == null) {
-            selectUri = getDefaultSelectedContactUri();
-        }
-
-        if (selectUri != null) {
-            mListFragment.setSelectedContactUri(selectUri);
-            mListFragment.requestSelectionOnScreen(false);
-        }
-
         if (replaceList) {
             getFragmentManager().openTransaction()
                     .replace(R.id.list_container, mListFragment)
@@ -357,6 +345,48 @@
         }
     }
 
+    @Override
+    public void onContactListFiltersLoaded() {
+        if (mListFragment instanceof DefaultContactBrowseListFragment) {
+            DefaultContactBrowseListFragment fragment =
+                    (DefaultContactBrowseListFragment) mListFragment;
+            restoreListSelection(fragment);
+
+            // Filters have been loaded - now we can start loading the list itself
+            fragment.startLoading();
+        }
+    }
+
+    @Override
+    public void onContactListFilterChanged() {
+        // If there was a request to show a specific contact, that's no longer the case
+        // because the user has explicitly changed the filter.
+        mRequest.setContactUri(null);
+
+        if (mListFragment instanceof DefaultContactBrowseListFragment) {
+            DefaultContactBrowseListFragment fragment =
+                    (DefaultContactBrowseListFragment) mListFragment;
+            restoreListSelection(fragment);
+
+            fragment.reloadData();
+        }
+    }
+
+    private void restoreListSelection(DefaultContactBrowseListFragment fragment) {
+        fragment.setFilter(mContactListFilterController.getFilter());
+        fragment.restoreSelectedUri(mPrefs);
+        fragment.requestSelectionOnScreen(false);
+        if (mContactContentDisplayed) {
+            setupContactDetailFragment(fragment.getSelectedContactUri());
+        }
+    }
+
+    @Override
+    public void onContactListFilterCustomizationRequest() {
+        startActivityForResult(new Intent(this, CustomContactListFilterActivity.class),
+                SUBACTIVITY_CUSTOMIZE_FILTER);
+    }
+
     private void setupContactDetailFragment(final Uri contactLookupUri) {
         if (mDetailFragment != null && contactLookupUri != null
                 && contactLookupUri.equals(mDetailFragment.getUri())) {
@@ -508,7 +538,8 @@
         @Override
         public void onViewContactAction(Uri contactLookupUri) {
             if (mContactContentDisplayed) {
-                setSelectedContactUri(contactLookupUri);
+                mListFragment.setSelectedContactUri(contactLookupUri);
+                mListFragment.saveSelectedUri(mPrefs);
                 setupContactDetailFragment(contactLookupUri);
             } else {
                 startActivity(new Intent(Intent.ACTION_VIEW, contactLookupUri));
@@ -568,6 +599,37 @@
         public void onFinishAction() {
             onBackPressed();
         }
+
+        @Override
+        public void onInvalidSelection() {
+            Uri requestedContactUri = mRequest.getContactUri();
+            if (requestedContactUri != null
+                    && mListFragment instanceof DefaultContactBrowseListFragment) {
+                // If a specific selection was requested, adjust the filter so
+                // that the requested selection is uncoditionally visible.
+                DefaultContactBrowseListFragment fragment =
+                        (DefaultContactBrowseListFragment) mListFragment;
+                ContactListFilter filter = new ContactListFilter(
+                        ContactListFilter.FILTER_TYPE_SINGLE_CONTACT);
+                fragment.setFilter(filter);
+                fragment.setSelectedContactUri(requestedContactUri);
+                fragment.saveSelectedUri(mPrefs);
+                fragment.reloadData();
+                if (mContactListFilterController != null) {
+                    mContactListFilterController.setContactListFilter(filter, true);
+                }
+            } else {
+                // Otherwise, choose the first contact on the list and select it
+                requestedContactUri = mListFragment.getFirstContactUri();
+                if (requestedContactUri != null) {
+                    mListFragment.setSelectedContactUri(requestedContactUri);
+                    mListFragment.requestSelectionOnScreen(false);
+                }
+            }
+            if (mContactContentDisplayed) {
+                setupContactDetailFragment(requestedContactUri);
+            }
+        }
     }
 
     private class DetailFragmentListener implements ContactDetailFragment.Listener {
@@ -768,6 +830,12 @@
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         switch (requestCode) {
+            case SUBACTIVITY_CUSTOMIZE_FILTER: {
+                if (resultCode == Activity.RESULT_OK) {
+                    mContactListFilterController.selectCustomFilter();
+                }
+                break;
+            }
             case SUBACTIVITY_EDIT_CONTACT: {
                 mListFragment.requestSelectionOnScreen(true);
                 break;
@@ -776,8 +844,15 @@
             case SUBACTIVITY_NEW_CONTACT: {
                 if (resultCode == RESULT_OK && mContactContentDisplayed) {
                     final Uri newContactUri = data.getData();
-                    setSelectedContactUri(newContactUri);
-                    setupContactDetailFragment(newContactUri);
+                    if (mContactContentDisplayed) {
+                        setupContactDetailFragment(newContactUri);
+                    }
+
+                    mRequest.setActionCode(ContactsRequest.ACTION_VIEW_CONTACT);
+                    mRequest.setContactUri(newContactUri);
+                    mListFragment.setSelectedContactUri(newContactUri);
+                    mListFragment.saveSelectedUri(mPrefs);
+                    mListFragment.requestSelectionOnScreen(true);
                 }
                 break;
             }
diff --git a/src/com/android/contacts/activities/ContactSearchActivity.java b/src/com/android/contacts/activities/ContactSearchActivity.java
index 0c8d141..615f182 100644
--- a/src/com/android/contacts/activities/ContactSearchActivity.java
+++ b/src/com/android/contacts/activities/ContactSearchActivity.java
@@ -159,6 +159,10 @@
         public void onFinishAction() {
             onBackPressed();
         }
+
+        @Override
+        public void onInvalidSelection() {
+        }
     }
 
     private PhoneNumberInteraction getPhoneNumberCallInteraction() {
diff --git a/src/com/android/contacts/activities/ContactSelectionActivity.java b/src/com/android/contacts/activities/ContactSelectionActivity.java
index cfe9710..290358a 100644
--- a/src/com/android/contacts/activities/ContactSelectionActivity.java
+++ b/src/com/android/contacts/activities/ContactSelectionActivity.java
@@ -21,9 +21,7 @@
 import com.android.contacts.list.ContactPickerFragment;
 import com.android.contacts.list.ContactsIntentResolver;
 import com.android.contacts.list.ContactsRequest;
-import com.android.contacts.list.DefaultContactBrowseListFragment;
 import com.android.contacts.list.DirectoryListLoader;
-import com.android.contacts.list.OnContactBrowserActionListener;
 import com.android.contacts.list.OnContactPickerActionListener;
 import com.android.contacts.list.OnPhoneNumberPickerActionListener;
 import com.android.contacts.list.OnPostalAddressPickerActionListener;
@@ -59,8 +57,6 @@
 
     private int mActionCode = -1;
 
-    private boolean mSearchInitiated;
-
     private ContactsRequest mRequest;
     private SearchView mSearchView;
 
@@ -255,10 +251,7 @@
     }
 
     public void setupActionListener() {
-        if (mListFragment instanceof DefaultContactBrowseListFragment) {
-            ((DefaultContactBrowseListFragment) mListFragment).setOnContactListActionListener(
-                    new ContactBrowserActionListener());
-        } else if (mListFragment instanceof ContactPickerFragment) {
+        if (mListFragment instanceof ContactPickerFragment) {
             ((ContactPickerFragment) mListFragment).setOnContactPickerActionListener(
                     new ContactPickerActionListener());
         } else if (mListFragment instanceof PhoneNumberPickerFragment) {
@@ -272,63 +265,6 @@
         }
     }
 
-    private final class ContactBrowserActionListener implements OnContactBrowserActionListener {
-        @Override
-        public void onViewContactAction(Uri contactLookupUri) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onCreateNewContactAction() {
-            Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
-            Bundle extras = getIntent().getExtras();
-            if (extras != null) {
-                intent.putExtras(extras);
-            }
-            startActivity(intent);
-        }
-
-        @Override
-        public void onEditContactAction(Uri contactLookupUri) {
-            Intent intent = new Intent(Intent.ACTION_EDIT, contactLookupUri);
-            Bundle extras = getIntent().getExtras();
-            if (extras != null) {
-                intent.putExtras(extras);
-            }
-            startActivity(intent);
-        }
-
-        @Override
-        public void onAddToFavoritesAction(Uri contactUri) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onRemoveFromFavoritesAction(Uri contactUri) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onCallContactAction(Uri contactUri) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onSmsContactAction(Uri contactUri) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onDeleteContactAction(Uri contactUri) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void onFinishAction() {
-            onBackPressed();
-        }
-    }
-
     private final class ContactPickerActionListener implements OnContactPickerActionListener {
         @Override
         public void onCreateNewContactAction() {
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
index f99134b..4c94c75 100644
--- a/src/com/android/contacts/list/ContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -21,6 +21,7 @@
 import android.app.LoaderManager.LoaderCallbacks;
 import android.content.CursorLoader;
 import android.content.Loader;
+import android.content.SharedPreferences;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
@@ -38,6 +39,7 @@
         ContactEntryListFragment<ContactListAdapter> {
 
     private static final String KEY_SELECTED_URI = "selectedUri";
+    private static final String KEY_SELECTION_VERIFIED = "selectionVerified";
 
     private static final int SELECTED_ID_LOADER = -3;
 
@@ -49,8 +51,10 @@
     private long mSelectedContactDirectoryId;
     private String mSelectedContactLookupKey;
     private int mSelectionVisibilityRequest;
+    private boolean mSelectionVerified;
+    private boolean mLoadingLookupKey;
 
-    private OnContactBrowserActionListener mListener;
+    protected OnContactBrowserActionListener mListener;
 
     private LoaderCallbacks<Cursor> mIdLoaderCallbacks = new LoaderCallbacks<Cursor>() {
 
@@ -66,6 +70,7 @@
 
         @Override
         public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+            mLoadingLookupKey = false;
             String lookupKey = null;
             if (data != null) {
                 if (data.moveToFirst()) {
@@ -76,7 +81,6 @@
                 mSelectedContactLookupKey = lookupKey;
                 configureContactSelection();
             }
-            return;
         }
     };
 
@@ -89,6 +93,7 @@
         }
 
         mSelectedContactUri = savedState.getParcelable(KEY_SELECTED_URI);
+        mSelectionVerified = savedState.getBoolean(KEY_SELECTION_VERIFIED);
         parseSelectedContactUri();
     }
 
@@ -96,6 +101,7 @@
     public void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
         outState.putParcelable(KEY_SELECTED_URI, mSelectedContactUri);
+        outState.putBoolean(KEY_SELECTION_VERIFIED, mSelectionVerified);
     }
 
     @Override
@@ -109,6 +115,7 @@
         if (isSelectionVisible() && mSelectedContactUri != null &&
                 (mSelectedContactDirectoryId == Directory.DEFAULT ||
                         mSelectedContactDirectoryId == Directory.LOCAL_INVISIBLE)) {
+            mLoadingLookupKey = true;
             getLoaderManager().restartLoader(SELECTED_ID_LOADER, null, mIdLoaderCallbacks);
         } else {
             getLoaderManager().stopLoader(SELECTED_ID_LOADER);
@@ -165,7 +172,7 @@
             }
 
             String directoryParam =
-                mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+                    mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
             mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam)
                     ? Directory.DEFAULT
                     : Long.parseLong(directoryParam);
@@ -193,6 +200,48 @@
         }
 
         adapter.setSelectedContact(mSelectedContactDirectoryId, mSelectedContactLookupKey);
+        checkSelection();
+    }
+
+    @Override
+    protected void onPartitionLoaded(int partitionIndex, Cursor data) {
+        super.onPartitionLoaded(partitionIndex, data);
+        checkSelection();
+    }
+
+    private void checkSelection() {
+        if (mSelectionVerified || isSearchMode()) {
+            return;
+        }
+
+        ContactListAdapter adapter = getAdapter();
+        if (adapter.isLoading() || mLoadingLookupKey) {
+            return;
+        }
+
+        if (adapter.hasValidSelection()) {
+            mSelectionVerified = true;
+            return;
+        }
+
+        notifyInvalidSelection();
+    }
+
+    public Uri getFirstContactUri() {
+        ContactListAdapter adapter = getAdapter();
+        return adapter.getFirstContactUri();
+    }
+
+    @Override
+    protected void startLoading() {
+        mSelectionVerified = false;
+        super.startLoading();
+    }
+
+    @Override
+    public void reloadData() {
+        mSelectionVerified = false;
+        super.reloadData();
     }
 
     public void setOnContactListActionListener(OnContactBrowserActionListener listener) {
@@ -231,6 +280,10 @@
         mListener.onSmsContactAction(contactUri);
     }
 
+    private void notifyInvalidSelection() {
+        mListener.onInvalidSelection();
+    }
+
     @Override
     protected void finish() {
         super.finish();
@@ -269,4 +322,10 @@
                     listView, position + listView.getHeaderViewsCount(), smooth);
         }
     }
+
+    public void saveSelectedUri(SharedPreferences preferences) {
+    }
+
+    public void restoreSelectedUri(SharedPreferences preferences) {
+    }
 }
diff --git a/src/com/android/contacts/list/ContactListAdapter.java b/src/com/android/contacts/list/ContactListAdapter.java
index 7e2c4c2..08b3e48 100644
--- a/src/com/android/contacts/list/ContactListAdapter.java
+++ b/src/com/android/contacts/list/ContactListAdapter.java
@@ -30,7 +30,6 @@
 import android.widget.ListView;
 import android.widget.QuickContactBadge;
 
-import java.util.List;
 
 /**
  * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type.
@@ -105,7 +104,6 @@
     private String mSelectedContactLookupKey;
 
     private ContactListFilter mFilter;
-    private List<ContactListFilter> mAllFilters;
 
     public ContactListAdapter(Context context) {
         super(context);
@@ -118,22 +116,14 @@
     }
 
     /**
-     * Returns a full set of all available list filters.
-     */
-    public List<ContactListFilter> getAllFilters() {
-        return mAllFilters;
-    }
-
-    /**
      * Returns the currently selected filter.
      */
     public ContactListFilter getFilter() {
         return mFilter;
     }
 
-    public void setFilter(ContactListFilter filter, List<ContactListFilter> allFilters) {
+    public void setFilter(ContactListFilter filter) {
         mFilter = filter;
-        mAllFilters = allFilters;
     }
 
     public long getSelectedContactDirectoryId() {
@@ -326,4 +316,31 @@
         }
         return position;
     }
+
+    public boolean hasValidSelection() {
+        return getSelectedContactPosition() != -1;
+    }
+
+    public Uri getFirstContactUri() {
+        int partitionCount = getPartitionCount();
+        for (int i = 0; i < partitionCount; i++) {
+            DirectoryPartition partition = (DirectoryPartition) getPartition(i);
+            if (partition.isLoading()) {
+                continue;
+            }
+
+            Cursor cursor = getCursor(i);
+            if (cursor == null) {
+                continue;
+            }
+
+            if (!cursor.moveToFirst()) {
+                continue;
+            }
+
+            return getContactUri(i, cursor);
+        }
+
+        return null;
+    }
 }
diff --git a/src/com/android/contacts/list/ContactListFilter.java b/src/com/android/contacts/list/ContactListFilter.java
index 0189bbb..2b212fd 100644
--- a/src/com/android/contacts/list/ContactListFilter.java
+++ b/src/com/android/contacts/list/ContactListFilter.java
@@ -30,6 +30,7 @@
     public static final int FILTER_TYPE_CUSTOM = -3;
     public static final int FILTER_TYPE_STARRED = -4;
     public static final int FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY = -5;
+    public static final int FILTER_TYPE_SINGLE_CONTACT = -6;
 
     public static final int FILTER_TYPE_ACCOUNT = 0;
     public static final int FILTER_TYPE_GROUP = 1;
@@ -38,13 +39,16 @@
     private static final String KEY_ACCOUNT_NAME = "filter.accountName";
     private static final String KEY_ACCOUNT_TYPE = "filter.accountType";
     private static final String KEY_GROUP_ID = "filter.groupId";
+    private static final String KEY_GROUP_SOURCE_ID = "filter.groupSourceId";
 
     public int filterType;
     public String accountType;
     public String accountName;
     public Drawable icon;
     public long groupId;
+    public String groupSourceId;
     public String title;
+    private String mId;
 
     public ContactListFilter(int filterType) {
         this.filterType = filterType;
@@ -60,11 +64,12 @@
     }
 
     public ContactListFilter(
-            String accountType, String accountName, long groupId, String title) {
+            String accountType, String accountName, long groupId, String groupSourceId, String title) {
         this.filterType = ContactListFilter.FILTER_TYPE_GROUP;
         this.accountType = accountType;
         this.accountName = accountName;
         this.groupId = groupId;
+        this.groupSourceId = groupSourceId;
         this.title = title;
     }
 
@@ -115,7 +120,9 @@
             code = code * 31 + accountType.hashCode();
             code = code * 31 + accountName.hashCode();
         }
-        if (groupId != 0) {
+        if (groupSourceId != null) {
+            code = code * 31 + groupSourceId.hashCode();
+        } else if (groupId != 0) {
             code = code * 31 + (int) groupId;
         }
         return code;
@@ -132,10 +139,17 @@
         }
 
         ContactListFilter otherFilter = (ContactListFilter) other;
-        return filterType == otherFilter.filterType
-                && TextUtils.equals(accountName, otherFilter.accountName)
-                && TextUtils.equals(accountType, otherFilter.accountType)
-                && groupId == otherFilter.groupId;
+        if (filterType != otherFilter.filterType
+                || !TextUtils.equals(accountName, otherFilter.accountName)
+                || !TextUtils.equals(accountType, otherFilter.accountType)) {
+            return false;
+        }
+
+        if (groupSourceId != null && otherFilter.groupSourceId != null) {
+            return groupSourceId.equals(otherFilter.groupSourceId);
+        }
+
+        return groupId == otherFilter.groupId;
     }
 
     public static void storeToPreferences(SharedPreferences prefs, ContactListFilter filter) {
@@ -144,6 +158,7 @@
             .putString(KEY_ACCOUNT_NAME, filter == null ? null : filter.accountName)
             .putString(KEY_ACCOUNT_TYPE, filter == null ? null : filter.accountType)
             .putLong(KEY_GROUP_ID, filter == null ? -1 : filter.groupId)
+            .putString(KEY_GROUP_SOURCE_ID, filter == null ? null : filter.groupSourceId)
             .apply();
     }
 
@@ -157,6 +172,30 @@
         filter.accountName = prefs.getString(KEY_ACCOUNT_NAME, null);
         filter.accountType = prefs.getString(KEY_ACCOUNT_TYPE, null);
         filter.groupId = prefs.getLong(KEY_GROUP_ID, -1);
+        filter.groupSourceId = prefs.getString(KEY_GROUP_SOURCE_ID, null);
         return filter;
     }
+
+    /**
+     * Returns a string that can be used as a stable persistent identifier for this filter.
+     */
+    public String getId() {
+        if (mId == null) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(filterType);
+            if (accountType != null) {
+                sb.append('-').append(accountType);
+            }
+            if (accountName != null) {
+                sb.append('-').append(accountName.replace('-', '_'));
+            }
+            if (groupSourceId != null) {
+                sb.append('-').append(groupSourceId);
+            } else if (groupId != 0) {
+                sb.append('-').append(groupId);
+            }
+            mId = sb.toString();
+        }
+        return mId;
+    }
 }
diff --git a/src/com/android/contacts/list/ContactListFilterController.java b/src/com/android/contacts/list/ContactListFilterController.java
index c342d75..4b890cf 100644
--- a/src/com/android/contacts/list/ContactListFilterController.java
+++ b/src/com/android/contacts/list/ContactListFilterController.java
@@ -249,7 +249,9 @@
     public void setContactListFilter(ContactListFilter filter, boolean persistent) {
         if (!filter.equals(mFilter)) {
             mFilter = filter;
-            ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter);
+            if (persistent) {
+                ContactListFilter.storeToPreferences(getSharedPreferences(), mFilter);
+            }
             if (mListeners != null) {
                notifyContactListFilterChanged();
             }
diff --git a/src/com/android/contacts/list/ContactListFilterLoader.java b/src/com/android/contacts/list/ContactListFilterLoader.java
index ae791ab..663a6a8 100644
--- a/src/com/android/contacts/list/ContactListFilterLoader.java
+++ b/src/com/android/contacts/list/ContactListFilterLoader.java
@@ -16,8 +16,8 @@
 
 package com.android.contacts.list;
 
-import com.android.contacts.model.BaseAccountType;
 import com.android.contacts.model.AccountTypes;
+import com.android.contacts.model.BaseAccountType;
 
 import android.accounts.Account;
 import android.content.AsyncTaskLoader;
@@ -29,7 +29,6 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 
 /**
@@ -44,6 +43,7 @@
             Groups.ACCOUNT_NAME,
             Groups.TITLE,
             Groups.AUTO_ADD,
+            Groups.SOURCE_ID,
         };
 
         public static final int ID = 0;
@@ -51,6 +51,7 @@
         public static final int ACCOUNT_NAME = 2;
         public static final int TITLE = 3;
         public static final int IS_DEFAULT_GROUP = 4;       // Using the AUTO_ADD group as default
+        public static final int SOURCE_ID = 5;
 
         private static final String SELECTION =
                 Groups.DELETED + "=0 AND " + Groups.FAVORITES + "=0";
@@ -74,7 +75,6 @@
             results.add(new ContactListFilter(account.type, account.name, icon, account.name));
         }
 
-        HashSet<Account> accountsWithGroups = new HashSet<Account>();
         ContentResolver resolver = context.getContentResolver();
 
         Cursor cursor = resolver.query(
@@ -82,6 +82,7 @@
         try {
             while (cursor.moveToNext()) {
                 long groupId = cursor.getLong(GroupQuery.ID);
+                String groupSourceId = cursor.getString(GroupQuery.SOURCE_ID);
                 String accountType = cursor.getString(GroupQuery.ACCOUNT_TYPE);
                 String accountName = cursor.getString(GroupQuery.ACCOUNT_NAME);
                 boolean defaultGroup = false;
@@ -94,12 +95,14 @@
                         if (filter.accountName.equals(accountName)
                                 && filter.accountType.equals(accountType)) {
                             filter.groupId = groupId;
+                            filter.groupSourceId = groupSourceId;
                             break;
                         }
                     }
                 } else {
                     String title = cursor.getString(GroupQuery.TITLE);
-                    results.add(new ContactListFilter(accountType, accountName, groupId, title));
+                    results.add(new ContactListFilter(
+                            accountType, accountName, groupId, groupSourceId, title));
                 }
             }
         } finally {
diff --git a/src/com/android/contacts/list/ContactListFilterView.java b/src/com/android/contacts/list/ContactListFilterView.java
index e065e91..cb9ec06 100644
--- a/src/com/android/contacts/list/ContactListFilterView.java
+++ b/src/com/android/contacts/list/ContactListFilterView.java
@@ -105,6 +105,10 @@
                 bindView(0, R.string.list_filter_phones);
                 break;
             }
+            case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: {
+                bindView(0, R.string.list_filter_single);
+                break;
+            }
             case ContactListFilter.FILTER_TYPE_ACCOUNT: {
                 if (mIcon != null) {
                     mIcon.setVisibility(View.VISIBLE);
diff --git a/src/com/android/contacts/list/ContactPickerFragment.java b/src/com/android/contacts/list/ContactPickerFragment.java
index 0937608..a0c29e1 100644
--- a/src/com/android/contacts/list/ContactPickerFragment.java
+++ b/src/com/android/contacts/list/ContactPickerFragment.java
@@ -152,7 +152,7 @@
         if (!isLegacyCompatibilityMode()) {
             DefaultContactListAdapter adapter = new DefaultContactListAdapter(getActivity());
             adapter.setFilter(
-                    new ContactListFilter(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS), null);
+                    new ContactListFilter(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS));
             adapter.setSectionHeaderDisplayEnabled(true);
             adapter.setDisplayPhotos(true);
             adapter.setQuickContactEnabled(false);
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 651228b..7729e28 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -17,9 +17,10 @@
 
 import com.android.contacts.R;
 
-import android.app.Activity;
-import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
 import android.database.Cursor;
+import android.net.Uri;
 import android.os.Bundle;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -28,22 +29,23 @@
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
+
 /**
  * Fragment containing a contact list used for browsing (as compared to
  * picking a contact with one of the PICK intents).
  */
-public class DefaultContactBrowseListFragment extends ContactBrowseListFragment
-        implements ContactListFilterController.ContactListFilterListener {
+public class DefaultContactBrowseListFragment extends ContactBrowseListFragment {
 
     private static final String KEY_FILTER_ENABLED = "filterEnabled";
 
-    private static final int REQUEST_CODE_CUSTOMIZE_FILTER = 3;
+    private static final String PERSISTENT_SELECTION_PREFIX = "defaultContactBrowserSelection";
 
     private View mCounterHeaderView;
     private View mSearchHeaderView;
 
     private boolean mFilterEnabled;
-    private ContactListFilterController mFilterController;
+    private ContactListFilter mFilter;
+    private String mPersistentSelectionPrefix = PERSISTENT_SELECTION_PREFIX;
 
     public DefaultContactBrowseListFragment() {
         setPhotoLoaderEnabled(true);
@@ -51,17 +53,8 @@
         setAizyEnabled(true);
     }
 
-    public void setContactListFilterController(ContactListFilterController filterController) {
-        mFilterController = filterController;
-        mFilterController.addListener(this);
-    }
-
-    @Override
-    public void onDetach() {
-        if (mFilterController != null) {
-            mFilterController.removeListener(this);
-        }
-        super.onDetach();
+    public void setFilter(ContactListFilter filter) {
+        mFilter = filter;
     }
 
     @Override
@@ -99,8 +92,8 @@
         super.configureAdapter();
 
         DefaultContactListAdapter adapter = (DefaultContactListAdapter)getAdapter();
-        if (adapter != null && mFilterEnabled && mFilterController != null) {
-            adapter.setFilter(mFilterController.getFilter(), mFilterController.getFilterList());
+        if (adapter != null && mFilter != null) {
+            adapter.setFilter(mFilter);
         }
     }
 
@@ -189,47 +182,47 @@
     }
 
     @Override
-    protected void startLoading() {
-        if (mFilterController != null && !mFilterController.isLoaded()) {
-            mFilterController.startLoading();
-        }
-
-        if (!mFilterEnabled || mFilterController == null || mFilterController.isLoaded()) {
+    public void startLoading() {
+        if (!mFilterEnabled || mFilter != null) {
             super.startLoading();
         }
     }
 
     @Override
-    protected void onPartitionLoaded(int partitionIndex, Cursor data) {
-        super.onPartitionLoaded(partitionIndex, data);
-        if (mFilterController != null) {
-            mFilterController.postDelayedRefresh();
+    public void saveSelectedUri(SharedPreferences preferences) {
+        if (isSearchMode()) {
+            return;
+        }
+
+        Editor editor = preferences.edit();
+        Uri uri = getSelectedContactUri();
+        if (uri == null) {
+            editor.remove(getPersistentSelectionKey());
+        } else {
+            editor.putString(getPersistentSelectionKey(), uri.toString());
+        }
+        editor.apply();
+    }
+
+    @Override
+    public void restoreSelectedUri(SharedPreferences preferences) {
+        if (isSearchMode()) {
+            return;
+        }
+
+        String selectedUri = preferences.getString(getPersistentSelectionKey(), null);
+        if (selectedUri == null) {
+            setSelectedContactUri(null);
+        } else {
+            setSelectedContactUri(Uri.parse(selectedUri));
         }
     }
 
-    @Override
-    public void onContactListFiltersLoaded() {
-        if (mFilterEnabled) {
-            // Filters have been loaded - now we can start loading the list itself
-            startLoading();
-        }
-    }
-
-    @Override
-    public void onContactListFilterChanged() {
-        reloadData();
-    }
-
-    @Override
-    public void onContactListFilterCustomizationRequest() {
-        startActivityForResult(new Intent(getContext(), CustomContactListFilterActivity.class),
-                REQUEST_CODE_CUSTOMIZE_FILTER);
-    }
-
-    @Override
-    public void onActivityResult(int requestCode, int resultCode, Intent data) {
-        if (requestCode == REQUEST_CODE_CUSTOMIZE_FILTER && resultCode == Activity.RESULT_OK) {
-            mFilterController.selectCustomFilter();
+    private String getPersistentSelectionKey() {
+        if (mFilter == null) {
+            return mPersistentSelectionPrefix;
+        } else {
+            return mPersistentSelectionPrefix + "-" + mFilter.getId();
         }
     }
 }
diff --git a/src/com/android/contacts/list/DefaultContactListAdapter.java b/src/com/android/contacts/list/DefaultContactListAdapter.java
index ede3597..577e5bb 100644
--- a/src/com/android/contacts/list/DefaultContactListAdapter.java
+++ b/src/com/android/contacts/list/DefaultContactListAdapter.java
@@ -17,6 +17,7 @@
 
 import com.android.contacts.preference.ContactsPreferences;
 
+import android.content.ContentUris;
 import android.content.Context;
 import android.content.CursorLoader;
 import android.content.SharedPreferences;
@@ -82,11 +83,20 @@
     }
 
     protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) {
-        Uri uri;
-        if (filter != null && filter.groupId != 0) {
-            uri = Data.CONTENT_URI;
-        } else {
-            uri = Contacts.CONTENT_URI;
+        Uri uri = Contacts.CONTENT_URI;
+        if (filter != null) {
+            if (filter.filterType == ContactListFilter.FILTER_TYPE_GROUP ||
+                    filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) {
+                uri = Data.CONTENT_URI;
+            } else if (filter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
+                String lookupKey = getSelectedContactLookupKey();
+                if (lookupKey != null) {
+                    uri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, lookupKey);
+                } else {
+                    // Non-existent contact
+                    uri = ContentUris.withAppendedId(Contacts.CONTENT_URI, 0);
+                }
+            }
         }
 
         if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) {
@@ -94,7 +104,9 @@
         }
 
         // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT
-        if (filter != null && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM) {
+        if (filter != null
+                && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM
+                && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) {
             uri = uri.buildUpon().appendQueryParameter(
                     ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
                     .build();
@@ -130,6 +142,11 @@
                 // filter
                 break;
             }
+            case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: {
+                // We have already added the lookup key to the URI, which takes care of this
+                // filter
+                break;
+            }
             case ContactListFilter.FILTER_TYPE_STARRED: {
                 selection.append(Contacts.STARRED + "!=0");
                 break;
diff --git a/src/com/android/contacts/list/OnContactBrowserActionListener.java b/src/com/android/contacts/list/OnContactBrowserActionListener.java
index 239f8e1..80d4838 100644
--- a/src/com/android/contacts/list/OnContactBrowserActionListener.java
+++ b/src/com/android/contacts/list/OnContactBrowserActionListener.java
@@ -68,4 +68,9 @@
      * Closes the contact browser.
      */
     void onFinishAction();
+
+    /**
+     * Invoked if the requested selected contact is not found in the list.
+     */
+    void onInvalidSelection();
 }