1/*
2 * Copyright (C) 2009 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 */
16
17package com.android.providers.contacts;
18
19import android.accounts.Account;
20import android.app.SearchManager;
21import android.content.ContentValues;
22import android.database.Cursor;
23import android.net.Uri;
24import android.provider.ContactsContract;
25import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
26import android.provider.ContactsContract.Contacts;
27import android.provider.ContactsContract.Data;
28import android.provider.ContactsContract.StatusUpdates;
29import android.test.suitebuilder.annotation.MediumTest;
30
31import com.android.providers.contacts.testutil.DataUtil;
32import com.android.providers.contacts.testutil.RawContactUtil;
33
34/**
35 * Unit tests for {@link GlobalSearchSupport}.
36 * <p>
37 * Run the test like this:
38 * <p>
39 * <code><pre>
40 * adb shell am instrument -e class com.android.providers.contacts.GlobalSearchSupportTest -w \
41 *         com.android.providers.contacts.tests/android.test.InstrumentationTestRunner
42 * </pre></code>
43 */
44@MediumTest
45public class GlobalSearchSupportTest extends BaseContactsProvider2Test {
46
47    public void testSearchSuggestionsNotInDefaultDirectory() throws Exception {
48        Account account = new Account("actname", "acttype");
49
50        // Creating an AUTO_ADD group will exclude all ungrouped contacts from global search
51        createGroup(account, "any", "any", 0 /* visible */, true /* auto-add */, false /* fav */);
52
53        long rawContactId = RawContactUtil.createRawContact(mResolver, account);
54        DataUtil.insertStructuredName(mResolver, rawContactId, "Deer", "Dough");
55
56        // Remove the new contact from all groups
57        mResolver.delete(Data.CONTENT_URI, Data.RAW_CONTACT_ID + "=" + rawContactId
58                + " AND " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE + "'", null);
59
60        Uri searchUri = new Uri.Builder().scheme("content").authority(ContactsContract.AUTHORITY)
61                .appendPath(SearchManager.SUGGEST_URI_PATH_QUERY).appendPath("D").build();
62
63        // If the contact is not in the "my contacts" group, nothing should be found
64        Cursor c = mResolver.query(searchUri, null, null, null, null);
65        assertEquals(0, c.getCount());
66        c.close();
67    }
68
69    public void testSearchSuggestionsByNameWithPhoto() throws Exception {
70        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
71                loadTestPhoto()).build();
72        new SuggestionTesterBuilder(contact).query("D").expectIcon1Uri(true).expectedText1(
73                "Deer Dough").build().test();
74    }
75
76    public void testSearchSuggestionsByEmailWithPhoto() {
77        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
78                loadTestPhoto()).email("foo@acme.com").build();
79        new SuggestionTesterBuilder(contact).query("foo@ac").expectIcon1Uri(true).expectedIcon2(
80                String.valueOf(StatusUpdates.getPresenceIconResourceId(StatusUpdates.OFFLINE)))
81                .expectedText1("Deer Dough").expectedText2("foo@acme.com").build().test();
82    }
83
84    public void testSearchSuggestionsByName() {
85        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
86                .build();
87        new SuggestionTesterBuilder(contact).query("D").expectedText1("Deer Dough").expectedText2(
88                null).build().test();
89    }
90
91    public void testSearchByNickname() {
92        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").nickname(
93                "Little Fawn").company("Google").build();
94        new SuggestionTesterBuilder(contact).query("L").expectedText1("Deer Dough").expectedText2(
95                "Little Fawn").build().test();
96    }
97
98    public void testSearchByCompany() {
99        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
100                .build();
101        new SuggestionTesterBuilder(contact).query("G").expectedText1("Deer Dough").expectedText2(
102                "Google").build().test();
103    }
104
105    public void testSearchByTitleWithCompany() {
106        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").company("Google")
107                .title("Software Engineer").build();
108        new SuggestionTesterBuilder(contact).query("S").expectIcon1Uri(false).expectedText1(
109                "Deer Dough").expectedText2("Software Engineer, Google").build().test();
110    }
111
112    public void testSearchSuggestionsByPhoneNumberOnNonPhone() throws Exception {
113        getContactsProvider().setIsPhone(false);
114
115        GoldenContact contact = new GoldenContactBuilder().name("Deer", "Dough").photo(
116                loadTestPhoto()).phone("1-800-4664-411").build();
117        new SuggestionTesterBuilder(contact).query("1800").expectIcon1Uri(true).expectedText1(
118                "Deer Dough").expectedText2("1-800-4664-411").build().test();
119    }
120
121    /**
122     * Tests that the quick search suggestion returns the expected contact
123     * information.
124     */
125    private final class SuggestionTester {
126
127        private final GoldenContact contact;
128
129        private final String query;
130
131        private final boolean expectIcon1Uri;
132
133        private final String expectedIcon2;
134
135        private final String expectedText1;
136
137        private final String expectedText2;
138
139        public SuggestionTester(SuggestionTesterBuilder builder) {
140            contact = builder.contact;
141            query = builder.query;
142            expectIcon1Uri = builder.expectIcon1Uri;
143            expectedIcon2 = builder.expectedIcon2;
144            expectedText1 = builder.expectedText1;
145            expectedText2 = builder.expectedText2;
146        }
147
148        /**
149         * Tests suggest and refresh queries from quick search box, then deletes the contact from
150         * the data base.
151         */
152        public void test() {
153
154            testQsbSuggest();
155            testContactIdQsbRefresh();
156            testLookupKeyQsbRefresh();
157
158            // Cleanup
159            contact.delete();
160        }
161
162        /**
163         * Tests that the contacts provider return the appropriate information from the golden
164         * contact in response to the suggestion query from the quick search box.
165         */
166        private void testQsbSuggest() {
167
168            Uri searchUri = new Uri.Builder().scheme("content").authority(
169                    ContactsContract.AUTHORITY).appendPath(SearchManager.SUGGEST_URI_PATH_QUERY)
170                    .appendPath(query).build();
171
172            Cursor c = mResolver.query(searchUri, null, null, null, null);
173            assertEquals(1, c.getCount());
174            c.moveToFirst();
175
176            String icon1 = c.getString(c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1));
177            if (expectIcon1Uri) {
178                assertTrue(icon1.startsWith("content:"));
179            } else {
180                assertEquals(String.valueOf(com.android.internal.R.drawable.ic_contact_picture),
181                        icon1);
182            }
183
184            // SearchManager does not declare a constant for _id
185            ContentValues values = getContactValues();
186            assertCursorValues(c, values);
187
188            c.close();
189        }
190
191        /**
192         * Returns the expected Quick Search Box content values for the golden contact.
193         */
194        private ContentValues getContactValues() {
195
196            ContentValues values = new ContentValues();
197            values.put("_id", contact.getContactId());
198            values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
199            values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
200
201            values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
202            values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA,
203                    Contacts.getLookupUri(contact.getContactId(), contact.getLookupKey())
204                            .toString());
205            values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contact.getLookupKey());
206            values.put(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, query);
207            return values;
208        }
209
210        /**
211         * Returns the expected Quick Search Box content values for the golden contact.
212         */
213        private ContentValues getRefreshValues() {
214
215            ContentValues values = new ContentValues();
216            values.put("_id", contact.getContactId());
217            values.put(SearchManager.SUGGEST_COLUMN_TEXT_1, expectedText1);
218            values.put(SearchManager.SUGGEST_COLUMN_TEXT_2, expectedText2);
219
220            values.put(SearchManager.SUGGEST_COLUMN_ICON_2, expectedIcon2);
221            values.put(SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID, contact.getLookupKey());
222            values.put(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, contact.getLookupKey());
223            return values;
224        }
225
226        /**
227         * Performs the refresh query and returns a cursor to the results.
228         *
229         * @param refreshId the final component path of the refresh query, which identifies which
230         *        contact to refresh.
231         */
232        private Cursor refreshQuery(String refreshId) {
233
234            // See if the same result is returned by a shortcut refresh
235            Uri refershUri = ContactsContract.AUTHORITY_URI.buildUpon().appendPath(
236                    SearchManager.SUGGEST_URI_PATH_SHORTCUT)
237                    .appendPath(refreshId)
238                    .appendQueryParameter(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA, query)
239                    .build();
240
241            String[] projection = new String[] {
242                    SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_ICON_2,
243                    SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2,
244                    SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID,
245                    SearchManager.SUGGEST_COLUMN_SHORTCUT_ID, "_id",
246            };
247
248            return mResolver.query(refershUri, projection, null, null, null);
249        }
250
251        /**
252         * Tests that the contacts provider returns an empty result in response to a refresh query
253         * from the quick search box that uses the contact id to identify the contact.  The empty
254         * result indicates that the shortcut is no longer valid, and the QSB will replace it with
255         * a new-style shortcut the next time they click on the contact.
256         *
257         * @see #testLookupKeyQsbRefresh()
258         */
259        private void testContactIdQsbRefresh() {
260
261            Cursor c = refreshQuery(String.valueOf(contact.getContactId()));
262            try {
263                assertEquals("Record count", 0, c.getCount());
264            } finally {
265                c.close();
266            }
267        }
268
269        /**
270         * Tests that the contacts provider return the appropriate information from the golden
271         * contact in response to the refresh query from the quick search box.  The refresh query
272         * uses the currently-supported mechanism of identifying the contact by the lookup key,
273         * which is more stable than the previously used contact id.
274         */
275        private void testLookupKeyQsbRefresh() {
276
277            Cursor c = refreshQuery(contact.getLookupKey());
278            try {
279                assertEquals("Record count", 1, c.getCount());
280                c.moveToFirst();
281                assertCursorValues(c, getRefreshValues());
282            } finally {
283                c.close();
284            }
285        }
286    }
287
288    /**
289     * Builds {@link SuggestionTester} objects. Unspecified boolean objects default to
290     * false. Unspecified String objects default to null.
291     */
292    private final class SuggestionTesterBuilder {
293
294        private final GoldenContact contact;
295
296        private String query;
297
298        private boolean expectIcon1Uri;
299
300        private String expectedIcon2;
301
302        private String expectedText1;
303
304        private String expectedText2;
305
306        public SuggestionTesterBuilder(GoldenContact contact) {
307            this.contact = contact;
308        }
309
310        /**
311         * Builds the {@link SuggestionTester} specified by this builder.
312         */
313        public SuggestionTester build() {
314            return new SuggestionTester(this);
315        }
316
317        /**
318         * The text of the user's query to quick search (i.e., what they typed
319         * in the search box).
320         */
321        public SuggestionTesterBuilder query(String value) {
322            query = value;
323            return this;
324        }
325
326        /**
327         * Whether to set Icon1, which in practice is the contact's photo.
328         * <p>
329         * TODO(tomo): Replace with actual expected value? This might be hard
330         * because the values look non-deterministic, such as
331         * "content://com.android.contacts/contacts/2015/photo"
332         */
333        public SuggestionTesterBuilder expectIcon1Uri(boolean value) {
334            expectIcon1Uri = value;
335            return this;
336        }
337
338        /**
339         * The value for Icon2, which in practice is the contact's Chat status
340         * (available, busy, etc.)
341         */
342        public SuggestionTesterBuilder expectedIcon2(String value) {
343            expectedIcon2 = value;
344            return this;
345        }
346
347        /**
348         * First line of suggestion text expected to be returned (required).
349         */
350        public SuggestionTesterBuilder expectedText1(String value) {
351            expectedText1 = value;
352            return this;
353        }
354
355        /**
356         * Second line of suggestion text expected to return (optional).
357         */
358        public SuggestionTesterBuilder expectedText2(String value) {
359            expectedText2 = value;
360            return this;
361        }
362    }
363}
364