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