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