Fixing the contacts indexer for UNICODE and last-name-first sorting.

This change may temporarily adversely affect the indexing in Japan.
If needed, I will take care of that in a separate CL.

Bug: 2407129
Change-Id: I2d96dab771243f68646edc49f0200d02e8c28bd9
diff --git a/src/com/android/contacts/ContactsListActivity.java b/src/com/android/contacts/ContactsListActivity.java
index 91a2781..a9c8b04 100644
--- a/src/com/android/contacts/ContactsListActivity.java
+++ b/src/com/android/contacts/ContactsListActivity.java
@@ -67,6 +67,7 @@
 import android.provider.Contacts.People;
 import android.provider.Contacts.PeopleColumns;
 import android.provider.Contacts.Phones;
+import android.provider.ContactsContract.ContactCounts;
 import android.provider.ContactsContract.Contacts;
 import android.provider.ContactsContract.Data;
 import android.provider.ContactsContract.Intents;
@@ -99,7 +100,6 @@
 import android.view.inputmethod.InputMethodManager;
 import android.widget.AbsListView;
 import android.widget.AdapterView;
-import android.widget.AlphabetIndexer;
 import android.widget.ArrayAdapter;
 import android.widget.Filter;
 import android.widget.ImageButton;
@@ -118,7 +118,6 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Locale;
 import java.util.Random;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -190,6 +189,9 @@
 
     public static final String AUTHORITIES_FILTER_KEY = "authorities";
 
+    private static final Uri CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS =
+            buildSectionIndexerUri(Contacts.CONTENT_URI);
+
     /** Mask for picker mode */
     static final int MODE_MASK_PICKER = 0x80000000;
     /** Mask for no presence mode */
@@ -736,12 +738,6 @@
         list.setFocusable(true);
         list.setOnCreateContextMenuListener(this);
 
-        if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
-            // Add the header for creating a new contact
-            View header = inflater.inflate(R.layout.create_new_contact, list, false);
-            list.addHeaderView(header);
-        }
-
         // Set the proper empty string
         setEmptyText();
 
@@ -1557,22 +1553,20 @@
 
             startActivity(intent);
             finish();
-        } else if (id != -1) {
-            // Subtract one if we have Create Contact at the top
-            if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
-                position--;
-            }
+        } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
+                && position == 0) {
+            Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI);
+            startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
+        } else if (mMode == MODE_JOIN_CONTACT && id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
+            mJoinModeShowAllContacts = false;
+            startQuery();
+        } else if (id > 0) {
             final Uri uri = getSelectedUri(position);
             if ((mMode & MODE_MASK_PICKER) == 0) {
                 final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                 startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
             } else if (mMode == MODE_JOIN_CONTACT) {
-                if (id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
-                    mJoinModeShowAllContacts = false;
-                    startQuery();
-                } else {
-                    returnPickerResult(null, null, uri);
-                }
+                returnPickerResult(null, null, uri);
             } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
                 // Started with query that should launch to view contact
                 final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
@@ -1591,10 +1585,6 @@
                     || mMode == MODE_LEGACY_PICK_PHONE) {
                 returnPickerResult(null, null, uri);
             }
-        } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
-                && position == 0) {
-            Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI);
-            startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
         } else {
             signalError();
         }
@@ -1810,17 +1800,19 @@
         }
     }
 
