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