summaryrefslogtreecommitdiffstats
path: root/src/com/android/messaging/ui/contact/ContactSectionIndexer.java
blob: 1d5abf31b70dd69a525f0ac91249cd6b750d7b83 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/*
 * Copyright (C) 2015 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.messaging.ui.contact;

import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract.Contacts;
import android.text.TextUtils;
import android.widget.SectionIndexer;

import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.LogUtil;

import java.util.ArrayList;

/**
 * Indexes contact alphabetical sections so we can report to the fast scrolling list view
 * where we are in the list when the user scrolls through the contact list, allowing us to show
 * alphabetical indicators for the fast scroller as well as list section headers.
 */
public class ContactSectionIndexer implements SectionIndexer {
    private String[] mSections;
    private ArrayList<Integer> mSectionStartingPositions;
    private static final String BLANK_HEADER_STRING = " ";

    public ContactSectionIndexer(final Cursor contactsCursor) {
        buildIndexer(contactsCursor);
    }

    @Override
    public Object[] getSections() {
        return mSections;
    }

    @Override
    public int getPositionForSection(final int sectionIndex) {
        if (mSectionStartingPositions.isEmpty()) {
            return 0;
        }
        // Clamp to the bounds of the section position array per Android API doc.
        return mSectionStartingPositions.get(
                Math.max(Math.min(sectionIndex, mSectionStartingPositions.size() - 1), 0));
    }

    @Override
    public int getSectionForPosition(final int position) {
        if (mSectionStartingPositions.isEmpty()) {
            return 0;
        }

        // Perform a binary search on the starting positions of the sections to the find the
        // section for the position.
        int left = 0;
        int right = mSectionStartingPositions.size() - 1;

        // According to getSectionForPosition()'s doc, we should always clamp the value when the
        // position is out of bound.
        if (position <= mSectionStartingPositions.get(left)) {
            return left;
        } else if (position >= mSectionStartingPositions.get(right)) {
            return right;
        }

        while (left <= right) {
            final int mid = (left + right) / 2;
            final int startingPos = mSectionStartingPositions.get(mid);
            final int nextStartingPos = mSectionStartingPositions.get(mid + 1);
            if (position >= startingPos && position < nextStartingPos) {
                return mid;
            } else if (position < startingPos) {
                right = mid - 1;
            } else if (position >= nextStartingPos) {
                left = mid + 1;
            }
        }
        Assert.fail("Invalid section indexer state: couldn't find section for pos " + position);
        return -1;
    }

    private boolean buildIndexerFromCursorExtras(final Cursor cursor) {
        if (cursor == null) {
            return false;
        }
        final Bundle cursorExtras = cursor.getExtras();
        if (cursorExtras == null) {
            return false;
        }
        final String[] sections = cursorExtras.getStringArray(
                Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
        final int[] counts = cursorExtras.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
        if (sections == null || counts == null) {
            return false;
        }

        if (sections.length != counts.length) {
            return false;
        }

        this.mSections = sections;
        mSectionStartingPositions = new ArrayList<Integer>(counts.length);
        int position = 0;
        for (int i = 0; i < counts.length; i++) {
            if (TextUtils.isEmpty(mSections[i])) {
                mSections[i] = BLANK_HEADER_STRING;
            } else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
                mSections[i] = mSections[i].trim();
            }

            mSectionStartingPositions.add(position);
            position += counts[i];
        }
        return true;
    }

    private void buildIndexerFromDisplayNames(final Cursor cursor) {
        // Loop through the contact cursor and get the starting position for each first character.
        // The result is stored into two arrays, one for the section header (i.e. the first
        // character), and one for the starting position, which is guaranteed to be sorted in
        // ascending order.
        final ArrayList<String> sections = new ArrayList<String>();
        mSectionStartingPositions = new ArrayList<Integer>();
        if (cursor != null) {
            cursor.moveToPosition(-1);
            int currentPosition = 0;
            while (cursor.moveToNext()) {
                // The sort key is typically the contact's display name, so for example, a contact
                // named "Bob" will go into section "B". The Contacts provider generally uses a
                // a slightly more sophisticated heuristic, but as a fallback this is good enough.
                final String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
                final String section = TextUtils.isEmpty(sortKey) ? BLANK_HEADER_STRING :
                    sortKey.substring(0, 1).toUpperCase();

                final int lastIndex = sections.size() - 1;
                final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null;
                if (!TextUtils.equals(currentSection, section)) {
                    sections.add(section);
                    mSectionStartingPositions.add(currentPosition);
                }
                currentPosition++;
            }
        }
        mSections = new String[sections.size()];
        sections.toArray(mSections);
    }

    private void buildIndexer(final Cursor cursor) {
        // First check if we get indexer label extras from the contact provider; if not, fall back
        // to building from display names.
        if (!buildIndexerFromCursorExtras(cursor)) {
            LogUtil.w(LogUtil.BUGLE_TAG, "contact provider didn't provide contact label " +
                    "information, fall back to using display name!");
            buildIndexerFromDisplayNames(cursor);
        }
    }
}