1 /* 2 * 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.features.preview 18 19 import android.content.ContentProvider 20 import android.content.ContentResolver.EXTRA_SIZE 21 import android.content.Context 22 import android.content.pm.PackageManager 23 import android.content.pm.UserProperties 24 import android.graphics.Point 25 import android.net.Uri 26 import android.os.Bundle 27 import android.os.Parcel 28 import android.os.UserHandle 29 import android.os.UserManager 30 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_BUFFERING 31 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_COMPLETED 32 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_PERMANENT_FAILURE 33 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE 34 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_MEDIA_SIZE_CHANGED 35 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_PAUSED 36 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_READY 37 import android.provider.CloudMediaProvider.CloudMediaSurfaceStateChangedCallback.PLAYBACK_STATE_STARTED 38 import android.provider.CloudMediaProviderContract.EXTRA_LOOPING_PLAYBACK_ENABLED 39 import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER 40 import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED 41 import android.provider.CloudMediaProviderContract.EXTRA_SURFACE_STATE_CALLBACK 42 import android.provider.CloudMediaProviderContract.METHOD_CREATE_SURFACE_CONTROLLER 43 import android.provider.ICloudMediaSurfaceController 44 import android.provider.ICloudMediaSurfaceStateChangedCallback 45 import android.test.mock.MockContentResolver 46 import android.view.Surface 47 import androidx.core.os.bundleOf 48 import androidx.lifecycle.ViewModel 49 import androidx.lifecycle.ViewModelProvider 50 import androidx.lifecycle.ViewModelStore 51 import androidx.test.ext.junit.runners.AndroidJUnit4 52 import androidx.test.filters.SmallTest 53 import androidx.test.platform.app.InstrumentationRegistry 54 import com.android.photopicker.R 55 import com.android.photopicker.core.configuration.provideTestConfigurationFlow 56 import com.android.photopicker.core.selection.SelectionImpl 57 import com.android.photopicker.core.user.UserMonitor 58 import com.android.photopicker.data.model.Media 59 import com.android.photopicker.data.model.MediaSource 60 import com.android.photopicker.test.utils.MockContentProviderWrapper 61 import com.android.photopicker.tests.utils.mockito.capture 62 import com.android.photopicker.tests.utils.mockito.mockSystemService 63 import com.android.photopicker.tests.utils.mockito.nonNullableEq 64 import com.android.photopicker.tests.utils.mockito.whenever 65 import com.google.common.truth.Truth.assertWithMessage 66 import java.time.LocalDateTime 67 import java.time.ZoneOffset 68 import kotlinx.coroutines.ExperimentalCoroutinesApi 69 import kotlinx.coroutines.flow.first 70 import kotlinx.coroutines.flow.toList 71 import kotlinx.coroutines.launch 72 import kotlinx.coroutines.test.StandardTestDispatcher 73 import kotlinx.coroutines.test.advanceTimeBy 74 import kotlinx.coroutines.test.runTest 75 import org.junit.Before 76 import org.junit.Test 77 import org.junit.runner.RunWith 78 import org.mockito.ArgumentCaptor 79 import org.mockito.Captor 80 import org.mockito.Mock 81 import org.mockito.Mockito.any 82 import org.mockito.Mockito.anyInt 83 import org.mockito.Mockito.anyString 84 import org.mockito.Mockito.isNull 85 import org.mockito.Mockito.times 86 import org.mockito.Mockito.verify 87 import org.mockito.MockitoAnnotations 88 89 @SmallTest 90 @RunWith(AndroidJUnit4::class) 91 @OptIn(ExperimentalCoroutinesApi::class) 92 class PreviewViewModelTest { 93 94 @Mock lateinit var mockContext: Context 95 @Mock lateinit var mockUserManager: UserManager 96 @Mock lateinit var mockPackageManager: PackageManager 97 @Mock lateinit var mockContentProvider: ContentProvider 98 @Mock lateinit var mockController: ICloudMediaSurfaceController.Stub 99 @Captor lateinit var controllerBundle: ArgumentCaptor<Bundle> 100 101 private lateinit var mockContentResolver: MockContentResolver 102 103 private val USER_HANDLE_PRIMARY: UserHandle 104 private val USER_ID_PRIMARY: Int = 0 105 106 init { 107 val parcel1 = Parcel.obtain() 108 parcel1.writeInt(USER_ID_PRIMARY) 109 parcel1.setDataPosition(0) 110 USER_HANDLE_PRIMARY = UserHandle(parcel1) 111 } 112 113 val TEST_MEDIA_IMAGE = 114 Media.Image( 115 mediaId = "id", 116 pickerId = 1000L, 117 authority = "a", 118 mediaSource = MediaSource.LOCAL, 119 mediaUri = 120 Uri.EMPTY.buildUpon() <lambda>null121 .apply { 122 scheme("content") 123 authority("media") 124 path("picker") 125 path("a") 126 path("id") 127 } 128 .build(), 129 glideLoadableUri = 130 Uri.EMPTY.buildUpon() <lambda>null131 .apply { 132 scheme("content") 133 authority(MockContentProviderWrapper.AUTHORITY) 134 path("id") 135 } 136 .build(), 137 dateTakenMillisLong = 123456789L, 138 sizeInBytes = 1000L, 139 mimeType = "image/png", 140 standardMimeTypeExtension = 1, 141 ) 142 143 val TEST_MEDIA_VIDEO = 144 Media.Video( 145 mediaId = "video_id", 146 pickerId = 987654321L, 147 authority = MockContentProviderWrapper.AUTHORITY, 148 mediaSource = MediaSource.LOCAL, 149 mediaUri = 150 Uri.EMPTY.buildUpon() <lambda>null151 .apply { 152 scheme("content") 153 authority("a") 154 path("video_id") 155 } 156 .build(), 157 glideLoadableUri = 158 Uri.EMPTY.buildUpon() <lambda>null159 .apply { 160 scheme("content") 161 authority("a") 162 path("video_id") 163 } 164 .build(), 165 dateTakenMillisLong = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) * 1000, 166 sizeInBytes = 1000L, 167 mimeType = "video/mp4", 168 standardMimeTypeExtension = 1, 169 duration = 10000, 170 ) 171 172 @Before setupnull173 fun setup() { 174 MockitoAnnotations.initMocks(this) 175 mockSystemService(mockContext, UserManager::class.java) { mockUserManager } 176 whenever(mockUserManager.getUserProperties(any(UserHandle::class.java))) { 177 UserProperties.Builder().build() 178 } 179 180 // Stub for MockContentResolver constructor 181 whenever(mockContext.getApplicationInfo()) { 182 InstrumentationRegistry.getInstrumentation().getContext().getApplicationInfo() 183 } 184 mockContentResolver = MockContentResolver(mockContext) 185 val provider = MockContentProviderWrapper(mockContentProvider) 186 mockContentResolver.addProvider(MockContentProviderWrapper.AUTHORITY, provider) 187 188 // Stubs for UserMonitor 189 whenever(mockContext.packageManager) { mockPackageManager } 190 whenever(mockContext.contentResolver) { mockContentResolver } 191 whenever(mockContext.createPackageContextAsUser(any(), anyInt(), any())) { mockContext } 192 whenever(mockContext.createContextAsUser(any(UserHandle::class.java), anyInt())) { 193 mockContext 194 } 195 whenever(mockUserManager.getUserBadge()) { 196 InstrumentationRegistry.getInstrumentation() 197 .getContext() 198 .getResources() 199 .getDrawable(R.drawable.android, /* theme= */ null) 200 } 201 whenever(mockUserManager.getProfileLabel()) { "label" } 202 203 // Stubs for creating the RemoteSurfaceController 204 whenever( 205 mockContentProvider.call( 206 /*authority= */ nonNullableEq(MockContentProviderWrapper.AUTHORITY), 207 /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), 208 /*arg=*/ isNull(), 209 /*extras=*/ capture(controllerBundle), 210 ) 211 ) { 212 bundleOf(EXTRA_SURFACE_CONTROLLER to mockController) 213 } 214 } 215 216 /** Ensures the view model can toggle items in the session selection. */ 217 @Test testToggleInSelectionUpdatesSelectionnull218 fun testToggleInSelectionUpdatesSelection() { 219 220 runTest { 221 val selection = 222 SelectionImpl<Media>( 223 scope = this.backgroundScope, 224 configuration = provideTestConfigurationFlow(scope = this.backgroundScope), 225 ) 226 227 val viewModel = 228 PreviewViewModel( 229 this.backgroundScope, 230 selection, 231 UserMonitor( 232 mockContext, 233 provideTestConfigurationFlow(scope = this.backgroundScope), 234 this.backgroundScope, 235 StandardTestDispatcher(this.testScheduler), 236 USER_HANDLE_PRIMARY 237 ), 238 ) 239 240 assertWithMessage("Unexpected selection start size") 241 .that(selection.snapshot().size) 242 .isEqualTo(0) 243 244 // Toggle the item into the selection 245 viewModel.toggleInSelection(TEST_MEDIA_IMAGE, {}) 246 247 // Wait for selection update. 248 advanceTimeBy(100) 249 250 assertWithMessage("Selection did not contain expected item") 251 .that(selection.snapshot()) 252 .contains(TEST_MEDIA_IMAGE) 253 254 // Toggle the item out of the selection 255 viewModel.toggleInSelection(TEST_MEDIA_IMAGE, {}) 256 257 advanceTimeBy(100) 258 259 assertWithMessage("Selection contains unexpected item") 260 .that(selection.snapshot()) 261 .doesNotContain(TEST_MEDIA_IMAGE) 262 } 263 } 264 265 /** Ensures the selection is not snapshotted until requested. */ 266 @Test testSnapshotSelectionnull267 fun testSnapshotSelection() { 268 269 runTest { 270 val selection = 271 SelectionImpl<Media>( 272 scope = this.backgroundScope, 273 configuration = provideTestConfigurationFlow(scope = this.backgroundScope), 274 initialSelection = setOf(TEST_MEDIA_IMAGE), 275 ) 276 277 val viewModel = 278 PreviewViewModel( 279 this.backgroundScope, 280 selection, 281 UserMonitor( 282 mockContext, 283 provideTestConfigurationFlow(scope = this.backgroundScope), 284 this.backgroundScope, 285 StandardTestDispatcher(this.testScheduler), 286 USER_HANDLE_PRIMARY 287 ), 288 ) 289 290 var snapshot = viewModel.selectionSnapshot.first() 291 292 assertWithMessage("Selection snapshot did not match expected") 293 .that(snapshot) 294 .isEqualTo(emptySet<Media>()) 295 296 viewModel.takeNewSelectionSnapshot() 297 298 // Wait for snapshot 299 advanceTimeBy(100) 300 301 snapshot = viewModel.selectionSnapshot.first() 302 303 assertWithMessage("Selection snapshot did not match expected") 304 .that(snapshot) 305 .isEqualTo(setOf(TEST_MEDIA_IMAGE)) 306 } 307 } 308 309 /** Ensures the creation parameters of remote surface controllers. */ 310 @Test testRemotePreviewControllerCreationnull311 fun testRemotePreviewControllerCreation() { 312 313 runTest { 314 val selection = 315 SelectionImpl<Media>( 316 scope = this.backgroundScope, 317 configuration = provideTestConfigurationFlow(scope = this.backgroundScope), 318 initialSelection = setOf(TEST_MEDIA_IMAGE), 319 ) 320 val viewModel = 321 PreviewViewModel( 322 this.backgroundScope, 323 selection, 324 UserMonitor( 325 mockContext, 326 provideTestConfigurationFlow(scope = this.backgroundScope), 327 this.backgroundScope, 328 StandardTestDispatcher(this.testScheduler), 329 USER_HANDLE_PRIMARY 330 ), 331 ) 332 333 val controller = 334 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) 335 336 assertWithMessage("Returned controller was not expected to be null") 337 .that(controller) 338 .isNotNull() 339 340 verify(mockContentProvider) 341 .call( 342 /*authority=*/ anyString(), 343 /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), 344 /*arg=*/ isNull(), 345 /*extras=*/ any(Bundle::class.java), 346 ) 347 348 val bundle = controllerBundle.getValue() 349 assertWithMessage("SurfaceStateChangedCallback was not provided") 350 .that(bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK)) 351 .isNotNull() 352 assertWithMessage("Surface controller was not looped by default") 353 // Default value from bundle is false so this fails if it wasn't set 354 .that(bundle.getBoolean(EXTRA_LOOPING_PLAYBACK_ENABLED, false)) 355 .isTrue() 356 assertWithMessage("Surface controller was not muted by default") 357 // Default value from bundle is false so this fails if it wasn't set 358 .that(bundle.getBoolean(EXTRA_SURFACE_CONTROLLER_AUDIO_MUTE_ENABLED, false)) 359 .isTrue() 360 } 361 } 362 363 /** Ensures that remote preview controllers are cached for authorities. */ 364 @Test testRemotePreviewControllersAreCachednull365 fun testRemotePreviewControllersAreCached() { 366 367 runTest { 368 val selection = 369 SelectionImpl<Media>( 370 scope = this.backgroundScope, 371 configuration = provideTestConfigurationFlow(scope = this.backgroundScope), 372 initialSelection = setOf(TEST_MEDIA_IMAGE), 373 ) 374 val viewModel = 375 PreviewViewModel( 376 this.backgroundScope, 377 selection, 378 UserMonitor( 379 mockContext, 380 provideTestConfigurationFlow(scope = this.backgroundScope), 381 this.backgroundScope, 382 StandardTestDispatcher(this.testScheduler), 383 USER_HANDLE_PRIMARY 384 ), 385 ) 386 387 val controller = 388 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) 389 val controllerTwo = 390 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) 391 392 assertWithMessage("Returned controller was not expected to be null") 393 .that(controller) 394 .isNotNull() 395 396 assertWithMessage("Returned controller was not expected to be null") 397 .that(controllerTwo) 398 .isNotNull() 399 400 assertWithMessage("Expected both controller instances to be the same") 401 .that(controller) 402 .isEqualTo(controllerTwo) 403 404 verify(mockContentProvider, times(1)) 405 .call( 406 /*authority=*/ anyString(), 407 /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), 408 /*arg=*/ isNull(), 409 /*extras=*/ any(Bundle::class.java), 410 ) 411 } 412 } 413 414 /** Ensures that remote preview controllers are destroyed when the view model is cleared. */ 415 @Test testRemotePreviewControllersAreDestroyednull416 fun testRemotePreviewControllersAreDestroyed() { 417 418 runTest { 419 // Setup a proxy to call the mocked controller, since IBinder uses onTransact under the 420 // hood and that is more complicated to verify. 421 val controllerProxy = 422 object : ICloudMediaSurfaceController.Stub() { 423 424 override fun onSurfaceCreated( 425 surfaceId: Int, 426 surface: Surface, 427 mediaId: String 428 ) {} 429 430 override fun onSurfaceChanged( 431 surfaceId: Int, 432 format: Int, 433 width: Int, 434 height: Int 435 ) {} 436 437 override fun onSurfaceDestroyed(surfaceId: Int) {} 438 override fun onMediaPlay(surfaceId: Int) {} 439 override fun onMediaPause(surfaceId: Int) {} 440 override fun onMediaSeekTo(surfaceId: Int, timestampMillis: Long) {} 441 override fun onConfigChange(bundle: Bundle) {} 442 override fun onDestroy() { 443 mockController.onDestroy() 444 } 445 override fun onPlayerCreate() {} 446 override fun onPlayerRelease() {} 447 } 448 449 whenever( 450 mockContentProvider.call( 451 /*authority= */ nonNullableEq(MockContentProviderWrapper.AUTHORITY), 452 /*method=*/ nonNullableEq(METHOD_CREATE_SURFACE_CONTROLLER), 453 /*arg=*/ isNull(), 454 /*extras=*/ capture(controllerBundle), 455 ) 456 ) { 457 bundleOf(EXTRA_SURFACE_CONTROLLER to controllerProxy) 458 } 459 val selection = 460 SelectionImpl<Media>( 461 scope = this.backgroundScope, 462 configuration = provideTestConfigurationFlow(scope = this.backgroundScope), 463 initialSelection = setOf(TEST_MEDIA_IMAGE), 464 ) 465 val viewModel = 466 PreviewViewModel( 467 this.backgroundScope, 468 selection, 469 UserMonitor( 470 mockContext, 471 provideTestConfigurationFlow(scope = this.backgroundScope), 472 this.backgroundScope, 473 StandardTestDispatcher(this.testScheduler), 474 USER_HANDLE_PRIMARY 475 ), 476 ) 477 478 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) 479 480 viewModel.callOnCleared() 481 verify(mockController).onDestroy() 482 } 483 } 484 485 /** Ensures that surface playback updates are emitted. */ 486 @Test testRemotePreviewSurfaceStateChangedCallbackEmitsUpdatesnull487 fun testRemotePreviewSurfaceStateChangedCallbackEmitsUpdates() { 488 489 runTest { 490 val selection = 491 SelectionImpl<Media>( 492 scope = this.backgroundScope, 493 configuration = provideTestConfigurationFlow(scope = this.backgroundScope), 494 initialSelection = setOf(TEST_MEDIA_IMAGE), 495 ) 496 val viewModel = 497 PreviewViewModel( 498 this.backgroundScope, 499 selection, 500 UserMonitor( 501 mockContext, 502 provideTestConfigurationFlow(scope = this.backgroundScope), 503 this.backgroundScope, 504 StandardTestDispatcher(this.testScheduler), 505 USER_HANDLE_PRIMARY 506 ), 507 ) 508 509 viewModel.getControllerForAuthority(MockContentProviderWrapper.AUTHORITY) 510 511 val bundle = controllerBundle.getValue() 512 val binder = bundle.getBinder(EXTRA_SURFACE_STATE_CALLBACK) 513 val callback = ICloudMediaSurfaceStateChangedCallback.Stub.asInterface(binder) 514 515 val emissions = mutableListOf<PlaybackInfo>() 516 backgroundScope.launch { 517 viewModel 518 .getPlaybackInfoForPlayer( 519 surfaceId = 1, 520 video = TEST_MEDIA_VIDEO, 521 ) 522 .toList(emissions) 523 } 524 525 callback.setPlaybackState( 526 1, 527 PLAYBACK_STATE_MEDIA_SIZE_CHANGED, 528 bundleOf(EXTRA_SIZE to Point(100, 200)) 529 ) 530 advanceTimeBy(100) 531 532 val mediaSizeChangedInfo = emissions.removeFirst() 533 assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") 534 .that(mediaSizeChangedInfo.state) 535 .isEqualTo(PlaybackState.MEDIA_SIZE_CHANGED) 536 assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") 537 .that(mediaSizeChangedInfo.surfaceId) 538 .isEqualTo(1) 539 assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") 540 .that(mediaSizeChangedInfo.authority) 541 .isEqualTo(MockContentProviderWrapper.AUTHORITY) 542 assertWithMessage("MEDIA_SIZE_CHANGED emitted state was invalid") 543 .that( 544 mediaSizeChangedInfo.playbackStateInfo?.getParcelable( 545 EXTRA_SIZE, 546 Point::class.java 547 ) 548 ) 549 .isEqualTo(Point(100, 200)) 550 551 callback.setPlaybackState(1, PLAYBACK_STATE_BUFFERING, null) 552 advanceTimeBy(100) 553 assertWithMessage("BUFFERING emitted state was invalid") 554 .that(emissions.removeFirst()) 555 .isEqualTo( 556 PlaybackInfo( 557 state = PlaybackState.BUFFERING, 558 surfaceId = 1, 559 authority = MockContentProviderWrapper.AUTHORITY 560 ) 561 ) 562 563 callback.setPlaybackState(1, PLAYBACK_STATE_READY, null) 564 advanceTimeBy(100) 565 assertWithMessage("READY emitted state was invalid") 566 .that(emissions.removeFirst()) 567 .isEqualTo( 568 PlaybackInfo( 569 state = PlaybackState.READY, 570 surfaceId = 1, 571 authority = MockContentProviderWrapper.AUTHORITY 572 ) 573 ) 574 575 callback.setPlaybackState(1, PLAYBACK_STATE_STARTED, null) 576 advanceTimeBy(100) 577 assertWithMessage("STARTED emitted state was invalid") 578 .that(emissions.removeFirst()) 579 .isEqualTo( 580 PlaybackInfo( 581 state = PlaybackState.STARTED, 582 surfaceId = 1, 583 authority = MockContentProviderWrapper.AUTHORITY 584 ) 585 ) 586 587 callback.setPlaybackState(1, PLAYBACK_STATE_PAUSED, null) 588 advanceTimeBy(100) 589 assertWithMessage("PAUSED emitted state was invalid") 590 .that(emissions.removeFirst()) 591 .isEqualTo( 592 PlaybackInfo( 593 state = PlaybackState.PAUSED, 594 surfaceId = 1, 595 authority = MockContentProviderWrapper.AUTHORITY 596 ) 597 ) 598 599 callback.setPlaybackState(1, PLAYBACK_STATE_COMPLETED, null) 600 advanceTimeBy(100) 601 assertWithMessage("COMPLETED emitted state was invalid") 602 .that(emissions.removeFirst()) 603 .isEqualTo( 604 PlaybackInfo( 605 state = PlaybackState.COMPLETED, 606 surfaceId = 1, 607 authority = MockContentProviderWrapper.AUTHORITY 608 ) 609 ) 610 611 callback.setPlaybackState(1, PLAYBACK_STATE_ERROR_PERMANENT_FAILURE, null) 612 advanceTimeBy(100) 613 assertWithMessage("ERROR_PERMANENT_FAILURE emitted state was invalid") 614 .that(emissions.removeFirst()) 615 .isEqualTo( 616 PlaybackInfo( 617 state = PlaybackState.ERROR_PERMANENT_FAILURE, 618 surfaceId = 1, 619 authority = MockContentProviderWrapper.AUTHORITY 620 ) 621 ) 622 623 callback.setPlaybackState(1, PLAYBACK_STATE_ERROR_RETRIABLE_FAILURE, null) 624 advanceTimeBy(100) 625 assertWithMessage("ERROR_RETRIABLE_FAILURE emitted state was invalid") 626 .that(emissions.removeFirst()) 627 .isEqualTo( 628 PlaybackInfo( 629 state = PlaybackState.ERROR_RETRIABLE_FAILURE, 630 surfaceId = 1, 631 authority = MockContentProviderWrapper.AUTHORITY 632 ) 633 ) 634 } 635 } 636 637 /** 638 * Extension function that will create new [ViewModelStore], add view model into it using 639 * [ViewModelProvider] and then call [ViewModelStore.clear], that will cause 640 * [ViewModel.onCleared] to be called 641 */ callOnClearednull642 private fun ViewModel.callOnCleared() { 643 val viewModelStore = ViewModelStore() 644 val viewModelProvider = 645 ViewModelProvider( 646 viewModelStore, 647 object : ViewModelProvider.Factory { 648 649 @Suppress("UNCHECKED_CAST") 650 override fun <T : ViewModel> create(modelClass: Class<T>): T = 651 this@callOnCleared as T 652 } 653 ) 654 viewModelProvider.get(this@callOnCleared::class.java) 655 viewModelStore.clear() // To call clear() in ViewModel 656 } 657 } 658