diff options
author | 2024-04-19 14:27:39 -0400 | |
---|---|---|
committer | 2024-04-23 16:51:41 -0400 | |
commit | d1b19e513ade6007688ff3afd55b531cff9219be (patch) | |
tree | a0843e88384a4b63477eb033888d7d640cb3e312 | |
parent | b4b571d55554ce2d0c1120f920195d792d3cfccd (diff) |
Fix incorrect cross-profile share check.
When this code was updated for new profiles infrastructure (ag/26444390)
a mistake was made in the interpretation of (tabOwnerUserHandleForLaunch),
mistaking it for 'tabOwner', ie: the user of the tab being presented. It
is not. This value represents the user which launched share sheet.
As a result, since this CL, no cross-profile "Blocked by IT" messages
would appear when no targets are available for cross-profile sharing.
The resulting UI would show an empty tab, or fall back to the "No Apps"
UI message.
While the UI has test coverage for this, the tests are run by forcing
calls to the reponsible component to return true or false, without
validation of the user ids provided.
Bug: 335142494
Test: atest IntentResolver-tests-unit
Flag: NONE
Change-Id: I32f455611e6ae6307d49c86d480962e3426b54c3
3 files changed, 161 insertions, 4 deletions
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index 9843cf8d..2a8fcfa4 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -840,7 +840,7 @@ public class ResolverListAdapter extends BaseAdapter { userHandle); } - public final List<Intent> getIntents() { + public List<Intent> getIntents() { // TODO: immutable copy? return mIntents; } diff --git a/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java index e6d5d1c4..2b4a7ada 100644 --- a/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java @@ -34,8 +34,8 @@ import com.android.intentresolver.shared.model.User; import java.util.List; /** - * Empty state provider that does not allow cross profile sharing, it will return a blocker - * in case if the profile of the current tab is not the same as the profile of the calling app. + * Empty state provider that informs about a lack of cross profile sharing. It will return + * an empty state in case there are no intents which can be forwarded to another profile. */ public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { @@ -79,7 +79,8 @@ public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { // Allow access to the tab when launched by the same user as the tab owner // or when there is at least one target which is permitted for cross-profile. - if (launchedAsSameUser || anyCrossProfileAllowedIntents(adapter, tabOwnerHandle)) { + if (launchedAsSameUser || anyCrossProfileAllowedIntents(adapter, + /* source = */ launchedAs.getHandle())) { return null; } diff --git a/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt new file mode 100644 index 00000000..fe3e844b --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 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.emptystate + +import android.content.Intent +import com.android.intentresolver.ProfileHelper +import com.android.intentresolver.ResolverListAdapter +import com.android.intentresolver.annotation.JavaInterop +import com.android.intentresolver.data.repository.FakeUserRepository +import com.android.intentresolver.domain.interactor.UserInteractor +import com.android.intentresolver.inject.FakeIntentResolverFlags +import com.android.intentresolver.shared.model.User +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyList +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.same +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@OptIn(JavaInterop::class) +class NoCrossProfileEmptyStateProviderTest { + + private val personalUser = User(0, User.Role.PERSONAL) + private val workUser = User(10, User.Role.WORK) + private val flags = FakeIntentResolverFlags() + private val personalBlocker = mock<EmptyState>() + private val workBlocker = mock<EmptyState>() + + private val userRepository = FakeUserRepository(listOf(personalUser, workUser)) + + private val personalIntents = listOf(Intent("PERSONAL")) + private val personalListAdapter = + mock<ResolverListAdapter> { + on { userHandle } doReturn personalUser.handle + on { intents } doReturn personalIntents + } + private val workIntents = listOf(Intent("WORK")) + private val workListAdapter = + mock<ResolverListAdapter> { + on { userHandle } doReturn workUser.handle + on { intents } doReturn workIntents + } + + // Pretend that no intent can ever be forwarded + val crossProfileIntentsChecker = + mock<CrossProfileIntentsChecker> { + on { + hasCrossProfileIntents( + /* intents = */ anyList(), + /* source = */ anyInt(), + /* target = */ anyInt() + ) + } doReturn false + } + private val sourceUserId = argumentCaptor<Int>() + private val targetUserId = argumentCaptor<Int>() + + @Test + fun testPersonalToWork() { + val userInteractor = UserInteractor(userRepository, launchedAs = personalUser.handle) + + val profileHelper = + ProfileHelper( + userInteractor, + CoroutineScope(Dispatchers.Unconfined), + Dispatchers.Unconfined, + flags + ) + + val provider = + NoCrossProfileEmptyStateProvider( + /* profileHelper = */ profileHelper, + /* noWorkToPersonalEmptyState = */ personalBlocker, + /* noPersonalToWorkEmptyState = */ workBlocker, + /* crossProfileIntentsChecker = */ crossProfileIntentsChecker + ) + + // Personal to personal, not blocked + assertThat(provider.getEmptyState(personalListAdapter)).isNull() + // Not called because sourceUser == targetUser + verify(crossProfileIntentsChecker, never()) + .hasCrossProfileIntents(anyList(), anyInt(), anyInt()) + + // Personal to work, blocked + assertThat(provider.getEmptyState(workListAdapter)).isSameInstanceAs(workBlocker) + + verify(crossProfileIntentsChecker, times(1)) + .hasCrossProfileIntents( + same(workIntents), + sourceUserId.capture(), + targetUserId.capture() + ) + assertThat(sourceUserId.firstValue).isEqualTo(personalUser.id) + assertThat(targetUserId.firstValue).isEqualTo(workUser.id) + } + + @Test + fun testWorkToPersonal() { + val userInteractor = UserInteractor(userRepository, launchedAs = workUser.handle) + + val profileHelper = + ProfileHelper( + userInteractor, + CoroutineScope(Dispatchers.Unconfined), + Dispatchers.Unconfined, + flags + ) + + val provider = + NoCrossProfileEmptyStateProvider( + /* profileHelper = */ profileHelper, + /* noWorkToPersonalEmptyState = */ personalBlocker, + /* noPersonalToWorkEmptyState = */ workBlocker, + /* crossProfileIntentsChecker = */ crossProfileIntentsChecker + ) + + // Work to work, not blocked + assertThat(provider.getEmptyState(workListAdapter)).isNull() + // Not called because sourceUser == targetUser + verify(crossProfileIntentsChecker, never()) + .hasCrossProfileIntents(anyList(), anyInt(), anyInt()) + + // Work to personal, blocked + assertThat(provider.getEmptyState(personalListAdapter)).isSameInstanceAs(personalBlocker) + + verify(crossProfileIntentsChecker, times(1)) + .hasCrossProfileIntents( + same(personalIntents), + sourceUserId.capture(), + targetUserId.capture() + ) + assertThat(sourceUserId.firstValue).isEqualTo(workUser.id) + assertThat(targetUserId.firstValue).isEqualTo(personalUser.id) + } +} |