-    Uri getUriToQuery() {
+    private Uri getUriToQuery() {
         switch(mMode) {
             case MODE_JOIN_CONTACT:
                 return getJoinSuggestionsUri(null);
             case MODE_FREQUENT:
             case MODE_STARRED:
+                return Contacts.CONTENT_URI;
+
             case MODE_DEFAULT:
             case MODE_INSERT_OR_EDIT_CONTACT:
             case MODE_PICK_CONTACT:
             case MODE_PICK_OR_CREATE_CONTACT:{
-                return Contacts.CONTENT_URI;
+                return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
             }
             case MODE_STREQUENT: {
                 return Contacts.CONTENT_STREQUENT_URI;
@@ -1830,13 +1822,13 @@
                 return People.CONTENT_URI;
             }
             case MODE_PICK_PHONE: {
-                return Phone.CONTENT_URI;
+                return buildSectionIndexerUri(Phone.CONTENT_URI);
             }
             case MODE_LEGACY_PICK_PHONE: {
                 return Phones.CONTENT_URI;
             }
             case MODE_PICK_POSTAL: {
-                return StructuredPostal.CONTENT_URI;
+                return buildSectionIndexerUri(StructuredPostal.CONTENT_URI);
             }
             case MODE_LEGACY_PICK_POSTAL: {
                 return ContactMethods.CONTENT_URI;
@@ -1847,7 +1839,7 @@
                 } else if (mQueryMode == QUERY_MODE_TEL) {
                     return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(mQueryData));
                 }
-                return Contacts.CONTENT_URI;
+                return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
             }
             case MODE_QUERY:
             case MODE_QUERY_PICK: {
@@ -2012,9 +2004,10 @@
 
     private Uri getContactFilterUri(String filter) {
         if (!TextUtils.isEmpty(filter)) {
-            return Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
+            return buildSectionIndexerUri(
+                    Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter)));
         } else {
-            return Contacts.CONTENT_URI;
+            return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
         }
     }
 
@@ -2026,6 +2019,11 @@
         }
     }
 
