1 /* 2 * Copyright (C) 2014 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 package com.android.server.notification; 17 18 import static com.google.common.truth.Truth.assertThat; 19 20 import static org.junit.Assert.assertEquals; 21 import static org.junit.Assert.assertFalse; 22 import static org.junit.Assert.assertNotNull; 23 import static org.junit.Assert.assertNull; 24 import static org.junit.Assert.assertTrue; 25 import static org.mockito.ArgumentMatchers.any; 26 import static org.mockito.ArgumentMatchers.contains; 27 import static org.mockito.ArgumentMatchers.eq; 28 import static org.mockito.ArgumentMatchers.isNull; 29 import static org.mockito.Mockito.doAnswer; 30 import static org.mockito.Mockito.mock; 31 import static org.mockito.Mockito.never; 32 import static org.mockito.Mockito.times; 33 import static org.mockito.Mockito.verify; 34 import static org.mockito.Mockito.when; 35 36 import android.app.Notification; 37 import android.app.NotificationChannel; 38 import android.app.Person; 39 import android.content.ContentProvider; 40 import android.content.ContentResolver; 41 import android.content.Context; 42 import android.database.Cursor; 43 import android.net.Uri; 44 import android.os.Bundle; 45 import android.os.UserManager; 46 import android.provider.ContactsContract; 47 import android.service.notification.StatusBarNotification; 48 import android.test.suitebuilder.annotation.SmallTest; 49 import android.text.SpannableString; 50 import android.util.ArraySet; 51 import android.util.LruCache; 52 53 import androidx.test.runner.AndroidJUnit4; 54 55 import com.android.server.UiServiceTestCase; 56 import com.android.server.notification.ValidateNotificationPeople.LookupResult; 57 import com.android.server.notification.ValidateNotificationPeople.PeopleRankingReconsideration; 58 59 import org.junit.Test; 60 import org.junit.runner.RunWith; 61 import org.mockito.ArgumentCaptor; 62 import org.mockito.invocation.InvocationOnMock; 63 import org.mockito.stubbing.Answer; 64 65 import java.util.ArrayList; 66 import java.util.Arrays; 67 import java.util.concurrent.TimeUnit; 68 69 @SmallTest 70 @RunWith(AndroidJUnit4.class) 71 public class ValidateNotificationPeopleTest extends UiServiceTestCase { 72 73 @Test testNoExtra()74 public void testNoExtra() throws Exception { 75 Bundle bundle = new Bundle(); 76 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 77 assertNull("lack of extra should return null", result); 78 } 79 80 @Test testSingleString()81 public void testSingleString() throws Exception { 82 String[] expected = { "foobar" }; 83 Bundle bundle = new Bundle(); 84 bundle.putString(Notification.EXTRA_PEOPLE_LIST, expected[0]); 85 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 86 assertStringArrayEquals("string should be in result[0]", expected, result); 87 } 88 89 @Test testSingleCharArray()90 public void testSingleCharArray() throws Exception { 91 String[] expected = { "foobar" }; 92 Bundle bundle = new Bundle(); 93 bundle.putCharArray(Notification.EXTRA_PEOPLE_LIST, expected[0].toCharArray()); 94 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 95 assertStringArrayEquals("char[] should be in result[0]", expected, result); 96 } 97 98 @Test testSingleCharSequence()99 public void testSingleCharSequence() throws Exception { 100 String[] expected = { "foobar" }; 101 Bundle bundle = new Bundle(); 102 bundle.putCharSequence(Notification.EXTRA_PEOPLE_LIST, new SpannableString(expected[0])); 103 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 104 assertStringArrayEquals("charSequence should be in result[0]", expected, result); 105 } 106 107 @Test testStringArraySingle()108 public void testStringArraySingle() throws Exception { 109 Bundle bundle = new Bundle(); 110 String[] expected = { "foobar" }; 111 bundle.putStringArray(Notification.EXTRA_PEOPLE_LIST, expected); 112 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 113 assertStringArrayEquals("wrapped string should be in result[0]", expected, result); 114 } 115 116 @Test testStringArrayMultiple()117 public void testStringArrayMultiple() throws Exception { 118 Bundle bundle = new Bundle(); 119 String[] expected = { "foo", "bar", "baz" }; 120 bundle.putStringArray(Notification.EXTRA_PEOPLE_LIST, expected); 121 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 122 assertStringArrayEquals("testStringArrayMultiple", expected, result); 123 } 124 125 @Test testStringArrayNulls()126 public void testStringArrayNulls() throws Exception { 127 Bundle bundle = new Bundle(); 128 String[] expected = { "foo", null, "baz" }; 129 bundle.putStringArray(Notification.EXTRA_PEOPLE_LIST, expected); 130 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 131 assertStringArrayEquals("testStringArrayNulls", expected, result); 132 } 133 134 @Test testCharSequenceArrayMultiple()135 public void testCharSequenceArrayMultiple() throws Exception { 136 Bundle bundle = new Bundle(); 137 String[] expected = { "foo", "bar", "baz" }; 138 CharSequence[] charSeqArray = new CharSequence[expected.length]; 139 for (int i = 0; i < expected.length; i++) { 140 charSeqArray[i] = new SpannableString(expected[i]); 141 } 142 bundle.putCharSequenceArray(Notification.EXTRA_PEOPLE_LIST, charSeqArray); 143 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 144 assertStringArrayEquals("testCharSequenceArrayMultiple", expected, result); 145 } 146 147 @Test testMixedCharSequenceArrayList()148 public void testMixedCharSequenceArrayList() throws Exception { 149 Bundle bundle = new Bundle(); 150 String[] expected = { "foo", "bar", "baz" }; 151 CharSequence[] charSeqArray = new CharSequence[expected.length]; 152 for (int i = 0; i < expected.length; i++) { 153 if (i % 2 == 0) { 154 charSeqArray[i] = expected[i]; 155 } else { 156 charSeqArray[i] = new SpannableString(expected[i]); 157 } 158 } 159 bundle.putCharSequenceArray(Notification.EXTRA_PEOPLE_LIST, charSeqArray); 160 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 161 assertStringArrayEquals("testMixedCharSequenceArrayList", expected, result); 162 } 163 164 @Test testStringArrayList()165 public void testStringArrayList() throws Exception { 166 Bundle bundle = new Bundle(); 167 String[] expected = { "foo", null, "baz" }; 168 final ArrayList<String> stringArrayList = new ArrayList<String>(expected.length); 169 for (int i = 0; i < expected.length; i++) { 170 stringArrayList.add(expected[i]); 171 } 172 bundle.putStringArrayList(Notification.EXTRA_PEOPLE_LIST, stringArrayList); 173 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 174 assertStringArrayEquals("testStringArrayList", expected, result); 175 } 176 177 @Test testCharSequenceArrayList()178 public void testCharSequenceArrayList() throws Exception { 179 Bundle bundle = new Bundle(); 180 String[] expected = { "foo", "bar", "baz" }; 181 final ArrayList<CharSequence> stringArrayList = 182 new ArrayList<CharSequence>(expected.length); 183 for (int i = 0; i < expected.length; i++) { 184 stringArrayList.add(new SpannableString(expected[i])); 185 } 186 bundle.putCharSequenceArrayList(Notification.EXTRA_PEOPLE_LIST, stringArrayList); 187 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 188 assertStringArrayEquals("testCharSequenceArrayList", expected, result); 189 } 190 191 @Test testPeopleArrayList()192 public void testPeopleArrayList() throws Exception { 193 Bundle bundle = new Bundle(); 194 String[] expected = { "name:test" , "tel:1234" }; 195 final ArrayList<Person> arrayList = 196 new ArrayList<>(expected.length); 197 arrayList.add(new Person.Builder().setName("test").build()); 198 arrayList.add(new Person.Builder().setUri(expected[1]).build()); 199 bundle.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, arrayList); 200 String[] result = ValidateNotificationPeople.getExtraPeople(bundle); 201 assertStringArrayEquals("testPeopleArrayList", expected, result); 202 } 203 204 @Test testSearchContacts_workContact_queriesWorkContactProvider()205 public void testSearchContacts_workContact_queriesWorkContactProvider() 206 throws Exception { 207 final int personalUserId = 0; 208 final int workUserId = 12; 209 final int contactId = 12345; 210 final Context mockContext = mock(Context.class); 211 when(mockContext.getUserId()).thenReturn(personalUserId); 212 final UserManager mockUserManager = mock(UserManager.class); 213 when(mockContext.getSystemService(UserManager.class)).thenReturn(mockUserManager); 214 when(mockUserManager.getProfileIds(personalUserId, /* enabledOnly= */ true)) 215 .thenReturn(new int[] {personalUserId, workUserId}); 216 when(mockUserManager.isManagedProfile(workUserId)).thenReturn(true); 217 final ContentResolver mockContentResolver = mock(ContentResolver.class); 218 when(mockContext.getContentResolver()).thenReturn(mockContentResolver); 219 final Uri lookupUri = Uri.withAppendedPath( 220 ContactsContract.Contacts.CONTENT_LOOKUP_URI, 221 ContactsContract.Contacts.ENTERPRISE_CONTACT_LOOKUP_PREFIX + contactId); 222 223 PeopleRankingReconsideration.searchContacts(mockContext, lookupUri); 224 225 ArgumentCaptor<Uri> queryUri = ArgumentCaptor.forClass(Uri.class); 226 verify(mockContentResolver).query( 227 queryUri.capture(), 228 any(), 229 /* selection= */ isNull(), 230 /* selectionArgs= */ isNull(), 231 /* sortOrder= */ isNull()); 232 assertEquals(workUserId, ContentProvider.getUserIdFromUri(queryUri.getValue())); 233 } 234 235 @Test testSearchContacts_personalContact_queriesPersonalContactProvider()236 public void testSearchContacts_personalContact_queriesPersonalContactProvider() 237 throws Exception { 238 final int personalUserId = 0; 239 final int workUserId = 12; 240 final int contactId = 12345; 241 final Context mockContext = mock(Context.class); 242 when(mockContext.getUserId()).thenReturn(personalUserId); 243 final UserManager mockUserManager = mock(UserManager.class); 244 when(mockContext.getSystemService(UserManager.class)).thenReturn(mockUserManager); 245 final ContentResolver mockContentResolver = mock(ContentResolver.class); 246 when(mockContext.getContentResolver()).thenReturn(mockContentResolver); 247 final Uri lookupUri = Uri.withAppendedPath( 248 ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId)); 249 250 PeopleRankingReconsideration.searchContacts(mockContext, lookupUri); 251 252 ArgumentCaptor<Uri> queryUri = ArgumentCaptor.forClass(Uri.class); 253 verify(mockContentResolver).query( 254 queryUri.capture(), 255 any(), 256 /* selection= */ isNull(), 257 /* selectionArgs= */ isNull(), 258 /* sortOrder= */ isNull()); 259 assertFalse(ContentProvider.uriHasUserId(queryUri.getValue())); 260 } 261 262 @Test testMergePhoneNumbers_noPhoneNumber()263 public void testMergePhoneNumbers_noPhoneNumber() { 264 // If merge phone number is called but the contacts lookup turned up no available 265 // phone number (HAS_PHONE_NUMBER is false), then no query should happen. 266 267 // setup of various bits required for querying 268 final Context mockContext = mock(Context.class); 269 final ContentResolver mockContentResolver = mock(ContentResolver.class); 270 when(mockContext.getContentResolver()).thenReturn(mockContentResolver); 271 final int contactId = 12345; 272 final Uri lookupUri = Uri.withAppendedPath( 273 ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId)); 274 275 // when the contact is looked up, we return a cursor that has one entry whose info is: 276 // _ID: 1 277 // LOOKUP_KEY: "testlookupkey" 278 // STARRED: 0 279 // HAS_PHONE_NUMBER: 0 280 Cursor cursor = makeMockCursor(1, "testlookupkey", 0, 0); 281 when(mockContentResolver.query(any(), any(), any(), any(), any())).thenReturn(cursor); 282 283 // call searchContacts and then mergePhoneNumbers, make sure we never actually 284 // query the content resolver for a phone number 285 PeopleRankingReconsideration.searchContactsAndLookupNumbers(mockContext, lookupUri); 286 verify(mockContentResolver, never()).query( 287 eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), 288 eq(ValidateNotificationPeople.PHONE_LOOKUP_PROJECTION), 289 contains(ContactsContract.Contacts.LOOKUP_KEY), 290 any(), // selection args 291 isNull()); // sort order 292 } 293 294 @Test testMergePhoneNumbers_hasNumber()295 public void testMergePhoneNumbers_hasNumber() { 296 // If merge phone number is called and the contact lookup has a phone number, 297 // make sure there's then a subsequent query for the phone number. 298 299 // setup of various bits required for querying 300 final Context mockContext = mock(Context.class); 301 final ContentResolver mockContentResolver = mock(ContentResolver.class); 302 when(mockContext.getContentResolver()).thenReturn(mockContentResolver); 303 final int contactId = 12345; 304 final Uri lookupUri = Uri.withAppendedPath( 305 ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId)); 306 307 // when the contact is looked up, we return a cursor that has one entry whose info is: 308 // _ID: 1 309 // LOOKUP_KEY: "testlookupkey" 310 // STARRED: 0 311 // HAS_PHONE_NUMBER: 1 312 Cursor cursor = makeMockCursor(1, "testlookupkey", 0, 1); 313 314 // make sure to add some specifics so this cursor is only returned for the 315 // contacts database lookup. 316 when(mockContentResolver.query(eq(lookupUri), any(), 317 isNull(), isNull(), isNull())).thenReturn(cursor); 318 319 // in the case of a phone lookup, return null cursor; that's not an error case 320 // and we're not checking the actual storing of the phone data here. 321 when(mockContentResolver.query(eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), 322 eq(ValidateNotificationPeople.PHONE_LOOKUP_PROJECTION), 323 contains(ContactsContract.Contacts.LOOKUP_KEY), 324 any(), isNull())).thenReturn(null); 325 326 // call searchContacts and then mergePhoneNumbers, and check that we query 327 // once for the 328 PeopleRankingReconsideration.searchContactsAndLookupNumbers(mockContext, lookupUri); 329 verify(mockContentResolver, times(1)).query( 330 eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), 331 eq(ValidateNotificationPeople.PHONE_LOOKUP_PROJECTION), 332 contains(ContactsContract.Contacts.LOOKUP_KEY), 333 eq(new String[] { "testlookupkey" }), // selection args 334 isNull()); // sort order 335 } 336 337 @Test testValidatePeople_needsLookupWhenNoCache()338 public void testValidatePeople_needsLookupWhenNoCache() { 339 final Context mockContext = mock(Context.class); 340 final ContentResolver mockContentResolver = mock(ContentResolver.class); 341 when(mockContext.getContentResolver()).thenReturn(mockContentResolver); 342 final NotificationUsageStats mockNotificationUsageStats = 343 mock(NotificationUsageStats.class); 344 345 // Create validator with empty cache 346 ValidateNotificationPeople vnp = new ValidateNotificationPeople(); 347 LruCache<String, LookupResult> cache = new LruCache<>(5); 348 vnp.initForTests(mockContext, mockNotificationUsageStats, cache); 349 350 NotificationRecord record = getNotificationRecord(); 351 String[] callNumber = new String[]{"tel:12345678910"}; 352 setNotificationPeople(record, callNumber); 353 354 // Returned ranking reconsideration not null indicates that there is a lookup to be done 355 RankingReconsideration rr = vnp.validatePeople(mockContext, record); 356 assertNotNull(rr); 357 } 358 359 @Test testValidatePeople_noLookupWhenCached_andPopulatesContactInfo()360 public void testValidatePeople_noLookupWhenCached_andPopulatesContactInfo() { 361 final Context mockContext = mock(Context.class); 362 final ContentResolver mockContentResolver = mock(ContentResolver.class); 363 when(mockContext.getContentResolver()).thenReturn(mockContentResolver); 364 when(mockContext.getUserId()).thenReturn(1); 365 final NotificationUsageStats mockNotificationUsageStats = 366 mock(NotificationUsageStats.class); 367 368 // Information to be passed in & returned from the lookup result 369 String lookup = "lookup:contactinfohere"; 370 String lookupTel = "16175551234"; 371 float affinity = 0.7f; 372 373 // Create a fake LookupResult for the data we'll pass in 374 LruCache<String, LookupResult> cache = new LruCache<>(5); 375 LookupResult lr = mock(LookupResult.class); 376 when(lr.getAffinity()).thenReturn(affinity); 377 when(lr.getPhoneNumbers()).thenReturn(new ArraySet<>(new String[]{lookupTel})); 378 when(lr.isExpired()).thenReturn(false); 379 cache.put(ValidateNotificationPeople.getCacheKey(1, lookup), lr); 380 381 // Create validator with the established cache 382 ValidateNotificationPeople vnp = new ValidateNotificationPeople(); 383 vnp.initForTests(mockContext, mockNotificationUsageStats, cache); 384 385 NotificationRecord record = getNotificationRecord(); 386 String[] peopleInfo = new String[]{lookup}; 387 setNotificationPeople(record, peopleInfo); 388 389 // Returned ranking reconsideration null indicates that there is no pending work to be done 390 RankingReconsideration rr = vnp.validatePeople(mockContext, record); 391 assertNull(rr); 392 393 // Confirm that the affinity & phone number made it into our record 394 assertEquals(affinity, record.getContactAffinity(), 1e-8); 395 assertNotNull(record.getPhoneNumbers()); 396 assertTrue(record.getPhoneNumbers().contains(lookupTel)); 397 } 398 399 @Test validatePeople_reconsiderationWillNotBeDelayed()400 public void validatePeople_reconsiderationWillNotBeDelayed() { 401 final Context mockContext = mock(Context.class); 402 final ContentResolver mockContentResolver = mock(ContentResolver.class); 403 when(mockContext.getContentResolver()).thenReturn(mockContentResolver); 404 ValidateNotificationPeople vnp = new ValidateNotificationPeople(); 405 vnp.initForTests(mockContext, mock(NotificationUsageStats.class), new LruCache<>(5)); 406 NotificationRecord record = getNotificationRecord(); 407 String[] callNumber = new String[]{"tel:12345678910"}; 408 setNotificationPeople(record, callNumber); 409 410 RankingReconsideration rr = vnp.validatePeople(mockContext, record); 411 412 assertThat(rr).isNotNull(); 413 assertThat(rr.getDelay(TimeUnit.MILLISECONDS)).isEqualTo(0); 414 } 415 416 // Creates a cursor that points to one item of Contacts data with the specified 417 // columns. makeMockCursor(int id, String lookupKey, int starred, int hasPhone)418 private Cursor makeMockCursor(int id, String lookupKey, int starred, int hasPhone) { 419 Cursor mockCursor = mock(Cursor.class); 420 when(mockCursor.moveToFirst()).thenReturn(true); 421 doAnswer(new Answer<Boolean>() { 422 boolean mAccessed = false; 423 @Override 424 public Boolean answer(InvocationOnMock invocation) throws Throwable { 425 if (!mAccessed) { 426 mAccessed = true; 427 return true; 428 } 429 return false; 430 } 431 432 }).when(mockCursor).moveToNext(); 433 434 // id 435 when(mockCursor.getColumnIndex(ContactsContract.Contacts._ID)).thenReturn(0); 436 when(mockCursor.getInt(0)).thenReturn(id); 437 438 // lookup key 439 when(mockCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)).thenReturn(1); 440 when(mockCursor.getString(1)).thenReturn(lookupKey); 441 442 // starred 443 when(mockCursor.getColumnIndex(ContactsContract.Contacts.STARRED)).thenReturn(2); 444 when(mockCursor.getInt(2)).thenReturn(starred); 445 446 // has phone number 447 when(mockCursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)).thenReturn(3); 448 when(mockCursor.getInt(3)).thenReturn(hasPhone); 449 450 return mockCursor; 451 } 452 assertStringArrayEquals(String message, String[] expected, String[] result)453 private void assertStringArrayEquals(String message, String[] expected, String[] result) { 454 String expectedString = Arrays.toString(expected); 455 String resultString = Arrays.toString(result); 456 assertEquals(message + ": arrays differ", expectedString, resultString); 457 } 458 getNotificationRecord()459 private NotificationRecord getNotificationRecord() { 460 StatusBarNotification sbn = mock(StatusBarNotification.class); 461 Notification notification = mock(Notification.class); 462 when(sbn.getNotification()).thenReturn(notification); 463 return new NotificationRecord(mContext, sbn, mock(NotificationChannel.class)); 464 } 465 setNotificationPeople(NotificationRecord r, String[] people)466 private void setNotificationPeople(NotificationRecord r, String[] people) { 467 Bundle extras = new Bundle(); 468 extras.putObject(Notification.EXTRA_PEOPLE_LIST, people); 469 r.getSbn().getNotification().extras = extras; 470 } 471 } 472