blob: 4e5fb0f8b2ec666337d6cf0047dd1318962bf9f3 [file] [log] [blame]
The Android Open Source Project5dc3b4f2008-10-21 07:00:00 -07001/*
2 * Copyright (C) 2008 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
17package com.android.contacts;
18
19import android.database.Cursor;
20import android.database.DataSetObserver;
21import android.util.SparseIntArray;
22
23/**
24 * This class essentially helps in building an index of section boundaries of a
25 * sorted column of a cursor. For instance, if a cursor contains a data set
26 * sorted by first name of a person or the title of a song, this class will
27 * perform a binary search to identify the first row that begins with a
28 * particular letter. The search is case-insensitive. The class caches the index
29 * such that subsequent queries for the same letter will return right away.
30 */
31public class AlphabetIndexer extends DataSetObserver {
32
33 protected Cursor mDataCursor;
34 protected int mColumnIndex;
35 protected Object[] mAlphabetArray;
36 private SparseIntArray mAlphaMap;
37 private java.text.Collator mCollator;
38
39 /**
40 * Constructs the indexer.
41 * @param cursor the cursor containing the data set
42 * @param columnIndex the column number in the cursor that is sorted
43 * alphabetically
44 * @param sections the array of objects that represent the sections. The
45 * toString() method of each item is called and the first letter of the
46 * String is used as the letter to search for.
47 */
48 public AlphabetIndexer(Cursor cursor, int columnIndex, Object[] sections) {
49 mDataCursor = cursor;
50 mColumnIndex = columnIndex;
51 mAlphabetArray = sections;
52 mAlphaMap = new SparseIntArray(26 /* Optimize for English */);
53 if (cursor != null) {
54 cursor.registerDataSetObserver(this);
55 }
56 // Get a Collator for the current locale for string comparisons.
57 mCollator = java.text.Collator.getInstance();
58 mCollator.setStrength(java.text.Collator.PRIMARY);
59 }
60
61 /**
62 * Sets a new cursor as the data set and resets the cache of indices.
63 * @param cursor the new cursor to use as the data set
64 */
65 public void setCursor(Cursor cursor) {
66 if (mDataCursor != null) {
67 mDataCursor.unregisterDataSetObserver(this);
68 }
69 mDataCursor = cursor;
70 if (cursor != null) {
71 mDataCursor.registerDataSetObserver(this);
72 }
73 mAlphaMap.clear();
74 }
75
76 /**
77 * Performs a binary search or cache lookup to find the first row that
78 * matches a given section's starting letter.
79 * @param sectionIndex the section to search for
80 * @return the row index of the first occurrence, or the nearest next letter.
81 * For instance, if searching for "T" and no "T" is found, then the first
82 * row starting with "U" or any higher letter is returned. If there is no
83 * data following "T" at all, then the list size is returned.
84 */
85 public int indexOf(int sectionIndex) {
86 final SparseIntArray alphaMap = mAlphaMap;
87 final Cursor cursor = mDataCursor;
88
89 if (cursor == null || mAlphabetArray == null) {
90 return 0;
91 }
92
93 // Check bounds
94 if (sectionIndex <= 0) {
95 return 0;
96 }
97 if (sectionIndex >= mAlphabetArray.length) {
98 sectionIndex = mAlphabetArray.length - 1;
99 }
100
101 int savedCursorPos = cursor.getPosition();
102
103 int count = cursor.getCount();
104 int start = 0;
105 int end = count;
106 int pos;
107
108 String letter = mAlphabetArray[sectionIndex].toString();
109 letter = letter.toUpperCase();
110 int key = letter.charAt(0);
111 // Check map
112 if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) {
113 // Is it approximate? Using negative value to indicate that it's
114 // an approximation and positive value when it is the accurate
115 // position.
116 if (pos < 0) {
117 pos = -pos;
118 end = pos;
119 } else {
120 // Not approximate, this is the confirmed start of section, return it
121 return pos;
122 }
123 }
124
125 // Do we have the position of the previous section?
126 if (sectionIndex > 0) {
127 int prevLetter =
128 mAlphabetArray[sectionIndex - 1].toString().charAt(0);
129 int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE);
130 if (prevLetterPos != Integer.MIN_VALUE) {
131 start = Math.abs(prevLetterPos);
132 }
133 }
134
135 // Now that we have a possibly optimized start and end, let's binary search
136
137 pos = (end + start) / 2;
138
139 while (pos < end) {
140 // Get letter at pos
141 cursor.moveToPosition(pos);
142 String curName = cursor.getString(mColumnIndex);
143 if (curName == null) {
144 if (pos == 0) {
145 break;
146 } else {
147 pos--;
148 continue;
149 }
150 }
151 int curLetter = Character.toUpperCase(curName.charAt(0));
152
153 if (curLetter != key) {
154 // Enter approximation in hash if a better solution doesn't exist
155 int curPos = alphaMap.get(curLetter, Integer.MIN_VALUE);
156 if (curPos == Integer.MIN_VALUE || Math.abs(curPos) > pos) {
157 // Negative pos indicates that it is an approximation
158 alphaMap.put(curLetter, -pos);
159 }
160 if (mCollator.compare(curName, letter) < 0) {
161 start = pos + 1;
162 if (start >= count) {
163 pos = count;
164 break;
165 }
166 } else {
167 end = pos;
168 }
169 } else {
170 // They're the same, but that doesn't mean it's the start
171 if (start == pos) {
172 // This is it
173 break;
174 } else {
175 // Need to go further lower to find the starting row
176 end = pos;
177 }
178 }
179 pos = (start + end) / 2;
180 }
181 alphaMap.put(key, pos);
182 cursor.moveToPosition(savedCursorPos);
183 return pos;
184 }
185
186 @Override
187 public void onChanged() {
188 super.onChanged();
189 mAlphaMap.clear();
190 }
191
192 @Override
193 public void onInvalidated() {
194 super.onInvalidated();
195 mAlphaMap.clear();
196 }
197}