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 
ContactSectionIndexer(final Cursor contactsCursor)40     public ContactSectionIndexer(final Cursor contactsCursor) {
41         buildIndexer(contactsCursor);
42     }
43 
44     @Override
getSections()45     public Object[] getSections() {
46         return mSections;
47     }
48 
49     @Override
getPositionForSection(final int sectionIndex)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
getSectionForPosition(final int position)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 
buildIndexerFromCursorExtras(final Cursor cursor)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 
buildIndexerFromDisplayNames(final Cursor cursor)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 
buildIndexer(final Cursor cursor)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