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