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