1/*******************************************************************************
2 *      Copyright (C) 2012 Google Inc.
3 *      Licensed to The Android Open Source Project.
4 *
5 *      Licensed under the Apache License, Version 2.0 (the "License");
6 *      you may not use this file except in compliance with the License.
7 *      You may obtain a copy of the License at
8 *
9 *           http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *      Unless required by applicable law or agreed to in writing, software
12 *      distributed under the License is distributed on an "AS IS" BASIS,
13 *      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *      See the License for the specific language governing permissions and
15 *      limitations under the License.
16 *******************************************************************************/
17
18package com.android.mail.providers;
19
20import android.database.Cursor;
21import android.database.MergeCursor;
22import android.net.Uri;
23import android.provider.BaseColumns;
24import android.provider.ContactsContract;
25import android.app.SearchManager;
26import android.content.ContentResolver;
27import android.content.Context;
28import android.text.TextUtils;
29
30import com.android.mail.R;
31import com.android.mail.utils.MatrixCursorWithCachedColumns;
32
33import java.util.ArrayList;
34
35/**
36 * Simple extension / instantiation of SearchRecentSuggestionsProvider, independent
37 * of mail account or account capabilities.  Offers suggestions from historical searches
38 * and contact email addresses on the device.
39 */
40public class SuggestionsProvider extends SearchRecentSuggestionsProvider {
41    /**
42     * Columns over the contacts database that we return in the {@link ContactsCursor}.
43     */
44    private static final String[] CONTACTS_COLUMNS = new String[] {
45            BaseColumns._ID,
46            SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_QUERY,
47            SearchManager.SUGGEST_COLUMN_ICON_1
48    };
49    private ArrayList<String> mFullQueryTerms;
50    /** Used for synchronization */
51    private final Object mTermsLock = new Object();
52    private final static String[] sContract = new String[] {
53            ContactsContract.CommonDataKinds.Email.DISPLAY_NAME,
54            ContactsContract.CommonDataKinds.Email.DATA
55    };
56    /**
57     * Minimum length of query before we start showing contacts suggestions.
58     */
59    static private final int MIN_QUERY_LENGTH_FOR_CONTACTS = 2;
60
61    public SuggestionsProvider(Context context) {
62        super(context);
63    }
64
65    @Override
66    public Cursor query(String query) {
67        Cursor mergeCursor = null;
68
69        synchronized (mTermsLock) {
70            mFullQueryTerms = null;
71            super.setFullQueryTerms(mFullQueryTerms);
72        }
73        // Get the custom suggestions for email which are from, to, etc.
74        if (query != null) {
75            // Tokenize the query.
76            String[] tokens = TextUtils.split(query,
77                    SearchRecentSuggestionsProvider.QUERY_TOKEN_SEPARATOR);
78            // There are multiple tokens, so query on the last token only.
79            if (tokens != null && tokens.length > 1) {
80                query = tokens[tokens.length - 1];
81                // Leave off the last token since we are auto completing on it.
82                synchronized (mTermsLock) {
83                    mFullQueryTerms = new ArrayList<String>();
84                    for (int i = 0, size = tokens.length - 1; i < size; i++) {
85                        mFullQueryTerms.add(tokens[i]);
86                    }
87                    super.setFullQueryTerms(mFullQueryTerms);
88                }
89            } else {
90                // Strip excess whitespace.
91                query = query.trim();
92            }
93            ArrayList<Cursor> cursors = new ArrayList<Cursor>();
94            // Pass query; at this point it is either the last term OR the
95            // only term.
96            final Cursor c = super.query(query);
97            if (c != null) {
98                cursors.add(c);
99            }
100
101            if (query.length() >= MIN_QUERY_LENGTH_FOR_CONTACTS) {
102                cursors.add(new ContactsCursor().query(query));
103            }
104
105            if (cursors.size() > 0) {
106                mergeCursor = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
107            }
108        }
109        return mergeCursor;
110    }
111
112    /**
113     * Utility class to return a cursor over the contacts database
114     */
115    private final class ContactsCursor extends MatrixCursorWithCachedColumns {
116        public ContactsCursor() {
117            super(CONTACTS_COLUMNS);
118        }
119
120        /**
121         * Searches over the contacts cursor with the specified query as the starting characters to
122         * match.
123         * @param query
124         * @return a cursor over the contacts database with the contacts matching the query.
125         */
126        public ContactsCursor query(String query) {
127            final Uri contactsUri = Uri.withAppendedPath(
128                    ContactsContract.CommonDataKinds.Email.CONTENT_FILTER_URI, Uri.encode(query));
129            final Cursor cursor = mContext.getContentResolver().query(
130                    contactsUri, sContract, null, null, null);
131            // We don't want to show a contact icon here. Leaving the SEARCH_ICON_1 field
132            // empty causes inconsistent behavior because the cursor is merged with the
133            // historical suggestions, which have an icon.  The solution is to show an empty icon
134            // instead.
135            final String emptyIcon = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
136                    + mContext.getPackageName() + "/" + R.drawable.empty;
137            if (cursor != null) {
138                final int nameIndex = cursor
139                        .getColumnIndex(ContactsContract.CommonDataKinds.Email.DISPLAY_NAME);
140                final int addressIndex = cursor
141                        .getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
142                String match;
143                while (cursor.moveToNext()) {
144                    match = cursor.getString(nameIndex);
145                    match = !TextUtils.isEmpty(match) ? match : cursor.getString(addressIndex);
146                    // The order of fields is:
147                    // _ID, SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_QUERY, SUGGEST_COLUMN_ICON_1
148                    addRow(new Object[] {0, match, createQuery(match), emptyIcon});
149                }
150                cursor.close();
151            }
152            return this;
153        }
154    }
155
156    private String createQuery(String inMatch) {
157        final StringBuilder query = new StringBuilder();
158        if (mFullQueryTerms != null) {
159            synchronized (mTermsLock) {
160                for (String token : mFullQueryTerms) {
161                    query.append(token).append(QUERY_TOKEN_SEPARATOR);
162                }
163            }
164        }
165        // Append the match as well.
166        query.append(inMatch);
167        // Example:
168        // Search terms in the searchbox are : "pdf test*"
169        // Contacts database contains: test@tester.com, test@other.com
170        // If the user taps "test@tester.com", the query passed with
171        // ACTION_SEARCH is:
172        // "pdf test@tester.com"
173        // If the user taps "test@other.com", the query passed with
174        // ACTION_SEARCH is:
175        // "pdf test@other.com"
176        return query.toString();
177    }
178}