Automatically selecting first found contact.

Change-Id: I232f37d1b5256c315d514a2c8dee9e9eeca5dcb7
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index dbdb1f0..89cace9 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -48,12 +48,9 @@
     private static final String EXTRA_KEY_QUERY = "navBar.query";
 
     private static final String KEY_MODE_DEFAULT = "mode_default";
-    private static final String KEY_MODE_SEARCH = "mode_search";
 
     private boolean mSearchMode;
     private String mQueryString;
-    private Bundle mSavedStateForSearchMode;
-    private Bundle mSavedStateForDefaultMode;
 
     private View mNavigationBar;
     private TextView mSearchLabel;
@@ -76,8 +73,6 @@
         if (savedState != null) {
             mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
             mQueryString = savedState.getString(EXTRA_KEY_QUERY);
-            mSavedStateForDefaultMode = savedState.getParcelable(KEY_MODE_DEFAULT);
-            mSavedStateForSearchMode = savedState.getParcelable(KEY_MODE_SEARCH);
         } else {
             mSearchMode = request.isSearchMode();
             mQueryString = request.getQueryString();
@@ -113,9 +108,21 @@
 
     @Override
     public void onFocusChange(View v, boolean hasFocus) {
-        if (v == mSearchView) {
-            setSearchMode(hasFocus);
+        if (v != mSearchView) {
+            return;
         }
+
+        // When we switch search mode on/off, the activity may need to change
+        // fragments, which may lead to focus temporarily leaving the search
+        // view or coming back to it, which could lead to an infinite loop.
+        // Postponing the change breaks that loop.
+        mNavigationBar.post(new Runnable() {
+
+            @Override
+            public void run() {
+                setSearchMode(mSearchView.hasFocus());
+            }
+        });
     }
 
     public boolean isSearchMode() {
@@ -195,31 +202,9 @@
         return false;
     }
 
-    public Bundle getSavedStateForSearchMode() {
-        return mSavedStateForSearchMode;
-    }
-
-    public void setSavedStateForSearchMode(Bundle state) {
-        mSavedStateForSearchMode = state;
-    }
-
-    public Bundle getSavedStateForDefaultMode() {
-        return mSavedStateForDefaultMode;
-    }
-
-    public void setSavedStateForDefaultMode(Bundle state) {
-        mSavedStateForDefaultMode = state;
-    }
-
     public void onSaveInstanceState(Bundle outState) {
         outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
         outState.putString(EXTRA_KEY_QUERY, mQueryString);
-        if (mSavedStateForDefaultMode != null) {
-            outState.putParcelable(KEY_MODE_DEFAULT, mSavedStateForDefaultMode);
-        }
-        if (mSavedStateForSearchMode != null) {
-            outState.putParcelable(KEY_MODE_SEARCH, mSavedStateForSearchMode);
-        }
     }
 
     @Override
diff --git a/src/com/android/contacts/activities/ContactBrowserActivity.java b/src/com/android/contacts/activities/ContactBrowserActivity.java
index b04d640..aa8b80d 100644
--- a/src/com/android/contacts/activities/ContactBrowserActivity.java
+++ b/src/com/android/contacts/activities/ContactBrowserActivity.java
@@ -51,6 +51,7 @@
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.Message;
 import android.preference.PreferenceManager;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
@@ -90,6 +91,23 @@
 
     private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
 
+    /**
+     * The id for a delayed message that triggers automatic selection of the first
+     * found contact in search mode.
+     */
+    private static final int MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT = 1;
+
+    /**
+     * The delay that is used for automatically selecting the first found contact.
+     */
+    private static final int DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS = 500;
+
+    /**
+     * The minimum number of characters in the search query that is required
+     * before we automatically select the first found contact.
+     */
+    private static final int AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH = 2;
+
     private static final String KEY_SEARCH_MODE = "searchMode";
 
     private DialogManager mDialogManager = new DialogManager(this);
@@ -128,17 +146,26 @@
         mContactListFilterController.addListener(this);
     }
 
+    private Handler getHandler() {
+        if (mHandler == null) {
+            mHandler = new Handler() {
+                @Override
+                public void handleMessage(Message msg) {
+                    if (msg.what == MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT) {
+                        selectFirstFoundContact();
+                    }
+                }
+            };
+        }
+        return mHandler;
+    }
+
     @Override
     public void onAttachFragment(Fragment fragment) {
         if (fragment instanceof ContactBrowseListFragment) {
             mListFragment = (ContactBrowseListFragment)fragment;
             mListFragment.setOnContactListActionListener(new ContactBrowserActionListener());
-            if (mListFragment instanceof DefaultContactBrowseListFragment
-                    && mContactListFilterController != null
-                    && mContactListFilterController.isLoaded()) {
-                ((DefaultContactBrowseListFragment) mListFragment).setFilter(
-                        mContactListFilterController.getFilter());
-            }
+            restoreListSelection();
         } else if (fragment instanceof ContactDetailFragment) {
             mDetailFragment = (ContactDetailFragment)fragment;
             mDetailFragment.setListener(mDetailFragmentListener);
@@ -188,7 +215,6 @@
         if (mHasActionBar) {
             mActionBarAdapter = new ActionBarAdapter(this);
             mActionBarAdapter.onCreate(savedState, mRequest, getActionBar());
-            mActionBarAdapter.setListener(this);
             mActionBarAdapter.setContactListFilterController(mContactListFilterController);
             // TODO: request may ask for FREQUENT - set the filter accordingly
             mAddContactImageView = new ImageView(this);
@@ -221,11 +247,9 @@
             }
 
             if (mHasActionBar) {
-                if (mActionBarAdapter.isSearchMode()) {
-                    mActionBarAdapter.setSavedStateForSearchMode(null);
-                    mActionBarAdapter.setSearchMode(false);
-                }
+                mActionBarAdapter.setSearchMode(false);
             }
+
             mListFragment.setSelectedContactUri(uri);
             mListFragment.requestSelectionOnScreen(true);
             if (mContactContentDisplayed) {
@@ -235,6 +259,22 @@
     }
 
     @Override
+    protected void onPause() {
+        if (mActionBarAdapter != null) {
+            mActionBarAdapter.setListener(null);
+        }
+        super.onPause();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (mActionBarAdapter != null) {
+            mActionBarAdapter.setListener(this);
+        }
+    }
+
+    @Override
     protected void onStart() {
         if (mContactListFilterController != null) {
             mContactListFilterController.startLoading();
@@ -284,28 +324,16 @@
             mSearchMode = searchMode;
             if (mSearchMode) {
                 mListFragment = createContactSearchFragment();
+                // When switching to the search mode, erase previous state of the search UI
+                mListFragment.saveSelectedUri(mPrefs);
             } else {
                 mListFragment = createListFragment(ContactsRequest.ACTION_DEFAULT);
                 mListFragment.requestSelectionOnScreen(false);
             }
         }
 
-        if (mHasActionBar) {
-            if (mSearchMode) {
-                Bundle savedState = mActionBarAdapter.getSavedStateForSearchMode();
-                if (savedState != null) {
-                    mListFragment.restoreSavedState(savedState);
-                    mActionBarAdapter.setSavedStateForSearchMode(null);
-                }
-
-                mListFragment.setQueryString(mActionBarAdapter.getQueryString());
-            } else {
-                Bundle savedState = mActionBarAdapter.getSavedStateForDefaultMode();
-                if (savedState != null) {
-                    mListFragment.restoreSavedState(savedState);
-                    mActionBarAdapter.setSavedStateForDefaultMode(null);
-                }
-            }
+        if (mHasActionBar && mSearchMode) {
+            mListFragment.setQueryString(mActionBarAdapter.getQueryString());
         }
 
         if (fromRequest) {
@@ -320,37 +348,26 @@
             getFragmentManager().openTransaction()
                     .replace(R.id.list_container, mListFragment)
                     .commit();
+
+            if (mContactContentDisplayed) {
+                setupContactDetailFragment(mListFragment.getSelectedContactUri());
+            }
         }
     }
 
     private void closeListFragment() {
         if (mListFragment != null) {
             mListFragment.setOnContactListActionListener(null);
-
-            if (mHasActionBar) {
-                Bundle state = new Bundle();
-                mListFragment.onSaveInstanceState(state);
-                if (mSearchMode) {
-                    mActionBarAdapter.setSavedStateForSearchMode(state);
-                } else {
-                    mActionBarAdapter.setSavedStateForDefaultMode(state);
-                }
-            }
-
             mListFragment = null;
         }
     }
 
     @Override
     public void onContactListFiltersLoaded() {
-        if (mListFragment instanceof DefaultContactBrowseListFragment) {
-            DefaultContactBrowseListFragment fragment =
-                    (DefaultContactBrowseListFragment) mListFragment;
-            restoreListSelection(fragment);
+        restoreListSelection();
 
-            // Filters have been loaded - now we can start loading the list itself
-            fragment.startLoading();
-        }
+        // Filters have been loaded - now we can start loading the list itself
+        mListFragment.startLoading();
     }
 
     @Override
@@ -359,25 +376,35 @@
         // because the user has explicitly changed the filter.
         mRequest.setContactUri(null);
 
-        if (mListFragment instanceof DefaultContactBrowseListFragment) {
-            DefaultContactBrowseListFragment fragment =
-                    (DefaultContactBrowseListFragment) mListFragment;
-            restoreListSelection(fragment);
-
-            fragment.reloadData();
-        }
+        restoreListSelection();
+        mListFragment.reloadData();
     }
 
-    private void restoreListSelection(DefaultContactBrowseListFragment fragment) {
-        fragment.setFilter(mContactListFilterController.getFilter());
-        fragment.restoreSelectedUri(mPrefs);
-        fragment.requestSelectionOnScreen(false);
+    /**
+     * Restores filter-specific persistent selection.
+     */
+    private void restoreListSelection() {
+        if (mListFragment instanceof DefaultContactBrowseListFragment
+                && mContactListFilterController != null
+                && mContactListFilterController.isLoaded()) {
+            DefaultContactBrowseListFragment fragment =
+                    (DefaultContactBrowseListFragment) mListFragment;
+            fragment.setFilter(mContactListFilterController.getFilter());
+            fragment.restoreSelectedUri(mPrefs);
+            fragment.requestSelectionOnScreen(false);
+        }
+
         if (mContactContentDisplayed) {
-            setupContactDetailFragment(fragment.getSelectedContactUri());
+            setupContactDetailFragment(mListFragment.getSelectedContactUri());
         }
     }
 
     private void showDefaultSelection() {
+        if (mSearchMode) {
+            selectFirstFoundContactAfterDelay();
+            return;
+        }
+
         Uri requestedContactUri = mRequest.getContactUri();
         if (requestedContactUri != null
                 && mListFragment instanceof DefaultContactBrowseListFragment) {
@@ -385,8 +412,8 @@
             // that the requested selection is unconditionally visible.
             DefaultContactBrowseListFragment fragment =
                     (DefaultContactBrowseListFragment) mListFragment;
-            ContactListFilter filter = new ContactListFilter(
-                    ContactListFilter.FILTER_TYPE_SINGLE_CONTACT);
+            ContactListFilter filter =
+                    new ContactListFilter(ContactListFilter.FILTER_TYPE_SINGLE_CONTACT);
             fragment.setFilter(filter);
             fragment.setSelectedContactUri(requestedContactUri);
             fragment.saveSelectedUri(mPrefs);
@@ -407,6 +434,39 @@
         }
     }
 
+    /**
+     * Automatically selects the first found contact in search mode.  The selection
+     * is updated after a delay to allow the user to type without to much UI churn
+     * and to save bandwidth on directory queries.
+     */
+    public void selectFirstFoundContactAfterDelay() {
+        Handler handler = getHandler();
+        handler.removeMessages(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT);
+        handler.sendEmptyMessageDelayed(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT,
+                DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS);
+    }
+
+    /**
+     * Selects the first contact in the list in search mode.
+     */
+    protected void selectFirstFoundContact() {
+        if (!mSearchMode) {
+            return;
+        }
+
+        Uri selectedUri = null;
+        String queryString = mListFragment.getQueryString();
+        if (queryString != null
+                && queryString.length() >= AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH) {
+            selectedUri = mListFragment.getFirstContactUri();
+        }
+
+        mListFragment.setSelectedContactUri(selectedUri);
+        if (mContactContentDisplayed) {
+            setupContactDetailFragment(selectedUri);
+        }
+    }
+
     @Override
     public void onContactListFilterCustomizationRequest() {
         startActivityForResult(new Intent(this, CustomContactListFilterActivity.class),
@@ -419,18 +479,14 @@
             return;
         }
 
-        // Already showing? Nothing to do
-        if (mDetailFragment != null) {
-            mDetailFragment.loadUri(contactLookupUri);
-            return;
+        if (mDetailFragment == null) {
+            mDetailFragment = new ContactDetailFragment();
+            getFragmentManager().openTransaction()
+                    .replace(R.id.detail_container, mDetailFragment)
+                    .commit();
         }
 
-        mDetailFragment = new ContactDetailFragment();
         mDetailFragment.loadUri(contactLookupUri);
-
-        getFragmentManager().openTransaction()
-                .replace(R.id.detail_container, mDetailFragment)
-                .commit();
     }
 
     /**
@@ -439,7 +495,6 @@
     @Override
     public void onAction() {
         configureListFragment(false /* from request */);
-        setupContactDetailFragment(mListFragment.getSelectedContactUri());
     }
 
     /**
diff --git a/src/com/android/contacts/list/ContactBrowseListFragment.java b/src/com/android/contacts/list/ContactBrowseListFragment.java
index 5a81f6a..ac4a2a7 100644
--- a/src/com/android/contacts/list/ContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/ContactBrowseListFragment.java
@@ -146,7 +146,7 @@
     }
 
     public void setSelectedContactUri(Uri uri) {
-        if (mSelectedContactUri == null
+        if ((mSelectedContactUri == null && uri != null)
                 || (mSelectedContactUri != null && !mSelectedContactUri.equals(uri))) {
             mSelectedContactUri = uri;
 
@@ -204,21 +204,21 @@
     }
 
     @Override
-    protected void onPartitionLoaded(int partitionIndex, Cursor data) {
-        super.onPartitionLoaded(partitionIndex, data);
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        super.onLoadFinished(loader, data);
         checkSelection();
     }
 
     private void checkSelection() {
-        if (mSelectionVerified || isSearchMode()) {
+        if (mSelectionVerified) {
+            return;
+        }
+
+        if (isLoading()) {
             return;
         }
 
         ContactListAdapter adapter = getAdapter();
-        if (adapter.isLoading() || mLoadingLookupKey) {
-            return;
-        }
-
         if (adapter.hasValidSelection()) {
             mSelectionVerified = true;
             requestSelectionOnScreenIfNeeded();
@@ -228,13 +228,18 @@
         notifyInvalidSelection();
     }
 
+    @Override
+    public boolean isLoading() {
+        return mLoadingLookupKey || super.isLoading();
+    }
+
     public Uri getFirstContactUri() {
         ContactListAdapter adapter = getAdapter();
         return adapter.getFirstContactUri();
     }
 
     @Override
-    protected void startLoading() {
+    public void startLoading() {
         mSelectionVerified = false;
         super.startLoading();
     }
diff --git a/src/com/android/contacts/list/ContactEntryListAdapter.java b/src/com/android/contacts/list/ContactEntryListAdapter.java
index cf03026..41136d4 100644
--- a/src/com/android/contacts/list/ContactEntryListAdapter.java
+++ b/src/com/android/contacts/list/ContactEntryListAdapter.java
@@ -126,7 +126,7 @@
     }
 
     @Override
-    public void clearPartitions() { 
+    public void clearPartitions() {
         int count = getPartitionCount();
         for (int i = 0; i < count; i++) {
             Partition partition = getPartition(i);
diff --git a/src/com/android/contacts/list/ContactEntryListFragment.java b/src/com/android/contacts/list/ContactEntryListFragment.java
index a4f4b72..fe80c3c 100644
--- a/src/com/android/contacts/list/ContactEntryListFragment.java
+++ b/src/com/android/contacts/list/ContactEntryListFragment.java
@@ -135,7 +135,12 @@
     private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
 
     private boolean mForceLoad;
-    private boolean mLoadDirectoryList;
+
+    private static final int STATUS_NOT_LOADED = 0;
+    private static final int STATUS_LOADING = 1;
+    private static final int STATUS_LOADED = 2;
+
+    private int mDirectoryListStatus = STATUS_NOT_LOADED;
 
     /**
      * Indicates whether we are doing the initial complete load of data (false) or
@@ -293,7 +298,7 @@
         }
 
         mForceLoad = false;
-        mLoadDirectoryList = true;
+        mDirectoryListStatus = STATUS_NOT_LOADED;
         mLoadPriorityDirectoriesOnly = true;
 
         startLoading();
@@ -393,6 +398,7 @@
 
         int loaderId = loader.getId();
         if (loaderId == DIRECTORY_LOADER_ID) {
+            mDirectoryListStatus = STATUS_LOADED;
             mAdapter.changeDirectories(data);
             startLoading();
         } else {
@@ -400,8 +406,8 @@
             if (isSearchMode()) {
                 int directorySearchMode = getDirectorySearchMode();
                 if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {
-                    if (mLoadDirectoryList) {
-                        mLoadDirectoryList = false;
+                    if (mDirectoryListStatus == STATUS_NOT_LOADED) {
+                        mDirectoryListStatus = STATUS_LOADING;
                         getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this);
                     } else {
                         startLoading();
@@ -423,8 +429,23 @@
             mAizy.setIndexer(mAdapter.getIndexer(), data.getCount());
         }
 
-        // TODO should probably only restore instance state after all directories are loaded
-        completeRestoreInstanceState();
+        if (!isLoading()) {
+            completeRestoreInstanceState();
+        }
+    }
+
+    public boolean isLoading() {
+        if (mAdapter != null && mAdapter.isLoading()) {
+            return true;
+        }
+
+        if (isSearchMode() && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE
+                && (mDirectoryListStatus == STATUS_NOT_LOADED
+                        || mDirectoryListStatus == STATUS_LOADING)) {
+            return true;
+        }
+
+        return false;
     }
 
     @Override
diff --git a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
index 7729e28..919c740 100644
--- a/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
+++ b/src/com/android/contacts/list/DefaultContactBrowseListFragment.java
@@ -39,6 +39,7 @@
     private static final String KEY_FILTER_ENABLED = "filterEnabled";
 
     private static final String PERSISTENT_SELECTION_PREFIX = "defaultContactBrowserSelection";
+    private static final String KEY_SEARCH_MODE_CONTACT_URI_SUFFIX = "search";
 
     private View mCounterHeaderView;
     private View mSearchHeaderView;
@@ -190,10 +191,6 @@
 
     @Override
     public void saveSelectedUri(SharedPreferences preferences) {
-        if (isSearchMode()) {
-            return;
-        }
-
         Editor editor = preferences.edit();
         Uri uri = getSelectedContactUri();
         if (uri == null) {
@@ -206,10 +203,6 @@
 
     @Override
     public void restoreSelectedUri(SharedPreferences preferences) {
-        if (isSearchMode()) {
-            return;
-        }
-
         String selectedUri = preferences.getString(getPersistentSelectionKey(), null);
         if (selectedUri == null) {
             setSelectedContactUri(null);
@@ -219,7 +212,9 @@
     }
 
     private String getPersistentSelectionKey() {
-        if (mFilter == null) {
+        if (isSearchMode()) {
+            return mPersistentSelectionPrefix + "-" + KEY_SEARCH_MODE_CONTACT_URI_SUFFIX;
+        } else if (mFilter == null) {
             return mPersistentSelectionPrefix;
         } else {
             return mPersistentSelectionPrefix + "-" + mFilter.getId();
diff --git a/src/com/android/contacts/views/detail/ContactDetailFragment.java b/src/com/android/contacts/views/detail/ContactDetailFragment.java
index d5368dc..fc9e78d 100644
--- a/src/com/android/contacts/views/detail/ContactDetailFragment.java
+++ b/src/com/android/contacts/views/detail/ContactDetailFragment.java
@@ -112,6 +112,8 @@
 
     private static final int LOADER_DETAILS = 1;
 
+    private static final String KEY_CONTACT_URI = "contactUri";
+
     private Context mContext;
     private View mView;
     private Uri mLookupUri;
@@ -184,6 +186,20 @@
     }
 
     @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        if (savedInstanceState != null) {
+            mLookupUri = savedInstanceState.getParcelable(KEY_CONTACT_URI);
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putParcelable(KEY_CONTACT_URI, mLookupUri);
+    }
+
+    @Override
     public void onAttach(Activity activity) {
         super.onAttach(activity);
         mContext = activity;