1 /*
<lambda>null2  * Copyright 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.photopicker.core.user
18 
19 import android.content.BroadcastReceiver
20 import android.content.ContentResolver
21 import android.content.Context
22 import android.content.Intent
23 import android.content.IntentFilter
24 import android.content.pm.PackageManager
25 import android.content.pm.ResolveInfo
26 import android.content.pm.UserProperties
27 import android.content.pm.UserProperties.SHOW_IN_QUIET_MODE_HIDDEN
28 import android.os.Parcel
29 import android.os.UserHandle
30 import android.os.UserManager
31 import android.provider.MediaStore
32 import androidx.test.ext.junit.runners.AndroidJUnit4
33 import androidx.test.filters.SmallTest
34 import androidx.test.platform.app.InstrumentationRegistry
35 import com.android.modules.utils.build.SdkLevel
36 import com.android.photopicker.R
37 import com.android.photopicker.core.configuration.TestPhotopickerConfiguration
38 import com.android.photopicker.core.configuration.provideTestConfigurationFlow
39 import com.android.photopicker.util.test.capture
40 import com.android.photopicker.util.test.mockSystemService
41 import com.android.photopicker.util.test.whenever
42 import com.google.common.truth.Truth.assertThat
43 import kotlinx.coroutines.ExperimentalCoroutinesApi
44 import kotlinx.coroutines.flow.first
45 import kotlinx.coroutines.flow.toList
46 import kotlinx.coroutines.launch
47 import kotlinx.coroutines.test.StandardTestDispatcher
48 import kotlinx.coroutines.test.advanceTimeBy
49 import kotlinx.coroutines.test.runTest
50 import org.junit.Assume.assumeFalse
51 import org.junit.Assume.assumeTrue
52 import org.junit.Before
53 import org.junit.Test
54 import org.junit.runner.RunWith
55 import org.mockito.ArgumentCaptor
56 import org.mockito.ArgumentMatchers.any
57 import org.mockito.ArgumentMatchers.anyInt
58 import org.mockito.Captor
59 import org.mockito.Mock
60 import org.mockito.Mockito.mock
61 import org.mockito.Mockito.verify
62 import org.mockito.MockitoAnnotations
63 
64 /** Unit tests for the [UserMonitor] */
65 @SmallTest
66 @RunWith(AndroidJUnit4::class)
67 @OptIn(ExperimentalCoroutinesApi::class)
68 class UserMonitorTest {
69 
70     private val PLATFORM_PROVIDED_PROFILE_LABEL = "Platform Label"
71 
72     private val USER_HANDLE_PRIMARY: UserHandle
73     private val USER_ID_PRIMARY: Int = 0
74     private val PRIMARY_PROFILE_BASE: UserProfile
75 
76     private val USER_HANDLE_MANAGED: UserHandle
77     private val USER_ID_MANAGED: Int = 10
78     private val MANAGED_PROFILE_BASE: UserProfile
79 
80     private val initialExpectedStatus: UserStatus
81     private val mockContentResolver: ContentResolver = mock(ContentResolver::class.java)
82 
83     private lateinit var userMonitor: UserMonitor
84 
85     @Mock lateinit var mockContext: Context
86     @Mock lateinit var mockUserManager: UserManager
87     @Mock lateinit var mockPackageManager: PackageManager
88     @Captor lateinit var broadcastReceiver: ArgumentCaptor<BroadcastReceiver>
89     @Captor lateinit var intentFilter: ArgumentCaptor<IntentFilter>
90     @Captor lateinit var flag: ArgumentCaptor<Int>
91 
92     init {
93 
94         val parcel1 = Parcel.obtain()
95         parcel1.writeInt(USER_ID_PRIMARY)
96         parcel1.setDataPosition(0)
97         USER_HANDLE_PRIMARY = UserHandle(parcel1)
98         parcel1.recycle()
99 
100         PRIMARY_PROFILE_BASE =
101             UserProfile(
102                 handle = USER_HANDLE_PRIMARY,
103                 profileType = UserProfile.ProfileType.PRIMARY,
104                 label = PLATFORM_PROVIDED_PROFILE_LABEL,
105             )
106 
107         val parcel2 = Parcel.obtain()
108         parcel2.writeInt(USER_ID_MANAGED)
109         parcel2.setDataPosition(0)
110         USER_HANDLE_MANAGED = UserHandle(parcel2)
111         parcel2.recycle()
112 
113         MANAGED_PROFILE_BASE =
114             UserProfile(
115                 handle = USER_HANDLE_MANAGED,
116                 profileType = UserProfile.ProfileType.MANAGED,
117                 label = PLATFORM_PROVIDED_PROFILE_LABEL,
118             )
119 
120         initialExpectedStatus =
121             UserStatus(
122                 activeUserProfile = PRIMARY_PROFILE_BASE,
123                 allProfiles = listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE),
124                 activeContentResolver = mockContentResolver,
125             )
126     }
127 
128     @Before
129     fun setup() {
130         MockitoAnnotations.initMocks(this)
131         val resources = InstrumentationRegistry.getInstrumentation().getContext().getResources()
132 
133         mockSystemService(mockContext, UserManager::class.java) { mockUserManager }
134         whenever(mockContext.packageManager) { mockPackageManager }
135         whenever(mockContext.contentResolver) { mockContentResolver }
136         whenever(mockContext.createPackageContextAsUser(any(), anyInt(), any())) { mockContext }
137         whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) {
138             mockContext
139         }
140 
141         // Initial setup state: Two profiles (Personal/Work), both enabled
142         whenever(mockUserManager.userProfiles) { listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED) }
143 
144         // Default responses for relevant UserManager apis
145         whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_PRIMARY)) { false }
146         whenever(mockUserManager.isManagedProfile(USER_ID_PRIMARY)) { false }
147         whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { false }
148         whenever(mockUserManager.isManagedProfile(USER_ID_MANAGED)) { true }
149         whenever(mockUserManager.getProfileParent(USER_HANDLE_MANAGED)) { USER_HANDLE_PRIMARY }
150 
151         val mockResolveInfo = mock(ResolveInfo::class.java)
152         whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
153         whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
154             listOf(mockResolveInfo)
155         }
156 
157         if (SdkLevel.isAtLeastV()) {
158             whenever(mockUserManager.getUserBadge()) {
159                 resources.getDrawable(R.drawable.android, /* theme= */ null)
160             }
161             whenever(mockUserManager.getProfileLabel()) { PLATFORM_PROVIDED_PROFILE_LABEL }
162             whenever(mockUserManager.getUserProperties(USER_HANDLE_PRIMARY)) {
163                 UserProperties.Builder().build()
164             }
165             // By default, allow managed profile to be available
166             whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
167                 UserProperties.Builder()
168                     .setCrossProfileContentSharingStrategy(
169                         UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT
170                     )
171                     .build()
172             }
173         }
174     }
175 
176     /** Ensures the initial [UserStatus] is emitted before any Broadcasts are received. */
177     @Test
178     fun testInitialUserStatusIsAvailable() {
179 
180         runTest { // this: TestScope
181             userMonitor =
182                 UserMonitor(
183                     mockContext,
184                     provideTestConfigurationFlow(
185                         scope = this.backgroundScope,
186                         defaultConfiguration =
187                             TestPhotopickerConfiguration.build {
188                                 action(MediaStore.ACTION_PICK_IMAGES)
189                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
190                             },
191                     ),
192                     this.backgroundScope,
193                     StandardTestDispatcher(this.testScheduler),
194                     USER_HANDLE_PRIMARY,
195                 )
196 
197             launch {
198                 val reportedStatus = userMonitor.userStatus.first()
199                 assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
200             }
201         }
202     }
203 
204     /** Ensures profiles with a cross profile forwarding intent are active */
205     @Test
206     fun testProfilesForCrossProfileIntentForwardingVPlus() {
207 
208         assumeTrue(SdkLevel.isAtLeastV())
209         whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
210             UserProperties.Builder()
211                 .setCrossProfileContentSharingStrategy(
212                     UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION
213                 )
214                 .build()
215         }
216 
217         val mockResolveInfo = mock(ResolveInfo::class.java)
218         whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
219         whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
220             listOf(mockResolveInfo)
221         }
222 
223         runTest { // this: TestScope
224             userMonitor =
225                 UserMonitor(
226                     mockContext,
227                     provideTestConfigurationFlow(
228                         scope = this.backgroundScope,
229                         defaultConfiguration =
230                             TestPhotopickerConfiguration.build {
231                                 action(MediaStore.ACTION_PICK_IMAGES)
232                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
233                             },
234                     ),
235                     this.backgroundScope,
236                     StandardTestDispatcher(this.testScheduler),
237                     USER_HANDLE_PRIMARY,
238                 )
239 
240             launch {
241                 val reportedStatus = userMonitor.userStatus.first()
242                 assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
243             }
244         }
245     }
246 
247     /** Ensures profiles with a cross profile forwarding intent are active */
248     @Test
249     fun testProfilesForCrossProfileIntentForwardingUMinus() {
250 
251         assumeFalse(SdkLevel.isAtLeastV())
252         val mockResolveInfo = mock(ResolveInfo::class.java)
253         whenever(mockResolveInfo.isCrossProfileIntentForwarderActivity()) { true }
254         whenever(mockPackageManager.queryIntentActivities(any(Intent::class.java), anyInt())) {
255             listOf(mockResolveInfo)
256         }
257 
258         runTest { // this: TestScope
259             userMonitor =
260                 UserMonitor(
261                     mockContext,
262                     provideTestConfigurationFlow(
263                         scope = this.backgroundScope,
264                         defaultConfiguration =
265                             TestPhotopickerConfiguration.build {
266                                 action(MediaStore.ACTION_PICK_IMAGES)
267                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
268                             },
269                     ),
270                     this.backgroundScope,
271                     StandardTestDispatcher(this.testScheduler),
272                     USER_HANDLE_PRIMARY,
273                 )
274 
275             launch {
276                 val reportedStatus = userMonitor.userStatus.first()
277                 assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
278             }
279         }
280     }
281 
282     /**
283      * Ensures that profiles that explicitly request not to be shown in sharing surfaces are not
284      * included
285      */
286     @Test
287     fun testIgnoresSharingDisabledProfiles() {
288         assumeTrue(SdkLevel.isAtLeastV())
289 
290         val parcel = Parcel.obtain()
291         parcel.writeInt(100)
292         parcel.setDataPosition(0)
293         val disabledSharingProfile = UserHandle(parcel)
294         parcel.recycle()
295 
296         // Initial setup state: Two profiles (Personal/Work), both enabled
297         whenever(mockUserManager.userProfiles) {
298             listOf(USER_HANDLE_PRIMARY, USER_HANDLE_MANAGED, disabledSharingProfile)
299         }
300         whenever(mockUserManager.getUserProperties(disabledSharingProfile)) {
301             UserProperties.Builder()
302                 .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_NO)
303                 .build()
304         }
305 
306         runTest { // this: TestScope
307             userMonitor =
308                 UserMonitor(
309                     mockContext,
310                     provideTestConfigurationFlow(
311                         scope = this.backgroundScope,
312                         defaultConfiguration =
313                             TestPhotopickerConfiguration.build {
314                                 action(MediaStore.ACTION_PICK_IMAGES)
315                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
316                             },
317                     ),
318                     this.backgroundScope,
319                     StandardTestDispatcher(this.testScheduler),
320                     USER_HANDLE_PRIMARY,
321                 )
322 
323             launch {
324                 val reportedStatus = userMonitor.userStatus.first()
325                 assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
326             }
327         }
328     }
329 
330     /** Ensures that displayable content for a profile is fetched from the platform on V+ */
331     @Test
332     fun testProfileDisplayablesFromPlatformOnV() {
333         assumeTrue(SdkLevel.isAtLeastV())
334 
335         runTest { // this: TestScope
336             userMonitor =
337                 UserMonitor(
338                     mockContext,
339                     provideTestConfigurationFlow(
340                         scope = this.backgroundScope,
341                         defaultConfiguration =
342                             TestPhotopickerConfiguration.build {
343                                 action(MediaStore.ACTION_PICK_IMAGES)
344                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
345                             },
346                     ),
347                     this.backgroundScope,
348                     StandardTestDispatcher(this.testScheduler),
349                     USER_HANDLE_PRIMARY,
350                 )
351 
352             launch {
353                 val reportedStatus = userMonitor.userStatus.first()
354                 // Just check the value isn't null, since the drawable gets converted to an
355                 // ImageBitmap
356                 assertThat(reportedStatus.activeUserProfile.icon).isNotNull()
357                 assertThat(reportedStatus.activeUserProfile.label)
358                     .isEqualTo(PLATFORM_PROVIDED_PROFILE_LABEL)
359             }
360         }
361     }
362 
363     /** Ensures that displayable content for a profile is not set before Android V */
364     @Test
365     fun testProfileDisplayablesPriorToV() {
366         assumeFalse(SdkLevel.isAtLeastV())
367 
368         runTest { // this: TestScope
369             userMonitor =
370                 UserMonitor(
371                     mockContext,
372                     provideTestConfigurationFlow(
373                         scope = this.backgroundScope,
374                         defaultConfiguration =
375                             TestPhotopickerConfiguration.build {
376                                 action(MediaStore.ACTION_PICK_IMAGES)
377                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
378                             },
379                     ),
380                     this.backgroundScope,
381                     StandardTestDispatcher(this.testScheduler),
382                     USER_HANDLE_PRIMARY,
383                 )
384 
385             launch {
386                 val reportedStatus = userMonitor.userStatus.first()
387                 // Just check the value isn't null, since the drawable gets converted to an
388                 // ImageBitmap
389                 assertThat(reportedStatus.activeUserProfile.icon).isNull()
390                 assertThat(reportedStatus.activeUserProfile.label).isNull()
391             }
392         }
393     }
394 
395     /**
396      * Ensures that a [BroadcastReceiver] is registered to listen for profile changes Note: This
397      * test is forked for SdkLevel R and earlier devices since [Intent.ACTION_PROFILE_ACCESSIBLE]
398      * and [Intent.ACTION_PROFILE_INACCESSIBLE] isn't available until SdkLevel 31.
399      */
400     @Test
401     fun testRegistersBroadcastReceiverSdkRMinus() {
402 
403         assumeFalse(SdkLevel.isAtLeastS())
404 
405         runTest { // this: TestScope
406             userMonitor =
407                 UserMonitor(
408                     mockContext,
409                     provideTestConfigurationFlow(
410                         scope = this.backgroundScope,
411                         defaultConfiguration =
412                             TestPhotopickerConfiguration.build {
413                                 action(MediaStore.ACTION_PICK_IMAGES)
414                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
415                             },
416                     ),
417                     this.backgroundScope,
418                     StandardTestDispatcher(this.testScheduler),
419                     USER_HANDLE_PRIMARY,
420                 )
421 
422             launch {
423                 val reportedStatus = userMonitor.userStatus.first()
424                 assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
425             }
426             advanceTimeBy(100)
427             verify(mockContext)
428                 .registerReceiver(capture(broadcastReceiver), capture(intentFilter), capture(flag))
429 
430             val receiver: BroadcastReceiver = broadcastReceiver.getValue()
431             val filter: IntentFilter = intentFilter.getValue()
432             val flagValue: Int = flag.getValue()
433 
434             assertThat(receiver).isNotNull()
435             assertThat(filter.countActions()).isEqualTo(2)
436             assertThat(filter.matchAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
437             assertThat(filter.matchAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue()
438             assertThat(flagValue).isEqualTo(0x4)
439         }
440     }
441 
442     /**
443      * Ensures that a [BroadcastReceiver] is registered to listen for profile changes Note: This
444      * test is forked for SdkLevel S devices since [Context.RECEIVER_NOT_EXPORTED] isn't available
445      * until SdkLevel 33.
446      */
447     @Test
448     fun testRegistersBroadcastReceiverSdkS() {
449 
450         assumeTrue(SdkLevel.isAtLeastS())
451         assumeFalse(SdkLevel.isAtLeastT())
452 
453         runTest { // this: TestScope
454             userMonitor =
455                 UserMonitor(
456                     mockContext,
457                     provideTestConfigurationFlow(
458                         scope = this.backgroundScope,
459                         defaultConfiguration =
460                             TestPhotopickerConfiguration.build {
461                                 action(MediaStore.ACTION_PICK_IMAGES)
462                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
463                             },
464                     ),
465                     this.backgroundScope,
466                     StandardTestDispatcher(this.testScheduler),
467                     USER_HANDLE_PRIMARY,
468                 )
469 
470             launch {
471                 val reportedStatus = userMonitor.userStatus.first()
472                 assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
473             }
474             advanceTimeBy(100)
475             verify(mockContext)
476                 .registerReceiver(capture(broadcastReceiver), capture(intentFilter), capture(flag))
477 
478             val receiver: BroadcastReceiver = broadcastReceiver.getValue()
479             val filter: IntentFilter = intentFilter.getValue()
480             val flagValue: Int = flag.getValue()
481 
482             assertThat(receiver).isNotNull()
483             assertThat(filter.countActions()).isEqualTo(4)
484             assertThat(filter.matchAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
485             assertThat(filter.matchAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue()
486             assertThat(filter.matchAction(Intent.ACTION_PROFILE_ACCESSIBLE)).isTrue()
487             assertThat(filter.matchAction(Intent.ACTION_PROFILE_INACCESSIBLE)).isTrue()
488             assertThat(flagValue).isEqualTo(0x4)
489         }
490     }
491 
492     /**
493      * Ensures that a [BroadcastReceiver] is registered to listen for profile changes Note: This
494      * test is forked for SdkLevel T and later devices.
495      */
496     @Test
497     fun testRegistersBroadcastReceiverSdkTPlus() {
498 
499         assumeTrue(SdkLevel.isAtLeastT())
500 
501         runTest { // this: TestScope
502             userMonitor =
503                 UserMonitor(
504                     mockContext,
505                     provideTestConfigurationFlow(
506                         scope = this.backgroundScope,
507                         defaultConfiguration =
508                             TestPhotopickerConfiguration.build {
509                                 action(MediaStore.ACTION_PICK_IMAGES)
510                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
511                             },
512                     ),
513                     this.backgroundScope,
514                     StandardTestDispatcher(this.testScheduler),
515                     USER_HANDLE_PRIMARY,
516                 )
517 
518             launch {
519                 val reportedStatus = userMonitor.userStatus.first()
520                 assertUserStatusIsEqualIgnoringFields(reportedStatus, initialExpectedStatus)
521             }
522             advanceTimeBy(100)
523             verify(mockContext)
524                 .registerReceiver(capture(broadcastReceiver), capture(intentFilter), capture(flag))
525 
526             val receiver: BroadcastReceiver = broadcastReceiver.getValue()
527             val filter: IntentFilter = intentFilter.getValue()
528             val flagValue: Int = flag.getValue()
529 
530             assertThat(receiver).isNotNull()
531             assertThat(filter.countActions()).isEqualTo(4)
532             assertThat(filter.matchAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE)).isTrue()
533             assertThat(filter.matchAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)).isTrue()
534             assertThat(filter.matchAction(Intent.ACTION_PROFILE_ACCESSIBLE)).isTrue()
535             assertThat(filter.matchAction(Intent.ACTION_PROFILE_INACCESSIBLE)).isTrue()
536             assertThat(flagValue).isEqualTo(Context.RECEIVER_NOT_EXPORTED)
537         }
538     }
539 
540     /** Ensures that the [BroadcastReceiver] updates the state. */
541     @Test
542     fun testUpdatesUserStatusOnBroadcast() {
543 
544         runTest { // this: TestScope
545             userMonitor =
546                 UserMonitor(
547                     mockContext,
548                     provideTestConfigurationFlow(
549                         scope = this.backgroundScope,
550                         defaultConfiguration =
551                             TestPhotopickerConfiguration.build {
552                                 action(MediaStore.ACTION_PICK_IMAGES)
553                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
554                             },
555                     ),
556                     this.backgroundScope,
557                     StandardTestDispatcher(this.testScheduler),
558                     USER_HANDLE_PRIMARY,
559                 )
560 
561             val emissions = mutableListOf<UserStatus>()
562 
563             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
564             advanceTimeBy(100)
565             verify(mockContext)
566                 .registerReceiver(capture(broadcastReceiver), capture(intentFilter), capture(flag))
567 
568             val receiver: BroadcastReceiver = broadcastReceiver.getValue()
569 
570             // Simulate the Work profile being disabled
571             whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
572             val intent = Intent()
573             intent.setAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
574             intent.putExtra(Intent.EXTRA_USER, USER_HANDLE_MANAGED)
575             receiver.onReceive(mockContext, intent)
576 
577             advanceTimeBy(100)
578 
579             val expectedUpdatedStatus =
580                 UserStatus(
581                     activeUserProfile = PRIMARY_PROFILE_BASE,
582                     allProfiles =
583                         listOf(
584                             PRIMARY_PROFILE_BASE,
585                             MANAGED_PROFILE_BASE.copy(
586                                 disabledReasons = setOf(UserProfile.DisabledReason.QUIET_MODE)
587                             ),
588                         ),
589                     activeContentResolver = mockContentResolver,
590                 )
591 
592             assertThat(emissions.size).isEqualTo(2)
593             assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialExpectedStatus)
594             assertUserStatusIsEqualIgnoringFields(emissions.get(1), expectedUpdatedStatus)
595         }
596     }
597 
598     /** Ensures that duplicate Broadcasts don't result in duplicate emissions */
599     @Test
600     fun testDuplicateBroadcastsDontEmitNewState() {
601 
602         runTest { // this: TestScope
603             userMonitor =
604                 UserMonitor(
605                     mockContext,
606                     provideTestConfigurationFlow(
607                         scope = this.backgroundScope,
608                         defaultConfiguration =
609                             TestPhotopickerConfiguration.build {
610                                 action(MediaStore.ACTION_PICK_IMAGES)
611                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
612                             },
613                     ),
614                     this.backgroundScope,
615                     StandardTestDispatcher(this.testScheduler),
616                     USER_HANDLE_PRIMARY,
617                 )
618 
619             val emissions = mutableListOf<UserStatus>()
620 
621             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
622             advanceTimeBy(100)
623             verify(mockContext)
624                 .registerReceiver(capture(broadcastReceiver), capture(intentFilter), capture(flag))
625 
626             val receiver: BroadcastReceiver = broadcastReceiver.getValue()
627 
628             // Simulate the Work profile being disabled
629             whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
630             val intent = Intent()
631             intent.setAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
632             intent.putExtra(Intent.EXTRA_USER, USER_HANDLE_MANAGED)
633             receiver.onReceive(mockContext, intent)
634 
635             advanceTimeBy(100)
636 
637             // A new state should be emitted for the disabled profile.
638             assertThat(emissions.size).isEqualTo(2)
639 
640             // Send a duplicate broadcast with the other action
641             val intent2 = Intent()
642             intent2.setAction(Intent.ACTION_PROFILE_INACCESSIBLE)
643             intent2.putExtra(Intent.EXTRA_USER, USER_HANDLE_MANAGED)
644             receiver.onReceive(mockContext, intent2)
645 
646             advanceTimeBy(100)
647 
648             // There should still only be two emissions.
649             assertThat(emissions.size).isEqualTo(2)
650         }
651     }
652 
653     /** Ensures that a requested profile switch succeeds, and updates subscribers with new state. */
654     @Test
655     fun testRequestSwitchActiveUserProfileSuccess() {
656 
657         runTest { // this: TestScope
658             userMonitor =
659                 UserMonitor(
660                     mockContext,
661                     provideTestConfigurationFlow(
662                         scope = this.backgroundScope,
663                         defaultConfiguration =
664                             TestPhotopickerConfiguration.build {
665                                 action(MediaStore.ACTION_PICK_IMAGES)
666                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
667                             },
668                     ),
669                     this.backgroundScope,
670                     StandardTestDispatcher(this.testScheduler),
671                     USER_HANDLE_PRIMARY,
672                 )
673 
674             val emissions = mutableListOf<UserStatus>()
675             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
676             advanceTimeBy(100)
677 
678             backgroundScope.launch {
679                 val switchResult =
680                     userMonitor.requestSwitchActiveUserProfile(
681                         UserProfile(handle = USER_HANDLE_MANAGED),
682                         mockContext,
683                     )
684                 assertThat(switchResult).isEqualTo(SwitchUserProfileResult.SUCCESS)
685             }
686 
687             advanceTimeBy(100)
688 
689             val expectedStatus =
690                 UserStatus(
691                     activeUserProfile = MANAGED_PROFILE_BASE,
692                     allProfiles = listOf(PRIMARY_PROFILE_BASE, MANAGED_PROFILE_BASE),
693                     activeContentResolver = mockContentResolver,
694                 )
695 
696             assertThat(emissions.size).isEqualTo(2)
697             assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialExpectedStatus)
698             assertUserStatusIsEqualIgnoringFields(emissions.get(1), expectedStatus)
699         }
700     }
701 
702     /** Ensures that a disabled profile cannot be made the active profile */
703     @Test
704     fun testRequestSwitchActiveUserProfileDisabled() {
705 
706         whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
707 
708         val initialState =
709             UserStatus(
710                 activeUserProfile =
711                     UserProfile(
712                         handle = USER_HANDLE_PRIMARY,
713                         profileType = UserProfile.ProfileType.PRIMARY,
714                     ),
715                 allProfiles =
716                     listOf(
717                         UserProfile(
718                             handle = USER_HANDLE_PRIMARY,
719                             profileType = UserProfile.ProfileType.PRIMARY,
720                         ),
721                         UserProfile(
722                             handle = USER_HANDLE_MANAGED,
723                             profileType = UserProfile.ProfileType.MANAGED,
724                             disabledReasons = setOf(UserProfile.DisabledReason.QUIET_MODE),
725                         ),
726                     ),
727                 activeContentResolver = mockContentResolver,
728             )
729 
730         runTest { // this: TestScope
731             userMonitor =
732                 UserMonitor(
733                     mockContext,
734                     provideTestConfigurationFlow(
735                         scope = this.backgroundScope,
736                         defaultConfiguration =
737                             TestPhotopickerConfiguration.build {
738                                 action(MediaStore.ACTION_PICK_IMAGES)
739                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
740                             },
741                     ),
742                     this.backgroundScope,
743                     StandardTestDispatcher(this.testScheduler),
744                     USER_HANDLE_PRIMARY,
745                 )
746 
747             val emissions = mutableListOf<UserStatus>()
748             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
749             advanceTimeBy(100)
750 
751             backgroundScope.launch {
752                 val switchResult =
753                     userMonitor.requestSwitchActiveUserProfile(
754                         UserProfile(handle = USER_HANDLE_MANAGED),
755                         mockContext,
756                     )
757                 assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_PROFILE_DISABLED)
758             }
759 
760             advanceTimeBy(100)
761 
762             assertThat(emissions.size).isEqualTo(1)
763             assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialState)
764         }
765     }
766 
767     /** Ensures that only known profiles can be made the active profile. */
768     @Test
769     fun testRequestSwitchActiveUserProfileUnknown() {
770 
771         runTest { // this: TestScope
772             userMonitor =
773                 UserMonitor(
774                     mockContext,
775                     provideTestConfigurationFlow(
776                         scope = this.backgroundScope,
777                         defaultConfiguration =
778                             TestPhotopickerConfiguration.build {
779                                 action(MediaStore.ACTION_PICK_IMAGES)
780                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
781                             },
782                     ),
783                     this.backgroundScope,
784                     StandardTestDispatcher(this.testScheduler),
785                     USER_HANDLE_PRIMARY,
786                 )
787 
788             val parcel = Parcel.obtain()
789             parcel.writeInt(/* userId */ 999) // Unknown user id
790             parcel.setDataPosition(0)
791             val unknownUserHandle = UserHandle(parcel)
792             parcel.recycle()
793 
794             val emissions = mutableListOf<UserStatus>()
795             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
796             advanceTimeBy(100)
797 
798             backgroundScope.launch {
799                 val switchResult =
800                     userMonitor.requestSwitchActiveUserProfile(
801                         UserProfile(handle = unknownUserHandle),
802                         mockContext,
803                     )
804                 assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_UNKNOWN_PROFILE)
805             }
806 
807             advanceTimeBy(100)
808 
809             assertThat(emissions.size).isEqualTo(1)
810             assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialExpectedStatus)
811         }
812     }
813 
814     /**
815      * Ensures that if the active profile becomes disabled, the active profile reverts to the
816      * process owner profile.
817      */
818     @Test
819     fun testActiveProfileBecomesDisabled() {
820 
821         runTest { // this: TestScope
822             userMonitor =
823                 UserMonitor(
824                     mockContext,
825                     provideTestConfigurationFlow(
826                         scope = this.backgroundScope,
827                         defaultConfiguration =
828                             TestPhotopickerConfiguration.build {
829                                 action(MediaStore.ACTION_PICK_IMAGES)
830                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
831                             },
832                     ),
833                     this.backgroundScope,
834                     StandardTestDispatcher(this.testScheduler),
835                     USER_HANDLE_PRIMARY,
836                 )
837 
838             val emissions = mutableListOf<UserStatus>()
839             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
840             advanceTimeBy(100)
841 
842             verify(mockContext)
843                 .registerReceiver(capture(broadcastReceiver), capture(intentFilter), capture(flag))
844 
845             backgroundScope.launch {
846                 val switchResult =
847                     userMonitor.requestSwitchActiveUserProfile(
848                         UserProfile(handle = USER_HANDLE_MANAGED),
849                         mockContext,
850                     )
851                 assertThat(switchResult).isEqualTo(SwitchUserProfileResult.SUCCESS)
852             }
853 
854             advanceTimeBy(100)
855             assertThat(emissions.last().activeUserProfile.identifier)
856                 .isEqualTo(MANAGED_PROFILE_BASE.identifier)
857 
858             val receiver: BroadcastReceiver = broadcastReceiver.getValue()
859 
860             // Simulate the Work profile being disabled
861             whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
862             val intent = Intent()
863             intent.setAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
864             intent.putExtra(Intent.EXTRA_USER, USER_HANDLE_MANAGED)
865             receiver.onReceive(mockContext, intent)
866             advanceTimeBy(100)
867 
868             assertThat(emissions.last().activeUserProfile.identifier)
869                 .isEqualTo(PRIMARY_PROFILE_BASE.identifier)
870         }
871     }
872 
873     @Test
874     fun testProfileDisableWhileInQuietModeVPlus() {
875         assumeTrue(SdkLevel.isAtLeastV())
876 
877         whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
878         whenever(mockUserManager.getUserProperties(USER_HANDLE_MANAGED)) {
879             UserProperties.Builder().setShowInQuietMode(SHOW_IN_QUIET_MODE_HIDDEN).build()
880         }
881 
882         val initialState =
883             UserStatus(
884                 activeUserProfile =
885                     UserProfile(
886                         handle = USER_HANDLE_PRIMARY,
887                         profileType = UserProfile.ProfileType.PRIMARY,
888                     ),
889                 allProfiles =
890                     listOf(
891                         UserProfile(
892                             handle = USER_HANDLE_PRIMARY,
893                             profileType = UserProfile.ProfileType.PRIMARY,
894                         ),
895                         UserProfile(
896                             handle = USER_HANDLE_MANAGED,
897                             profileType = UserProfile.ProfileType.MANAGED,
898                             disabledReasons =
899                                 setOf(
900                                     UserProfile.DisabledReason.QUIET_MODE,
901                                     UserProfile.DisabledReason.QUIET_MODE_DO_NOT_SHOW,
902                                 ),
903                         ),
904                     ),
905                 activeContentResolver = mockContentResolver,
906             )
907 
908         runTest { // this: TestScope
909             userMonitor =
910                 UserMonitor(
911                     mockContext,
912                     provideTestConfigurationFlow(
913                         scope = this.backgroundScope,
914                         defaultConfiguration =
915                             TestPhotopickerConfiguration.build {
916                                 action(MediaStore.ACTION_PICK_IMAGES)
917                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
918                             },
919                     ),
920                     this.backgroundScope,
921                     StandardTestDispatcher(this.testScheduler),
922                     USER_HANDLE_PRIMARY,
923                 )
924 
925             val emissions = mutableListOf<UserStatus>()
926             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
927             advanceTimeBy(100)
928 
929             backgroundScope.launch {
930                 val switchResult =
931                     userMonitor.requestSwitchActiveUserProfile(
932                         UserProfile(handle = USER_HANDLE_MANAGED),
933                         mockContext,
934                     )
935                 assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_PROFILE_DISABLED)
936             }
937 
938             advanceTimeBy(100)
939 
940             assertThat(emissions.size).isEqualTo(1)
941             assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialState)
942         }
943     }
944 
945     @Test
946     fun testProfileDisableWhileInQuietModeUMinus() {
947         assumeFalse(SdkLevel.isAtLeastV())
948 
949         whenever(mockUserManager.isQuietModeEnabled(USER_HANDLE_MANAGED)) { true }
950 
951         val initialState =
952             UserStatus(
953                 activeUserProfile =
954                     UserProfile(
955                         handle = USER_HANDLE_PRIMARY,
956                         profileType = UserProfile.ProfileType.PRIMARY,
957                     ),
958                 allProfiles =
959                     listOf(
960                         UserProfile(
961                             handle = USER_HANDLE_PRIMARY,
962                             profileType = UserProfile.ProfileType.PRIMARY,
963                         ),
964                         UserProfile(
965                             handle = USER_HANDLE_MANAGED,
966                             profileType = UserProfile.ProfileType.MANAGED,
967                             disabledReasons = setOf(UserProfile.DisabledReason.QUIET_MODE),
968                         ),
969                     ),
970                 activeContentResolver = mockContentResolver,
971             )
972 
973         runTest { // this: TestScope
974             userMonitor =
975                 UserMonitor(
976                     mockContext,
977                     provideTestConfigurationFlow(
978                         scope = this.backgroundScope,
979                         defaultConfiguration =
980                             TestPhotopickerConfiguration.build {
981                                 action(MediaStore.ACTION_PICK_IMAGES)
982                                 intent(Intent(MediaStore.ACTION_PICK_IMAGES))
983                             },
984                     ),
985                     this.backgroundScope,
986                     StandardTestDispatcher(this.testScheduler),
987                     USER_HANDLE_PRIMARY,
988                 )
989 
990             val emissions = mutableListOf<UserStatus>()
991             backgroundScope.launch { userMonitor.userStatus.toList(emissions) }
992             advanceTimeBy(100)
993 
994             backgroundScope.launch {
995                 val switchResult =
996                     userMonitor.requestSwitchActiveUserProfile(
997                         UserProfile(handle = USER_HANDLE_MANAGED),
998                         mockContext,
999                     )
1000                 assertThat(switchResult).isEqualTo(SwitchUserProfileResult.FAILED_PROFILE_DISABLED)
1001             }
1002 
1003             advanceTimeBy(100)
1004 
1005             assertThat(emissions.size).isEqualTo(1)
1006             assertUserStatusIsEqualIgnoringFields(emissions.get(0), initialState)
1007         }
1008     }
1009 
1010     /**
1011      * Custom compare for [UserStatus] that ignores differences in specific [UserProfile] fields:
1012      * - Icon
1013      * - Label
1014      */
1015     private fun assertUserStatusIsEqualIgnoringFields(a: UserStatus, b: UserStatus) {
1016         val bWithIgnoredFields =
1017             b.copy(
1018                 activeUserProfile =
1019                     b.activeUserProfile.copy(
1020                         icon = a.activeUserProfile.icon,
1021                         label = a.activeUserProfile.label,
1022                     ),
1023                 allProfiles =
1024                     b.allProfiles.mapIndexed { index, profile ->
1025                         profile.copy(
1026                             icon = a.allProfiles.get(index).icon,
1027                             label = a.allProfiles.get(index).label,
1028                         )
1029                     },
1030             )
1031 
1032         assertThat(a).isEqualTo(bWithIgnoredFields)
1033     }
1034 }
1035