blob: 4e5fb0f8b2ec666337d6cf0047dd1318962bf9f3 [file] [log] [blame]
/*
* Copyright (C) 2008 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.database.Cursor;
import android.database.DataSetObserver;
import android.util.SparseIntArray;
/**
* This class essentially helps in building an index of section boundaries of a
* sorted column of a cursor. For instance, if a cursor contains a data set
* sorted by first name of a person or the title of a song, this class will
* perform a binary search to identify the first row that begins with a
* particular letter. The search is case-insensitive. The class caches the index
* such that subsequent queries for the same letter will return right away.
*/
public class AlphabetIndexer extends DataSetObserver {
protected Cursor mDataCursor;
protected int mColumnIndex;
protected Object[] mAlphabetArray;
private SparseIntArray mAlphaMap;
private java.text.Collator mCollator;
/**
* Constructs the indexer.
* @param cursor the cursor containing the data set
* @param columnIndex the column number in the cursor that is sorted
* alphabetically
* @param sections the array of objects that represent the sections. The
* toString() method of each item is called and the first letter of the
* String is used as the letter to search for.
*/
public AlphabetIndexer(Cursor cursor, int columnIndex, Object[] sections) {
mDataCursor = cursor;
mColumnIndex = columnIndex;
mAlphabetArray = sections;
mAlphaMap = new SparseIntArray(26 /* Optimize for English */);
if (cursor != null) {
cursor.registerDataSetObserver(this);
}
// Get a Collator for the current locale for string comparisons.
mCollator = java.text.Collator.getInstance();
mCollator.setStrength(java.text.Collator.PRIMARY);
}
/**
* Sets a new cursor as the data set and resets the cache of indices.
* @param cursor the new cursor to use as the data set
*/
public void setCursor(Cursor cursor) {
if (mDataCursor != null) {
mDataCursor.unregisterDataSetObserver(this);
}
mDataCursor = cursor;
if (cursor != null) {
mDataCursor.registerDataSetObserver(this);
}
mAlphaMap.clear();
}
/**
* Performs a binary search or cache lookup to find the first row that
* matches a given section's starting letter.
* @param sectionIndex the section to search for
* @return the row index of the first occurrence, or the nearest next letter.
* For instance, if searching for "T" and no "T" is found, then the first
* row starting with "U" or any higher letter is returned. If there is no
* data following "T" at all, then the list size is returned.
*/
public int indexOf(int sectionIndex) {
final SparseIntArray alphaMap = mAlphaMap;
final Cursor cursor = mDataCursor;
if (cursor == null || mAlphabetArray == null) {
return 0;
}
// Check bounds
if (sectionIndex <= 0) {
return 0;
}
if (sectionIndex >= mAlphabetArray.length) {
sectionIndex = mAlphabetArray.length - 1;
}
int savedCursorPos = cursor.getPosition();
int count = cursor.getCount();
int start = 0;
int end = count;
int pos;
String letter = mAlphabetArray[sectionIndex].toString();
letter = letter.toUpperCase();
int key = letter.charAt(0);
// Check map
if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) {
// Is it approximate? Using negative value to indicate that it's
// an approximation and positive value when it is the accurate
// position.
if (pos < 0) {
pos = -pos;
end = pos;
} else {
// Not approximate, this is the confirmed start of section, return it
return pos;
}
}
// Do we have the position of the previous section?
if (sectionIndex > 0) {
int prevLetter =
mAlphabetArray[sectionIndex - 1].toString().charAt(0);
int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE);
if (prevLetterPos != Integer.MIN_VALUE) {
start = Math.abs(prevLetterPos);
}
}
// Now that we have a possibly optimized start and end, let's binary search
pos = (end + start) / 2;
while (pos < end) {
// Get letter at pos
cursor.moveToPosition(pos);
String curName = cursor.getString(mColumnIndex);
if (curName == null) {
if (pos == 0) {
break;
} else {
pos--;
continue;
}
}
int curLetter = Character.toUpperCase(curName.charAt(0));
if (curLetter != key) {
// Enter approximation in hash if a better solution doesn't exist
int curPos = alphaMap.get(curLetter, Integer.MIN_VALUE);
if (curPos == Integer.MIN_VALUE || Math.abs(curPos) > pos) {
// Negative pos indicates that it is an approximation
alphaMap.put(curLetter, -pos);
}
if (mCollator.compare(curName, letter) < 0) {
start = pos + 1;
if (start >= count) {
pos = count;
break;
}
} else {
end = pos;
}
} else {
// They're the same, but that doesn't mean it's the start
if (start == pos) {
// This is it
break;
} else {
// Need to go further lower to find the starting row
end = pos;
}
}
pos = (start + end) / 2;
}
alphaMap.put(key, pos);
cursor.moveToPosition(savedCursorPos);
return pos;
}
@Override
public void onChanged() {
super.onChanged();
mAlphaMap.clear();
}
@Override
public void onInvalidated() {
super.onInvalidated();
mAlphaMap.clear();
}
}