diff options
| author | 2023-03-13 23:11:31 -0700 | |
|---|---|---|
| committer | 2023-03-13 23:11:31 -0700 | |
| commit | 7b21dc4a35cae1218308a2f04fc61d6247faa17b (patch) | |
| tree | 1448e0c76772f25db02c8931f588e0d32673d1d4 /java/tests/src | |
| parent | cc64c57aa426bf71e88dc073b8197748fd720856 (diff) | |
| parent | 1606e219c8db1c233713f9dc2546225533718eca (diff) | |
Merge Android 13 QPR2
Bug: 273316506
Merged-In: Ia56e92ed5358ca66185f5011abd139392ee73785
Change-Id: Ib152678de052bf41ad0716401561c7e505614fe5
Diffstat (limited to 'java/tests/src')
21 files changed, 4450 insertions, 1260 deletions
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java deleted file mode 100644 index e4146cc5..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2020 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.intentresolver; - -import com.android.internal.logging.InstanceId; -import com.android.internal.logging.UiEventLogger; -import com.android.internal.util.FrameworkStatsLog; - -import java.util.ArrayList; -import java.util.List; - -public class ChooserActivityLoggerFake implements ChooserActivityLogger { - static class CallRecord { - // shared fields between all logs - public int atomId; - public String packageName; - public InstanceId instanceId; - - // generic log field - public UiEventLogger.UiEventEnum event; - - // share started fields - public String mimeType; - public int appProvidedDirect; - public int appProvidedApp; - public boolean isWorkprofile; - public int previewType; - public String intent; - - // share completed fields - public int targetType; - public int positionPicked; - public boolean isPinned; - - CallRecord(int atomId, UiEventLogger.UiEventEnum eventId, - String packageName, InstanceId instanceId) { - this.atomId = atomId; - this.packageName = packageName; - this.instanceId = instanceId; - this.event = eventId; - } - - CallRecord(int atomId, String packageName, InstanceId instanceId, String mimeType, - int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType, - String intent) { - this.atomId = atomId; - this.packageName = packageName; - this.instanceId = instanceId; - this.mimeType = mimeType; - this.appProvidedDirect = appProvidedDirect; - this.appProvidedApp = appProvidedApp; - this.isWorkprofile = isWorkprofile; - this.previewType = previewType; - this.intent = intent; - } - - CallRecord(int atomId, String packageName, InstanceId instanceId, int targetType, - int positionPicked, boolean isPinned) { - this.atomId = atomId; - this.packageName = packageName; - this.instanceId = instanceId; - this.targetType = targetType; - this.positionPicked = positionPicked; - this.isPinned = isPinned; - } - - } - private List<CallRecord> mCalls = new ArrayList<>(); - - public int numCalls() { - return mCalls.size(); - } - - List<CallRecord> getCalls() { - return mCalls; - } - - CallRecord get(int index) { - return mCalls.get(index); - } - - UiEventLogger.UiEventEnum event(int index) { - return mCalls.get(index).event; - } - - public void removeCallsForUiEventsOfType(int uiEventType) { - mCalls.removeIf( - call -> - (call.atomId == FrameworkStatsLog.UI_EVENT_REPORTED) - && (call.event.getId() == uiEventType)); - } - - @Override - public void logShareStarted(int eventId, String packageName, String mimeType, - int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType, - String intent) { - mCalls.add(new CallRecord(FrameworkStatsLog.SHARESHEET_STARTED, packageName, - getInstanceId(), mimeType, appProvidedDirect, appProvidedApp, isWorkprofile, - previewType, intent)); - } - - @Override - public void logShareTargetSelected(int targetType, String packageName, int positionPicked, - boolean isPinned) { - mCalls.add(new CallRecord(FrameworkStatsLog.RANKING_SELECTED, packageName, getInstanceId(), - SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(), positionPicked, - isPinned)); - } - - @Override - public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) { - mCalls.add(new CallRecord(FrameworkStatsLog.UI_EVENT_REPORTED, - event, "", instanceId)); - } - - @Override - public InstanceId getInstanceId() { - return InstanceId.fakeInstanceId(-1); - } -} diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java new file mode 100644 index 00000000..705a3228 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2020 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.intentresolver; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.AdditionalMatchers.gt; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import android.content.Intent; +import android.metrics.LogMaker; + +import com.android.intentresolver.ChooserActivityLogger.FrameworkStatsLogger; +import com.android.intentresolver.ChooserActivityLogger.SharesheetStandardEvent; +import com.android.intentresolver.ChooserActivityLogger.SharesheetStartedEvent; +import com.android.intentresolver.ChooserActivityLogger.SharesheetTargetSelectedEvent; +import com.android.internal.logging.InstanceId; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.UiEventLogger; +import com.android.internal.logging.UiEventLogger.UiEventEnum; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.FrameworkStatsLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public final class ChooserActivityLoggerTest { + @Mock private UiEventLogger mUiEventLog; + @Mock private FrameworkStatsLogger mFrameworkLog; + @Mock private MetricsLogger mMetricsLogger; + + private ChooserActivityLogger mChooserLogger; + + @Before + public void setUp() { + //Mockito.reset(mUiEventLog, mFrameworkLog, mMetricsLogger); + mChooserLogger = new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger); + } + + @After + public void tearDown() { + verifyNoMoreInteractions(mUiEventLog); + verifyNoMoreInteractions(mFrameworkLog); + verifyNoMoreInteractions(mMetricsLogger); + } + + @Test + public void testLogChooserActivityShown_personalProfile() { + final boolean isWorkProfile = false; + final String mimeType = "application/TestType"; + final long systemCost = 456; + + mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost); + + ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); + verify(mMetricsLogger).write(eventCaptor.capture()); + LogMaker event = eventCaptor.getValue(); + + assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN); + assertThat(event.getSubtype()).isEqualTo(MetricsEvent.PARENT_PROFILE); + assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType); + assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS)) + .isEqualTo(systemCost); + } + + @Test + public void testLogChooserActivityShown_workProfile() { + final boolean isWorkProfile = true; + final String mimeType = "application/TestType"; + final long systemCost = 456; + + mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost); + + ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); + verify(mMetricsLogger).write(eventCaptor.capture()); + LogMaker event = eventCaptor.getValue(); + + assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN); + assertThat(event.getSubtype()).isEqualTo(MetricsEvent.MANAGED_PROFILE); + assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType); + assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS)) + .isEqualTo(systemCost); + } + + @Test + public void testLogShareStarted() { + final int eventId = -1; // Passed-in eventId is unused. TODO: remove from method signature. + final String packageName = "com.test.foo"; + final String mimeType = "text/plain"; + final int appProvidedDirectTargets = 123; + final int appProvidedAppTargets = 456; + final boolean workProfile = true; + final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_FILE; + final String intentAction = Intent.ACTION_SENDTO; + + mChooserLogger.logShareStarted( + eventId, + packageName, + mimeType, + appProvidedDirectTargets, + appProvidedAppTargets, + workProfile, + previewType, + intentAction); + + verify(mFrameworkLog).write( + eq(FrameworkStatsLog.SHARESHEET_STARTED), + eq(SharesheetStartedEvent.SHARE_STARTED.getId()), + eq(packageName), + /* instanceId=*/ gt(0), + eq(mimeType), + eq(appProvidedDirectTargets), + eq(appProvidedAppTargets), + eq(workProfile), + eq(FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE), + eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO)); + } + + @Test + public void testLogShareTargetSelected() { + final int targetType = ChooserActivityLogger.SELECTION_TYPE_SERVICE; + final String packageName = "com.test.foo"; + final int positionPicked = 123; + final int directTargetAlsoRanked = -1; + final int callerTargetCount = 0; + final boolean isPinned = true; + final boolean isSuccessfullySelected = true; + final long selectionCost = 456; + + mChooserLogger.logShareTargetSelected( + targetType, + packageName, + positionPicked, + directTargetAlsoRanked, + callerTargetCount, + /* directTargetHashed= */ null, + isPinned, + isSuccessfullySelected, + selectionCost); + + verify(mFrameworkLog).write( + eq(FrameworkStatsLog.RANKING_SELECTED), + eq(SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId()), + eq(packageName), + /* instanceId=*/ gt(0), + eq(positionPicked), + eq(isPinned)); + + ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); + verify(mMetricsLogger).write(eventCaptor.capture()); + LogMaker event = eventCaptor.getValue(); + assertThat(event.getCategory()).isEqualTo( + MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET); + assertThat(event.getSubtype()).isEqualTo(positionPicked); + } + + @Test + public void testLogActionSelected() { + mChooserLogger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY); + + verify(mFrameworkLog).write( + eq(FrameworkStatsLog.RANKING_SELECTED), + eq(SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()), + eq(""), + /* instanceId=*/ gt(0), + eq(-1), + eq(false)); + + ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); + verify(mMetricsLogger).write(eventCaptor.capture()); + LogMaker event = eventCaptor.getValue(); + assertThat(event.getCategory()).isEqualTo( + MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET); + assertThat(event.getSubtype()).isEqualTo(1); + } + + @Test + public void testLogDirectShareTargetReceived() { + final int category = MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER; + final int latency = 123; + + mChooserLogger.logDirectShareTargetReceived(category, latency); + + ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); + verify(mMetricsLogger).write(eventCaptor.capture()); + LogMaker event = eventCaptor.getValue(); + assertThat(event.getCategory()).isEqualTo(category); + assertThat(event.getSubtype()).isEqualTo(latency); + } + + @Test + public void testLogActionShareWithPreview() { + final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT; + + mChooserLogger.logActionShareWithPreview(previewType); + + ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class); + verify(mMetricsLogger).write(eventCaptor.capture()); + LogMaker event = eventCaptor.getValue(); + assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_SHARE_WITH_PREVIEW); + assertThat(event.getSubtype()).isEqualTo(previewType); + } + + @Test + public void testLogSharesheetTriggered() { + mChooserLogger.logSharesheetTriggered(); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_TRIGGERED), eq(0), isNull(), any()); + } + + @Test + public void testLogSharesheetAppLoadComplete() { + mChooserLogger.logSharesheetAppLoadComplete(); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE), eq(0), isNull(), any()); + } + + @Test + public void testLogSharesheetDirectLoadComplete() { + mChooserLogger.logSharesheetDirectLoadComplete(); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE), + eq(0), + isNull(), + any()); + } + + @Test + public void testLogSharesheetDirectLoadTimeout() { + mChooserLogger.logSharesheetDirectLoadTimeout(); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT), eq(0), isNull(), any()); + } + + @Test + public void testLogSharesheetProfileChanged() { + mChooserLogger.logSharesheetProfileChanged(); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED), eq(0), isNull(), any()); + } + + @Test + public void testLogSharesheetExpansionChanged_collapsed() { + mChooserLogger.logSharesheetExpansionChanged(/* isCollapsed=*/ true); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_COLLAPSED), eq(0), isNull(), any()); + } + + @Test + public void testLogSharesheetExpansionChanged_expanded() { + mChooserLogger.logSharesheetExpansionChanged(/* isCollapsed=*/ false); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_EXPANDED), eq(0), isNull(), any()); + } + + @Test + public void testLogSharesheetAppShareRankingTimeout() { + mChooserLogger.logSharesheetAppShareRankingTimeout(); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT), + eq(0), + isNull(), + any()); + } + + @Test + public void testLogSharesheetEmptyDirectShareRow() { + mChooserLogger.logSharesheetEmptyDirectShareRow(); + verify(mUiEventLog).logWithInstanceId( + eq(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW), + eq(0), + isNull(), + any()); + } + + @Test + public void testDifferentLoggerInstancesUseDifferentInstanceIds() { + ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class); + ChooserActivityLogger chooserLogger2 = + new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger); + + final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY; + final String packageName = "com.test.foo"; + final int positionPicked = 123; + final int directTargetAlsoRanked = -1; + final int callerTargetCount = 0; + final boolean isPinned = true; + final boolean isSuccessfullySelected = true; + final long selectionCost = 456; + + mChooserLogger.logShareTargetSelected( + targetType, + packageName, + positionPicked, + directTargetAlsoRanked, + callerTargetCount, + /* directTargetHashed= */ null, + isPinned, + isSuccessfullySelected, + selectionCost); + + chooserLogger2.logShareTargetSelected( + targetType, + packageName, + positionPicked, + directTargetAlsoRanked, + callerTargetCount, + /* directTargetHashed= */ null, + isPinned, + isSuccessfullySelected, + selectionCost); + + verify(mFrameworkLog, times(2)).write( + anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean()); + + int id1 = idIntCaptor.getAllValues().get(0); + int id2 = idIntCaptor.getAllValues().get(1); + + assertThat(id1).isGreaterThan(0); + assertThat(id2).isGreaterThan(0); + assertThat(id1).isNotEqualTo(id2); + } + + @Test + public void testUiAndFrameworkEventsUseSameInstanceIdForSameLoggerInstance() { + ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor<InstanceId> idObjectCaptor = ArgumentCaptor.forClass(InstanceId.class); + + final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY; + final String packageName = "com.test.foo"; + final int positionPicked = 123; + final int directTargetAlsoRanked = -1; + final int callerTargetCount = 0; + final boolean isPinned = true; + final boolean isSuccessfullySelected = true; + final long selectionCost = 456; + + mChooserLogger.logShareTargetSelected( + targetType, + packageName, + positionPicked, + directTargetAlsoRanked, + callerTargetCount, + /* directTargetHashed= */ null, + isPinned, + isSuccessfullySelected, + selectionCost); + + verify(mFrameworkLog).write( + anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean()); + + mChooserLogger.logSharesheetTriggered(); + verify(mUiEventLog).logWithInstanceId( + any(UiEventEnum.class), anyInt(), any(), idObjectCaptor.capture()); + + assertThat(idIntCaptor.getValue()).isGreaterThan(0); + assertThat(idObjectCaptor.getValue().getId()).isEqualTo(idIntCaptor.getValue()); + } + + @Test + public void testTargetSelectionCategories() { + assertThat(ChooserActivityLogger.getTargetSelectionCategory( + ChooserActivityLogger.SELECTION_TYPE_SERVICE)) + .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET); + assertThat(ChooserActivityLogger.getTargetSelectionCategory( + ChooserActivityLogger.SELECTION_TYPE_APP)) + .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET); + assertThat(ChooserActivityLogger.getTargetSelectionCategory( + ChooserActivityLogger.SELECTION_TYPE_STANDARD)) + .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET); + assertThat(ChooserActivityLogger.getTargetSelectionCategory( + ChooserActivityLogger.SELECTION_TYPE_COPY)).isEqualTo(0); + assertThat(ChooserActivityLogger.getTargetSelectionCategory( + ChooserActivityLogger.SELECTION_TYPE_NEARBY)).isEqualTo(0); + assertThat(ChooserActivityLogger.getTargetSelectionCategory( + ChooserActivityLogger.SELECTION_TYPE_EDIT)).isEqualTo(0); + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index 080f1e41..5df0d4a2 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -16,21 +16,28 @@ package com.android.intentresolver; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.os.UserHandle; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.TargetInfo; -import com.android.internal.logging.MetricsLogger; +import com.android.intentresolver.shortcuts.ShortcutLoader; -import java.util.List; +import java.util.function.Consumer; import java.util.function.Function; +import kotlin.jvm.functions.Function2; + /** * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing. * We cannot directly mock the activity created since instrumentation creates it, so instead we use @@ -49,7 +56,8 @@ public class ChooserActivityOverrideData { @SuppressWarnings("Since15") public Function<PackageManager, PackageManager> createPackageManager; public Function<TargetInfo, Boolean> onSafelyStartCallback; - public Function<ChooserListAdapter, Void> onQueryDirectShareTargets; + public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader> + shortcutLoaderFactory = (userHandle, callback) -> null; public ResolverListController resolverListController; public ResolverListController workResolverListController; public Boolean isVoiceInteraction; @@ -57,21 +65,20 @@ public class ChooserActivityOverrideData { public Cursor resolverCursor; public boolean resolverForceException; public Bitmap previewThumbnail; - public MetricsLogger metricsLogger; public ChooserActivityLogger chooserActivityLogger; public int alternateProfileSetting; public Resources resources; public UserHandle workProfileUserHandle; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; - public boolean isWorkProfileUserRunning; - public boolean isWorkProfileUserUnlocked; - public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector; + public Integer myUserId; + public QuietModeManager mQuietModeManager; + public MyUserIdProvider mMyUserIdProvider; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public PackageManager packageManager; public void reset() { onSafelyStartCallback = null; - onQueryDirectShareTargets = null; isVoiceInteraction = null; createPackageManager = null; previewThumbnail = null; @@ -80,23 +87,15 @@ public class ChooserActivityOverrideData { resolverForceException = false; resolverListController = mock(ResolverListController.class); workResolverListController = mock(ResolverListController.class); - metricsLogger = mock(MetricsLogger.class); - chooserActivityLogger = new ChooserActivityLoggerFake(); + chooserActivityLogger = mock(ChooserActivityLogger.class); alternateProfileSetting = 0; resources = null; workProfileUserHandle = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; - isWorkProfileUserRunning = true; - isWorkProfileUserUnlocked = true; + myUserId = null; packageManager = null; - multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() { - @Override - public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, - int targetUserId) { - return hasCrossProfileIntents; - } - + mQuietModeManager = new QuietModeManager() { @Override public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { return isQuietModeEnabled; @@ -107,7 +106,28 @@ public class ChooserActivityOverrideData { UserHandle workProfileUserHandle) { isQuietModeEnabled = enabled; } + + @Override + public void markWorkProfileEnabledBroadcastReceived() { + } + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } }; + shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); + + mMyUserIdProvider = new MyUserIdProvider() { + @Override + public int getMyUserId() { + return myUserId != null ? myUserId : UserHandle.myUserId(); + } + }; + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); } private ChooserActivityOverrideData() {} diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt new file mode 100644 index 00000000..58f6b733 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 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.intentresolver + +import android.content.ComponentName +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.ChooserListAdapter.LoadDirectShareIconTask +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.chooser.SelectableTargetInfo +import com.android.intentresolver.chooser.TargetInfo +import com.android.internal.R +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@RunWith(AndroidJUnit4::class) +class ChooserListAdapterTest { + private val packageManager = mock<PackageManager> { + whenever( + resolveActivity(any(), any<ResolveInfoFlags>()) + ).thenReturn(mock()) + } + private val context = InstrumentationRegistry.getInstrumentation().getContext() + private val resolverListController = mock<ResolverListController>() + private val chooserActivityLogger = mock<ChooserActivityLogger>() + + private fun createChooserListAdapter( + taskProvider: (TargetInfo?) -> LoadDirectShareIconTask + ) = object : ChooserListAdapter( + context, + emptyList(), + emptyArray(), + emptyList(), + false, + resolverListController, + null, + Intent(), + mock(), + packageManager, + chooserActivityLogger, + mock(), + 0 + ) { + override fun createLoadDirectShareIconTask( + info: SelectableTargetInfo + ): LoadDirectShareIconTask = taskProvider(info) + } + + @Before + fun setup() { + // ChooserListAdapter reads DeviceConfig and needs a permission for that. + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG") + } + + @Test + fun testDirectShareTargetLoadingIconIsStarted() { + val view = createView() + val viewHolder = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolder + val targetInfo = createSelectableTargetInfo() + val iconTask = mock<LoadDirectShareIconTask>() + val testSubject = createChooserListAdapter { iconTask } + testSubject.onBindView(view, targetInfo, 0) + + verify(iconTask, times(1)).loadIcon() + } + + @Test + fun testOnlyOneTaskPerTarget() { + val view = createView() + val viewHolderOne = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderOne + val targetInfo = createSelectableTargetInfo() + val iconTaskOne = mock<LoadDirectShareIconTask>() + val testTaskProvider = mock<() -> LoadDirectShareIconTask> { + whenever(invoke()).thenReturn(iconTaskOne) + } + val testSubject = createChooserListAdapter { testTaskProvider.invoke() } + testSubject.onBindView(view, targetInfo, 0) + + val viewHolderTwo = ResolverListAdapter.ViewHolder(view) + view.tag = viewHolderTwo + whenever(testTaskProvider()).thenReturn(mock()) + + testSubject.onBindView(view, targetInfo, 0) + + verify(iconTaskOne, times(1)).loadIcon() + verify(testTaskProvider, times(1)).invoke() + } + + private fun createSelectableTargetInfo(): TargetInfo = + SelectableTargetInfo.newSelectableTargetInfo( + /* sourceInfo = */ DisplayResolveInfo.newDisplayResolveInfo( + Intent(), + ResolverDataProvider.createResolveInfo(2, 0), + "label", + "extended info", + Intent(), + /* resolveInfoPresentationGetter= */ null + ), + /* backupResolveInfo = */ mock(), + /* resolvedIntent = */ Intent(), + /* chooserTarget = */ createChooserTarget( + "Target", 0.5f, ComponentName("pkg", "Class"), "id-1" + ), + /* modifiedScore = */ 1f, + /* shortcutInfo = */ createShortcutInfo("id-1", ComponentName("pkg", "Class"), 1), + /* appTarget */ null, + /* referrerFillInIntent = */ Intent() + ) + + private fun createView(): View { + val view = FrameLayout(context) + TextView(context).apply { + id = R.id.text1 + view.addView(this) + } + TextView(context).apply { + id = R.id.text2 + view.addView(this) + } + ImageView(context).apply { + id = R.id.icon + view.addView(this) + } + return view + } +} diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 0e9f010e..97de97f5 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -19,11 +19,13 @@ package com.android.intentresolver; import static org.mockito.Mockito.when; import android.annotation.Nullable; +import android.app.prediction.AppPredictor; import android.app.usage.UsageStatsManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -33,19 +35,18 @@ import android.net.Uri; import android.os.UserHandle; import android.util.Size; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter; -import com.android.intentresolver.ChooserActivityLogger; -import com.android.intentresolver.ChooserActivityOverrideData; -import com.android.intentresolver.ChooserListAdapter; -import com.android.intentresolver.IChooserWrapper; -import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; -import com.android.intentresolver.ResolverListController; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; -import com.android.internal.logging.MetricsLogger; +import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.util.List; +import java.util.function.Consumer; /** * Simple wrapper around chooser activity to be able to initiate it under test. For more @@ -64,25 +65,34 @@ public class ChooserWrapperActivity } @Override - protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter( - Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed) { - AbstractMultiProfilePagerAdapter multiProfilePagerAdapter = - super.createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed); - multiProfilePagerAdapter.setInjector(sOverrides.multiPagerAdapterInjector); - return multiProfilePagerAdapter; - } - - @Override - public ChooserListAdapter createChooserListAdapter(Context context, List<Intent> payloadIntents, - Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed, - ResolverListController resolverListController) { + public ChooserListAdapter createChooserListAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + ResolverListController resolverListController, + UserHandle userHandle, + Intent targetIntent, + ChooserRequestParameters chooserRequest, + int maxTargetsPerRow) { PackageManager packageManager = sOverrides.packageManager == null ? context.getPackageManager() : sOverrides.packageManager; - return new ChooserListAdapter(context, payloadIntents, initialIntents, rList, - filterLastUsed, resolverListController, - this, this, packageManager, - getChooserActivityLogger()); + return new ChooserListAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + resolverListController, + userHandle, + targetIntent, + this, + packageManager, + getChooserActivityLogger(), + chooserRequest, + maxTargetsPerRow); } @Override @@ -119,7 +129,7 @@ public class ChooserWrapperActivity @Override protected TargetInfo getNearbySharingTarget(Intent originalIntent) { - return new ChooserWrapperActivity.EmptyTargetInfo(); + return NotSelectableTargetInfo.newEmptyTargetInfo(); } @Override @@ -139,6 +149,30 @@ public class ChooserWrapperActivity } @Override + protected MyUserIdProvider createMyUserIdProvider() { + if (sOverrides.mMyUserIdProvider != null) { + return sOverrides.mMyUserIdProvider; + } + return super.createMyUserIdProvider(); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + @Override + protected QuietModeManager createQuietModeManager() { + if (sOverrides.mQuietModeManager != null) { + return sOverrides.mQuietModeManager; + } + return super.createQuietModeManager(); + } + + @Override public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) { if (sOverrides.onSafelyStartCallback != null && sOverrides.onSafelyStartCallback.apply(cti)) { @@ -187,11 +221,6 @@ public class ChooserWrapperActivity } @Override - protected MetricsLogger getMetricsLogger() { - return sOverrides.metricsLogger; - } - - @Override public ChooserActivityLogger getChooserActivityLogger() { return sOverrides.chooserActivityLogger; } @@ -220,8 +249,13 @@ public class ChooserWrapperActivity @Override public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, CharSequence pInfo, Intent replacementIntent, - @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) { - return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, replacementIntent, + @Nullable TargetPresentationGetter resolveInfoPresentationGetter) { + return DisplayResolveInfo.newDisplayResolveInfo( + originalIntent, + pri, + pLabel, + pInfo, + replacementIntent, resolveInfoPresentationGetter); } @@ -242,32 +276,18 @@ public class ChooserWrapperActivity } @Override - protected void queryDirectShareTargets(ChooserListAdapter adapter, - boolean skipAppPredictionService) { - if (sOverrides.onQueryDirectShareTargets != null) { - sOverrides.onQueryDirectShareTargets.apply(adapter); - } - super.queryDirectShareTargets(adapter, skipAppPredictionService); - } - - @Override - protected boolean isQuietModeEnabled(UserHandle userHandle) { - return sOverrides.isQuietModeEnabled; - } - - @Override - protected boolean isUserRunning(UserHandle userHandle) { - if (userHandle.equals(UserHandle.SYSTEM)) { - return super.isUserRunning(userHandle); - } - return sOverrides.isWorkProfileUserRunning; - } - - @Override - protected boolean isUserUnlocked(UserHandle userHandle) { - if (userHandle.equals(UserHandle.SYSTEM)) { - return super.isUserUnlocked(userHandle); + protected ShortcutLoader createShortcutLoader( + Context context, + AppPredictor appPredictor, + UserHandle userHandle, + IntentFilter targetIntentFilter, + Consumer<ShortcutLoader.Result> callback) { + ShortcutLoader shortcutLoader = + sOverrides.shortcutLoaderFactory.invoke(userHandle, callback); + if (shortcutLoader != null) { + return shortcutLoader; } - return sOverrides.isWorkProfileUserUnlocked; + return super.createShortcutLoader( + context, appPredictor, userHandle, targetIntentFilter, callback); } } diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java index f81cd023..af897a47 100644 --- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java +++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java @@ -22,9 +22,10 @@ import android.content.Intent; import android.content.pm.ResolveInfo; import android.os.UserHandle; -import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter; import com.android.intentresolver.chooser.DisplayResolveInfo; +import java.util.concurrent.Executor; + /** * Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in * order to expose the internals for override/inspection. Implementations should apply the overrides @@ -38,7 +39,8 @@ public interface IChooserWrapper { UsageStatsManager getUsageStatsManager(); DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, CharSequence pInfo, Intent replacementIntent, - @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter); + @Nullable TargetPresentationGetter resolveInfoPresentationGetter); UserHandle getCurrentUserHandle(); ChooserActivityLogger getChooserActivityLogger(); + Executor getMainExecutor(); } diff --git a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt new file mode 100644 index 00000000..159c6d6a --- /dev/null +++ b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2022 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.intentresolver + +/** + * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects + * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not + * be null"). To fix this, we can use methods that modify the return type to be nullable. This + * causes Kotlin to skip the null checks. + * Cloned from frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt + */ + +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatcher +import org.mockito.Mockito +import org.mockito.stubbing.OngoingStubbing + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> eq(obj: T): T = Mockito.eq<T>(obj) + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> any(type: Class<T>): T = Mockito.any<T>(type) +inline fun <reified T> any(): T = any(T::class.java) + +/** + * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher) + +/** + * Kotlin type-inferred version of Mockito.nullable() + */ +inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java) + +/** + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException + * when null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = + ArgumentCaptor.forClass(T::class.java) + +/** + * Helper function for creating new mocks, without the need to pass in a [Class] instance. + * + * Generic T is nullable because implicitly bounded by Any?. + * + * @param apply builder function to simplify stub configuration by improving type inference. + */ +inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java) + .apply(apply) + +/** + * Helper function for stubbing methods without the need to use backticks. + * + * @see Mockito.when + */ +fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall) + +/** + * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when + * kotlin tests are mocking kotlin objects and the methods take non-null parameters: + * + * java.lang.NullPointerException: capture() must not be null + */ +class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) { + private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz) + fun capture(): T = wrapped.capture() + val value: T + get() = wrapped.value + val allValues: List<T> + get() = wrapped.allValues +} + +/** + * Helper function for creating an argumentCaptor in kotlin. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> = + KotlinArgumentCaptor(T::class.java) + +/** + * Helper function for creating and using a single-use ArgumentCaptor in kotlin. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured = captor.value + * + * becomes: + * + * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) } + * + * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. + */ +inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T = + kotlinArgumentCaptor<T>().apply { block() }.value + +/** + * Variant of [withArgCaptor] for capturing multiple arguments. + * + * val captor = argumentCaptor<Foo>() + * verify(...).someMethod(captor.capture()) + * val captured: List<Foo> = captor.allValues + * + * becomes: + * + * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) } + */ +inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> = + kotlinArgumentCaptor<T>().apply{ block() }.allValues diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java new file mode 100644 index 00000000..62c16ff5 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java @@ -0,0 +1,858 @@ +/* + * 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.intentresolver; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.swipeUp; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.intentresolver.MatcherUtils.first; +import static com.android.intentresolver.ResolverWrapperActivity.sOverrides; + +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.fail; + +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.RemoteException; +import android.os.UserHandle; +import android.text.TextUtils; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.test.InstrumentationRegistry; +import androidx.test.espresso.Espresso; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; +import com.android.intentresolver.widget.ResolverDrawerLayout; +import com.android.internal.R; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +/** + * Resolver activity instrumentation tests + */ +@RunWith(AndroidJUnit4.class) +public class ResolverActivityTest { + protected Intent getConcreteIntentForLaunch(Intent clientIntent) { + clientIntent.setClass( + androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(), + ResolverWrapperActivity.class); + return clientIntent; + } + + @Rule + public ActivityTestRule<ResolverWrapperActivity> mActivityRule = + new ActivityTestRule<>(ResolverWrapperActivity.class, false, false); + + @Before + public void setup() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOverrides.reset(); + } + + @Test + public void twoOptionsAndUserSelectsOne() throws InterruptedException { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + waitForIdle(); + + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + onView(withText(toChoose.activityInfo.name)) + .perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Ignore // Failing - b/144929805 + @Test + public void setMaxHeight() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + waitForIdle(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + final View viewPager = activity.findViewById(R.id.profile_pager); + final int initialResolverHeight = viewPager.getHeight(); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + R.id.contentPanel); + ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight + = initialResolverHeight - 1; + // Force a relayout + layout.invalidate(); + layout.requestLayout(); + }); + waitForIdle(); + assertThat("Drawer should be capped at maxHeight", + viewPager.getHeight() == (initialResolverHeight - 1)); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + R.id.contentPanel); + ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight + = initialResolverHeight + 1; + // Force a relayout + layout.invalidate(); + layout.requestLayout(); + }); + waitForIdle(); + assertThat("Drawer should not change height if its height is less than maxHeight", + viewPager.getHeight() == initialResolverHeight); + } + + @Ignore // Failing - b/144929805 + @Test + public void setShowAtTopToTrue() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + waitForIdle(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + final View viewPager = activity.findViewById(R.id.profile_pager); + final View divider = activity.findViewById(R.id.divider); + final RelativeLayout profileView = + (RelativeLayout) activity.findViewById(R.id.profile_button).getParent(); + assertThat("Drawer should show at bottom by default", + profileView.getBottom() + divider.getHeight() == viewPager.getTop() + && profileView.getTop() > 0); + + activity.runOnUiThread(() -> { + ResolverDrawerLayout layout = (ResolverDrawerLayout) + activity.findViewById( + R.id.contentPanel); + layout.setShowAtTop(true); + }); + waitForIdle(); + assertThat("Drawer should show at top with new attribute", + profileView.getBottom() + divider.getHeight() == viewPager.getTop() + && profileView.getTop() == 0); + } + + @Test + public void hasLastChosenActivity() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); + ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + onView(withId(R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileOneOption() throws Exception { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + markWorkProfileUserAvailable(); + + ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); + Intent sendIntent = createSendImageIntent(); + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().getCount(), is(1)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10); + // We pick the first one as there is another one in the work profile side + onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) + .perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + + @Test + public void hasLastChosenActivityAndOtherProfile() throws Exception { + // In this case we prefer the other profile and don't display anything about the last + // chosen activity. + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the other profile slot + assertThat(activity.getAdapter().getCount(), is(2)); + + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + // Confirm that the button bar is disabled by default + onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List<ResolvedComponentInfo> stableCopy = + createResolvedComponentsForTestWithOtherProfile(2); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + onView(withId(R.id.button_once)).perform(click()); + waitForIdle(); + assertThat(chosen[0], is(toChoose)); + } + + @Test + public void testWorkTab_displayedWhenWorkProfileUserAvailable() { + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(R.id.tabs)).check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { + Intent sendIntent = createSendImageIntent(); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + } + + @Test + public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, + new ArrayList<>(workResolvedComponentInfos)); + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); + // The work list adapter must be populated in advance before tapping the other tab + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_workTabUsesExpectedAdapter() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_personalTabUsesExpectedAdapter() { + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + markWorkProfileUserAvailable(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + + assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); + assertThat(activity.getPersonalListAdapter().getCount(), is(2)); + } + + @Test + public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + + waitForIdle(); + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets() + throws InterruptedException { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + assertThat(activity.getWorkListAdapter().getCount(), is(4)); + } + + @Test + public void testWorkTab_headerIsVisibleInPersonalTab() { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createOpenWebsiteIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + TextView headerText = activity.findViewById(R.id.title); + String initialText = headerText.getText().toString(); + assertFalse(initialText.isEmpty(), "Header text is empty."); + assertThat(headerText.getVisibility(), is(View.VISIBLE)); + } + + @Test + public void testWorkTab_switchTabs_headerStaysSame() { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(1); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createOpenWebsiteIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + TextView headerText = activity.findViewById(R.id.title); + String initialText = headerText.getText().toString(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + + waitForIdle(); + String currentText = headerText.getText().toString(); + assertThat(headerText.getVisibility(), is(View.VISIBLE)); + assertThat(String.format("Header text is not the same when switching tabs, personal profile" + + " header was %s but work profile header is %s", initialText, currentText), + TextUtils.equals(initialText, currentText)); + } + + @Test + public void testWorkTab_noPersonalApps_canStartWorkApps() + throws InterruptedException { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf( + withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), + isDisplayed()))) + .perform(click()); + onView(withId(R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0))); + } + + @Test + public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_workProfileDisabled_emptyStateShown() { + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + sOverrides.isQuietModeEnabled = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_turn_on_work_apps)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + sOverrides.hasCrossProfileIntents = false; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + } + + @Test + public void testMiniResolver() { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(1); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(1); + // Personal profile only has a browser + personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed())); + } + + @Test + public void testMiniResolver_noCurrentProfileTarget() { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(0); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(1); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // Need to ensure mini resolver doesn't trigger here. + assertNotMiniResolver(); + } + + private void assertNotMiniResolver() { + try { + onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed())); + } catch (NoMatchingViewException e) { + return; + } + fail("Mini resolver present but shouldn't be"); + } + + @Test + public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { + markWorkProfileUserAvailable(); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(0); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + sOverrides.isQuietModeEnabled = true; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + onView(withText(R.string.resolver_work_tab)).perform(click()); + waitForIdle(); + + onView(withText(R.string.resolver_no_work_apps_available)) + .check(matches(isDisplayed())); + } + + @Test + public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_autolaunch() { + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + ResolveInfo[] chosen = new ResolveInfo[1]; + sOverrides.onSafelyStartCallback = targetInfo -> { + chosen[0] = targetInfo.getResolveInfo(); + return true; + }; + + mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0))); + } + + @Test + public void testLayoutWithDefault_withWorkTab_neverShown() throws RemoteException { + markWorkProfileUserAvailable(); + + // In this case we prefer the other profile and don't display anything about the last + // chosen activity. + Intent sendIntent = createSendImageIntent(); + List<ResolvedComponentInfo> resolvedComponentInfos = + createResolvedComponentsForTest(2); + + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + waitForIdle(); + + // The other entry is filtered to the last used slot + assertThat(activity.getAdapter().hasFilteredItem(), is(false)); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); + } + + private Intent createSendImageIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("image/jpeg"); + return sendIntent; + } + + private Intent createOpenWebsiteIntent() { + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_VIEW); + sendIntent.setData(Uri.parse("https://google.com")); + return sendIntent; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + if (i == 0) { + infoList.add( + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + } else { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + } + return infoList; + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private void markWorkProfileUserAvailable() { + ResolverWrapperActivity.sOverrides.workProfileUserHandle = UserHandle.of(10); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + } +} diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java index 33e7123f..fb928e09 100644 --- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java +++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java @@ -32,7 +32,7 @@ import android.test.mock.MockResources; /** * Utility class used by resolver tests to create mock data */ -class ResolverDataProvider { +public class ResolverDataProvider { static private int USER_SOMEONE_ELSE = 10; @@ -52,12 +52,12 @@ class ResolverDataProvider { createResolverIntent(i), createResolveInfo(i, userId)); } - static ComponentName createComponentName(int i) { + public static ComponentName createComponentName(int i) { final String name = "component" + i; return new ComponentName("foo.bar." + name, name); } - static ResolveInfo createResolveInfo(int i, int userId) { + public static ResolveInfo createResolveInfo(int i, int userId) { final ResolveInfo resolveInfo = new ResolveInfo(); resolveInfo.activityInfo = createActivityInfo(i); resolveInfo.targetUserId = userId; @@ -93,11 +93,17 @@ class ResolverDataProvider { public String setResolveInfoLabel; } + /** Create a {@link PackageManagerMockedInfo} with all distinct labels. */ static PackageManagerMockedInfo createPackageManagerMockedInfo(boolean hasOverridePermission) { - final String appLabel = "app_label"; - final String activityLabel = "activity_label"; - final String resolveInfoLabel = "resolve_info_label"; + return createPackageManagerMockedInfo( + hasOverridePermission, "app_label", "activity_label", "resolve_info_label"); + } + static PackageManagerMockedInfo createPackageManagerMockedInfo( + boolean hasOverridePermission, + String appLabel, + String activityLabel, + String resolveInfoLabel) { MockContext ctx = new MockContext() { @Override public PackageManager getPackageManager() { diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java new file mode 100644 index 00000000..239bffe0 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2017 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.intentresolver; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.usage.UsageStatsManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Bundle; +import android.os.UserHandle; + +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; +import com.android.intentresolver.chooser.TargetInfo; + +import java.util.List; +import java.util.function.Function; + +/* + * Simple wrapper around chooser activity to be able to initiate it under test + */ +public class ResolverWrapperActivity extends ResolverActivity { + static final OverrideData sOverrides = new OverrideData(); + private UsageStatsManager mUsm; + + public ResolverWrapperActivity() { + super(/* isIntentPicker= */ true); + } + + // ResolverActivity inspects the launched-from UID at onCreate and needs to see some + // non-negative value in the test. + @Override + public int getLaunchedFromUid() { + return 1234; + } + + @Override + public ResolverListAdapter createResolverListAdapter(Context context, + List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList, + boolean filterLastUsed, UserHandle userHandle) { + return new ResolverWrapperAdapter( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + createListController(userHandle), + userHandle, + payloadIntents.get(0), // TODO: extract upstream + this); + } + + @Override + protected MyUserIdProvider createMyUserIdProvider() { + if (sOverrides.mMyUserIdProvider != null) { + return sOverrides.mMyUserIdProvider; + } + return super.createMyUserIdProvider(); + } + + @Override + protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { + if (sOverrides.mCrossProfileIntentsChecker != null) { + return sOverrides.mCrossProfileIntentsChecker; + } + return super.createCrossProfileIntentsChecker(); + } + + @Override + protected QuietModeManager createQuietModeManager() { + if (sOverrides.mQuietModeManager != null) { + return sOverrides.mQuietModeManager; + } + return super.createQuietModeManager(); + } + + ResolverWrapperAdapter getAdapter() { + return (ResolverWrapperAdapter) mMultiProfilePagerAdapter.getActiveListAdapter(); + } + + ResolverListAdapter getPersonalListAdapter() { + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0)); + } + + ResolverListAdapter getWorkListAdapter() { + if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) { + return null; + } + return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1)); + } + + @Override + public boolean isVoiceInteraction() { + if (sOverrides.isVoiceInteraction != null) { + return sOverrides.isVoiceInteraction; + } + return super.isVoiceInteraction(); + } + + @Override + public void safelyStartActivity(TargetInfo cti) { + if (sOverrides.onSafelyStartCallback != null && + sOverrides.onSafelyStartCallback.apply(cti)) { + return; + } + super.safelyStartActivity(cti); + } + + @Override + protected ResolverListController createListController(UserHandle userHandle) { + if (userHandle == UserHandle.SYSTEM) { + when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); + return sOverrides.resolverListController; + } + when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.workResolverListController; + } + + @Override + public PackageManager getPackageManager() { + if (sOverrides.createPackageManager != null) { + return sOverrides.createPackageManager.apply(super.getPackageManager()); + } + return super.getPackageManager(); + } + + protected UserHandle getCurrentUserHandle() { + return mMultiProfilePagerAdapter.getCurrentUserHandle(); + } + + @Override + protected UserHandle getWorkProfileUserHandle() { + return sOverrides.workProfileUserHandle; + } + + @Override + public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { + super.startActivityAsUser(intent, options, user); + } + + /** + * We cannot directly mock the activity created since instrumentation creates it. + * <p> + * Instead, we use static instances of this object to modify behavior. + */ + static class OverrideData { + @SuppressWarnings("Since15") + public Function<PackageManager, PackageManager> createPackageManager; + public Function<TargetInfo, Boolean> onSafelyStartCallback; + public ResolverListController resolverListController; + public ResolverListController workResolverListController; + public Boolean isVoiceInteraction; + public UserHandle workProfileUserHandle; + public Integer myUserId; + public boolean hasCrossProfileIntents; + public boolean isQuietModeEnabled; + public QuietModeManager mQuietModeManager; + public MyUserIdProvider mMyUserIdProvider; + public CrossProfileIntentsChecker mCrossProfileIntentsChecker; + + public void reset() { + onSafelyStartCallback = null; + isVoiceInteraction = null; + createPackageManager = null; + resolverListController = mock(ResolverListController.class); + workResolverListController = mock(ResolverListController.class); + workProfileUserHandle = null; + myUserId = null; + hasCrossProfileIntents = true; + isQuietModeEnabled = false; + + mQuietModeManager = new QuietModeManager() { + @Override + public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) { + return isQuietModeEnabled; + } + + @Override + public void requestQuietModeEnabled(boolean enabled, + UserHandle workProfileUserHandle) { + isQuietModeEnabled = enabled; + } + + @Override + public void markWorkProfileEnabledBroadcastReceived() { + } + + @Override + public boolean isWaitingToEnableWorkProfile() { + return false; + } + }; + + mMyUserIdProvider = new MyUserIdProvider() { + @Override + public int getMyUserId() { + return myUserId != null ? myUserId : UserHandle.myUserId(); + } + }; + + mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); + when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) + .thenAnswer(invocation -> hasCrossProfileIntents); + } + } +} diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java new file mode 100644 index 00000000..a53b41d1 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 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.intentresolver; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ResolveInfo; +import android.os.UserHandle; + +import androidx.test.espresso.idling.CountingIdlingResource; + +import com.android.intentresolver.chooser.DisplayResolveInfo; + +import java.util.List; + +public class ResolverWrapperAdapter extends ResolverListAdapter { + + private CountingIdlingResource mLabelIdlingResource = + new CountingIdlingResource("LoadLabelTask"); + + public ResolverWrapperAdapter( + Context context, + List<Intent> payloadIntents, + Intent[] initialIntents, + List<ResolveInfo> rList, + boolean filterLastUsed, + ResolverListController resolverListController, + UserHandle userHandle, + Intent targetIntent, + ResolverListCommunicator resolverListCommunicator) { + super( + context, + payloadIntents, + initialIntents, + rList, + filterLastUsed, + resolverListController, + userHandle, + targetIntent, + resolverListCommunicator, + false); + } + + public CountingIdlingResource getLabelIdlingResource() { + return mLabelIdlingResource; + } + + @Override + protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) { + return new LoadLabelWrapperTask(info); + } + + class LoadLabelWrapperTask extends LoadLabelTask { + + protected LoadLabelWrapperTask(DisplayResolveInfo dri) { + super(dri); + } + + @Override + protected void onPreExecute() { + mLabelIdlingResource.increment(); + } + + @Override + protected void onPostExecute(CharSequence[] result) { + super.onPostExecute(result); + mLabelIdlingResource.decrement(); + } + } +} diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt new file mode 100644 index 00000000..a8d6f978 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2022 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.intentresolver + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ResolveInfo +import android.content.pm.ShortcutInfo +import android.service.chooser.ChooserTarget +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.chooser.TargetInfo +import androidx.test.filters.SmallTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +private const val PACKAGE_A = "package.a" +private const val PACKAGE_B = "package.b" +private const val CLASS_NAME = "./MainActivity" + +@SmallTest +class ShortcutSelectionLogicTest { + private val packageTargets = HashMap<String, Array<ChooserTarget>>().apply { + arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg -> + // shortcuts in reverse priority order + val targets = Array(3) { i -> + createChooserTarget( + "Shortcut $i", + (i + 1).toFloat() / 10f, + ComponentName(pkg, CLASS_NAME), + pkg.shortcutId(i), + ) + } + this[pkg] = targets + } + } + + private val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( + Intent(), + ResolverDataProvider.createResolveInfo(3, 0), + "label", + "extended info", + Intent(), + /* resolveInfoPresentationGetter= */ null) + + private val otherBaseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( + Intent(), + ResolverDataProvider.createResolveInfo(4, 0), + "label 2", + "extended info 2", + Intent(), + /* resolveInfoPresentationGetter= */ null) + + private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) = + this[pkg]?.get(idx) ?: error("missing package $pkg") + + @Test + fun testAddShortcuts_no_limits() { + val serviceResults = ArrayList<TargetInfo>() + val sc1 = packageTargets[PACKAGE_A, 0] + val sc2 = packageTargets[PACKAGE_A, 1] + val testSubject = ShortcutSelectionLogic( + /* maxShortcutTargetsPerApp = */ 1, + /* applySharingAppLimits = */ false + ) + + val isUpdated = testSubject.addServiceResults( + /* origTarget = */ baseDisplayInfo, + /* origTargetScore = */ 0.1f, + /* targets = */ listOf(sc1, sc2), + /* isShortcutResult = */ true, + /* directShareToShortcutInfos = */ emptyMap(), + /* directShareToAppTargets = */ emptyMap(), + /* userContext = */ mock(), + /* targetIntent = */ mock(), + /* refererFillInIntent = */ mock(), + /* maxRankedTargets = */ 4, + /* serviceTargets = */ serviceResults + ) + + assertTrue("Updates are expected", isUpdated) + assertShortcutsInOrder( + listOf(sc2, sc1), + serviceResults, + "Two shortcuts are expected as we do not apply per-app shortcut limit" + ) + } + + @Test + fun testAddShortcuts_same_package_with_per_package_limit() { + val serviceResults = ArrayList<TargetInfo>() + val sc1 = packageTargets[PACKAGE_A, 0] + val sc2 = packageTargets[PACKAGE_A, 1] + val testSubject = ShortcutSelectionLogic( + /* maxShortcutTargetsPerApp = */ 1, + /* applySharingAppLimits = */ true + ) + + val isUpdated = testSubject.addServiceResults( + /* origTarget = */ baseDisplayInfo, + /* origTargetScore = */ 0.1f, + /* targets = */ listOf(sc1, sc2), + /* isShortcutResult = */ true, + /* directShareToShortcutInfos = */ emptyMap(), + /* directShareToAppTargets = */ emptyMap(), + /* userContext = */ mock(), + /* targetIntent = */ mock(), + /* refererFillInIntent = */ mock(), + /* maxRankedTargets = */ 4, + /* serviceTargets = */ serviceResults + ) + + assertTrue("Updates are expected", isUpdated) + assertShortcutsInOrder( + listOf(sc2), + serviceResults, + "One shortcut is expected as we apply per-app shortcut limit" + ) + } + + @Test + fun testAddShortcuts_same_package_no_per_app_limit_with_target_limit() { + val serviceResults = ArrayList<TargetInfo>() + val sc1 = packageTargets[PACKAGE_A, 0] + val sc2 = packageTargets[PACKAGE_A, 1] + val testSubject = ShortcutSelectionLogic( + /* maxShortcutTargetsPerApp = */ 1, + /* applySharingAppLimits = */ false + ) + + val isUpdated = testSubject.addServiceResults( + /* origTarget = */ baseDisplayInfo, + /* origTargetScore = */ 0.1f, + /* targets = */ listOf(sc1, sc2), + /* isShortcutResult = */ true, + /* directShareToShortcutInfos = */ emptyMap(), + /* directShareToAppTargets = */ emptyMap(), + /* userContext = */ mock(), + /* targetIntent = */ mock(), + /* refererFillInIntent = */ mock(), + /* maxRankedTargets = */ 1, + /* serviceTargets = */ serviceResults + ) + + assertTrue("Updates are expected", isUpdated) + assertShortcutsInOrder( + listOf(sc2), + serviceResults, + "One shortcut is expected as we apply overall shortcut limit" + ) + } + + @Test + fun testAddShortcuts_different_packages_with_per_package_limit() { + val serviceResults = ArrayList<TargetInfo>() + val pkgAsc1 = packageTargets[PACKAGE_A, 0] + val pkgAsc2 = packageTargets[PACKAGE_A, 1] + val pkgBsc1 = packageTargets[PACKAGE_B, 0] + val pkgBsc2 = packageTargets[PACKAGE_B, 1] + val testSubject = ShortcutSelectionLogic( + /* maxShortcutTargetsPerApp = */ 1, + /* applySharingAppLimits = */ true + ) + + testSubject.addServiceResults( + /* origTarget = */ baseDisplayInfo, + /* origTargetScore = */ 0.1f, + /* targets = */ listOf(pkgAsc1, pkgAsc2), + /* isShortcutResult = */ true, + /* directShareToShortcutInfos = */ emptyMap(), + /* directShareToAppTargets = */ emptyMap(), + /* userContext = */ mock(), + /* targetIntent = */ mock(), + /* refererFillInIntent = */ mock(), + /* maxRankedTargets = */ 4, + /* serviceTargets = */ serviceResults + ) + testSubject.addServiceResults( + /* origTarget = */ otherBaseDisplayInfo, + /* origTargetScore = */ 0.2f, + /* targets = */ listOf(pkgBsc1, pkgBsc2), + /* isShortcutResult = */ true, + /* directShareToShortcutInfos = */ emptyMap(), + /* directShareToAppTargets = */ emptyMap(), + /* userContext = */ mock(), + /* targetIntent = */ mock(), + /* refererFillInIntent = */ mock(), + /* maxRankedTargets = */ 4, + /* serviceTargets = */ serviceResults + ) + + assertShortcutsInOrder( + listOf(pkgBsc2, pkgAsc2), + serviceResults, + "Two shortcuts are expected as we apply per-app shortcut limit" + ) + } + + @Test + fun testAddShortcuts_pinned_shortcut() { + val serviceResults = ArrayList<TargetInfo>() + val sc1 = packageTargets[PACKAGE_A, 0] + val sc2 = packageTargets[PACKAGE_A, 1] + val testSubject = ShortcutSelectionLogic( + /* maxShortcutTargetsPerApp = */ 1, + /* applySharingAppLimits = */ false + ) + + val isUpdated = testSubject.addServiceResults( + /* origTarget = */ baseDisplayInfo, + /* origTargetScore = */ 0.1f, + /* targets = */ listOf(sc1, sc2), + /* isShortcutResult = */ true, + /* directShareToShortcutInfos = */ mapOf( + sc1 to createShortcutInfo( + PACKAGE_A.shortcutId(1), + sc1.componentName, 1).apply { + addFlags(ShortcutInfo.FLAG_PINNED) + } + ), + /* directShareToAppTargets = */ emptyMap(), + /* userContext = */ mock(), + /* targetIntent = */ mock(), + /* refererFillInIntent = */ mock(), + /* maxRankedTargets = */ 4, + /* serviceTargets = */ serviceResults + ) + + assertTrue("Updates are expected", isUpdated) + assertShortcutsInOrder( + listOf(sc1, sc2), + serviceResults, + "Two shortcuts are expected as we do not apply per-app shortcut limit" + ) + } + + @Test + fun test_available_caller_shortcuts_count_is_limited() { + val serviceResults = ArrayList<TargetInfo>() + val sc1 = packageTargets[PACKAGE_A, 0] + val sc2 = packageTargets[PACKAGE_A, 1] + val sc3 = packageTargets[PACKAGE_A, 2] + val testSubject = ShortcutSelectionLogic( + /* maxShortcutTargetsPerApp = */ 1, + /* applySharingAppLimits = */ true + ) + val context = mock<Context> { + whenever(packageManager).thenReturn(mock()) + } + + testSubject.addServiceResults( + /* origTarget = */ baseDisplayInfo, + /* origTargetScore = */ 0f, + /* targets = */ listOf(sc1, sc2, sc3), + /* isShortcutResult = */ false, + /* directShareToShortcutInfos = */ emptyMap(), + /* directShareToAppTargets = */ emptyMap(), + /* userContext = */ context, + /* targetIntent = */ mock(), + /* refererFillInIntent = */ mock(), + /* maxRankedTargets = */ 4, + /* serviceTargets = */ serviceResults + ) + + assertShortcutsInOrder( + listOf(sc3, sc2), + serviceResults, + "At most two caller-provided shortcuts are allowed" + ) + } + + // TODO: consider renaming. Not all `ChooserTarget`s are "shortcuts" and many of our test cases + // add results with `isShortcutResult = false` and `directShareToShortcutInfos = emptyMap()`. + private fun assertShortcutsInOrder( + expected: List<ChooserTarget>, actual: List<TargetInfo>, msg: String? = "" + ) { + assertEquals(msg, expected.size, actual.size) + for (i in expected.indices) { + assertEquals( + "Unexpected item at position $i", + expected[i].componentName, + actual[i].chooserTargetComponentName + ) + assertEquals( + "Unexpected item at position $i", + expected[i].title, + actual[i].displayLabel + ) + } + } + + private fun String.shortcutId(id: Int) = "$this.$id" +} diff --git a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt new file mode 100644 index 00000000..e62672a3 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2022 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.intentresolver + +import com.android.intentresolver.ResolverDataProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +/** + * Unit tests for the various implementations of {@link TargetPresentationGetter}. + * TODO: consider expanding to cover icon logic (not just labels/sublabels). + * TODO: these are conceptually "acceptance tests" that provide comprehensive coverage of the + * apparent variations in the legacy implementation. The tests probably don't have to be so + * exhaustive if we're able to impose a simpler design on the implementation. + */ +class TargetPresentationGetterTest { + fun makeResolveInfoPresentationGetter( + withSubstitutePermission: Boolean, + appLabel: String, + activityLabel: String, + resolveInfoLabel: String): TargetPresentationGetter { + val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo( + withSubstitutePermission, appLabel, activityLabel, resolveInfoLabel) + val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100) + return factory.makePresentationGetter(testPackageInfo.resolveInfo) + } + + fun makeActivityInfoPresentationGetter( + withSubstitutePermission: Boolean, + appLabel: String?, + activityLabel: String?): TargetPresentationGetter { + val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo( + withSubstitutePermission, appLabel, activityLabel, "") + val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100) + return factory.makePresentationGetter(testPackageInfo.activityInfo) + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + false, "app_label", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + false, "app_label", "app_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, there's no logic to dedupe the labels. + // TODO: this matches our observations in the legacy code, but is it the right behavior? It + // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in + // the UI at least, but maybe that logic should be pulled back to the "presentation"? + assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_nullRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(false, null, "activity_label") + assertThat(presentationGetter.getLabel()).isNull() + assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(false, "", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("") + assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label") + } + + @Test + fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter(false, "app_label", "") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, empty sublabels are passed through as-is. + assertThat(presentationGetter.getSubLabel()).isEqualTo("") + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + true, "app_label", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, the same ("activity") label is requested as both the label + // and sublabel, even though the other value ("app_label") was distinct. Thus this behaves the + // same as a dupe. + assertThat(presentationGetter.getSubLabel()).isEqualTo(null) + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter( + true, "app_label", "app_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // With the substitute permission, duped sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_nullRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", null) + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // With the substitute permission, null inputs are a special case that produces null outputs + // (i.e., they're not simply passed-through from the inputs). + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedLabel() { + val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", "") + // Empty "labels" are taken as-is and (unlike nulls) don't prompt a fallback to the sublabel. + // Thus (as in the previous case with substitute permission & "distinct" labels), this is + // treated as a dupe. + assertThat(presentationGetter.getLabel()).isEqualTo("") + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeActivityInfoPresentationGetter(true, "", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, empty sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testResolveInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + false, "app_label", "activity_label", "resolve_info_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label") + } + + @Test + fun testResolveInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + false, "app_label", "activity_label", "app_label") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, there's no logic to dedupe the labels. + // TODO: this matches our observations in the legacy code, but is it the right behavior? It + // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in + // the UI at least, but maybe that logic should be pulled back to the "presentation"? + assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label") + } + + @Test + fun testResolveInfoLabels_noSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + false, "app_label", "activity_label", "") + assertThat(presentationGetter.getLabel()).isEqualTo("app_label") + // Without the substitute permission, empty sublabels are passed through as-is. + assertThat(presentationGetter.getSubLabel()).isEqualTo("") + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "activity_label", "resolve_info_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label") + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "activity_label", "activity_label") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, duped sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "activity_label", "") + assertThat(presentationGetter.getLabel()).isEqualTo("activity_label") + // With the substitute permission, empty sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } + + @Test + fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedLabelAndSublabel() { + val presentationGetter = makeResolveInfoPresentationGetter( + true, "app_label", "", "") + assertThat(presentationGetter.getLabel()).isEqualTo("") + // With the substitute permission, empty sublabels get converted to nulls. + assertThat(presentationGetter.getSubLabel()).isNull() + } +} diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt new file mode 100644 index 00000000..849cfbab --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestApplication.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 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.intentresolver + +import android.app.Application +import android.content.Context +import android.os.UserHandle + +class TestApplication : Application() { + + // return the current context as a work profile doesn't really exist in these tests + override fun createContextAsUser(user: UserHandle, flags: Int): Context = this +}
\ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/TestHelpers.kt b/java/tests/src/com/android/intentresolver/TestHelpers.kt new file mode 100644 index 00000000..5b583fef --- /dev/null +++ b/java/tests/src/com/android/intentresolver/TestHelpers.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 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.intentresolver + +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetId +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager.ShareShortcutInfo +import android.os.Bundle +import android.service.chooser.ChooserTarget +import org.mockito.Mockito.`when` as whenever + +internal fun createShareShortcutInfo( + id: String, + componentName: ComponentName, + rank: Int +): ShareShortcutInfo = + ShareShortcutInfo( + createShortcutInfo(id, componentName, rank), + componentName + ) + +internal fun createShortcutInfo( + id: String, + componentName: ComponentName, + rank: Int +): ShortcutInfo { + val context = mock<Context>() + whenever(context.packageName).thenReturn(componentName.packageName) + return ShortcutInfo.Builder(context, id) + .setShortLabel("Short Label $id") + .setLongLabel("Long Label $id") + .setActivity(componentName) + .setRank(rank) + .build() +} + +internal fun createAppTarget(shortcutInfo: ShortcutInfo) = + AppTarget( + AppTargetId(shortcutInfo.id), + shortcutInfo, + shortcutInfo.activity?.className ?: error("missing activity info") + ) + +fun createChooserTarget( + title: String, score: Float, componentName: ComponentName, shortcutId: String +): ChooserTarget = + ChooserTarget( + title, + null, + score, + componentName, + Bundle().apply { putString(Intent.EXTRA_SHORTCUT_ID, shortcutId) } + ) diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index b901fc1e..af2557ef 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -38,21 +38,18 @@ import static com.android.intentresolver.MatcherUtils.first; import static com.google.common.truth.Truth.assertThat; -import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.greaterThan; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -78,26 +75,29 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Icon; -import android.metrics.LogMaker; import android.net.Uri; +import android.os.Bundle; import android.os.UserHandle; import android.provider.DeviceConfig; import android.service.chooser.ChooserTarget; +import android.util.HashedStringCache; +import android.util.Pair; +import android.util.SparseArray; import android.view.View; import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; -import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.FrameworkStatsLog; -import com.android.internal.widget.GridLayoutManager; -import com.android.internal.widget.RecyclerView; import org.hamcrest.Description; import org.hamcrest.Matcher; @@ -117,6 +117,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; /** @@ -130,7 +131,6 @@ import java.util.function.Function; * TODO: this can simply be renamed to "ChooserActivityTest" if that's ever unambiguous (i.e., if * there's no risk of confusion with the framework tests that currently share the same name). */ -@Ignore("investigate b/241944046 and re-enabled") @RunWith(Parameterized.class) public class UnbundledChooserActivityTest { @@ -252,13 +252,31 @@ public class UnbundledChooserActivityTest { mTestNum = testNum; } + private void setDeviceConfigProperty( + @NonNull String propertyName, + @NonNull String value) { + // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly + // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently + // configure in {@link #setup()}. + // TODO: is it really appropriate that this is always set with makeDefault=true? + boolean valueWasSet = DeviceConfig.setProperty( + DeviceConfig.NAMESPACE_SYSTEMUI, + propertyName, + value, + true /* makeDefault */); + if (!valueWasSet) { + throw new IllegalStateException( + "Could not set " + propertyName + " to " + value); + } + } + public void cleanOverrideData() { ChooserActivityOverrideData.getInstance().reset(); ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride; - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + + setDeviceConfigProperty( SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, - Boolean.toString(true), - true /* makeDefault*/); + Boolean.toString(true)); } @Test @@ -282,7 +300,7 @@ public class UnbundledChooserActivityTest { waitForIdle(); assertThat(activity.getAdapter().getCount(), is(2)); assertThat(activity.getAdapter().getServiceTargetCount(), is(0)); - onView(withIdFromRuntimeResource("title")).check(matches(withText("chooser test"))); + onView(withId(android.R.id.title)).check(matches(withText("chooser test"))); } @Test @@ -302,8 +320,8 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test")); waitForIdle(); - onView(withIdFromRuntimeResource("title")) - .check(matches(withTextFromRuntimeResource("whichSendApplication"))); + onView(withId(android.R.id.title)) + .check(matches(withText(com.android.internal.R.string.whichSendApplication))); } @Test @@ -323,8 +341,8 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("title")) - .check(matches(withTextFromRuntimeResource("whichSendApplication"))); + onView(withId(android.R.id.title)) + .check(matches(withText(com.android.internal.R.string.whichSendApplication))); } @Test @@ -344,9 +362,9 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_title")) + onView(withId(com.android.internal.R.id.content_preview_title)) .check(matches(not(isDisplayed()))); - onView(withIdFromRuntimeResource("content_preview_thumbnail")) + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) .check(matches(not(isDisplayed()))); } @@ -368,11 +386,11 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_title")) + onView(withId(com.android.internal.R.id.content_preview_title)) .check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_title")) + onView(withId(com.android.internal.R.id.content_preview_title)) .check(matches(withText(previewTitle))); - onView(withIdFromRuntimeResource("content_preview_thumbnail")) + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) .check(matches(not(isDisplayed()))); } @@ -395,8 +413,9 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_title")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_thumbnail")) + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) .check(matches(not(isDisplayed()))); } @@ -405,7 +424,7 @@ public class UnbundledChooserActivityTest { String previewTitle = "My Content Preview Title"; Intent sendIntent = createSendTextIntentWithPreview(previewTitle, Uri.parse("android.resource://com.android.frameworks.coretests/" - + com.android.frameworks.coretests.R.drawable.test320x240)); + + R.drawable.test320x240)); ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -421,8 +440,9 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_title")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_thumbnail")) + onView(withId(com.android.internal.R.id.content_preview_title)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_thumbnail)) .check(matches(isDisplayed())); } @@ -447,7 +467,7 @@ public class UnbundledChooserActivityTest { waitForIdle(); assertThat(activity.getAdapter().getCount(), is(2)); - onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist()); + onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); ResolveInfo[] chosen = new ResolveInfo[1]; ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { @@ -580,8 +600,8 @@ public class UnbundledChooserActivityTest { waitForIdle(); assertThat(activity.isFinishing(), is(false)); - onView(withIdFromRuntimeResource("empty")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("profile_pager")).check(matches(not(isDisplayed()))); + onView(withId(android.R.id.empty)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.profile_pager)).check(matches(not(isDisplayed()))); InstrumentationRegistry.getInstrumentation().runOnMainSync( () -> wrapper.getAdapter().handlePackagesChanged() ); @@ -619,9 +639,7 @@ public class UnbundledChooserActivityTest { } @Test @Ignore - public void hasOtherProfileOneOption() throws Exception { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; + public void hasOtherProfileOneOption() { List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4); @@ -647,7 +665,6 @@ public class UnbundledChooserActivityTest { List<ResolvedComponentInfo> stableCopy = createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10); waitForIdle(); - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) .perform(click()); @@ -657,9 +674,6 @@ public class UnbundledChooserActivityTest { @Test @Ignore public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; - Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3); @@ -697,9 +711,6 @@ public class UnbundledChooserActivityTest { @Test @Ignore public void hasLastChosenActivityAndOtherProfile() throws Exception { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; - Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3); @@ -748,8 +759,8 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click()); + onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); ClipboardManager clipboard = (ClipboardManager) activity.getSystemService( Context.CLIPBOARD_SERVICE); ClipData clipData = clipboard.getPrimaryClip(); @@ -762,7 +773,7 @@ public class UnbundledChooserActivityTest { } @Test - public void copyTextToClipboardLogging() throws Exception { + public void copyTextToClipboardLogging() { Intent sendIntent = createSendTextIntent(); List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -772,24 +783,17 @@ public class UnbundledChooserActivityTest { Mockito.anyBoolean(), Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click()); - - verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture()); + onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); - // The last captured event is the selection of the target. - assertThat(logMakerCaptor.getValue().getCategory(), - is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET)); - assertThat(logMakerCaptor.getValue().getSubtype(), is(1)); + ChooserActivityLogger logger = activity.getChooserActivityLogger(); + verify(logger, times(1)).logActionSelected(eq(ChooserActivityLogger.SELECTION_TYPE_COPY)); } - @Test @Ignore public void testNearbyShareLogging() throws Exception { @@ -806,52 +810,11 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("chooser_nearby_button")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("chooser_nearby_button")).perform(click()); - - ChooserActivityLoggerFake logger = - (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); + onView(withId(com.android.internal.R.id.chooser_nearby_button)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_nearby_button)).perform(click()); // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. - logger.removeCallsForUiEventsOfType( - ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_DIRECT_LOAD_COMPLETE.getId()); - - // SHARESHEET_TRIGGERED: - assertThat(logger.event(0).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); - - // SHARESHEET_STARTED: - assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); - assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); - assertThat(logger.get(1).mimeType, is("text/plain")); - assertThat(logger.get(1).packageName, is( - InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName())); - assertThat(logger.get(1).appProvidedApp, is(0)); - assertThat(logger.get(1).appProvidedDirect, is(0)); - assertThat(logger.get(1).isWorkprofile, is(false)); - assertThat(logger.get(1).previewType, is(3)); - - // SHARESHEET_APP_LOAD_COMPLETE: - assertThat(logger.event(2).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - - // Next are just artifacts of test set-up: - assertThat(logger.event(3).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId())); - assertThat(logger.event(4).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId())); - - // SHARESHEET_NEARBY_TARGET_SELECTED: - assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); - assertThat(logger.get(5).targetType, - is(ChooserActivityLogger - .SharesheetTargetSelectedEvent.SHARESHEET_NEARBY_TARGET_SELECTED.getId())); - - // No more events. - assertThat(logger.numCalls(), is(6)); } @@ -860,7 +823,7 @@ public class UnbundledChooserActivityTest { public void testEditImageLogs() throws Exception { Intent sendIntent = createSendImageIntent( Uri.parse("android.resource://com.android.frameworks.coretests/" - + com.android.frameworks.coretests.R.drawable.test320x240)); + + R.drawable.test320x240)); ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); ChooserActivityOverrideData.getInstance().isImageType = true; @@ -877,59 +840,17 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("chooser_edit_button")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("chooser_edit_button")).perform(click()); - - ChooserActivityLoggerFake logger = - (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); + onView(withId(com.android.internal.R.id.chooser_edit_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_edit_button)).perform(click()); // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. - logger.removeCallsForUiEventsOfType( - ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_DIRECT_LOAD_COMPLETE.getId()); - - // SHARESHEET_TRIGGERED: - assertThat(logger.event(0).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); - - // SHARESHEET_STARTED: - assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); - assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); - assertThat(logger.get(1).mimeType, is("image/png")); - assertThat(logger.get(1).packageName, is( - InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName())); - assertThat(logger.get(1).appProvidedApp, is(0)); - assertThat(logger.get(1).appProvidedDirect, is(0)); - assertThat(logger.get(1).isWorkprofile, is(false)); - assertThat(logger.get(1).previewType, is(1)); - - // SHARESHEET_APP_LOAD_COMPLETE: - assertThat(logger.event(2).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - - // Next are just artifacts of test set-up: - assertThat(logger.event(3).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId())); - assertThat(logger.event(4).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId())); - - // SHARESHEET_EDIT_TARGET_SELECTED: - assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); - assertThat(logger.get(5).targetType, - is(ChooserActivityLogger - .SharesheetTargetSelectedEvent.SHARESHEET_EDIT_TARGET_SELECTED.getId())); - - // No more events. - assertThat(logger.numCalls(), is(6)); } @Test public void oneVisibleImagePreview() throws InterruptedException { Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + com.android.frameworks.coretests.R.drawable.test320x240); + + R.drawable.test320x240); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); @@ -952,20 +873,20 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_image_1_large")) + onView(withId(com.android.internal.R.id.content_preview_image_1_large)) .check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_image_2_large")) + onView(withId(com.android.internal.R.id.content_preview_image_2_large)) .check(matches(not(isDisplayed()))); - onView(withIdFromRuntimeResource("content_preview_image_2_small")) + onView(withId(com.android.internal.R.id.content_preview_image_2_small)) .check(matches(not(isDisplayed()))); - onView(withIdFromRuntimeResource("content_preview_image_3_small")) + onView(withId(com.android.internal.R.id.content_preview_image_3_small)) .check(matches(not(isDisplayed()))); } @Test public void twoVisibleImagePreview() throws InterruptedException { Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + com.android.frameworks.coretests.R.drawable.test320x240); + + R.drawable.test320x240); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); @@ -989,20 +910,20 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_image_1_large")) + onView(withId(com.android.internal.R.id.content_preview_image_1_large)) .check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_image_2_large")) + onView(withId(com.android.internal.R.id.content_preview_image_2_large)) .check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_image_2_small")) + onView(withId(com.android.internal.R.id.content_preview_image_2_small)) .check(matches(not(isDisplayed()))); - onView(withIdFromRuntimeResource("content_preview_image_3_small")) + onView(withId(com.android.internal.R.id.content_preview_image_3_small)) .check(matches(not(isDisplayed()))); } @Test public void threeOrMoreVisibleImagePreview() throws InterruptedException { Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + com.android.frameworks.coretests.R.drawable.test320x240); + + R.drawable.test320x240); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); @@ -1029,13 +950,13 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_image_1_large")) + onView(withId(com.android.internal.R.id.content_preview_image_1_large)) .check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_image_2_large")) + onView(withId(com.android.internal.R.id.content_preview_image_2_large)) .check(matches(not(isDisplayed()))); - onView(withIdFromRuntimeResource("content_preview_image_2_small")) + onView(withId(com.android.internal.R.id.content_preview_image_2_small)) .check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_image_3_small")) + onView(withId(com.android.internal.R.id.content_preview_image_3_small)) .check(matches(isDisplayed())); } @@ -1044,25 +965,12 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); - waitForIdle(); - verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture()); - assertThat(logMakerCaptor.getAllValues().get(0).getCategory(), - is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)); - assertThat(logMakerCaptor - .getAllValues().get(0) - .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS), - is(notNullValue())); - assertThat(logMakerCaptor - .getAllValues().get(0) - .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE), - is(TEST_MIME_TYPE)); - assertThat(logMakerCaptor - .getAllValues().get(0) - .getSubtype(), - is(MetricsEvent.PARENT_PROFILE)); + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); + ChooserActivityLogger logger = activity.getChooserActivityLogger(); + waitForIdle(); + + verify(logger).logChooserActivityShown(eq(false), eq(TEST_MIME_TYPE), anyLong()); } @Test @@ -1071,49 +979,32 @@ public class UnbundledChooserActivityTest { sendIntent.setType(TEST_MIME_TYPE); ChooserActivityOverrideData.getInstance().alternateProfileSetting = MetricsEvent.MANAGED_PROFILE; - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); - waitForIdle(); - verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture()); - assertThat(logMakerCaptor.getAllValues().get(0).getCategory(), - is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)); - assertThat(logMakerCaptor - .getAllValues().get(0) - .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS), - is(notNullValue())); - assertThat(logMakerCaptor - .getAllValues().get(0) - .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE), - is(TEST_MIME_TYPE)); - assertThat(logMakerCaptor - .getAllValues().get(0) - .getSubtype(), - is(MetricsEvent.MANAGED_PROFILE)); + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test")); + ChooserActivityLogger logger = activity.getChooserActivityLogger(); + waitForIdle(); + + verify(logger).logChooserActivityShown(eq(true), eq(TEST_MIME_TYPE), anyLong()); } @Test public void testEmptyPreviewLogging() { Intent sendIntent = createSendTextIntentWithPreview(null, null); - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "empty preview logger test")); + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity( + Intent.createChooser(sendIntent, "empty preview logger test")); + ChooserActivityLogger logger = activity.getChooserActivityLogger(); waitForIdle(); - verify(mockLogger, Mockito.times(1)).write(logMakerCaptor.capture()); - // First invocation is from onCreate - assertThat(logMakerCaptor.getAllValues().get(0).getCategory(), - is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)); + verify(logger).logChooserActivityShown(eq(false), eq(null), anyLong()); } @Test public void testTitlePreviewLogging() { Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null); - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent( @@ -1122,20 +1013,19 @@ public class UnbundledChooserActivityTest { Mockito.anyBoolean(), Mockito.isA(List.class))).thenReturn(resolvedComponentInfos); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); + // Second invocation is from onCreate - verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture()); - assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(), - is(CONTENT_PREVIEW_TEXT)); - assertThat(logMakerCaptor.getAllValues().get(0).getCategory(), - is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)); + ChooserActivityLogger logger = activity.getChooserActivityLogger(); + Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_TEXT)); } @Test public void testImagePreviewLogging() { Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + com.android.frameworks.coretests.R.drawable.test320x240); + + R.drawable.test320x240); ArrayList<Uri> uris = new ArrayList<>(); uris.add(uri); @@ -1157,16 +1047,11 @@ public class UnbundledChooserActivityTest { Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture()); - // First invocation is from onCreate - assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(), - is(CONTENT_PREVIEW_IMAGE)); - assertThat(logMakerCaptor.getAllValues().get(0).getCategory(), - is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)); + ChooserActivityLogger logger = activity.getChooserActivityLogger(); + Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_IMAGE)); } @Test @@ -1192,10 +1077,11 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_filename")) + onView(withId(com.android.internal.R.id.content_preview_filename)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_filename)) .check(matches(withText("app.pdf"))); - onView(withIdFromRuntimeResource("content_preview_file_icon")) + onView(withId(com.android.internal.R.id.content_preview_file_icon)) .check(matches(isDisplayed())); } @@ -1225,11 +1111,11 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_filename")) + onView(withId(com.android.internal.R.id.content_preview_filename)) .check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_filename")) + onView(withId(com.android.internal.R.id.content_preview_filename)) .check(matches(withText("app.pdf + 2 files"))); - onView(withIdFromRuntimeResource("content_preview_file_icon")) + onView(withId(com.android.internal.R.id.content_preview_file_icon)) .check(matches(isDisplayed())); } @@ -1258,10 +1144,11 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_filename")) + onView(withId(com.android.internal.R.id.content_preview_filename)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_filename)) .check(matches(withText("app.pdf"))); - onView(withIdFromRuntimeResource("content_preview_file_icon")) + onView(withId(com.android.internal.R.id.content_preview_file_icon)) .check(matches(isDisplayed())); } @@ -1297,10 +1184,11 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("content_preview_filename")) + onView(withId(com.android.internal.R.id.content_preview_filename)) + .check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.content_preview_filename)) .check(matches(withText("app.pdf + 1 file"))); - onView(withIdFromRuntimeResource("content_preview_file_icon")) + onView(withId(com.android.internal.R.id.content_preview_file_icon)) .check(matches(isDisplayed())); } @@ -1347,9 +1235,11 @@ public class UnbundledChooserActivityTest { is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST)); } + // This test is too long and too slow and should not be taken as an example for future tests. @Test - public void testConvertToChooserTarget_predictionService() { + public void testDirectTargetSelectionLogging() { Intent sendIntent = createSendTextIntent(); + // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); when( ChooserActivityOverrideData @@ -1362,80 +1252,82 @@ public class UnbundledChooserActivityTest { Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - final ChooserActivity activity = - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - - List<ShareShortcutInfo> shortcuts = createShortcuts(activity); - - int[] expectedOrderAllShortcuts = {0, 1, 2, 3}; - float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.98f, 0.97f}; - - List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts, - null, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); - assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets, - expectedOrderAllShortcuts, expectedScoreAllShortcuts); - - List<ShareShortcutInfo> subset = new ArrayList<>(); - subset.add(shortcuts.get(1)); - subset.add(shortcuts.get(2)); - subset.add(shortcuts.get(3)); - - int[] expectedOrderSubset = {1, 2, 3}; - float[] expectedScoreSubset = {0.99f, 0.98f, 0.97f}; + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + createShortcutLoaderFactory(); - chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null, - TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE); - assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets, - expectedOrderSubset, expectedScoreSubset); - } - - @Test - public void testConvertToChooserTarget_shortcutManager() { - Intent sendIntent = createSendTextIntent(); - List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(resolvedComponentInfos); - - final ChooserActivity activity = + // Start activity + final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - List<ShareShortcutInfo> shortcuts = createShortcuts(activity); - - int[] expectedOrderAllShortcuts = {2, 0, 3, 1}; - float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.99f, 0.98f}; + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); - List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts, - null, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER); - assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets, - expectedOrderAllShortcuts, expectedScoreAllShortcuts); + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets(1, ""); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[] { + new ShortcutLoader.ShortcutResultInfo( + appTargets.getValue()[0], + serviceTargets + ) + }, + new HashMap<>(), + new HashMap<>() + ); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); - List<ShareShortcutInfo> subset = new ArrayList<>(); - subset.add(shortcuts.get(1)); - subset.add(shortcuts.get(2)); - subset.add(shortcuts.get(3)); + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); - int[] expectedOrderSubset = {2, 3, 1}; - float[] expectedScoreSubset = {1.0f, 0.99f, 0.98f}; + // Click on the direct target + String name = serviceTargets.get(0).getTitle().toString(); + onView(withText(name)) + .perform(click()); + waitForIdle(); - chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null, - TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER); - assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets, - expectedOrderSubset, expectedScoreSubset); + ArgumentCaptor<HashedStringCache.HashResult> hashCaptor = + ArgumentCaptor.forClass(HashedStringCache.HashResult.class); + verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected( + eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE), + /* packageName= */ any(), + /* positionPicked= */ anyInt(), + /* directTargetAlsoRanked= */ eq(-1), + /* numCallerProvided= */ anyInt(), + /* directTargetHashed= */ hashCaptor.capture(), + /* isPinned= */ anyBoolean(), + /* successfullySelected= */ anyBoolean(), + /* selectionCost= */ anyLong()); + String hashedName = hashCaptor.getValue().hashedString; + assertThat( + "Hash is not predictable but must be obfuscated", + hashedName, is(not(name))); } // This test is too long and too slow and should not be taken as an example for future tests. - @Test @Ignore - public void testDirectTargetSelectionLogging() throws InterruptedException { + @Test + public void testDirectTargetLoggingWithRankedAppTarget() { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1450,44 +1342,56 @@ public class UnbundledChooserActivityTest { Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - // Set up resources - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - // Create direct share target - List<ChooserTarget> serviceTargets = createDirectShareTargets(1, ""); - ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + createShortcutLoaderFactory(); // Start activity final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); - // Insert the direct share target - Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); - directShareToShortcutInfos.put(serviceTargets.get(0), null); - InstrumentationRegistry.getInstrumentation().runOnMainSync( - () -> activity.getAdapter().addServiceResults( - activity.createTestDisplayResolveInfo(sendIntent, - ri, - "testLabel", - "testInfo", - sendIntent, - /* resolveInfoPresentationGetter */ null), - serviceTargets, - TARGET_TYPE_CHOOSER_TARGET, - directShareToShortcutInfos) - ); + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); - // Thread.sleep shouldn't be a thing in an integration test but it's - // necessary here because of the way the code is structured - // TODO: restructure the tests b/129870719 - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[] { + new ShortcutLoader.ShortcutResultInfo( + appTargets.getValue()[0], + serviceTargets + ) + }, + new HashMap<>(), + new HashMap<>() + ); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); - assertThat("Chooser should have 3 targets (2 apps, 1 direct)", - activity.getAdapter().getCount(), is(3)); - assertThat("Chooser should have exactly one selectable direct target", - activity.getAdapter().getSelectableServiceTargetCount(), is(1)); - assertThat("The resolver info must match the resolver info used to create the target", - activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); // Click on the direct target String name = serviceTargets.get(0).getTitle().toString(); @@ -1495,24 +1399,29 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); - // Currently we're seeing 3 invocations - // 1. ChooserActivity.onCreate() - // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView() - // 3. ChooserActivity.startSelected -- which is the one we're after - verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture()); - assertThat(logMakerCaptor.getAllValues().get(2).getCategory(), - is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET)); - String hashedName = (String) logMakerCaptor - .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME); - assertThat("Hash is not predictable but must be obfuscated", - hashedName, is(not(name))); - assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor - .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1)); + verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected( + eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE), + /* packageName= */ any(), + /* positionPicked= */ anyInt(), + /* directTargetAlsoRanked= */ eq(0), + /* numCallerProvided= */ anyInt(), + /* directTargetHashed= */ any(), + /* isPinned= */ anyBoolean(), + /* successfullySelected= */ anyBoolean(), + /* selectionCost= */ anyLong()); } - // This test is too long and too slow and should not be taken as an example for future tests. - @Test @Ignore - public void testDirectTargetLoggingWithRankedAppTarget() throws InterruptedException { + @Test + public void testShortcutTargetWithApplyAppLimits() { + // Set up resources + ChooserActivityOverrideData.getInstance().resources = Mockito.spy( + InstrumentationRegistry.getInstrumentation().getContext().getResources()); + when( + ChooserActivityOverrideData + .getInstance() + .resources + .getInteger(R.integer.config_maxShortcutTargetsPerApp)) + .thenReturn(1); Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1527,64 +1436,67 @@ public class UnbundledChooserActivityTest { Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - // Set up resources - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); - // Create direct share target - List<ChooserTarget> serviceTargets = createDirectShareTargets(1, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + createShortcutLoaderFactory(); // Start activity - final IChooserWrapper activity = (IChooserWrapper) - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - - // Insert the direct share target - Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); - directShareToShortcutInfos.put(serviceTargets.get(0), null); - InstrumentationRegistry.getInstrumentation().runOnMainSync( - () -> activity.getAdapter().addServiceResults( - activity.createTestDisplayResolveInfo(sendIntent, - ri, - "testLabel", - "testInfo", - sendIntent, - /* resolveInfoPresentationGetter */ null), - serviceTargets, - TARGET_TYPE_CHOOSER_TARGET, - directShareToShortcutInfos) - ); - // Thread.sleep shouldn't be a thing in an integration test but it's - // necessary here because of the way the code is structured - // TODO: restructure the tests b/129870719 - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); + final IChooserWrapper activity = (IChooserWrapper) mActivityRule + .launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); - assertThat("Chooser should have 3 targets (2 apps, 1 direct)", - activity.getAdapter().getCount(), is(3)); - assertThat("Chooser should have exactly one selectable direct target", - activity.getAdapter().getSelectableServiceTargetCount(), is(1)); - assertThat("The resolver info must match the resolver info used to create the target", - activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); - // Click on the direct target - String name = serviceTargets.get(0).getTitle().toString(); - onView(withText(name)) - .perform(click()); + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[] { + new ShortcutLoader.ShortcutResultInfo( + appTargets.getValue()[0], + serviceTargets + ) + }, + new HashMap<>(), + new HashMap<>() + ); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); waitForIdle(); - // Currently we're seeing 3 invocations - // 1. ChooserActivity.onCreate() - // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView() - // 3. ChooserActivity.startSelected -- which is the one we're after - verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture()); - assertThat(logMakerCaptor.getAllValues().get(2).getCategory(), - is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET)); - assertThat("The packages should match for app target and direct target", logMakerCaptor - .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0)); + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly one selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is("testTitle0")); } - @Test @Ignore - public void testShortcutTargetWithApplyAppLimits() throws InterruptedException { + @Test + public void testShortcutTargetWithoutApplyAppLimits() { + setDeviceConfigProperty( + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, + Boolean.toString(false)); // Set up resources ChooserActivityOverrideData.getInstance().resources = Mockito.spy( InstrumentationRegistry.getInstrumentation().getContext().getResources()); @@ -1592,8 +1504,7 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData .getInstance() .resources - .getInteger( - getRuntimeResourceId("config_maxShortcutTargetsPerApp", "integer"))) + .getInteger(R.integer.config_maxShortcutTargetsPerApp)) .thenReturn(1); Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed @@ -1608,56 +1519,72 @@ public class UnbundledChooserActivityTest { Mockito.anyBoolean(), Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - // Create direct share target - List<ChooserTarget> serviceTargets = createDirectShareTargets(2, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + createShortcutLoaderFactory(); // Start activity - final ChooserActivity activity = + final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - final IChooserWrapper wrapper = (IChooserWrapper) activity; + waitForIdle(); - // Insert the direct share target - Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); - List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity); - directShareToShortcutInfos.put(serviceTargets.get(0), - shortcutInfos.get(0).getShortcutInfo()); - directShareToShortcutInfos.put(serviceTargets.get(1), - shortcutInfos.get(1).getShortcutInfo()); - InstrumentationRegistry.getInstrumentation().runOnMainSync( - () -> wrapper.getAdapter().addServiceResults( - wrapper.createTestDisplayResolveInfo(sendIntent, - ri, - "testLabel", - "testInfo", - sendIntent, - /* resolveInfoPresentationGetter */ null), - serviceTargets, - TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, - directShareToShortcutInfos) + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets( + 2, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[] { + new ShortcutLoader.ShortcutResultInfo( + appTargets.getValue()[0], + serviceTargets + ) + }, + new HashMap<>(), + new HashMap<>() ); - // Thread.sleep shouldn't be a thing in an integration test but it's - // necessary here because of the way the code is structured - // TODO: restructure the tests b/129870719 - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); - assertThat("Chooser should have 3 targets (2 apps, 1 direct)", - wrapper.getAdapter().getCount(), is(3)); - assertThat("Chooser should have exactly one selectable direct target", - wrapper.getAdapter().getSelectableServiceTargetCount(), is(1)); - assertThat("The resolver info must match the resolver info used to create the target", - wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri)); - assertThat("The display label must match", - wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0")); + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 4 targets (2 apps, 2 direct)", + activeAdapter.getCount(), + is(4)); + assertThat( + "Chooser should have exactly two selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(2)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activeAdapter.getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is("testTitle0")); + assertThat( + "The display label must match", + activeAdapter.getItem(1).getDisplayLabel(), + is("testTitle1")); } - @Test @Ignore - public void testShortcutTargetWithoutApplyAppLimits() throws InterruptedException { - DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, + @Test + public void testLaunchWithCallerProvidedTarget() { + setDeviceConfigProperty( SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, - Boolean.toString(false), - true /* makeDefault*/); + Boolean.toString(false)); // Set up resources ChooserActivityOverrideData.getInstance().resources = Mockito.spy( InstrumentationRegistry.getInstrumentation().getContext().getResources()); @@ -1665,10 +1592,9 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData .getInstance() .resources - .getInteger( - getRuntimeResourceId("config_maxShortcutTargetsPerApp", "integer"))) + .getInteger(R.integer.config_maxShortcutTargetsPerApp)) .thenReturn(1); - Intent sendIntent = createSendTextIntent(); + // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); when( @@ -1681,50 +1607,61 @@ public class UnbundledChooserActivityTest { Mockito.anyBoolean(), Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - // Create direct share target - List<ChooserTarget> serviceTargets = createDirectShareTargets(2, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + + // set caller-provided target + Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + String callerTargetLabel = "Caller Target"; + ChooserTarget[] targets = new ChooserTarget[] { + new ChooserTarget( + callerTargetLabel, + Icon.createWithBitmap(createBitmap()), + 0.1f, + resolvedComponentInfos.get(0).name, + new Bundle()) + }; + chooserIntent.putExtra(Intent.EXTRA_CHOOSER_TARGETS, targets); + + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + createShortcutLoaderFactory(); // Start activity - final ChooserActivity activity = - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - final IChooserWrapper wrapper = (IChooserWrapper) activity; + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(chooserIntent); + waitForIdle(); - // Insert the direct share target - Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); - List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity); - directShareToShortcutInfos.put(serviceTargets.get(0), - shortcutInfos.get(0).getShortcutInfo()); - directShareToShortcutInfos.put(serviceTargets.get(1), - shortcutInfos.get(1).getShortcutInfo()); - InstrumentationRegistry.getInstrumentation().runOnMainSync( - () -> wrapper.getAdapter().addServiceResults( - wrapper.createTestDisplayResolveInfo(sendIntent, - ri, - "testLabel", - "testInfo", - sendIntent, - /* resolveInfoPresentationGetter */ null), - serviceTargets, - TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE, - directShareToShortcutInfos) - ); - // Thread.sleep shouldn't be a thing in an integration test but it's - // necessary here because of the way the code is structured - // TODO: restructure the tests b/129870719 - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); - assertThat("Chooser should have 4 targets (2 apps, 2 direct)", - wrapper.getAdapter().getCount(), is(4)); - assertThat("Chooser should have exactly two selectable direct target", - wrapper.getAdapter().getSelectableServiceTargetCount(), is(2)); - assertThat("The resolver info must match the resolver info used to create the target", - wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri)); - assertThat("The display label must match", - wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0")); - assertThat("The display label must match", - wrapper.getAdapter().getItem(1).getDisplayLabel(), is("testTitle1")); + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + ShortcutLoader.Result result = new ShortcutLoader.Result( + true, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[0], + new HashMap<>(), + new HashMap<>()); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); + + final ChooserListAdapter activeAdapter = activity.getAdapter(); + assertThat( + "Chooser should have 3 targets (2 apps, 1 direct)", + activeAdapter.getCount(), + is(3)); + assertThat( + "Chooser should have exactly two selectable direct target", + activeAdapter.getSelectableServiceTargetCount(), + is(1)); + assertThat( + "The display label must match", + activeAdapter.getItem(0).getDisplayLabel(), + is(callerTargetLabel)); } @Test @@ -1742,7 +1679,7 @@ public class UnbundledChooserActivityTest { .getContext().getResources().getConfiguration())); waitForIdle(); - onView(withIdFromRuntimeResource("resolver_list")) + onView(withId(com.android.internal.R.id.resolver_list)) .check(matches(withGridColumnCount(6))); } @@ -1760,8 +1697,7 @@ public class UnbundledChooserActivityTest { } private void testDirectTargetLoggingWithAppTargetNotRanked( - int orientation, int appTargetsExpected - ) throws InterruptedException { + int orientation, int appTargetsExpected) { Configuration configuration = new Configuration(InstrumentationRegistry.getInstrumentation().getContext() .getResources().getConfiguration()); @@ -1790,18 +1726,14 @@ public class UnbundledChooserActivityTest { Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - // Set up resources - MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger; - ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class); // Create direct share target List<ChooserTarget> serviceTargets = createDirectShareTargets(1, resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName); ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0); // Start activity - final IChooserWrapper activity = (IChooserWrapper) + final IChooserWrapper wrapper = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - final IChooserWrapper wrapper = (IChooserWrapper) activity; // Insert the direct share target Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); directShareToShortcutInfos.put(serviceTargets.get(0), null); @@ -1815,12 +1747,9 @@ public class UnbundledChooserActivityTest { /* resolveInfoPresentationGetter */ null), serviceTargets, TARGET_TYPE_CHOOSER_TARGET, - directShareToShortcutInfos) + directShareToShortcutInfos, + /* directShareToAppTargets */ null) ); - // Thread.sleep shouldn't be a thing in an integration test but it's - // necessary here because of the way the code is structured - // TODO: restructure the tests b/129870719 - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); assertThat( String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)", @@ -1837,21 +1766,22 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); - // Currently we're seeing 3 invocations - // 1. ChooserActivity.onCreate() - // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView() - // 3. ChooserActivity.startSelected -- which is the one we're after - verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture()); - assertThat(logMakerCaptor.getAllValues().get(2).getCategory(), - is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET)); - assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor - .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1)); + ChooserActivityLogger logger = wrapper.getChooserActivityLogger(); + verify(logger, times(1)).logShareTargetSelected( + eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE), + /* packageName= */ any(), + /* positionPicked= */ anyInt(), + // The packages sholdn't match for app target and direct target: + /* directTargetAlsoRanked= */ eq(-1), + /* numCallerProvided= */ anyInt(), + /* directTargetHashed= */ any(), + /* isPinned= */ anyBoolean(), + /* successfullySelected= */ anyBoolean(), + /* selectionCost= */ anyLong()); } @Test public void testWorkTab_displayedWhenWorkProfileUserAvailable() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); markWorkProfileUserAvailable(); @@ -1859,26 +1789,22 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withIdFromRuntimeResource("tabs")).check(matches(isDisplayed())); + onView(withId(android.R.id.tabs)).check(matches(isDisplayed())); } @Test public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withIdFromRuntimeResource("tabs")).check(matches(not(isDisplayed()))); + onView(withId(android.R.id.tabs)).check(matches(not(isDisplayed()))); } @Test public void testWorkTab_eachTabUsesExpectedAdapter() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; int personalProfileTargets = 3; int otherProfileTargets = 1; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -1897,7 +1823,7 @@ public class UnbundledChooserActivityTest { waitForIdle(); assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0)); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10)); assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets)); assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); @@ -1905,8 +1831,6 @@ public class UnbundledChooserActivityTest { @Test public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -1920,16 +1844,14 @@ public class UnbundledChooserActivityTest { final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets)); } @Test @Ignore - public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; + public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); @@ -1945,13 +1867,10 @@ public class UnbundledChooserActivityTest { return true; }; - final IChooserWrapper activity = (IChooserWrapper) - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - // wait for the share sheet to expand - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); onView(first(allOf( withText(workResolvedComponentInfos.get(0) @@ -1964,8 +1883,6 @@ public class UnbundledChooserActivityTest { @Test public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -1979,18 +1896,17 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_cross_profile_blocked")) + onView(withText(R.string.resolver_cross_profile_blocked)) .check(matches(isDisplayed())); } @Test public void testWorkTab_workProfileDisabled_emptyStateShown() { - // enable the work tab feature flag markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -2002,22 +1918,19 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); - ResolverActivity.ENABLE_TABBED_VIEW = true; mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_turn_on_work_apps")) + onView(withText(R.string.resolver_turn_on_work_apps)) .check(matches(isDisplayed())); } @Test public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTest(3); @@ -2029,20 +1942,18 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_no_work_apps_available")) + onView(withText(R.string.resolver_no_work_apps_available)) .check(matches(isDisplayed())); } @Ignore // b/220067877 @Test public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTest(3); @@ -2056,19 +1967,17 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_cross_profile_blocked")) + onView(withText(R.string.resolver_cross_profile_blocked)) .check(matches(isDisplayed())); } @Test public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTest(3); @@ -2081,12 +1990,12 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_no_work_apps_available")) + onView(withText(R.string.resolver_no_work_apps_available)) .check(matches(isDisplayed())); } @@ -2115,7 +2024,7 @@ public class UnbundledChooserActivityTest { // timeout everywhere instead of introducing one to fix this particular test. assertThat(activity.getAdapter().getCount(), is(2)); - onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist()); + onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); ResolveInfo[] chosen = new ResolveInfo[1]; ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { @@ -2128,53 +2037,11 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); - ChooserActivityLoggerFake logger = - (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); - // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. - logger.removeCallsForUiEventsOfType( - ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_DIRECT_LOAD_COMPLETE.getId()); - - // SHARESHEET_TRIGGERED: - assertThat(logger.event(0).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); - - // SHARESHEET_STARTED: - assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); - assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); - assertThat(logger.get(1).mimeType, is("text/plain")); - assertThat(logger.get(1).packageName, is( - InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName())); - assertThat(logger.get(1).appProvidedApp, is(0)); - assertThat(logger.get(1).appProvidedDirect, is(0)); - assertThat(logger.get(1).isWorkprofile, is(false)); - assertThat(logger.get(1).previewType, is(3)); - - // SHARESHEET_APP_LOAD_COMPLETE: - assertThat(logger.event(2).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - - // Next are just artifacts of test set-up: - assertThat(logger.event(3).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId())); - assertThat(logger.event(4).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId())); - - // SHARESHEET_APP_TARGET_SELECTED: - assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); - assertThat(logger.get(5).targetType, - is(ChooserActivityLogger - .SharesheetTargetSelectedEvent.SHARESHEET_APP_TARGET_SELECTED.getId())); - - // No more events. - assertThat(logger.numCalls(), is(6)); } - @Test @Ignore - public void testDirectTargetLogging() throws InterruptedException { + @Test + public void testDirectTargetLogging() { Intent sendIntent = createSendTextIntent(); // We need app targets for direct targets to get displayed List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -2189,41 +2056,59 @@ public class UnbundledChooserActivityTest { Mockito.isA(List.class))) .thenReturn(resolvedComponentInfos); - // Create direct share target - List<ChooserTarget> serviceTargets = createDirectShareTargets(1, - resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); - ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0); + // create test shortcut loader factory, remember loaders and their callbacks + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + new SparseArray<>(); + ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = + (userHandle, callback) -> { + Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = + new Pair<>(mock(ShortcutLoader.class), callback); + shortcutLoaders.put(userHandle.getIdentifier(), pair); + return pair.first; + }; // Start activity final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); - // Insert the direct share target - Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>(); - directShareToShortcutInfos.put(serviceTargets.get(0), null); - InstrumentationRegistry.getInstrumentation().runOnMainSync( - () -> activity.getAdapter().addServiceResults( - activity.createTestDisplayResolveInfo(sendIntent, - ri, - "testLabel", - "testInfo", - sendIntent, - /* resolveInfoPresentationGetter */ null), - serviceTargets, - TARGET_TYPE_CHOOSER_TARGET, - directShareToShortcutInfos) + // verify that ShortcutLoader was queried + ArgumentCaptor<DisplayResolveInfo[]> appTargets = + ArgumentCaptor.forClass(DisplayResolveInfo[].class); + verify(shortcutLoaders.get(0).first, times(1)) + .queryShortcuts(appTargets.capture()); + + // send shortcuts + assertThat( + "Wrong number of app targets", + appTargets.getValue().length, + is(resolvedComponentInfos.size())); + List<ChooserTarget> serviceTargets = createDirectShareTargets(1, + resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName); + ShortcutLoader.Result result = new ShortcutLoader.Result( + // TODO: test another value as well + false, + appTargets.getValue(), + new ShortcutLoader.ShortcutResultInfo[] { + new ShortcutLoader.ShortcutResultInfo( + appTargets.getValue()[0], + serviceTargets + ) + }, + new HashMap<>(), + new HashMap<>() ); - // Thread.sleep shouldn't be a thing in an integration test but it's - // necessary here because of the way the code is structured - // TODO: restructure the tests b/129870719 - Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs); + activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result)); + waitForIdle(); assertThat("Chooser should have 3 targets (2 apps, 1 direct)", activity.getAdapter().getCount(), is(3)); assertThat("Chooser should have exactly one selectable direct target", activity.getAdapter().getSelectableServiceTargetCount(), is(1)); - assertThat("The resolver info must match the resolver info used to create the target", - activity.getAdapter().getItem(0).getResolveInfo(), is(ri)); + assertThat( + "The resolver info must match the resolver info used to create the target", + activity.getAdapter().getItem(0).getResolveInfo(), + is(resolvedComponentInfos.get(0).getResolveInfoAt(0))); // Click on the direct target String name = serviceTargets.get(0).getTitle().toString(); @@ -2231,34 +2116,18 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); - ChooserActivityLoggerFake logger = - (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); - assertThat(logger.numCalls(), is(6)); - // first one should be SHARESHEET_TRIGGERED uievent - assertThat(logger.get(0).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); - assertThat(logger.get(0).event.getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); - // second one should be SHARESHEET_STARTED event - assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); - assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); - assertThat(logger.get(1).mimeType, is("text/plain")); - assertThat(logger.get(1).packageName, is( - InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName())); - assertThat(logger.get(1).appProvidedApp, is(0)); - assertThat(logger.get(1).appProvidedDirect, is(0)); - assertThat(logger.get(1).isWorkprofile, is(false)); - assertThat(logger.get(1).previewType, is(3)); - // third one should be SHARESHEET_APP_LOAD_COMPLETE uievent - assertThat(logger.get(2).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED)); - assertThat(logger.get(2).event.getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - // fourth and fifth are just artifacts of test set-up - // sixth one should be ranking atom with SHARESHEET_COPY_TARGET_SELECTED event - assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); - assertThat(logger.get(5).targetType, - is(ChooserActivityLogger - .SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId())); + ChooserActivityLogger logger = activity.getChooserActivityLogger(); + ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class); + verify(logger, times(1)).logShareTargetSelected( + eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE), + /* packageName= */ any(), + /* positionPicked= */ anyInt(), + /* directTargetAlsoRanked= */ anyInt(), + /* numCallerProvided= */ anyInt(), + /* directTargetHashed= */ any(), + /* isPinned= */ anyBoolean(), + /* successfullySelected= */ anyBoolean(), + /* selectionCost= */ anyLong()); } @Test @Ignore @@ -2290,44 +2159,7 @@ public class UnbundledChooserActivityTest { assertThat("Chooser should have no direct targets", activity.getAdapter().getSelectableServiceTargetCount(), is(0)); - ChooserActivityLoggerFake logger = - (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); - // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. - logger.removeCallsForUiEventsOfType( - ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_DIRECT_LOAD_COMPLETE.getId()); - - // SHARESHEET_TRIGGERED: - assertThat(logger.event(0).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); - - // SHARESHEET_STARTED: - assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); - assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); - assertThat(logger.get(1).mimeType, is("text/plain")); - assertThat(logger.get(1).packageName, is( - InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName())); - assertThat(logger.get(1).appProvidedApp, is(0)); - assertThat(logger.get(1).appProvidedDirect, is(0)); - assertThat(logger.get(1).isWorkprofile, is(false)); - assertThat(logger.get(1).previewType, is(3)); - - // SHARESHEET_APP_LOAD_COMPLETE: - assertThat(logger.event(2).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - - // SHARESHEET_EMPTY_DIRECT_SHARE_ROW: - assertThat(logger.event(3).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId())); - - // Next is just an artifact of test set-up: - assertThat(logger.event(4).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId())); - - assertThat(logger.numCalls(), is(5)); } @Ignore // b/220067877 @@ -2351,58 +2183,14 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed())); - onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click()); - - ChooserActivityLoggerFake logger = - (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); + onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click()); // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. - logger.removeCallsForUiEventsOfType( - ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_DIRECT_LOAD_COMPLETE.getId()); - - // SHARESHEET_TRIGGERED: - assertThat(logger.event(0).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); - - // SHARESHEET_STARTED: - assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); - assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); - assertThat(logger.get(1).mimeType, is("text/plain")); - assertThat(logger.get(1).packageName, is( - InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName())); - assertThat(logger.get(1).appProvidedApp, is(0)); - assertThat(logger.get(1).appProvidedDirect, is(0)); - assertThat(logger.get(1).isWorkprofile, is(false)); - assertThat(logger.get(1).previewType, is(3)); - - // SHARESHEET_APP_LOAD_COMPLETE: - assertThat(logger.event(2).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - - // Next are just artifacts of test set-up: - assertThat(logger.event(3).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId())); - assertThat(logger.event(4).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId())); - - // SHARESHEET_COPY_TARGET_SELECTED: - assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED)); - assertThat(logger.get(5).targetType, - is(ChooserActivityLogger - .SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId())); - - // No more events. - assertThat(logger.numCalls(), is(6)); } @Test @Ignore("b/222124533") public void testSwitchProfileLogging() throws InterruptedException { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -2416,134 +2204,16 @@ public class UnbundledChooserActivityTest { final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_personal_tab")).perform(click()); + onView(withText(R.string.resolver_personal_tab)).perform(click()); waitForIdle(); - ChooserActivityLoggerFake logger = - (ChooserActivityLoggerFake) activity.getChooserActivityLogger(); - // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events. - logger.removeCallsForUiEventsOfType( - ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_DIRECT_LOAD_COMPLETE.getId()); - - // SHARESHEET_TRIGGERED: - assertThat(logger.event(0).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId())); - - // SHARESHEET_STARTED: - assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED)); - assertThat(logger.get(1).intent, is(Intent.ACTION_SEND)); - assertThat(logger.get(1).mimeType, is(TEST_MIME_TYPE)); - assertThat(logger.get(1).packageName, is( - InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName())); - assertThat(logger.get(1).appProvidedApp, is(0)); - assertThat(logger.get(1).appProvidedDirect, is(0)); - assertThat(logger.get(1).isWorkprofile, is(false)); - assertThat(logger.get(1).previewType, is(3)); - - // SHARESHEET_APP_LOAD_COMPLETE: - assertThat(logger.event(2).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - - // Next is just an artifact of test set-up: - assertThat(logger.event(3).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId())); - - // SHARESHEET_PROFILE_CHANGED: - assertThat(logger.event(4).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_PROFILE_CHANGED.getId())); - - // Repeat the loading steps in the new profile: - - // SHARESHEET_APP_LOAD_COMPLETE: - assertThat(logger.event(5).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId())); - - // Next is again an artifact of test set-up: - assertThat(logger.event(6).getId(), - is(ChooserActivityLogger - .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId())); - - // SHARESHEET_PROFILE_CHANGED: - assertThat(logger.event(7).getId(), - is(ChooserActivityLogger.SharesheetStandardEvent - .SHARESHEET_PROFILE_CHANGED.getId())); - - // No more events (this profile was already loaded). - assertThat(logger.numCalls(), is(8)); - } - - @Test - public void testAutolaunch_singleTarget_wifthWorkProfileAndTabbedViewOff_noAutolaunch() { - ResolverActivity.ENABLE_TABBED_VIEW = false; - List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); - Intent sendIntent = createSendTextIntent(); - sendIntent.setType(TEST_MIME_TYPE); - ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); - return true; - }; - waitForIdle(); - - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); - waitForIdle(); - - assertTrue(chosen[0] == null); - } - - @Test - public void testAutolaunch_singleTarget_noWorkProfile_autolaunch() { - ResolverActivity.ENABLE_TABBED_VIEW = false; - List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTest(1); - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntent( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class))) - .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); - Intent sendIntent = createSendTextIntent(); - sendIntent.setType(TEST_MIME_TYPE); - ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); - return true; - }; - waitForIdle(); - - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); - waitForIdle(); - - assertThat(chosen[0], is(personalResolvedComponentInfos.get(0).getResolveInfoAt(0))); } @Test public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_autolaunch() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -2559,7 +2229,7 @@ public class UnbundledChooserActivityTest { return true; }; - mActivityRule.launchActivity(sendIntent); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); waitForIdle(); assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0))); @@ -2591,7 +2261,7 @@ public class UnbundledChooserActivityTest { when( ChooserActivityOverrideData .getInstance().packageManager - .resolveActivity(any(Intent.class), anyInt())) + .resolveActivity(any(Intent.class), any())) .thenReturn(ri); waitForIdle(); @@ -2605,8 +2275,6 @@ public class UnbundledChooserActivityTest { @Test public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); int workProfileTargets = 1; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -2624,7 +2292,7 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData .getInstance() .packageManager - .resolveActivity(any(Intent.class), anyInt())) + .resolveActivity(any(Intent.class), any())) .thenReturn(createFakeResolveInfo()); waitForIdle(); @@ -2637,8 +2305,6 @@ public class UnbundledChooserActivityTest { @Test public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); int workProfileTargets = 4; List<ResolvedComponentInfo> personalResolvedComponentInfos = @@ -2657,24 +2323,22 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData .getInstance() .packageManager - .resolveActivity(any(Intent.class), anyInt())) + .resolveActivity(any(Intent.class), any())) .thenReturn(createFakeResolveInfo()); mActivityRule.launchActivity(chooserIntent); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_cross_profile_blocked")) + onView(withText(R.string.resolver_cross_profile_blocked)) .check(matches(isDisplayed())); } @Test public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTest(3); @@ -2691,17 +2355,17 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData .getInstance() .packageManager - .resolveActivity(any(Intent.class), anyInt())) + .resolveActivity(any(Intent.class), any())) .thenReturn(createFakeResolveInfo()); mActivityRule.launchActivity(chooserIntent); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withTextFromRuntimeResource("resolver_no_work_apps_available")) + onView(withText(R.string.resolver_no_work_apps_available)) .check(matches(isDisplayed())); } @@ -2726,7 +2390,7 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData .getInstance() .packageManager - .resolveActivity(any(Intent.class), anyInt())) + .resolveActivity(any(Intent.class), any())) .thenReturn(ri); waitForIdle(); @@ -2740,150 +2404,35 @@ public class UnbundledChooserActivityTest { } @Test - public void testWorkTab_selectingWorkTabWithPausedWorkProfile_directShareTargetsNotQueried() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; - markWorkProfileUserAvailable(); - List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(3); - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); - ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true; - boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; - ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = - chooserListAdapter -> { - isQueryDirectShareCalledOnWorkProfile[0] = - (chooserListAdapter.getUserHandle().getIdentifier() == 10); - return null; - }; - Intent sendIntent = createSendTextIntent(); - sendIntent.setType(TEST_MIME_TYPE); - - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); - waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) - .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); - waitForIdle(); - - assertFalse("Direct share targets were queried on a paused work profile", - isQueryDirectShareCalledOnWorkProfile[0]); - } - - @Test - public void testWorkTab_selectingWorkTabWithNotRunningWorkUser_directShareTargetsNotQueried() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; - markWorkProfileUserAvailable(); - List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(3); - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); - ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false; - boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; - ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = - chooserListAdapter -> { - isQueryDirectShareCalledOnWorkProfile[0] = - (chooserListAdapter.getUserHandle().getIdentifier() == 10); - return null; - }; - Intent sendIntent = createSendTextIntent(); - sendIntent.setType(TEST_MIME_TYPE); - - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); - waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) - .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); - waitForIdle(); - - assertFalse("Direct share targets were queried on a locked work profile user", - isQueryDirectShareCalledOnWorkProfile[0]); - } - - @Test - public void testWorkTab_workUserNotRunning_workTargetsShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; - markWorkProfileUserAvailable(); - List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(3); - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); - Intent sendIntent = createSendTextIntent(); - sendIntent.setType(TEST_MIME_TYPE); - ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false; - - final ChooserActivity activity = - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); - final IChooserWrapper wrapper = (IChooserWrapper) activity; - waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")).perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); - waitForIdle(); - - assertEquals(3, wrapper.getWorkListAdapter().getCount()); - } - - @Test - public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; + public void test_query_shortcut_loader_for_the_selected_tab() { markWorkProfileUserAvailable(); List<ResolvedComponentInfo> personalResolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(3); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); - ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false; - boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false }; - ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets = - chooserListAdapter -> { - isQueryDirectShareCalledOnWorkProfile[0] = - (chooserListAdapter.getUserHandle().getIdentifier() == 10); - return null; - }; + ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class); + ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class); + final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>(); + shortcutLoaders.put(0, personalProfileShortcutLoader); + shortcutLoaders.put(10, workProfileShortcutLoader); + ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = + (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null); Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); waitForIdle(); - assertFalse("Direct share targets were queried on a locked work profile user", - isQueryDirectShareCalledOnWorkProfile[0]); - } - - @Test - public void testWorkTab_workUserLocked_workTargetsShown() { - // enable the work tab feature flag - ResolverActivity.ENABLE_TABBED_VIEW = true; - markWorkProfileUserAvailable(); - List<ResolvedComponentInfo> personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List<ResolvedComponentInfo> workResolvedComponentInfos = - createResolvedComponentsForTest(3); - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); - Intent sendIntent = createSendTextIntent(); - sendIntent.setType(TEST_MIME_TYPE); - ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false; + verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any()); - final ChooserActivity activity = - mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test")); - final IChooserWrapper wrapper = (IChooserWrapper) activity; - waitForIdle(); - onView(withIdFromRuntimeResource("contentPanel")) - .perform(swipeUp()); - onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click()); + onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - assertEquals(3, wrapper.getWorkListAdapter().getCount()); + verify(workProfileShortcutLoader, times(1)).queryShortcuts(any()); } private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { @@ -3092,21 +2641,6 @@ public class UnbundledChooserActivityTest { return shortcuts; } - private void assertCorrectShortcutToChooserTargetConversion(List<ShareShortcutInfo> shortcuts, - List<ChooserTarget> chooserTargets, int[] expectedOrder, float[] expectedScores) { - assertEquals(expectedOrder.length, chooserTargets.size()); - for (int i = 0; i < chooserTargets.size(); i++) { - ChooserTarget ct = chooserTargets.get(i); - ShortcutInfo si = shortcuts.get(expectedOrder[i]).getShortcutInfo(); - ComponentName cn = shortcuts.get(expectedOrder[i]).getTargetComponent(); - - assertEquals(si.getId(), ct.getIntentExtras().getString(Intent.EXTRA_SHORTCUT_ID)); - assertEquals(si.getShortLabel(), ct.getTitle()); - assertThat(Math.abs(expectedScores[i] - ct.getScore()) < 0.000001, is(true)); - assertEquals(cn.flattenToString(), ct.getComponentName().flattenToString()); - } - } - private void markWorkProfileUserAvailable() { ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); } @@ -3147,14 +2681,6 @@ public class UnbundledChooserActivityTest { .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); } - private Matcher<View> withIdFromRuntimeResource(String id) { - return withId(getRuntimeResourceId(id, "id")); - } - - private Matcher<View> withTextFromRuntimeResource(String id) { - return withText(getRuntimeResourceId(id, "string")); - } - private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) { return new GridRecyclerSpanCountMatcher(Matchers.is(columnCount)); } @@ -3214,25 +2740,17 @@ public class UnbundledChooserActivityTest { .thenReturn(targetsPerRow); } - // ChooserWrapperActivity inherits from the framework ChooserActivity, so if the framework - // resources have been updated since the framework was last built/pushed, the inherited behavior - // (which is the focus of our testing) will still be implemented in terms of the old resource - // IDs; then when we try to assert those IDs in tests (e.g. `onView(withText(R.string.foo))`), - // the expected values won't match. The tests can instead call this method (with the same - // general semantics as Resources#getIdentifier() e.g. `getRuntimeResourceId("foo", "string")`) - // to refer to the resource by that name in the runtime chooser, regardless of whether the - // framework code on the device is up-to-date. - // TODO: is there a better way to do this? (Other than abandoning inheritance-based DI wrapper?) - private int getRuntimeResourceId(String name, String defType) { - int id = -1; - if (ChooserActivityOverrideData.getInstance().resources != null) { - id = ChooserActivityOverrideData.getInstance().resources.getIdentifier( - name, defType, "android"); - } else { - id = mActivityRule.getActivity().getResources().getIdentifier(name, defType, "android"); - } - assertThat(id, greaterThan(0)); - - return id; + private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> + createShortcutLoaderFactory() { + SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders = + new SparseArray<>(); + ChooserActivityOverrideData.getInstance().shortcutLoaderFactory = + (userHandle, callback) -> { + Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair = + new Pair<>(mock(ShortcutLoader.class), callback); + shortcutLoaders.put(userHandle.getIdentifier(), pair); + return pair.first; + }; + return shortcutLoaders; } } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java new file mode 100644 index 00000000..f1febed2 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java @@ -0,0 +1,467 @@ +/* + * Copyright (C) 2022 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.intentresolver; + +import static android.testing.PollingCheck.waitFor; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.action.ViewActions.click; +import static androidx.test.espresso.action.ViewActions.swipeUp; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isSelected; +import static androidx.test.espresso.matcher.ViewMatchers.withId; +import static androidx.test.espresso.matcher.ViewMatchers.withText; + +import static com.android.intentresolver.ChooserWrapperActivity.sOverrides; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL; +import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK; + +import static org.hamcrest.CoreMatchers.not; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import android.companion.DeviceFilter; +import android.content.Intent; +import android.os.UserHandle; + +import androidx.test.InstrumentationRegistry; +import androidx.test.espresso.NoMatchingViewException; +import androidx.test.rule.ActivityTestRule; + +import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo; +import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; +import com.android.internal.R; + +import junit.framework.AssertionFailedError; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@DeviceFilter.MediumType +@RunWith(Parameterized.class) +public class UnbundledChooserActivityWorkProfileTest { + + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); + private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10); + + @Rule + public ActivityTestRule<ChooserWrapperActivity> mActivityRule = + new ActivityTestRule<>(ChooserWrapperActivity.class, false, + false); + private final TestCase mTestCase; + + public UnbundledChooserActivityWorkProfileTest(TestCase testCase) { + mTestCase = testCase; + } + + @Before + public void cleanOverrideData() { + // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the + // permissions we require (which we'll read from the manifest at runtime). + InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(); + + sOverrides.reset(); + } + + @Test + public void testBlocker() { + setUpPersonalAndWorkComponentInfos(); + sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents(); + sOverrides.myUserId = mTestCase.getMyUserHandle().getIdentifier(); + + launchActivity(mTestCase.getIsSendAction()); + switchToTab(mTestCase.getTab()); + + switch (mTestCase.getExpectedBlocker()) { + case NO_BLOCKER: + assertNoBlockerDisplayed(); + break; + case PERSONAL_PROFILE_SHARE_BLOCKER: + assertCantSharePersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_SHARE_BLOCKER: + assertCantShareWorkAppsBlockerDisplayed(); + break; + case PERSONAL_PROFILE_ACCESS_BLOCKER: + assertCantAccessPersonalAppsBlockerDisplayed(); + break; + case WORK_PROFILE_ACCESS_BLOCKER: + assertCantAccessWorkAppsBlockerDisplayed(); + break; + } + } + + @Parameterized.Parameters(name = "{0}") + public static Collection tests() { + return Arrays.asList( + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_SHARE_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ PERSONAL_PROFILE_SHARE_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ true, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ WORK, + /* expectedBlocker= */ WORK_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ WORK_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ PERSONAL_PROFILE_ACCESS_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ true, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ), + new TestCase( + /* isSendAction= */ false, + /* hasCrossProfileIntents= */ false, + /* myUserHandle= */ PERSONAL_USER_HANDLE, + /* tab= */ PERSONAL, + /* expectedBlocker= */ NO_BLOCKER + ) + ); + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile( + int numberOfResults, int userId) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add( + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + } + return infoList; + } + + private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) { + List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + } + return infoList; + } + + private void setUpPersonalAndWorkComponentInfos() { + markWorkProfileUserAvailable(); + int workProfileTargets = 4; + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, + /* userId */ WORK_USER_HANDLE.getIdentifier()); + List<ResolvedComponentInfo> workResolvedComponentInfos = + createResolvedComponentsForTest(workProfileTargets); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + } + + private void setupResolverControllers( + List<ResolvedComponentInfo> personalResolvedComponentInfos, + List<ResolvedComponentInfo> workResolvedComponentInfos) { + when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos); + when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + eq(UserHandle.SYSTEM))) + .thenReturn(new ArrayList<>(personalResolvedComponentInfos)); + } + + private void waitForIdle() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } + + private void markWorkProfileUserAvailable() { + ChooserWrapperActivity.sOverrides.workProfileUserHandle = WORK_USER_HANDLE; + } + + private void assertCantAccessWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantAccessPersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_access_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantShareWorkAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_work_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertCantSharePersonalAppsBlockerDisplayed() { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(isDisplayed())); + onView(withText(R.string.resolver_cant_share_with_personal_apps_explanation)) + .check(matches(isDisplayed())); + } + + private void assertNoBlockerDisplayed() { + try { + onView(withText(R.string.resolver_cross_profile_blocked)) + .check(matches(not(isDisplayed()))); + } catch (NoMatchingViewException ignored) { + } + } + + private void switchToTab(Tab tab) { + final int stringId = tab == Tab.WORK ? R.string.resolver_work_tab + : R.string.resolver_personal_tab; + + waitFor(() -> { + onView(withText(stringId)).perform(click()); + waitForIdle(); + + try { + onView(withText(stringId)).check(matches(isSelected())); + return true; + } catch (AssertionFailedError e) { + return false; + } + }); + + onView(withId(R.id.contentPanel)) + .perform(swipeUp()); + waitForIdle(); + } + + private Intent createTextIntent(boolean isSendAction) { + Intent sendIntent = new Intent(); + if (isSendAction) { + sendIntent.setAction(Intent.ACTION_SEND); + } + sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending"); + sendIntent.setType("text/plain"); + return sendIntent; + } + + private void launchActivity(boolean isSendAction) { + Intent sendIntent = createTextIntent(isSendAction); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test")); + waitForIdle(); + } + + public static class TestCase { + private final boolean mIsSendAction; + private final boolean mHasCrossProfileIntents; + private final UserHandle mMyUserHandle; + private final Tab mTab; + private final ExpectedBlocker mExpectedBlocker; + + public enum ExpectedBlocker { + NO_BLOCKER, + PERSONAL_PROFILE_SHARE_BLOCKER, + WORK_PROFILE_SHARE_BLOCKER, + PERSONAL_PROFILE_ACCESS_BLOCKER, + WORK_PROFILE_ACCESS_BLOCKER + } + + public enum Tab { + WORK, + PERSONAL + } + + public TestCase(boolean isSendAction, boolean hasCrossProfileIntents, + UserHandle myUserHandle, Tab tab, ExpectedBlocker expectedBlocker) { + mIsSendAction = isSendAction; + mHasCrossProfileIntents = hasCrossProfileIntents; + mMyUserHandle = myUserHandle; + mTab = tab; + mExpectedBlocker = expectedBlocker; + } + + public boolean getIsSendAction() { + return mIsSendAction; + } + + public boolean hasCrossProfileIntents() { + return mHasCrossProfileIntents; + } + + public UserHandle getMyUserHandle() { + return mMyUserHandle; + } + + public Tab getTab() { + return mTab; + } + + public ExpectedBlocker getExpectedBlocker() { + return mExpectedBlocker; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder("test"); + + if (mTab == WORK) { + result.append("WorkTab_"); + } else { + result.append("PersonalTab_"); + } + + if (mIsSendAction) { + result.append("sendAction_"); + } else { + result.append("notSendAction_"); + } + + if (mHasCrossProfileIntents) { + result.append("hasCrossProfileIntents_"); + } else { + result.append("doesNotHaveCrossProfileIntents_"); + } + + if (mMyUserHandle.equals(PERSONAL_USER_HANDLE)) { + result.append("myUserIsPersonal_"); + } else { + result.append("myUserIsWork_"); + } + + if (mExpectedBlocker == ExpectedBlocker.NO_BLOCKER) { + result.append("thenNoBlocker"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == PERSONAL_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnPersonalProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_ACCESS_BLOCKER) { + result.append("thenAccessBlockerOnWorkProfile"); + } else if (mExpectedBlocker == WORK_PROFILE_SHARE_BLOCKER) { + result.append("thenShareBlockerOnWorkProfile"); + } + + return result.toString(); + } + } +} diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt new file mode 100644 index 00000000..7c2b07a9 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2022 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.intentresolver.chooser + +import android.app.prediction.AppTarget +import android.app.prediction.AppTargetId +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ResolveInfo +import android.os.UserHandle +import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.createChooserTarget +import com.android.intentresolver.createShortcutInfo +import com.android.intentresolver.mock +import com.android.intentresolver.ResolverDataProvider +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class TargetInfoTest { + private val context = InstrumentationRegistry.getInstrumentation().getContext() + + @Test + fun testNewEmptyTargetInfo() { + val info = NotSelectableTargetInfo.newEmptyTargetInfo() + assertThat(info.isEmptyTargetInfo()).isTrue() + assertThat(info.isChooserTargetInfo()).isTrue() // From legacy inheritance model. + assertThat(info.hasDisplayIcon()).isFalse() + assertThat(info.getDisplayIconHolder().getDisplayIcon()).isNull() + } + + @Test + fun testNewPlaceholderTargetInfo() { + val info = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context) + assertThat(info.isPlaceHolderTargetInfo()).isTrue() + assertThat(info.isChooserTargetInfo()).isTrue() // From legacy inheritance model. + assertThat(info.hasDisplayIcon()).isTrue() + // TODO: test infrastructure isn't set up to assert anything about the icon itself. + } + + @Test + fun testNewSelectableTargetInfo() { + val resolvedIntent = Intent() + val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( + resolvedIntent, + ResolverDataProvider.createResolveInfo(1, 0), + "label", + "extended info", + resolvedIntent, + /* resolveInfoPresentationGetter= */ null) + val chooserTarget = createChooserTarget( + "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id") + val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3) + val appTarget = AppTarget( + AppTargetId("id"), + chooserTarget.componentName.packageName, + chooserTarget.componentName.className, + UserHandle.CURRENT) + + val targetInfo = SelectableTargetInfo.newSelectableTargetInfo( + baseDisplayInfo, + mock(), + resolvedIntent, + chooserTarget, + 0.1f, + shortcutInfo, + appTarget, + mock(), + ) + assertThat(targetInfo.isSelectableTargetInfo).isTrue() + assertThat(targetInfo.isChooserTargetInfo).isTrue() // From legacy inheritance model. + assertThat(targetInfo.displayResolveInfo).isSameInstanceAs(baseDisplayInfo) + assertThat(targetInfo.chooserTargetComponentName).isEqualTo(chooserTarget.componentName) + assertThat(targetInfo.directShareShortcutId).isEqualTo(shortcutInfo.id) + assertThat(targetInfo.directShareShortcutInfo).isSameInstanceAs(shortcutInfo) + assertThat(targetInfo.directShareAppTarget).isSameInstanceAs(appTarget) + assertThat(targetInfo.resolvedIntent).isSameInstanceAs(resolvedIntent) + // TODO: make more meaningful assertions about the behavior of a selectable target. + } + + @Test + fun test_SelectableTargetInfo_componentName_no_source_info() { + val chooserTarget = createChooserTarget( + "title", 0.3f, ResolverDataProvider.createComponentName(1), "test_shortcut_id") + val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(2), 3) + val appTarget = AppTarget( + AppTargetId("id"), + chooserTarget.componentName.packageName, + chooserTarget.componentName.className, + UserHandle.CURRENT) + val pkgName = "org.package" + val className = "MainActivity" + val backupResolveInfo = ResolveInfo().apply { + activityInfo = ActivityInfo().apply { + packageName = pkgName + name = className + } + } + + val targetInfo = SelectableTargetInfo.newSelectableTargetInfo( + null, + backupResolveInfo, + mock(), + chooserTarget, + 0.1f, + shortcutInfo, + appTarget, + mock(), + ) + assertThat(targetInfo.resolvedComponentName).isEqualTo(ComponentName(pkgName, className)) + } + + @Test + fun testNewDisplayResolveInfo() { + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") + intent.setType("text/plain") + + val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0) + + val targetInfo = DisplayResolveInfo.newDisplayResolveInfo( + intent, + resolveInfo, + "label", + "extended info", + intent, + /* resolveInfoPresentationGetter= */ null) + assertThat(targetInfo.isDisplayResolveInfo()).isTrue() + assertThat(targetInfo.isMultiDisplayResolveInfo()).isFalse() + assertThat(targetInfo.isChooserTargetInfo()).isFalse() + } + + @Test + fun testNewMultiDisplayResolveInfo() { + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") + intent.setType("text/plain") + + val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0) + val firstTargetInfo = DisplayResolveInfo.newDisplayResolveInfo( + intent, + resolveInfo, + "label 1", + "extended info 1", + intent, + /* resolveInfoPresentationGetter= */ null) + val secondTargetInfo = DisplayResolveInfo.newDisplayResolveInfo( + intent, + resolveInfo, + "label 2", + "extended info 2", + intent, + /* resolveInfoPresentationGetter= */ null) + + val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo( + listOf(firstTargetInfo, secondTargetInfo)) + + assertThat(multiTargetInfo.isMultiDisplayResolveInfo()).isTrue() + assertThat(multiTargetInfo.isDisplayResolveInfo()).isTrue() // From legacy inheritance. + assertThat(multiTargetInfo.isChooserTargetInfo()).isFalse() + + assertThat(multiTargetInfo.getExtendedInfo()).isNull() + + assertThat(multiTargetInfo.getAllDisplayTargets()) + .containsExactly(firstTargetInfo, secondTargetInfo) + + assertThat(multiTargetInfo.hasSelected()).isFalse() + assertThat(multiTargetInfo.getSelectedTarget()).isNull() + + multiTargetInfo.setSelected(1) + + assertThat(multiTargetInfo.hasSelected()).isTrue() + assertThat(multiTargetInfo.getSelectedTarget()).isEqualTo(secondTargetInfo) + + // TODO: consider exercising activity-start behavior. + // TODO: consider exercising DisplayResolveInfo base class behavior. + } +} diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java new file mode 100644 index 00000000..448718cd --- /dev/null +++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2019 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.intentresolver.model; + +import static junit.framework.Assert.assertEquals; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ResolveInfo; +import android.os.Message; + +import androidx.test.InstrumentationRegistry; + +import com.android.intentresolver.ResolverActivity; + +import org.junit.Test; + +import java.util.List; + +public class AbstractResolverComparatorTest { + + @Test + public void testPinned() { + ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("package", "class"), new Intent(), new ResolveInfo() + ); + r1.setPinned(true); + + ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("zackage", "zlass"), new Intent(), new ResolveInfo() + ); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context); + + assertEquals("Pinned ranks over unpinned", -1, comparator.compare(r1, r2)); + assertEquals("Unpinned ranks under pinned", 1, comparator.compare(r2, r1)); + } + + + @Test + public void testBothPinned() { + ResolveInfo pmInfo1 = new ResolveInfo(); + pmInfo1.activityInfo = new ActivityInfo(); + pmInfo1.activityInfo.packageName = "aaa"; + + ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("package", "class"), new Intent(), pmInfo1); + r1.setPinned(true); + + ResolveInfo pmInfo2 = new ResolveInfo(); + pmInfo2.activityInfo = new ActivityInfo(); + pmInfo2.activityInfo.packageName = "zzz"; + ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo( + new ComponentName("zackage", "zlass"), new Intent(), pmInfo2); + r2.setPinned(true); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context); + + assertEquals("Both pinned should rank alphabetically", -1, comparator.compare(r1, r2)); + } + + private AbstractResolverComparator getTestComparator(Context context) { + Intent intent = new Intent(); + + AbstractResolverComparator testComparator = + new AbstractResolverComparator(context, intent) { + + @Override + int compare(ResolveInfo lhs, ResolveInfo rhs) { + // Used for testing pinning, so we should never get here --- the overrides + // should determine the result instead. + return 1; + } + + @Override + void doCompute(List<ResolverActivity.ResolvedComponentInfo> targets) {} + + @Override + public float getScore(ComponentName name) { + return 0; + } + + @Override + void handleResultMessage(Message message) {} + }; + return testComparator; + } + +} diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt new file mode 100644 index 00000000..5756a0cd --- /dev/null +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2022 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.intentresolver.shortcuts + +import android.app.prediction.AppPredictor +import android.content.ComponentName +import android.content.Context +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ApplicationInfoFlags +import android.content.pm.ShortcutManager +import android.os.UserHandle +import android.os.UserManager +import androidx.test.filters.SmallTest +import com.android.intentresolver.any +import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.createAppTarget +import com.android.intentresolver.createShareShortcutInfo +import com.android.intentresolver.createShortcutInfo +import com.android.intentresolver.mock +import com.android.intentresolver.whenever +import org.junit.Assert.assertArrayEquals +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.util.concurrent.Executor +import java.util.function.Consumer + +@SmallTest +class ShortcutLoaderTest { + private val appInfo = ApplicationInfo().apply { + enabled = true + flags = 0 + } + private val pm = mock<PackageManager> { + whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo) + } + private val context = mock<Context> { + whenever(packageManager).thenReturn(pm) + whenever(createContextAsUser(any(), anyInt())).thenReturn(this) + } + private val executor = ImmediateExecutor() + private val intentFilter = mock<IntentFilter>() + private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() + private val callback = mock<Consumer<ShortcutLoader.Result>>() + + @Test + fun test_app_predictor_result() { + val componentName = ComponentName("pkg", "Class") + val appTarget = mock<DisplayResolveInfo> { + whenever(resolvedComponentName).thenReturn(componentName) + } + val appTargets = arrayOf(appTarget) + val testSubject = ShortcutLoader( + context, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + executor, + executor, + callback + ) + + testSubject.queryShortcuts(appTargets) + + verify(appPredictor, times(1)).requestPredictionUpdate() + val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) + verify(appPredictor, times(1)) + .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) + + val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) + val matchingAppTarget = createAppTarget(matchingShortcutInfo) + val shortcuts = listOf( + matchingAppTarget, + // mismatching shortcut + createAppTarget( + createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + ) + appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts) + + val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) + verify(callback, times(1)).accept(resultCaptor.capture()) + + val result = resultCaptor.value + assertTrue("An app predictor result is expected", result.isFromAppPredictor) + assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) + assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) + assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) + for (shortcut in result.shortcutsByApp[0].shortcuts) { + assertEquals( + "Wrong AppTarget in the cache", + matchingAppTarget, + result.directShareAppTargetCache[shortcut] + ) + assertEquals( + "Wrong ShortcutInfo in the cache", + matchingShortcutInfo, + result.directShareShortcutInfoCache[shortcut] + ) + } + } + + @Test + fun test_shortcut_manager_result() { + val componentName = ComponentName("pkg", "Class") + val appTarget = mock<DisplayResolveInfo> { + whenever(resolvedComponentName).thenReturn(componentName) + } + val appTargets = arrayOf(appTarget) + val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) + val shortcutManagerResult = listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = mock<ShortcutManager> { + whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + val testSubject = ShortcutLoader( + context, + null, + UserHandle.of(0), + true, + intentFilter, + executor, + executor, + callback + ) + + testSubject.queryShortcuts(appTargets) + + val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) + verify(callback, times(1)).accept(resultCaptor.capture()) + + val result = resultCaptor.value + assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) + assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) + assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) + assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) + for (shortcut in result.shortcutsByApp[0].shortcuts) { + assertTrue( + "AppTargets are not expected the cache of a ShortcutManager result", + result.directShareAppTargetCache.isEmpty() + ) + assertEquals( + "Wrong ShortcutInfo in the cache", + matchingShortcutInfo, + result.directShareShortcutInfoCache[shortcut] + ) + } + } + + @Test + fun test_fallback_to_shortcut_manager() { + val componentName = ComponentName("pkg", "Class") + val appTarget = mock<DisplayResolveInfo> { + whenever(resolvedComponentName).thenReturn(componentName) + } + val appTargets = arrayOf(appTarget) + val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) + val shortcutManagerResult = listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = mock<ShortcutManager> { + whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + val testSubject = ShortcutLoader( + context, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + executor, + executor, + callback + ) + + testSubject.queryShortcuts(appTargets) + + verify(appPredictor, times(1)).requestPredictionUpdate() + val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java) + verify(appPredictor, times(1)) + .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) + appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList()) + + val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java) + verify(callback, times(1)).accept(resultCaptor.capture()) + + val result = resultCaptor.value + assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) + assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) + assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) + assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) + for (shortcut in result.shortcutsByApp[0].shortcuts) { + assertTrue( + "AppTargets are not expected the cache of a ShortcutManager result", + result.directShareAppTargetCache.isEmpty() + ) + assertEquals( + "Wrong ShortcutInfo in the cache", + matchingShortcutInfo, + result.directShareShortcutInfoCache[shortcut] + ) + } + } + + @Test + fun test_do_not_call_services_for_not_running_work_profile() { + testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) + } + + @Test + fun test_do_not_call_services_for_locked_work_profile() { + testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) + } + + @Test + fun test_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { + testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) + } + + @Test + fun test_call_services_for_not_running_main_profile() { + testAlwaysCallSystemForMainProfile(isUserRunning = false) + } + + @Test + fun test_call_services_for_locked_main_profile() { + testAlwaysCallSystemForMainProfile(isUserUnlocked = false) + } + + @Test + fun test_call_services_if_quite_mode_is_enabled_for_main_profile() { + testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) + } + + private fun testDisabledWorkProfileDoNotCallSystem( + isUserRunning: Boolean = true, + isUserUnlocked: Boolean = true, + isQuietModeEnabled: Boolean = false + ) { + val userHandle = UserHandle.of(10) + val userManager = mock<UserManager> { + whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) + whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) + whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) + } + whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager); + val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() + val callback = mock<Consumer<ShortcutLoader.Result>>() + val testSubject = ShortcutLoader( + context, + appPredictor, + userHandle, + false, + intentFilter, + executor, + executor, + callback + ) + + testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + + verify(appPredictor, never()).requestPredictionUpdate() + } + + private fun testAlwaysCallSystemForMainProfile( + isUserRunning: Boolean = true, + isUserUnlocked: Boolean = true, + isQuietModeEnabled: Boolean = false + ) { + val userHandle = UserHandle.of(10) + val userManager = mock<UserManager> { + whenever(isUserRunning(userHandle)).thenReturn(isUserRunning) + whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked) + whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled) + } + whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager); + val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() + val callback = mock<Consumer<ShortcutLoader.Result>>() + val testSubject = ShortcutLoader( + context, + appPredictor, + userHandle, + true, + intentFilter, + executor, + executor, + callback + ) + + testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + + verify(appPredictor, times(1)).requestPredictionUpdate() + } +} + +private class ImmediateExecutor : Executor { + override fun execute(r: Runnable) { + r.run() + } +} diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt new file mode 100644 index 00000000..e0de005d --- /dev/null +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2022 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.intentresolver.shortcuts + +import android.app.prediction.AppTarget +import android.content.ComponentName +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager.ShareShortcutInfo +import android.service.chooser.ChooserTarget +import com.android.intentresolver.createAppTarget +import com.android.intentresolver.createShareShortcutInfo +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Test + +private const val PACKAGE = "org.package" + +class ShortcutToChooserTargetConverterTest { + private val testSubject = ShortcutToChooserTargetConverter() + private val ranks = arrayOf(3 ,7, 1 ,3) + private val shortcuts = ranks + .foldIndexed(ArrayList<ShareShortcutInfo>(ranks.size)) { i, acc, rank -> + val id = i + 1 + acc.add( + createShareShortcutInfo( + id = "id-$i", + componentName = ComponentName(PACKAGE, "Class$id"), + rank, + ) + ) + acc + } + + @Test + fun testConvertToChooserTarget_predictionService() { + val appTargets = shortcuts.map { createAppTarget(it.shortcutInfo) } + val expectedOrderAllShortcuts = intArrayOf(0, 1, 2, 3) + val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.98f, 0.97f) + val appTargetCache = HashMap<ChooserTarget, AppTarget>() + val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>() + + var chooserTargets = testSubject.convertToChooserTarget( + shortcuts, + shortcuts, + appTargets, + appTargetCache, + shortcutInfoCache, + ) + + assertCorrectShortcutToChooserTargetConversion( + shortcuts, + chooserTargets, + expectedOrderAllShortcuts, + expectedScoreAllShortcuts, + ) + assertAppTargetCache(chooserTargets, appTargetCache) + assertShortcutInfoCache(chooserTargets, shortcutInfoCache) + + val subset = shortcuts.subList(1, shortcuts.size) + val expectedOrderSubset = intArrayOf(1, 2, 3) + val expectedScoreSubset = floatArrayOf(0.99f, 0.98f, 0.97f) + appTargetCache.clear() + shortcutInfoCache.clear() + + chooserTargets = testSubject.convertToChooserTarget( + subset, + shortcuts, + appTargets, + appTargetCache, + shortcutInfoCache, + ) + + assertCorrectShortcutToChooserTargetConversion( + shortcuts, + chooserTargets, + expectedOrderSubset, + expectedScoreSubset, + ) + assertAppTargetCache(chooserTargets, appTargetCache) + assertShortcutInfoCache(chooserTargets, shortcutInfoCache) + } + + @Test + fun testConvertToChooserTarget_shortcutManager() { + val testSubject = ShortcutToChooserTargetConverter() + val expectedOrderAllShortcuts = intArrayOf(2, 0, 3, 1) + val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.99f, 0.98f) + val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>() + + var chooserTargets = testSubject.convertToChooserTarget( + shortcuts, + shortcuts, + null, + null, + shortcutInfoCache, + ) + + assertCorrectShortcutToChooserTargetConversion( + shortcuts, chooserTargets, + expectedOrderAllShortcuts, expectedScoreAllShortcuts + ) + assertShortcutInfoCache(chooserTargets, shortcutInfoCache) + + val subset: MutableList<ShareShortcutInfo> = java.util.ArrayList() + subset.add(shortcuts[1]) + subset.add(shortcuts[2]) + subset.add(shortcuts[3]) + val expectedOrderSubset = intArrayOf(2, 3, 1) + val expectedScoreSubset = floatArrayOf(1.0f, 0.99f, 0.98f) + shortcutInfoCache.clear() + + chooserTargets = testSubject.convertToChooserTarget( + subset, + shortcuts, + null, + null, + shortcutInfoCache, + ) + + assertCorrectShortcutToChooserTargetConversion( + shortcuts, chooserTargets, + expectedOrderSubset, expectedScoreSubset + ) + assertShortcutInfoCache(chooserTargets, shortcutInfoCache) + } + + private fun assertCorrectShortcutToChooserTargetConversion( + shortcuts: List<ShareShortcutInfo>, + chooserTargets: List<ChooserTarget>, + expectedOrder: IntArray, + expectedScores: FloatArray, + ) { + assertEquals("Unexpected ChooserTarget count", expectedOrder.size, chooserTargets.size) + for (i in chooserTargets.indices) { + val ct = chooserTargets[i] + val si = shortcuts[expectedOrder[i]].shortcutInfo + val cn = shortcuts[expectedOrder[i]].targetComponent + assertEquals(si.id, ct.intentExtras.getString(Intent.EXTRA_SHORTCUT_ID)) + assertEquals(si.label, ct.title) + assertEquals(expectedScores[i], ct.score) + assertEquals(cn, ct.componentName) + } + } + + private fun assertAppTargetCache( + chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, AppTarget> + ) { + for (ct in chooserTargets) { + val target = cache[ct] + assertNotNull("AppTarget is missing", target) + } + } + + private fun assertShortcutInfoCache( + chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, ShortcutInfo> + ) { + for (ct in chooserTargets) { + val si = cache[ct] + assertNotNull("AppTarget is missing", si) + } + } +} |