1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.intentresolver.emptystate 18 19 import android.content.Intent 20 import com.android.intentresolver.ProfileHelper 21 import com.android.intentresolver.ResolverListAdapter 22 import com.android.intentresolver.annotation.JavaInterop 23 import com.android.intentresolver.data.repository.DevicePolicyResources 24 import com.android.intentresolver.data.repository.FakeUserRepository 25 import com.android.intentresolver.domain.interactor.UserInteractor 26 import com.android.intentresolver.inject.FakeIntentResolverFlags 27 import com.android.intentresolver.shared.model.User 28 import com.google.common.truth.Truth.assertThat 29 import com.google.common.truth.Truth.assertWithMessage 30 import kotlinx.coroutines.CoroutineScope 31 import kotlinx.coroutines.Dispatchers 32 import org.junit.Test 33 import org.mockito.Mockito.never 34 import org.mockito.kotlin.any 35 import org.mockito.kotlin.argumentCaptor 36 import org.mockito.kotlin.doReturn 37 import org.mockito.kotlin.mock 38 import org.mockito.kotlin.same 39 import org.mockito.kotlin.times 40 import org.mockito.kotlin.verify 41 import org.mockito.verification.VerificationMode 42 43 @OptIn(JavaInterop::class) 44 class NoCrossProfileEmptyStateProviderTest { 45 46 private val personalUser = User(0, User.Role.PERSONAL) 47 private val workUser = User(10, User.Role.WORK) 48 private val privateUser = User(11, User.Role.PRIVATE) 49 private val flags = FakeIntentResolverFlags() 50 51 private val userRepository = FakeUserRepository(listOf(personalUser, workUser, privateUser)) 52 53 private val personalIntents = listOf(Intent("PERSONAL")) 54 private val personalListAdapter = <lambda>null55 mock<ResolverListAdapter> { 56 on { userHandle } doReturn personalUser.handle 57 on { intents } doReturn personalIntents 58 } 59 private val workIntents = listOf(Intent("WORK")) 60 private val workListAdapter = <lambda>null61 mock<ResolverListAdapter> { 62 on { userHandle } doReturn workUser.handle 63 on { intents } doReturn workIntents 64 } 65 private val privateIntents = listOf(Intent("PRIVATE")) 66 private val privateListAdapter = <lambda>null67 mock<ResolverListAdapter> { 68 on { userHandle } doReturn privateUser.handle 69 on { intents } doReturn privateIntents 70 } 71 72 private val devicePolicyResources = <lambda>null73 mock<DevicePolicyResources> { 74 on { crossProfileBlocked } doReturn "Cross profile blocked" 75 on { toPersonalBlockedByPolicyMessage(any()) } doReturn "Blocked to Personal" 76 on { toWorkBlockedByPolicyMessage(any()) } doReturn "Blocked to Work" 77 on { toPrivateBlockedByPolicyMessage(any()) } doReturn "Blocked to Private" 78 } 79 80 // If asked, no intent can ever be forwarded between any pair of users. 81 private val crossProfileIntentsChecker = <lambda>null82 mock<CrossProfileIntentsChecker> { 83 on { 84 hasCrossProfileIntents( 85 /* intents = */ any(), 86 /* source = */ any(), 87 /* target = */ any() 88 ) 89 } doReturn false /* Never allow */ 90 } 91 92 @Test verifyTestSetupnull93 fun verifyTestSetup() { 94 assertThat(workListAdapter.userHandle).isEqualTo(workUser.handle) 95 assertThat(personalListAdapter.userHandle).isEqualTo(personalUser.handle) 96 assertThat(privateListAdapter.userHandle).isEqualTo(privateUser.handle) 97 } 98 99 @Test sameProfilePermittednull100 fun sameProfilePermitted() { 101 val profileHelper = createProfileHelper(launchedAs = workUser) 102 103 val provider = 104 NoCrossProfileEmptyStateProvider( 105 profileHelper, 106 devicePolicyResources, 107 crossProfileIntentsChecker, 108 /* isShare = */ true 109 ) 110 111 // Work to work, not blocked 112 assertThat(provider.getEmptyState(workListAdapter)).isNull() 113 114 crossProfileIntentsChecker.verifyCalled(never()) 115 } 116 117 @Test testPersonalToWorknull118 fun testPersonalToWork() { 119 val profileHelper = createProfileHelper(launchedAs = personalUser) 120 121 val provider = 122 NoCrossProfileEmptyStateProvider( 123 profileHelper, 124 devicePolicyResources, 125 crossProfileIntentsChecker, 126 /* isShare = */ true 127 ) 128 129 val result = provider.getEmptyState(workListAdapter) 130 assertThat(result).isNotNull() 131 assertThat(result?.title).isEqualTo("Cross profile blocked") 132 assertThat(result?.subtitle).isEqualTo("Blocked to Work") 133 134 crossProfileIntentsChecker.verifyCalled(times(1), workIntents, personalUser, workUser) 135 } 136 137 @Test testWorkToPersonalnull138 fun testWorkToPersonal() { 139 val profileHelper = createProfileHelper(launchedAs = workUser) 140 141 val provider = 142 NoCrossProfileEmptyStateProvider( 143 profileHelper, 144 devicePolicyResources, 145 crossProfileIntentsChecker, 146 /* isShare = */ true 147 ) 148 149 val result = provider.getEmptyState(personalListAdapter) 150 assertThat(result).isNotNull() 151 assertThat(result?.title).isEqualTo("Cross profile blocked") 152 assertThat(result?.subtitle).isEqualTo("Blocked to Personal") 153 154 crossProfileIntentsChecker.verifyCalled(times(1), personalIntents, workUser, personalUser) 155 } 156 157 @Test testWorkToPrivatenull158 fun testWorkToPrivate() { 159 val profileHelper = createProfileHelper(launchedAs = workUser) 160 161 val provider = 162 NoCrossProfileEmptyStateProvider( 163 profileHelper, 164 devicePolicyResources, 165 crossProfileIntentsChecker, 166 /* isShare = */ true 167 ) 168 169 val result = provider.getEmptyState(privateListAdapter) 170 assertThat(result).isNotNull() 171 assertThat(result?.title).isEqualTo("Cross profile blocked") 172 assertThat(result?.subtitle).isEqualTo("Blocked to Private") 173 174 // effective target user is personalUser due to "delegate from parent" 175 crossProfileIntentsChecker.verifyCalled(times(1), privateIntents, workUser, personalUser) 176 } 177 178 @Test testPrivateToPersonalnull179 fun testPrivateToPersonal() { 180 val profileHelper = createProfileHelper(launchedAs = privateUser) 181 182 val provider = 183 NoCrossProfileEmptyStateProvider( 184 profileHelper, 185 devicePolicyResources, 186 crossProfileIntentsChecker, 187 /* isShare = */ true 188 ) 189 190 // Private -> Personal is always allowed: 191 // Private delegates to the parent profile for policy; so personal->personal is allowed. 192 assertThat(provider.getEmptyState(personalListAdapter)).isNull() 193 194 crossProfileIntentsChecker.verifyCalled(never()) 195 } 196 createProfileHelpernull197 private fun createProfileHelper(launchedAs: User): ProfileHelper { 198 val userInteractor = UserInteractor(userRepository, launchedAs = launchedAs.handle) 199 200 return ProfileHelper( 201 userInteractor, 202 CoroutineScope(Dispatchers.Unconfined), 203 Dispatchers.Unconfined, 204 flags 205 ) 206 } 207 CrossProfileIntentsCheckernull208 private fun CrossProfileIntentsChecker.verifyCalled( 209 mode: VerificationMode, 210 list: List<Intent>? = null, 211 sourceUser: User? = null, 212 targetUser: User? = null, 213 ) { 214 val sourceUserId = argumentCaptor<Int>() 215 val targetUserId = argumentCaptor<Int>() 216 217 verify(this, mode) 218 .hasCrossProfileIntents(same(list), sourceUserId.capture(), targetUserId.capture()) 219 sourceUser?.apply { 220 assertWithMessage("hasCrossProfileIntents: source") 221 .that(sourceUserId.firstValue) 222 .isEqualTo(id) 223 } 224 targetUser?.apply { 225 assertWithMessage("hasCrossProfileIntents: target") 226 .that(targetUserId.firstValue) 227 .isEqualTo(id) 228 } 229 } 230 } 231