Home | History | Annotate | Download | only in contact
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.messaging.ui.contact;
     17 
     18 import android.database.Cursor;
     19 import android.os.Bundle;
     20 import android.provider.ContactsContract.Contacts;
     21 import android.text.TextUtils;
     22 import android.widget.SectionIndexer;
     23 
     24 import com.android.messaging.util.Assert;
     25 import com.android.messaging.util.ContactUtil;
     26 import com.android.messaging.util.LogUtil;
     27 
     28 import java.util.ArrayList;
     29 
     30 /**
     31  * Indexes contact alphabetical sections so we can report to the fast scrolling list view
     32  * where we are in the list when the user scrolls through the contact list, allowing us to show
     33  * alphabetical indicators for the fast scroller as well as list section headers.
     34  */
     35 public class ContactSectionIndexer implements SectionIndexer {
     36     private String[] mSections;
     37     private ArrayList<Integer> mSectionStartingPositions;
     38     private static final String BLANK_HEADER_STRING = " ";
     39 
     40     public ContactSectionIndexer(final Cursor contactsCursor) {
     41         buildIndexer(contactsCursor);
     42     }
     43 
     44     @Override
     45     public Object[] getSections() {
     46         return mSections;
     47     }
     48 
     49     @Override
     50     public int getPositionForSection(final int sectionIndex) {
     51         if (mSectionStartingPositions.isEmpty()) {
     52             return 0;
     53         }
     54         // Clamp to the bounds of the section position array per Android API doc.
     55         return mSectionStartingPositions.get(
     56                 Math.max(Math.min(sectionIndex, mSectionStartingPositions.size() - 1), 0));
     57     }
     58 
     59     @Override
     60     public int getSectionForPosition(final int position) {
     61         if (mSectionStartingPositions.isEmpty()) {
     62             return 0;
     63         }
     64 
     65         // Perform a binary search on the starting positions of the sections to the find the
     66         // section for the position.
     67         int left = 0;
     68         int right = mSectionStartingPositions.size() - 1;
     69 
     70         // According to getSectionForPosition()'s doc, we should always clamp the value when the
     71         // position is out of bound.
     72         if (position <= mSectionStartingPositions.get(left)) {
     73             return left;
     74         } else if (position >= mSectionStartingPositions.get(right)) {
     75             return right;
     76         }
     77 
     78         while (left <= right) {
     79             final int mid = (left + right) / 2;
     80             final int startingPos = mSectionStartingPositions.get(mid);
     81             final int nextStartingPos = mSectionStartingPositions.get(mid + 1);
     82             if (position >= startingPos && position < nextStartingPos) {
     83                 return mid;
     84             } else if (position < startingPos) {
     85                 right = mid - 1;
     86             } else if (position >= nextStartingPos) {
     87                 left = mid + 1;
     88             }
     89         }
     90         Assert.fail("Invalid section indexer state: couldn't find section for pos " + position);
     91         return -1;
     92     }
     93 
     94     private boolean buildIndexerFromCursorExtras(final Cursor cursor) {
     95         if (cursor == null) {
     96             return false;
     97         }
     98         final Bundle cursorExtras = cursor.getExtras();
     99         if (cursorExtras == null) {
    100             return false;
    101         }
    102         final String[] sections = cursorExtras.getStringArray(
    103                 Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
    104         final int[] counts = cursorExtras.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
    105         if (sections == null || counts == null) {
    106             return false;
    107         }
    108 
    109         if (sections.length != counts.length) {
    110             return false;
    111         }
    112 
    113         this.mSections = sections;
    114         mSectionStartingPositions = new ArrayList<Integer>(counts.length);
    115         int position = 0;
    116         for (int i = 0; i < counts.length; i++) {
    117             if (TextUtils.isEmpty(mSections[i])) {
    118                 mSections[i] = BLANK_HEADER_STRING;
    119             } else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
    120                 mSections[i] = mSections[i].trim();
    121             }
    122 
    123             mSectionStartingPositions.add(position);
    124             position += counts[i];
    125         }
    126         return true;
    127     }
    128 
    129     private void buildIndexerFromDisplayNames(final Cursor cursor) {
    130         // Loop through the contact cursor and get the starting position for each first character.
    131         // The result is stored into two arrays, one for the section header (i.e. the first
    132         // character), and one for the starting position, which is guaranteed to be sorted in
    133         // ascending order.
    134         final ArrayList<String> sections = new ArrayList<String>();
    135         mSectionStartingPositions = new ArrayList<Integer>();
    136         if (cursor != null) {
    137             cursor.moveToPosition(-1);
    138             int currentPosition = 0;
    139             while (cursor.moveToNext()) {
    140                 // The sort key is typically the contact's display name, so for example, a contact
    141                 // named "Bob" will go into section "B". The Contacts provider generally uses a
    142                 // a slightly more sophisticated heuristic, but as a fallback this is good enough.
    143                 final String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
    144                 final String section = TextUtils.isEmpty(sortKey) ? BLANK_HEADER_STRING :
    145                     sortKey.substring(0, 1).toUpperCase();
    146 
    147                 final int lastIndex = sections.size() - 1;
    148                 final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null;
    149                 if (!TextUtils.equals(currentSection, section)) {
    150                     sections.add(section);
    151                     mSectionStartingPositions.add(currentPosition);
    152                 }
    153                 currentPosition++;
    154             }
    155         }
    156         mSections = new String[sections.size()];
    157         sections.toArray(mSections);
    158     }
    159 
    160     private void buildIndexer(final Cursor cursor) {
    161         // First check if we get indexer label extras from the contact provider; if not, fall back
    162         // to building from display names.
    163         if (!buildIndexerFromCursorExtras(cursor)) {
    164             LogUtil.w(LogUtil.BUGLE_TAG, "contact provider didn't provide contact label " +
    165                     "information, fall back to using display name!");
    166             buildIndexerFromDisplayNames(cursor);
    167         }
    168     }
    169 }
    170