| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.contacts; |
| |
| import android.annotation.TargetApi; |
| import android.app.job.JobScheduler; |
| import android.content.ContentProvider; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.pm.ShortcutInfo; |
| import android.content.pm.ShortcutManager; |
| import android.database.Cursor; |
| import android.database.MatrixCursor; |
| import android.net.Uri; |
| import android.os.Build; |
| import android.provider.ContactsContract; |
| import android.provider.ContactsContract.Contacts; |
| import android.support.test.filters.SdkSuppress; |
| import android.test.AndroidTestCase; |
| import android.test.mock.MockContentResolver; |
| import android.test.suitebuilder.annotation.SmallTest; |
| |
| import com.android.contacts.test.mocks.MockContentProvider; |
| |
| import org.hamcrest.BaseMatcher; |
| import org.hamcrest.Description; |
| import org.hamcrest.Matcher; |
| import org.hamcrest.Matchers; |
| import org.mockito.ArgumentCaptor; |
| |
| import java.lang.reflect.Method; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.List; |
| |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.hamcrest.Matchers.equalTo; |
| import static org.mockito.Matchers.anyString; |
| import static org.mockito.Matchers.eq; |
| import static org.mockito.Mockito.mock; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.when; |
| |
| @TargetApi(Build.VERSION_CODES.N_MR1) |
| @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N_MR1) |
| @SmallTest |
| public class DynamicShortcutsTests extends AndroidTestCase { |
| |
| |
| @Override |
| protected void tearDown() throws Exception { |
| super.tearDown(); |
| |
| // Clean up the job if it was scheduled by these tests. |
| final JobScheduler scheduler = (JobScheduler) getContext() |
| .getSystemService(Context.JOB_SCHEDULER_SERVICE); |
| scheduler.cancel(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID); |
| } |
| |
| // Basic smoke test to make sure the queries executed by DynamicShortcuts are valid as well |
| // as the integration with ShortcutManager. Note that this may change the state of the shortcuts |
| // on the device it is executed on. |
| public void test_refresh_doesntCrash() { |
| final DynamicShortcuts sut = new DynamicShortcuts(getContext()); |
| sut.refresh(); |
| // Pass because it didn't throw an exception. |
| } |
| |
| public void test_createShortcutFromRow_hasCorrectResult() { |
| final DynamicShortcuts sut = createDynamicShortcuts(); |
| |
| final Cursor row = queryResult( |
| // ID, LOOKUP_KEY, DISPLAY_NAME_PRIMARY |
| 1l, "lookup_key", "John Smith" |
| ); |
| |
| row.moveToFirst(); |
| final ShortcutInfo shortcut = sut.builderForContactShortcut(row).build(); |
| |
| assertEquals("lookup_key", shortcut.getId()); |
| assertEquals(Contacts.getLookupUri(1, "lookup_key"), shortcut.getIntent().getData()); |
| assertEquals(ContactsContract.QuickContact.ACTION_QUICK_CONTACT, |
| shortcut.getIntent().getAction()); |
| assertEquals("John Smith", shortcut.getShortLabel()); |
| assertEquals("John Smith", shortcut.getLongLabel()); |
| assertEquals(1l, shortcut.getExtras().getLong(Contacts._ID)); |
| } |
| |
| public void test_builderForContactShortcut_returnsNullWhenNameIsNull() { |
| final DynamicShortcuts sut = createDynamicShortcuts(); |
| |
| final ShortcutInfo.Builder shortcut = sut.builderForContactShortcut(1l, "lookup_key", null); |
| |
| assertNull(shortcut); |
| } |
| |
| public void test_builderForContactShortcut_ellipsizesLongNamesForLabels() { |
| final DynamicShortcuts sut = createDynamicShortcuts(); |
| sut.setShortLabelMaxLength(5); |
| sut.setLongLabelMaxLength(10); |
| |
| final ShortcutInfo shortcut = sut.builderForContactShortcut(1l, "lookup_key", |
| "123456789 1011").build(); |
| |
| assertEquals("1234…", shortcut.getShortLabel()); |
| assertEquals("123456789…", shortcut.getLongLabel()); |
| } |
| |
| public void test_updatePinned_disablesShortcutsForRemovedContacts() throws Exception { |
| final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); |
| when(mockShortcutManager.getPinnedShortcuts()).thenReturn( |
| Collections.singletonList(makeDynamic(shortcutFor(1l, "key1", "name1")))); |
| |
| final DynamicShortcuts sut = createDynamicShortcuts(emptyResolver(), mockShortcutManager); |
| |
| sut.updatePinned(); |
| |
| verify(mockShortcutManager).disableShortcuts( |
| eq(Collections.singletonList("key1")), anyString()); |
| } |
| |
| public void test_updatePinned_updatesExistingShortcutsWithMatchingKeys() throws Exception { |
| final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); |
| when(mockShortcutManager.getPinnedShortcuts()).thenReturn( |
| Arrays.asList( |
| makeDynamic(shortcutFor(1l, "key1", "name1")), |
| makeDynamic(shortcutFor(2l, "key2", "name2")), |
| makeDynamic(shortcutFor(3l, "key3", "name3")) |
| )); |
| |
| final DynamicShortcuts sut = createDynamicShortcuts(resolverWithExpectedQueries( |
| queryForSingleRow(Contacts.getLookupUri(1l, "key1"), 11l, "key1", "New Name1"), |
| queryForSingleRow(Contacts.getLookupUri(2l, "key2"), 2l, "key2", "name2"), |
| queryForSingleRow(Contacts.getLookupUri(3l, "key3"), 33l, "key3", "name3") |
| ), mockShortcutManager); |
| |
| sut.updatePinned(); |
| |
| final ArgumentCaptor<List<ShortcutInfo>> updateArgs = |
| ArgumentCaptor.forClass((Class) List.class); |
| |
| verify(mockShortcutManager).disableShortcuts( |
| eq(Collections.<String>emptyList()), anyString()); |
| verify(mockShortcutManager).updateShortcuts(updateArgs.capture()); |
| |
| final List<ShortcutInfo> arg = updateArgs.getValue(); |
| assertThat(arg.size(), equalTo(3)); |
| assertThat(arg.get(0), |
| isShortcutForContact(11l, "key1", "New Name1")); |
| assertThat(arg.get(1), |
| isShortcutForContact(2l, "key2", "name2")); |
| assertThat(arg.get(2), |
| isShortcutForContact(33l, "key3", "name3")); |
| } |
| |
| public void test_refresh_setsDynamicShortcutsToStrequentContacts() { |
| final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); |
| when(mockShortcutManager.getPinnedShortcuts()).thenReturn( |
| Collections.<ShortcutInfo>emptyList()); |
| final DynamicShortcuts sut = createDynamicShortcuts(resolverWithExpectedQueries( |
| queryFor(Contacts.CONTENT_STREQUENT_URI, |
| 1l, "starred_key", "starred name", |
| 2l, "freq_key", "freq name", |
| 3l, "starred_2", "Starred Two")), mockShortcutManager); |
| |
| sut.refresh(); |
| |
| final ArgumentCaptor<List<ShortcutInfo>> updateArgs = |
| ArgumentCaptor.forClass((Class) List.class); |
| |
| verify(mockShortcutManager).setDynamicShortcuts(updateArgs.capture()); |
| |
| final List<ShortcutInfo> arg = updateArgs.getValue(); |
| assertThat(arg.size(), equalTo(3)); |
| assertThat(arg.get(0), isShortcutForContact(1l, "starred_key", "starred name")); |
| assertThat(arg.get(1), isShortcutForContact(2l, "freq_key", "freq name")); |
| assertThat(arg.get(2), isShortcutForContact(3l, "starred_2", "Starred Two")); |
| } |
| |
| public void test_refresh_skipsContactsWithNullName() { |
| final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); |
| when(mockShortcutManager.getPinnedShortcuts()).thenReturn( |
| Collections.<ShortcutInfo>emptyList()); |
| final DynamicShortcuts sut = createDynamicShortcuts(resolverWithExpectedQueries( |
| queryFor(Contacts.CONTENT_STREQUENT_URI, |
| 1l, "key1", "first", |
| 2l, "key2", "second", |
| 3l, "key3", null, |
| 4l, null, null, |
| 5l, "key5", "fifth", |
| 6l, "key6", "sixth")), mockShortcutManager); |
| |
| sut.refresh(); |
| |
| final ArgumentCaptor<List<ShortcutInfo>> updateArgs = |
| ArgumentCaptor.forClass((Class) List.class); |
| |
| verify(mockShortcutManager).setDynamicShortcuts(updateArgs.capture()); |
| |
| final List<ShortcutInfo> arg = updateArgs.getValue(); |
| assertThat(arg.size(), equalTo(3)); |
| assertThat(arg.get(0), isShortcutForContact(1l, "key1", "first")); |
| assertThat(arg.get(1), isShortcutForContact(2l, "key2", "second")); |
| assertThat(arg.get(2), isShortcutForContact(5l, "key5", "fifth")); |
| |
| |
| // Also verify that it doesn't crash if there are fewer than 3 valid strequent contacts |
| createDynamicShortcuts(resolverWithExpectedQueries( |
| queryFor(Contacts.CONTENT_STREQUENT_URI, |
| 1l, "key1", "first", |
| 2l, "key2", "second", |
| 3l, "key3", null, |
| 4l, null, null)), mock(ShortcutManager.class)).refresh(); |
| } |
| |
| |
| public void test_handleFlagDisabled_stopsJob() { |
| final ShortcutManager mockShortcutManager = mock(ShortcutManager.class); |
| final JobScheduler mockJobScheduler = mock(JobScheduler.class); |
| final DynamicShortcuts sut = createDynamicShortcuts(emptyResolver(), mockShortcutManager, |
| mockJobScheduler); |
| |
| sut.handleFlagDisabled(); |
| |
| verify(mockJobScheduler).cancel(eq(ContactsJobService.DYNAMIC_SHORTCUTS_JOB_ID)); |
| } |
| |
| |
| public void test_scheduleUpdateJob_schedulesJob() { |
| final DynamicShortcuts sut = new DynamicShortcuts(getContext()); |
| sut.scheduleUpdateJob(); |
| assertThat(DynamicShortcuts.isJobScheduled(getContext()), Matchers.is(true)); |
| } |
| |
| private Matcher<ShortcutInfo> isShortcutForContact(final long id, |
| final String lookupKey, final String name) { |
| return new BaseMatcher<ShortcutInfo>() { |
| @Override |
| public boolean matches(Object o) { |
| if (!(o instanceof ShortcutInfo)) return false; |
| final ShortcutInfo other = (ShortcutInfo)o; |
| return id == other.getExtras().getLong(Contacts._ID) |
| && lookupKey.equals(other.getId()) |
| && name.equals(other.getLongLabel()) |
| && name.equals(other.getShortLabel()); |
| } |
| |
| @Override |
| public void describeTo(Description description) { |
| description.appendText("Should be a shortcut for contact with _ID=" + id + |
| " lookup=" + lookupKey + " and display_name=" + name); |
| } |
| }; |
| } |
| |
| private ShortcutInfo shortcutFor(long contactId, String lookupKey, String name) { |
| return new DynamicShortcuts(getContext()) |
| .builderForContactShortcut(contactId, lookupKey, name).build(); |
| } |
| |
| private ContentResolver emptyResolver() { |
| final MockContentProvider provider = new MockContentProvider(); |
| provider.expect(MockContentProvider.Query.forAnyUri()) |
| .withAnyProjection() |
| .withAnySelection() |
| .withAnySortOrder() |
| .returnEmptyCursor(); |
| return resolverWithContactsProvider(provider); |
| } |
| |
| private MockContentProvider.Query queryFor(Uri uri, Object... rows) { |
| final MockContentProvider.Query query = MockContentProvider.Query |
| .forUrisMatching(uri.getAuthority(), uri.getPath()) |
| .withProjection(DynamicShortcuts.PROJECTION) |
| .withAnySelection() |
| .withAnySortOrder(); |
| |
| populateQueryRows(query, DynamicShortcuts.PROJECTION.length, rows); |
| return query; |
| } |
| |
| private MockContentProvider.Query queryForSingleRow(Uri uri, Object... row) { |
| return new MockContentProvider.Query(uri) |
| .withProjection(DynamicShortcuts.PROJECTION) |
| .withAnySelection() |
| .withAnySortOrder() |
| .returnRow(row); |
| } |
| |
| private ContentResolver resolverWithExpectedQueries(MockContentProvider.Query... queries) { |
| final MockContentProvider provider = new MockContentProvider(); |
| for (MockContentProvider.Query query : queries) { |
| provider.expect(query); |
| } |
| return resolverWithContactsProvider(provider); |
| } |
| |
| private ContentResolver resolverWithContactsProvider(ContentProvider provider) { |
| final MockContentResolver resolver = new MockContentResolver(); |
| resolver.addProvider(ContactsContract.AUTHORITY, provider); |
| return resolver; |
| } |
| |
| private DynamicShortcuts createDynamicShortcuts() { |
| return createDynamicShortcuts(emptyResolver(), mock(ShortcutManager.class)); |
| } |
| |
| |
| private DynamicShortcuts createDynamicShortcuts(ContentResolver resolver, |
| ShortcutManager shortcutManager) { |
| return createDynamicShortcuts(resolver, shortcutManager, mock(JobScheduler.class)); |
| } |
| |
| private DynamicShortcuts createDynamicShortcuts(ContentResolver resolver, |
| ShortcutManager shortcutManager, JobScheduler jobScheduler) { |
| final DynamicShortcuts result = new DynamicShortcuts(getContext(), resolver, |
| shortcutManager, jobScheduler); |
| // Use very long label limits to make checking shortcuts easier to understand |
| result.setShortLabelMaxLength(100); |
| result.setLongLabelMaxLength(100); |
| return result; |
| } |
| |
| private void populateQueryRows(MockContentProvider.Query query, int numColumns, |
| Object... rows) { |
| for (int i = 0; i < rows.length; i += numColumns) { |
| Object[] row = new Object[numColumns]; |
| for (int j = 0; j < numColumns; j++) { |
| row[j] = rows[i + j]; |
| } |
| query.returnRow(row); |
| } |
| } |
| |
| private Cursor queryResult(Object... values) { |
| return queryResult(DynamicShortcuts.PROJECTION, values); |
| } |
| |
| // Ugly hack because the API is hidden. Alternative is to actually set the shortcut on the real |
| // ShortcutManager but this seems simpler for now. |
| private ShortcutInfo makeDynamic(ShortcutInfo shortcutInfo) throws Exception { |
| final Method addFlagsMethod = ShortcutInfo.class.getMethod("addFlags", int.class); |
| // 1 = FLAG_DYNAMIC |
| addFlagsMethod.invoke(shortcutInfo, 1); |
| return shortcutInfo; |
| } |
| |
| private Cursor queryResult(String[] columns, Object... values) { |
| MatrixCursor result = new MatrixCursor(new String[] { |
| Contacts._ID, Contacts.LOOKUP_KEY, |
| Contacts.DISPLAY_NAME_PRIMARY |
| }); |
| for (int i = 0; i < values.length; i += columns.length) { |
| MatrixCursor.RowBuilder builder = result.newRow(); |
| for (int j = 0; j < columns.length; j++) { |
| builder.add(values[i + j]); |
| } |
| } |
| return result; |
| } |
| } |