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. The authority fro for this provider is obtained
39 * through the MailAppProvider as follows:
40 * final String AUTHORITY = MailAppProvider.getInstance().getSuggestionAuthority()
41 * It needs to be done after the MailAppProvider is constructed.
42 */
43public class SuggestionsProvider extends SearchRecentSuggestionsProvider {
44    /**
45     * Mode used in the constructor of SuggestionsProvider.
46     */
47    public final static int MODE = DATABASE_MODE_QUERIES;
48    /**
49     * Columns over the contacts database that we return in the {@link ContactsCursor}.
50     */
51    private static final String[] CONTACTS_COLUMNS = new String[] {
52            BaseColumns._ID,
53            SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_QUERY,
54            SearchManager.SUGGEST_COLUMN_ICON_1
55    };
56    private ArrayList<String> mFullQueryTerms;
57    /** Used for synchronization */
58    private final Object mTermsLock = new Object();
59    private final static String[] sContract = new String[] {
60            ContactsContract.CommonDataKinds.Email.DISPLAY_NAME,
61            ContactsContract.CommonDataKinds.Email.DATA
62    };
63    /**
64     * Minimum length of query before we start showing contacts suggestions.
65     */
66    static private final int MIN_QUERY_LENGTH_FOR_CONTACTS = 2;
67
68    public SuggestionsProvider() {
69        super();
70    }
71
72    @Override
73    public boolean onCreate() {
74        final String authority = getContext().getString(R.string.suggestions_authority);
75        setupSuggestions(authority, MODE);
76        super.onCreate();
77        return true;
78    }
79
80    @Override
81    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
82            String sortOrder) {
83        String query = selectionArgs[0];
84        MergeCursor mergeCursor = null;
85
86        synchronized (mTermsLock) {
87            mFullQueryTerms = null;
88            super.setFullQueryTerms(mFullQueryTerms);
89        }
90        // Get the custom suggestions for email which are from, to, etc.
91        if (query != null) {
92            // Tokenize the query.
93            String[] tokens = TextUtils.split(query,
94                    SearchRecentSuggestionsProvider.QUERY_TOKEN_SEPARATOR);
95            // There are multiple tokens, so query on the last token only.
96            if (tokens != null && tokens.length > 1) {
97                query = tokens[tokens.length - 1];
98                // Leave off the last token since we are auto completing on it.
99                synchronized (mTermsLock) {
100                    mFullQueryTerms = new ArrayList<String>();
101                    for (int i = 0, size = tokens.length - 1; i < size; i++) {
102                        mFullQueryTerms.add(tokens[i]);
103                    }
104                    super.setFullQueryTerms(mFullQueryTerms);
105                }
106            } else {
107                // Strip excess whitespace.
108                query = query.trim();
109            }
110            ArrayList<Cursor> cursors = new ArrayList<Cursor>();
111            // Pass query; at this point it is either the last term OR the
112            // only term.
113            cursors.add(super.query(uri, projection, selection, new String[] { query }, sortOrder));
114
115            if (query.length() >= MIN_QUERY_LENGTH_FOR_CONTACTS) {
116                cursors.add(new ContactsCursor().query(query));
117            }
118            mergeCursor = new MergeCursor(cursors.toArray(new Cursor[cursors.size()]));
119        }
120        return mergeCursor;
121    }
122
123    /**
124     * Utility class to return a cursor over the contacts database
125     */
126    private final class ContactsCursor extends MatrixCursorWithCachedColumns {
127        private final Context mContext;
128        public ContactsCursor() {
129            super(CONTACTS_COLUMNS);
130            mContext = getContext();
131        }
132
133        /**
134         * Searches over the contacts cursor with the specified query as the starting characters to
135         * match.
136         * @param query
137         * @return a cursor over the contacts database with the contacts matching the query.
138         */
139        public ContactsCursor query(String query) {
140            final Uri contactsUri = Uri.withAppendedPath(
141                    ContactsContract.CommonDataKinds.Email.CONTENT_FILTER_URI, Uri.encode(query));
142            final Cursor cursor = mContext.getContentResolver().query(
143                    contactsUri, sContract, null, null, null);
144            // We don't want to show a contact icon here. Leaving the SEARCH_ICON_1 field
145            // empty causes inconsistent behavior because the cursor is merged with the
146            // historical suggestions, which have an icon.  The solution is to show an empty icon
147            // instead.
148            final String emptyIcon = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
149                    + mContext.getPackageName() + "/" + R.drawable.empty;
150            if (cursor != null) {
151                final int nameIndex = cursor
152                        .getColumnIndex(ContactsContract.CommonDataKinds.Email.DISPLAY_NAME);
153                final int addressIndex = cursor
154                        .getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
155                String match;
156                while (cursor.moveToNext()) {
157                    match = cursor.getString(nameIndex);
158                    match = !TextUtils.isEmpty(match) ? match : cursor.getString(addressIndex);
159                    // The order of fields is:
160                    // _ID, SUGGEST_COLUMN_TEXT_1, SUGGEST_COLUMN_QUERY, SUGGEST_COLUMN_ICON_1
161                    addRow(new Object[] {0, match, createQuery(match), emptyIcon});
162                }
163                cursor.close();
164            }
165            return this;
166        }
167    }
168
169    private String createQuery(String inMatch) {
170        final StringBuilder query = new StringBuilder();
171        if (mFullQueryTerms != null) {
172            synchronized (mTermsLock) {
173                for (int i = 0, size = mFullQueryTerms.size(); i < size; i++) {
174                    query.append(mFullQueryTerms.get(i)).append(QUERY_TOKEN_SEPARATOR);
175                }
176            }
177        }
178        // Append the match as well.
179        query.append(inMatch);
180        // Example:
181        // Search terms in the searchbox are : "pdf test*"
182        // Contacts database contains: test@tester.com, test@other.com
183        // If the user taps "test@tester.com", the query passed with
184        // ACTION_SEARCH is:
185        // "pdf test@tester.com"
186        // If the user taps "test@other.com", the query passed with
187        // ACTION_SEARCH is:
188        // "pdf test@other.com"
189        return query.toString();
190    }
191}