• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.contacts;
17 
18 import android.annotation.TargetApi;
19 import android.app.job.JobScheduler;
20 import android.content.ContentProvider;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.pm.ShortcutInfo;
24 import android.content.pm.ShortcutManager;
25 import android.database.Cursor;
26 import android.database.MatrixCursor;
27 import android.net.Uri;
28 import android.os.Build;
29 import android.provider.ContactsContract;
30 import android.provider.ContactsContract.Contacts;
31 import android.support.test.filters.SdkSuppress;
32 import android.test.AndroidTestCase;
33 import android.test.mock.MockContentResolver;
34 import android.test.suitebuilder.annotation.SmallTest;
35 
36 import com.android.contacts.test.mocks.MockContentProvider;
37 
38 import org.hamcrest.BaseMatcher;
39 import org.hamcrest.Description;
40 import org.hamcrest.Matcher;
41 import org.hamcrest.Matchers;
42 import org.mockito.ArgumentCaptor;
43 
44 import java.lang.reflect.Method;
45 import java.util.Arrays;
46 import java.util.Collections;
47 import java.util.List;
48 
49 import static org.hamcrest.MatcherAssert.assertThat;
50 import static org.hamcrest.Matchers.equalTo;
51 import static org.mockito.Matchers.anyString;
52 import static org.mockito.Matchers.eq;
53 import static org.mockito.Mockito.mock;
54 import static org.mockito.Mockito.verify;
55 import static org.mockito.Mockito.when;
56 
57 @TargetApi(Build.VERSION_CODES.N_MR1)
58 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N_MR1)
59 @SmallTest
60 public class DynamicShortcutsTests extends AndroidTestCase {
61 
62 
63     @Override
tearDown()64     protected void tearDown() throws Exception {
65         super.tearDown();
66 
67         // Clean up the job if it was scheduled by these tests.
68         final JobScheduler scheduler = (JobScheduler) getContext()
69                 .getSystemService(Context.JOB_SCHEDULER_SERVICE);
70         scheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID);
71     }
72 
73     // Basic smoke test to make sure the queries executed by DynamicShortcuts are valid as well
74     // as the integration with ShortcutManager. Note that this may change the state of the shortcuts
75     // on the device it is executed on.
test_refresh_doesntCrash()76     public void test_refresh_doesntCrash() {
77         final DynamicShortcuts sut = new DynamicShortcuts(getContext());
78         sut.refresh();
79         // Pass because it didn't throw an exception.
80     }
81 
test_createShortcutFromRow_hasCorrectResult()82     public void test_createShortcutFromRow_hasCorrectResult() {
83         final DynamicShortcuts sut = createDynamicShortcuts();
84 
85         final Cursor row = queryResult(
86                 // ID, LOOKUP_KEY, DISPLAY_NAME_PRIMARY
87                 1l, "lookup_key", "John Smith"
88         );
89 
90         row.moveToFirst();
91         final ShortcutInfo shortcut = sut.builderForContactShortcut(row).build();
92 
93         assertEquals("lookup_key", shortcut.getId());
94         assertEquals(Contacts.getLookupUri(1, "lookup_key"), shortcut.getIntent().getData());
95         assertEquals(ContactsContract.QuickContact.ACTION_QUICK_CONTACT,
96                 shortcut.getIntent().getAction());
97         assertEquals("John Smith", shortcut.getShortLabel());
98         assertEquals("John Smith", shortcut.getLongLabel());
99         assertEquals(1l, shortcut.getExtras().getLong(Contacts._ID));
100     }
101 
test_builderForContactShortcut_returnsNullWhenNameIsNull()102     public void test_builderForContactShortcut_returnsNullWhenNameIsNull() {
103         final DynamicShortcuts sut = createDynamicShortcuts();
104 
105         final ShortcutInfo.Builder shortcut = sut.builderForContactShortcut(1l, "lookup_key", null);
106 
107         assertNull(shortcut);
108     }
109 
test_builderForContactShortcut_ellipsizesLongNamesForLabels()110     public void test_builderForContactShortcut_ellipsizesLongNamesForLabels() {
111         final DynamicShortcuts sut = createDynamicShortcuts();
112         sut.setShortLabelMaxLength(5);
113         sut.setLongLabelMaxLength(10);
114 
115         final ShortcutInfo shortcut = sut.builderForContactShortcut(1l, "lookup_key",
116                 "123456789 1011").build();
117 
118         assertEquals("1234…", shortcut.getShortLabel());
119         assertEquals("123456789…", shortcut.getLongLabel());
120     }
121 
test_updatePinned_disablesShortcutsForRemovedContacts()122     public void test_updatePinned_disablesShortcutsForRemovedContacts() throws Exception {
123         final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
124         when(mockShortcutManager.getPinnedShortcuts()).thenReturn(
125                 Collections.singletonList(makeDynamic(shortcutFor(1l, "key1", "name1"))));
126 
127         final DynamicShortcuts sut = createDynamicShortcuts(emptyResolver(), mockShortcutManager);
128 
129         sut.updatePinned();
130 
131         verify(mockShortcutManager).disableShortcuts(
132                 eq(Collections.singletonList("key1")), anyString());
133     }
134 
test_updatePinned_updatesExistingShortcutsWithMatchingKeys()135     public void test_updatePinned_updatesExistingShortcutsWithMatchingKeys() throws Exception {
136         final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
137         when(mockShortcutManager.getPinnedShortcuts()).thenReturn(
138                 Arrays.asList(
139                         makeDynamic(shortcutFor(1l, "key1", "name1")),
140                         makeDynamic(shortcutFor(2l, "key2", "name2")),
141                         makeDynamic(shortcutFor(3l, "key3", "name3"))
142                 ));
143 
144         final DynamicShortcuts sut = createDynamicShortcuts(resolverWithExpectedQueries(
145                 queryForSingleRow(Contacts.getLookupUri(1l, "key1"), 11l, "key1", "New Name1"),
146                 queryForSingleRow(Contacts.getLookupUri(2l, "key2"), 2l, "key2", "name2"),
147                 queryForSingleRow(Contacts.getLookupUri(3l, "key3"), 33l, "key3", "name3")
148         ), mockShortcutManager);
149 
150         sut.updatePinned();
151 
152         final ArgumentCaptor<List<ShortcutInfo>> updateArgs =
153                 ArgumentCaptor.forClass((Class) List.class);
154 
155         verify(mockShortcutManager).disableShortcuts(
156                 eq(Collections.<String>emptyList()), anyString());
157         verify(mockShortcutManager).updateShortcuts(updateArgs.capture());
158 
159         final List<ShortcutInfo> arg = updateArgs.getValue();
160         assertThat(arg.size(), equalTo(3));
161         assertThat(arg.get(0),
162                 isShortcutForContact(11l, "key1", "New Name1"));
163         assertThat(arg.get(1),
164                 isShortcutForContact(2l, "key2", "name2"));
165         assertThat(arg.get(2),
166                 isShortcutForContact(33l, "key3", "name3"));
167     }
168 
test_refresh_setsDynamicShortcutsToStrequentContacts()169     public void test_refresh_setsDynamicShortcutsToStrequentContacts() {
170         final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
171         when(mockShortcutManager.getPinnedShortcuts()).thenReturn(
172                 Collections.<ShortcutInfo>emptyList());
173         final DynamicShortcuts sut = createDynamicShortcuts(resolverWithExpectedQueries(
174                 queryFor(Contacts.CONTENT_STREQUENT_URI,
175                         1l, "starred_key", "starred name",
176                         2l, "freq_key", "freq name",
177                         3l, "starred_2", "Starred Two")), mockShortcutManager);
178 
179         sut.refresh();
180 
181         final ArgumentCaptor<List<ShortcutInfo>> updateArgs =
182                 ArgumentCaptor.forClass((Class) List.class);
183 
184         verify(mockShortcutManager).setDynamicShortcuts(updateArgs.capture());
185 
186         final List<ShortcutInfo> arg = updateArgs.getValue();
187         assertThat(arg.size(), equalTo(3));
188         assertThat(arg.get(0), isShortcutForContact(1l, "starred_key", "starred name"));
189         assertThat(arg.get(1), isShortcutForContact(2l, "freq_key", "freq name"));
190         assertThat(arg.get(2), isShortcutForContact(3l, "starred_2", "Starred Two"));
191     }
192 
test_refresh_skipsContactsWithNullName()193     public void test_refresh_skipsContactsWithNullName() {
194         final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
195         when(mockShortcutManager.getPinnedShortcuts()).thenReturn(
196                 Collections.<ShortcutInfo>emptyList());
197         final DynamicShortcuts sut = createDynamicShortcuts(resolverWithExpectedQueries(
198                 queryFor(Contacts.CONTENT_STREQUENT_URI,
199                         1l, "key1", "first",
200                         2l, "key2", "second",
201                         3l, "key3", null,
202                         4l, null, null,
203                         5l, "key5", "fifth",
204                         6l, "key6", "sixth")), mockShortcutManager);
205 
206         sut.refresh();
207 
208         final ArgumentCaptor<List<ShortcutInfo>> updateArgs =
209                 ArgumentCaptor.forClass((Class) List.class);
210 
211         verify(mockShortcutManager).setDynamicShortcuts(updateArgs.capture());
212 
213         final List<ShortcutInfo> arg = updateArgs.getValue();
214         assertThat(arg.size(), equalTo(3));
215         assertThat(arg.get(0), isShortcutForContact(1l, "key1", "first"));
216         assertThat(arg.get(1), isShortcutForContact(2l, "key2", "second"));
217         assertThat(arg.get(2), isShortcutForContact(5l, "key5", "fifth"));
218 
219 
220         // Also verify that it doesn't crash if there are fewer than 3 valid strequent contacts
221         createDynamicShortcuts(resolverWithExpectedQueries(
222                 queryFor(Contacts.CONTENT_STREQUENT_URI,
223                         1l, "key1", "first",
224                         2l, "key2", "second",
225                         3l, "key3", null,
226                         4l, null, null)), mock(ShortcutManager.class)).refresh();
227     }
228 
229 
test_handleFlagDisabled_stopsJob()230     public void test_handleFlagDisabled_stopsJob() {
231         final ShortcutManager mockShortcutManager = mock(ShortcutManager.class);
232         final JobScheduler mockJobScheduler = mock(JobScheduler.class);
233         final DynamicShortcuts sut = createDynamicShortcuts(emptyResolver(), mockShortcutManager,
234                 mockJobScheduler);
235 
236         sut.handleFlagDisabled();
237 
238         verify(mockJobScheduler).cancel(eq(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID));
239     }
240 
241 
test_scheduleUpdateJob_schedulesJob()242     public void test_scheduleUpdateJob_schedulesJob() {
243         final DynamicShortcuts sut = new DynamicShortcuts(getContext());
244         sut.scheduleUpdateJob();
245         assertThat(DynamicShortcuts.isJobScheduled(getContext()), Matchers.is(true));
246     }
247 
isShortcutForContact(final long id, final String lookupKey, final String name)248     private Matcher<ShortcutInfo> isShortcutForContact(final long id,
249             final String lookupKey, final String name) {
250         return new BaseMatcher<ShortcutInfo>() {
251             @Override
252             public boolean matches(Object o) {
253                 if (!(o instanceof  ShortcutInfo)) return false;
254                 final ShortcutInfo other = (ShortcutInfo)o;
255                 return id == other.getExtras().getLong(Contacts._ID)
256                         && lookupKey.equals(other.getId())
257                         && name.equals(other.getLongLabel())
258                         && name.equals(other.getShortLabel());
259             }
260 
261             @Override
262             public void describeTo(Description description) {
263                 description.appendText("Should be a shortcut for contact with _ID=" + id +
264                         " lookup=" + lookupKey + " and display_name=" + name);
265             }
266         };
267     }
268 
269     private ShortcutInfo shortcutFor(long contactId, String lookupKey, String name) {
270         return new DynamicShortcuts(getContext())
271                 .builderForContactShortcut(contactId, lookupKey, name).build();
272     }
273 
274     private ContentResolver emptyResolver() {
275         final MockContentProvider provider = new MockContentProvider();
276         provider.expect(MockContentProvider.Query.forAnyUri())
277                 .withAnyProjection()
278                 .withAnySelection()
279                 .withAnySortOrder()
280                 .returnEmptyCursor();
281         return resolverWithContactsProvider(provider);
282     }
283 
284     private MockContentProvider.Query queryFor(Uri uri, Object... rows) {
285         final MockContentProvider.Query query = MockContentProvider.Query
286                 .forUrisMatching(uri.getAuthority(), uri.getPath())
287                 .withProjection(DynamicShortcuts.PROJECTION)
288                 .withAnySelection()
289                 .withAnySortOrder();
290 
291         populateQueryRows(query, DynamicShortcuts.PROJECTION.length, rows);
292         return query;
293     }
294 
295     private MockContentProvider.Query queryForSingleRow(Uri uri, Object... row) {
296         return new MockContentProvider.Query(uri)
297                 .withProjection(DynamicShortcuts.PROJECTION)
298                 .withAnySelection()
299                 .withAnySortOrder()
300                 .returnRow(row);
301     }
302 
303     private ContentResolver resolverWithExpectedQueries(MockContentProvider.Query... queries) {
304         final MockContentProvider provider = new MockContentProvider();
305         for (MockContentProvider.Query query : queries) {
306             provider.expect(query);
307         }
308         return resolverWithContactsProvider(provider);
309     }
310 
311     private ContentResolver resolverWithContactsProvider(ContentProvider provider) {
312         final MockContentResolver resolver = new MockContentResolver();
313         resolver.addProvider(ContactsContract.AUTHORITY, provider);
314         return resolver;
315     }
316 
317     private DynamicShortcuts createDynamicShortcuts() {
318         return createDynamicShortcuts(emptyResolver(), mock(ShortcutManager.class));
319     }
320 
321 
322     private DynamicShortcuts createDynamicShortcuts(ContentResolver resolver,
323             ShortcutManager shortcutManager) {
324         return createDynamicShortcuts(resolver, shortcutManager, mock(JobScheduler.class));
325     }
326 
327     private DynamicShortcuts createDynamicShortcuts(ContentResolver resolver,
328             ShortcutManager shortcutManager, JobScheduler jobScheduler) {
329         final DynamicShortcuts result = new DynamicShortcuts(getContext(), resolver,
330                 shortcutManager, jobScheduler);
331         // Use very long label limits to make checking shortcuts easier to understand
332         result.setShortLabelMaxLength(100);
333         result.setLongLabelMaxLength(100);
334         return result;
335     }
336 
337     private void populateQueryRows(MockContentProvider.Query query, int numColumns,
338             Object... rows) {
339         for (int i = 0; i < rows.length; i += numColumns) {
340             Object[] row = new Object[numColumns];
341             for (int j = 0; j < numColumns; j++) {
342                 row[j] = rows[i + j];
343             }
344             query.returnRow(row);
345         }
346     }
347 
348     private Cursor queryResult(Object... values) {
349         return queryResult(DynamicShortcuts.PROJECTION, values);
350     }
351 
352     // Ugly hack because the API is hidden. Alternative is to actually set the shortcut on the real
353     // ShortcutManager but this seems simpler for now.
354     private ShortcutInfo makeDynamic(ShortcutInfo shortcutInfo) throws Exception {
355         final Method addFlagsMethod = ShortcutInfo.class.getMethod("addFlags", int.class);
356         // 1 = FLAG_DYNAMIC
357         addFlagsMethod.invoke(shortcutInfo, 1);
358         return shortcutInfo;
359     }
360 
361     private Cursor queryResult(String[] columns, Object... values) {
362         MatrixCursor result = new MatrixCursor(new String[] {
363                 Contacts._ID, Contacts.LOOKUP_KEY,
364                 Contacts.DISPLAY_NAME_PRIMARY
365         });
366         for (int i = 0; i < values.length; i += columns.length) {
367             MatrixCursor.RowBuilder builder = result.newRow();
368             for (int j = 0; j < columns.length; j++) {
369                 builder.add(values[i + j]);
370             }
371         }
372         return result;
373     }
374 }
375