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 */
16package com.android.messaging.ui.contact;
17
18import android.database.Cursor;
19import android.os.Bundle;
20import android.provider.ContactsContract.Contacts;
21import android.text.TextUtils;
22import android.widget.SectionIndexer;
23
24import com.android.messaging.util.Assert;
25import com.android.messaging.util.ContactUtil;
26import com.android.messaging.util.LogUtil;
27
28import 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 */
35public 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