+    private static Uri buildSectionIndexerUri(Uri uri) {
+        return uri.buildUpon()
+                .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
+    }
+
     private Uri getJoinSuggestionsUri(String filter) {
         Builder builder = Contacts.CONTENT_URI.buildUpon();
         builder.appendEncodedPath(String.valueOf(mQueryAggregateId));
@@ -2445,16 +2443,13 @@
     private final class ContactItemListAdapter extends ResourceCursorAdapter
             implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
         private SectionIndexer mIndexer;
-        private String mAlphabet;
         private boolean mLoading = true;
         private CharSequence mUnknownNameText;
         private boolean mDisplayPhotos = false;
         private boolean mDisplayCallButton = false;
         private boolean mDisplayAdditionalData = true;
-        private HashSet<ImageView> mItemsMissingImages = null;
         private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
         private boolean mDisplaySectionHeaders = true;
-        private int[] mSectionPositions;
         private Cursor mSuggestionsCursor;
         private int mSuggestionsCursorCount;
 
@@ -2463,8 +2458,7 @@
         public ContactItemListAdapter(Context context) {
             super(context, R.layout.contacts_list_item, null, false);
 
-            mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
-
+            mHandler = new ImageFetchHandler();
             mUnknownNameText = context.getText(android.R.string.unknownName);
             switch (mMode) {
                 case MODE_LEGACY_PICK_POSTAL:
@@ -2517,15 +2511,6 @@
             mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
         }
 
-        private SectionIndexer getNewIndexer(Cursor cursor) {
-            if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) {
-                return new JapaneseContactListIndexer(cursor,
-                        SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX);
-            } else {
-                return new AlphabetIndexer(cursor, getSummaryDisplayNameColumnIndex(), mAlphabet);
-            }
-        }
-
         /**
          * Callback on the UI thread when the content observer on the backing cursor fires.
          * Instead of calling requery we need to do an async query so that the requery doesn't
@@ -2566,7 +2551,9 @@
 
         @Override
         public int getItemViewType(int position) {
-            if (position == 0 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
+            if (position == 0
+                    && ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0
+                            || (mMode & MODE_MASK_CREATE_NEW) != 0)) {
                 return IGNORE_ITEM_VIEW_TYPE;
             }
             if (isShowAllContactsItemPosition(position)) {
@@ -2591,18 +2578,21 @@
                 return getTotalContactCountView(parent);
             }
 
+            if (position == 0 && (mMode & MODE_MASK_CREATE_NEW) != 0) {
+                // Add the header for creating a new contact
+                return getLayoutInflater().inflate(R.layout.create_new_contact, parent, false);
+            }
+
             if (isShowAllContactsItemPosition(position)) {
-                LayoutInflater inflater =
-                    (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-                return inflater.inflate(R.layout.contacts_list_show_all_item, parent, false);
+                return getLayoutInflater().
+                        inflate(R.layout.contacts_list_show_all_item, parent, false);
             }
 
             // Handle the separator specially
             int separatorId = getSeparatorId(position);
             if (separatorId != 0) {
-                LayoutInflater inflater =
-                        (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-                TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
+                TextView view = (TextView) getLayoutInflater().
+                        inflate(R.layout.list_separator, parent, false);
                 view.setText(separatorId);
                 return view;
             }
@@ -2900,7 +2890,7 @@
             } else {
                 final int section = getSectionForPosition(position);
                 if (getPositionForSection(section) == position) {
-                    String title = mIndexer.getSections()[section].toString().trim();
+                    String title = (String)mIndexer.getSections()[section];
                     if (!TextUtils.isEmpty(title)) {
                         cache.headerText.setText(title);
                         cache.header.setVisibility(View.VISIBLE);
@@ -2948,30 +2938,19 @@
         }
 
         private void updateIndexer(Cursor cursor) {
-            if (mIndexer == null) {
-                mIndexer = getNewIndexer(cursor);
-            } else {
-                if (Locale.getDefault().equals(Locale.JAPAN)) {
-                    if (mIndexer instanceof JapaneseContactListIndexer) {
-                        ((JapaneseContactListIndexer)mIndexer).setCursor(cursor);
-                    } else {
-                        mIndexer = getNewIndexer(cursor);
-                    }
-                } else {
-                    if (mIndexer instanceof AlphabetIndexer) {
-                        ((AlphabetIndexer)mIndexer).setCursor(cursor);
-                    } else {
-                        mIndexer = getNewIndexer(cursor);
-                    }
-                }
+            if (cursor == null) {
+                mIndexer = null;
+                return;
             }
 
-            int sectionCount = mIndexer.getSections().length;
-            if (mSectionPositions == null || mSectionPositions.length != sectionCount) {
-                mSectionPositions = new int[sectionCount];
-            }
-            for (int i = 0; i < sectionCount; i++) {
-                mSectionPositions[i] = ListView.INVALID_POSITION;
+            Bundle bundle = cursor.getExtras();
+            if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
+                String sections[] =
+                    bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
+                int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
+                mIndexer = new ContactsSectionIndexer(sections, counts);
+            } else {
+                mIndexer = null;
             }
         }
 
@@ -2985,7 +2964,7 @@
         }
 
         public Object [] getSections() {
-            if (mMode == MODE_STARRED) {
+            if (mIndexer == null) {
                 return new String[] { " " };
             } else {
                 return mIndexer.getSections();
@@ -2993,58 +2972,19 @@
         }
 
         public int getPositionForSection(int sectionIndex) {
-            if (mMode == MODE_STARRED) {
-                return -1;
-            }
-
-            if (sectionIndex < 0 || sectionIndex >= mSectionPositions.length) {
-                return -1;
-            }
-
             if (mIndexer == null) {
-                Cursor cursor = mAdapter.getCursor();
-                if (cursor == null) {
-                    // No cursor, the section doesn't exist so just return 0
-                    return 0;
-                }
-                mIndexer = getNewIndexer(cursor);
+                return -1;
             }
 
-            int position = mSectionPositions[sectionIndex];
-            if (position == ListView.INVALID_POSITION) {
-                position = mSectionPositions[sectionIndex] =
-                        mIndexer.getPositionForSection(sectionIndex);
-            }
-
-            return position;
+            return mIndexer.getPositionForSection(sectionIndex);
         }
 
         public int getSectionForPosition(int position) {
-            // The current implementations of SectionIndexers (specifically the Japanese indexer)
-            // only work in one direction: given a section they can calculate the position.
-            // Here we are using that existing functionality to do the reverse mapping. We are
-            // performing binary search in the mSectionPositions array, which itself is populated
-            // lazily using the "forward" mapping supported by the indexer.
-
-            int start = 0;
-            int end = mSectionPositions.length;
-            while (start != end) {
-
-                // We are making the binary search slightly asymmetrical, because the
-                // user is more likely to be scrolling the list from the top down.
-                int pivot = start + (end - start) / 4;
-
-                int value = getPositionForSection(pivot);
-                if (value <= position) {
-                    start = pivot + 1;
-                } else {
-                    end = pivot;
-                }
+            if (mIndexer == null) {
+                return -1;
             }
 
-            // The variable "start" cannot be 0, as long as the indexer is implemented properly
-            // and actually maps position = 0 to section = 0
-            return start - 1;
+            return mIndexer.getSectionForPosition(position);
         }
 
         @Override
@@ -3104,7 +3044,10 @@
             if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
                 pos--;
             }
-            if (mSuggestionsCursorCount != 0) {
+
+            if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
+                return pos - 1;
+            } else if (mSuggestionsCursorCount != 0) {
                 // When showing suggestions, we have 2 additional list items: the "Suggestions"
                 // and "All contacts" separators.
                 if (pos < mSuggestionsCursorCount + 2) {
@@ -3217,7 +3160,7 @@
             int realPosition = getRealPosition(position);
             int section = getSectionForPosition(realPosition);
 
-            String title = mIndexer.getSections()[section].toString().trim();
+            String title = (String)mIndexer.getSections()[section];
             cache.titleView.setText(title);
 
             if (alpha == 255) {
diff --git a/src/com/android/contacts/ContactsSectionIndexer.java b/src/com/android/contacts/ContactsSectionIndexer.java
new file mode 100644
index 0000000..01d461f
--- /dev/null
+++ b/src/com/android/contacts/ContactsSectionIndexer.java
@@ -0,0 +1,96 @@
+/*
+ * 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;
+
+import android.widget.SectionIndexer;
+
+import java.util.Arrays;
+
+/**
+ * A section indexer that is configured with precomputed section titles and
+ * their respective counts.
+ */
+public class ContactsSectionIndexer implements SectionIndexer {
+
+    private final String[] mSections;
+    private final int[] mPositions;
+    private final int mCount;
+
+    /**
+     * Constructor.
+     *
+     * @param sections a non-null array
+     * @param counts a non-null array of the same size as <code>sections</code>
+     */
+    public ContactsSectionIndexer(String[] sections, int[] counts) {
+        if (sections == null || counts == null) {
+            throw new NullPointerException();
+        }
+
+        if (sections.length != counts.length) {
+            throw new IllegalArgumentException(
+                    "The sections and counts arrays must have the same length");
+        }
+
+        // TODO process sections/counts based on current locale and/or specific section titles
+
+        this.mSections = sections;
+        mPositions = new int[counts.length];
+        int position = 0;
+        for (int i = 0; i < counts.length; i++) {
+            if (mSections[i] == null) {
+                mSections[i] = " ";
+            } else {
+                mSections[i] = mSections[i].trim();
+            }
+
+            mPositions[i] = position;
+            position += counts[i];
+        }
+        mCount = position;
+    }
+
+    public Object[] getSections() {
+        return mSections;
+    }
+
+    public int getPositionForSection(int section) {
+        if (section < 0 || section >= mSections.length) {
+            return -1;
+        }
+
+        return mPositions[section];
+    }
+
+    public int getSectionForPosition(int position) {
+        if (position < 0 || position >= mCount) {
+            return -1;
+        }
+
+        int index = Arrays.binarySearch(mPositions, position);
+
+        /*
+         * Consider this example: section positions are 0, 3, 5; the supplied
+         * position is 4. The section corresponding to position 4 starts at
+         * position 3, so the expected return value is 1. Binary search will not
+         * find 4 in the array and thus will return -insertPosition-1, i.e. -3.
+         * To get from that number to the expected value of 1 we need to negate
+         * and subtract 2.
+         */
+        return index >= 0 ? index : -index - 2;
+    }
+}