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