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