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 18 19 import android.content.ClipData 20 import android.content.ComponentName 21 import android.content.Intent 22 import android.content.pm.PackageManager.ApplicationInfoFlags 23 import android.content.pm.PackageManager.NameNotFoundException 24 import android.content.pm.PackageManager.PackageInfoFlags 25 import android.net.Uri 26 import android.os.Bundle 27 import android.os.UserHandle 28 import android.provider.MediaStore 29 import android.util.Log 30 import androidx.activity.ComponentActivity 31 import androidx.activity.OnBackPressedCallback 32 import androidx.activity.SystemBarStyle 33 import androidx.activity.compose.setContent 34 import androidx.activity.enableEdgeToEdge 35 import androidx.annotation.VisibleForTesting 36 import androidx.compose.runtime.CompositionLocalProvider 37 import androidx.compose.runtime.getValue 38 import androidx.compose.ui.graphics.Color 39 import androidx.compose.ui.graphics.toArgb 40 import androidx.lifecycle.Lifecycle 41 import androidx.lifecycle.compose.collectAsStateWithLifecycle 42 import androidx.lifecycle.flowWithLifecycle 43 import androidx.lifecycle.lifecycleScope 44 import com.android.modules.utils.build.SdkLevel 45 import com.android.photopicker.core.Background 46 import com.android.photopicker.core.PhotopickerAppWithBottomSheet 47 import com.android.photopicker.core.banners.BannerManager 48 import com.android.photopicker.core.configuration.ConfigurationManager 49 import com.android.photopicker.core.configuration.IllegalIntentExtraException 50 import com.android.photopicker.core.configuration.LocalPhotopickerConfiguration 51 import com.android.photopicker.core.events.Event 52 import com.android.photopicker.core.events.Events 53 import com.android.photopicker.core.events.LocalEvents 54 import com.android.photopicker.core.events.PhotopickerEventLogger 55 import com.android.photopicker.core.events.Telemetry 56 import com.android.photopicker.core.events.dispatchReportPhotopickerApiInfoEvent 57 import com.android.photopicker.core.events.dispatchReportPhotopickerMediaItemStatusEvent 58 import com.android.photopicker.core.events.dispatchReportPhotopickerSessionInfoEvent 59 import com.android.photopicker.core.features.FeatureManager 60 import com.android.photopicker.core.features.LocalFeatureManager 61 import com.android.photopicker.core.selection.GrantsAwareSelectionImpl 62 import com.android.photopicker.core.selection.LocalSelection 63 import com.android.photopicker.core.selection.Selection 64 import com.android.photopicker.core.theme.PhotopickerTheme 65 import com.android.photopicker.core.user.UserMonitor 66 import com.android.photopicker.data.DataService 67 import com.android.photopicker.data.model.Media 68 import com.android.photopicker.extensions.canHandleGetContentIntentMimeTypes 69 import com.android.photopicker.features.preparemedia.PrepareMediaFeature 70 import com.android.photopicker.features.preparemedia.PrepareMediaResult 71 import com.android.photopicker.features.preparemedia.PrepareMediaResult.PrepareMediaFailed 72 import com.android.photopicker.features.preparemedia.PrepareMediaResult.PreparedMedia 73 import com.android.photopicker.util.LocalLocalizationHelper 74 import com.android.photopicker.util.rememberLocalizationHelper 75 import dagger.Lazy 76 import dagger.hilt.android.AndroidEntryPoint 77 import dagger.hilt.android.scopes.ActivityRetainedScoped 78 import javax.inject.Inject 79 import kotlinx.coroutines.CompletableDeferred 80 import kotlinx.coroutines.CoroutineDispatcher 81 import kotlinx.coroutines.flow.Flow 82 import kotlinx.coroutines.flow.MutableSharedFlow 83 import kotlinx.coroutines.flow.first 84 import kotlinx.coroutines.flow.receiveAsFlow 85 import kotlinx.coroutines.flow.runningFold 86 import kotlinx.coroutines.launch 87 import kotlinx.coroutines.withContext 88 89 /** 90 * This is the main entrypoint into the Android Photopicker. 91 * 92 * This class is responsible for bootstrapping the launched activity, session related dependencies, 93 * and providing the compose ui entrypoint in [[PhotopickerApp]] with everything it needs. 94 */ 95 @AndroidEntryPoint(ComponentActivity::class) 96 class MainActivity : Hilt_MainActivity() { 97 98 @Inject @ActivityRetainedScoped lateinit var configurationManager: ConfigurationManager 99 @Inject @ActivityRetainedScoped lateinit var bannerManager: Lazy<BannerManager> 100 @Inject @ActivityRetainedScoped lateinit var processOwnerUserHandle: UserHandle 101 @Inject @ActivityRetainedScoped lateinit var selection: Lazy<Selection<Media>> 102 @Inject @ActivityRetainedScoped lateinit var dataService: Lazy<DataService> 103 // This needs to be injected lazily, to defer initialization until the action can be set 104 // on the ConfigurationManager. 105 @Inject @ActivityRetainedScoped lateinit var featureManager: Lazy<FeatureManager> 106 @Inject @Background lateinit var background: CoroutineDispatcher 107 @Inject lateinit var userMonitor: Lazy<UserMonitor> 108 109 // Events requires the feature manager, so initialize this lazily until the action is set. 110 @Inject lateinit var events: Lazy<Events> 111 112 companion object { 113 val TAG: String = "Photopicker" 114 } 115 116 /** 117 * Keeps track of the result set for the calling activity that launched the photopicker for 118 * logging purposes 119 */ 120 private var activityResultSet = 0 121 122 /** 123 * Keeps track of whether or not the picker was closed by using the standard android back 124 * gesture instead of the picker bottom sheet swipe down 125 */ 126 private var isPickerClosedByBackGesture = false 127 128 private lateinit var photopickerEventLogger: PhotopickerEventLogger 129 130 /** 131 * A flow used to trigger the preparer. When media is ready to be prepared it should be provided 132 * to the preparer by emitting into this flow. 133 * 134 * The main activity should create a new [_prepareDeferred] before emitting, and then monitor 135 * that deferred to obtain the result of the prepare operation that this flow will trigger. 136 */ 137 private val prepareMedia: MutableSharedFlow<Set<Media>> = MutableSharedFlow() 138 139 /** 140 * A deferred which tracks the current state of any prepare operation requested by the main 141 * activity. 142 */ 143 private var _prepareDeferred: CompletableDeferred<PrepareMediaResult> = CompletableDeferred() 144 145 /** 146 * Public access to the deferred, behind a getter. (To ensure any access to this property always 147 * obtains the latest value) 148 */ 149 val prepareDeferred: CompletableDeferred<PrepareMediaResult> 150 get() { 151 return _prepareDeferred 152 } 153 154 /** 155 * A top level flow that listens for disruptive data events from the [DataService]. This flow 156 * will emit when the DataService detects that its data is inaccurate or stale and will be used 157 * to force refresh the UI and navigate the user back to the start destination. 158 */ 159 private val disruptiveDataNotification: Flow<Int> by lazy { 160 dataService.get().disruptiveDataUpdateChannel.receiveAsFlow().runningFold(initial = 0) { 161 prev, 162 _ -> 163 prev + 1 164 } 165 } 166 167 override fun onCreate(savedInstanceState: Bundle?) { 168 super.onCreate(savedInstanceState) 169 170 // [ACTION_GET_CONTENT]: Check to see if Photopicker should handle this session, or if the 171 // user should instead be referred to [com.android.documentsui]. This is necessary because 172 // Photopicker has a higher priority for "image/*" and "video/*" mimetypes that DocumentsUi. 173 // An unfortunate side effect is that a mimetype of "*/*" also matches Photopicker's 174 // intent-filter, and in that case, the user is not in a pure media selection mode, so refer 175 // the user to DocumentsUi to handle all file types. 176 if (shouldRerouteGetContentRequest()) { 177 referToDocumentsUi() 178 } 179 180 // Set a Black color scrim behind the status bar. 181 enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.Black.toArgb())) 182 183 // Set the action before allowing FeatureManager to be initialized, so that it receives 184 // the correct config with this activity's action. 185 try { 186 getIntent()?.let { configurationManager.setIntent(it) } 187 } catch (exception: IllegalIntentExtraException) { 188 // If the incoming intent contains intent extras that are not supported in the current 189 // configuration, then cancel the activity and close. 190 Log.e(TAG, "Unable to start Photopicker with illegal configuration", exception) 191 setResult(RESULT_CANCELED) 192 activityResultSet = RESULT_CANCELED 193 finish() 194 } 195 196 // Add information about the caller to the configuration. 197 setCallerInConfiguration() 198 199 // Begin listening for events before starting the UI. 200 listenForEvents() 201 202 // Picker event logger starts listening for events dispatched throughout the app 203 photopickerEventLogger = PhotopickerEventLogger(dataService) 204 photopickerEventLogger.start(lifecycleScope, background, events.get()) 205 206 /* 207 * In single select sessions, the activity needs to end after a media object is selected, 208 * so register a listener to the selection so the activity can handle calling 209 * [onMediaSelectionConfirmed] itself. 210 * 211 * For multi-select, the activity has to wait for onMediaSelectionConfirmed to be called 212 * by the selection bar click handler, or for the [Event.MediaSelectionConfirmed], in 213 * the event the user ends the session from the [PreviewFeature] 214 */ 215 listenForSelectionIfSingleSelect() 216 217 setContent { 218 val photopickerConfiguration by 219 configurationManager.configuration.collectAsStateWithLifecycle() 220 // Provide values to the entire compose stack. 221 CompositionLocalProvider( 222 LocalFeatureManager provides featureManager.get(), 223 LocalPhotopickerConfiguration provides photopickerConfiguration, 224 LocalSelection provides selection.get(), 225 LocalEvents provides events.get(), 226 LocalLocalizationHelper provides rememberLocalizationHelper(), 227 ) { 228 PhotopickerTheme(config = photopickerConfiguration) { 229 PhotopickerAppWithBottomSheet( 230 onDismissRequest = ::finish, 231 onMediaSelectionConfirmed = { 232 lifecycleScope.launch { 233 // Move the work off the UI dispatcher. 234 withContext(background) { onMediaSelectionConfirmed() } 235 } 236 }, 237 prepareMedia = prepareMedia, 238 obtainPreparerDeferred = { prepareDeferred }, 239 disruptiveDataNotification, 240 ) 241 } 242 } 243 } 244 // Check if the picker was closed by the back gesture instead of simply swiping it down 245 onBackPressedDispatcher.addCallback( 246 this, 247 object : OnBackPressedCallback(true) { 248 override fun handleOnBackPressed() { 249 isPickerClosedByBackGesture = true 250 } 251 }, 252 ) 253 254 // Log the picker launch details 255 reportPhotopickerApiInfo() 256 } 257 258 override fun onResume() { 259 super.onResume() 260 Log.d(TAG, "MainActivity OnResume") 261 262 // Initialize / Refresh the banner state, it's possible that external state has changed if 263 // the activity is returning from the background. 264 lifecycleScope.launch { 265 withContext(background) { 266 // Always ensure providers before requesting a banner refresh, banners depend on 267 // having accurate provider information to generate the correct banners. 268 dataService.get().ensureProviders() 269 bannerManager.get().refreshBanners() 270 } 271 } 272 } 273 274 /** Dispatches an event to log all details with which the photopicker launched */ 275 private fun reportPhotopickerApiInfo() { 276 val intentAction = 277 when (intent.action) { 278 MediaStore.ACTION_PICK_IMAGES -> Telemetry.PickerIntentAction.ACTION_PICK_IMAGES 279 Intent.ACTION_GET_CONTENT -> Telemetry.PickerIntentAction.ACTION_GET_CONTENT 280 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP -> 281 Telemetry.PickerIntentAction.ACTION_USER_SELECT 282 else -> Telemetry.PickerIntentAction.UNSET_PICKER_INTENT_ACTION 283 } 284 285 dispatchReportPhotopickerApiInfoEvent( 286 coroutineScope = lifecycleScope, 287 lazyEvents = events, 288 photopickerConfiguration = configurationManager.configuration.value, 289 pickerIntentAction = intentAction, 290 ) 291 } 292 293 /** 294 * A collector that starts when Photopicker is running in single-select mode. This collector 295 * will trigger [onMediaSelectionConfirmed] when the first (and only) item is selected. 296 */ 297 private fun listenForSelectionIfSingleSelect() { 298 299 // Only set up a collector if the selection limit is 1, otherwise the [SelectionBarFeature] 300 // will be enabled for the user to confirm the selection. 301 if (configurationManager.configuration.value.selectionLimit == 1) { 302 lifecycleScope.launch { 303 selection.get().flow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { 304 if (it.size == 1) { 305 launch { onMediaSelectionConfirmed() } 306 } 307 } 308 } 309 } 310 } 311 312 /** Setup an [Event] listener for the [MainActivity] to monitor the event bus. */ 313 private fun listenForEvents() { 314 lifecycleScope.launch { 315 events.get().flow.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { event 316 -> 317 when (event) { 318 is Event.BrowseToDocumentsUi -> referToDocumentsUi() 319 else -> {} 320 } 321 } 322 } 323 } 324 325 override fun finish() { 326 reportSessionInfo() 327 super.finish() 328 } 329 330 /** Dispatches an event to log all the final state details of the picker */ 331 private fun reportSessionInfo() { 332 val pickerStatus = 333 if (activityResultSet == RESULT_CANCELED) { 334 Telemetry.PickerStatus.CANCELED 335 } else { 336 Telemetry.PickerStatus.CONFIRMED 337 } 338 val pickerCloseMethod = 339 if (isPickerClosedByBackGesture) { 340 Telemetry.PickerCloseMethod.BACK_BUTTON 341 } else if (pickerStatus == Telemetry.PickerStatus.CONFIRMED) { 342 Telemetry.PickerCloseMethod.SELECTION_CONFIRMED 343 } else { 344 Telemetry.PickerCloseMethod.SWIPE_DOWN 345 } 346 347 dispatchReportPhotopickerSessionInfoEvent( 348 coroutineScope = lifecycleScope, 349 lazyEvents = events, 350 photopickerConfiguration = configurationManager.configuration.value, 351 lazyDataService = dataService, 352 lazyUserMonitor = userMonitor, 353 lazyMediaSelection = selection, 354 pickerStatus = pickerStatus, 355 pickerCloseMethod = pickerCloseMethod, 356 ) 357 } 358 359 /** 360 * Sets the caller related fields in [PhotopickerConfiguration] with the calling application's 361 * information, if available. This should only be called once and will cause a configuration 362 * update. 363 */ 364 private fun setCallerInConfiguration() { 365 366 val pm = getPackageManager() 367 368 var callingPackage: String? 369 var callingPackageUid: Int? 370 371 when (getIntent()?.getAction()) { 372 // For permission mode, the caller will always be the permission controller, 373 // and the permission controller will pass the UID of the app. 374 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP -> { 375 376 callingPackageUid = getIntent()?.extras?.getInt(Intent.EXTRA_UID) 377 checkNotNull(callingPackageUid) { 378 "Photopicker cannot run in permission mode without Intent.EXTRA_UID set." 379 } 380 callingPackage = 381 callingPackageUid.let { 382 // In the case of multiple packages sharing a uid, use the first one. 383 pm.getPackagesForUid(it)?.first() 384 } 385 } 386 387 // Extract the caller from the activity class inputs 388 else -> { 389 callingPackage = getCallingPackage() 390 callingPackageUid = 391 callingPackage?.let { 392 try { 393 if (SdkLevel.isAtLeastT()) { 394 // getPackageUid API is T+ 395 pm.getPackageUid(it, PackageInfoFlags.of(0)) 396 } else { 397 // Fallback for S or lower 398 pm.getPackageUid(it, /* flags= */ 0) 399 } 400 } catch (e: NameNotFoundException) { 401 null 402 } 403 } 404 } 405 } 406 407 val callingPackageLabel: String? = 408 callingPackage?.let { 409 try { 410 if (SdkLevel.isAtLeastT()) { 411 // getApplicationInfo API is T+ 412 pm.getApplicationLabel( 413 pm.getApplicationInfo(it, ApplicationInfoFlags.of(0)) 414 ) 415 .toString() // convert CharSequence to String 416 } else { 417 // Fallback for S or lower 418 pm.getApplicationLabel(pm.getApplicationInfo(it, /* flags= */ 0)) 419 .toString() // convert CharSequence to String 420 } 421 } catch (e: NameNotFoundException) { 422 null 423 } 424 } 425 configurationManager.setCaller( 426 callingPackage = callingPackage, 427 callingPackageUid = callingPackageUid, 428 callingPackageLabel = callingPackageLabel, 429 ) 430 } 431 432 /** 433 * Entrypoint for confirming the set of selected media and preparing the media for the calling 434 * application. 435 * 436 * This should be called when the user has confirmed their selection, and would like to exit 437 * photopicker and grant access to the media to the calling application. 438 * 439 * This will result in access being issued to the calling app if the media can be successfully 440 * prepared. 441 */ 442 @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) 443 suspend fun onMediaSelectionConfirmed() { 444 445 val snapshot = selection.get().snapshot() 446 var selection = snapshot 447 // Determine if any prepare of the selected media needs to happen, and 448 // await the result of the preparer before proceeding. 449 if (featureManager.get().isFeatureEnabled(PrepareMediaFeature::class.java)) { 450 451 // Create a new [CompletableDeferred] that represents the result of this 452 // prepare operation 453 _prepareDeferred = CompletableDeferred() 454 prepareMedia.emit(snapshot) 455 456 // Await a response from the deferred before proceeding. 457 // This will suspend until the response is available. 458 val prepareResult = _prepareDeferred.await() 459 if (prepareResult is PreparedMedia) { 460 selection = prepareResult.preparedMedia 461 } else { 462 if (prepareResult !is PrepareMediaFailed) { 463 Log.e(TAG, "Expected prepare result object was not a PrepareMediaFailed") 464 } 465 466 // The prepare failed, so the activity cannot be completed. 467 return 468 } 469 } 470 471 val deselectionSnapshot = this.selection.get().getDeselection().toHashSet() 472 onMediaSelectionReady(selection, deselectionSnapshot) 473 } 474 475 /** 476 * This will end the activity. 477 * 478 * This method should be called when the user has confirmed their selection of media and would 479 * like to exit the Photopicker. All Media preparing should be completed before this method is 480 * invoked. This method will then arrange for the correct data to be returned based on the 481 * configuration Photopicker is running under. 482 * 483 * When this method is complete, the Photopicker session will end. 484 * 485 * @param selection The prepared media that is ready to be returned to the caller. 486 * @see [setResultForApp] for modes where the Photopicker returns media directly to the caller 487 * @see [issueGrantsForApp] for permission mode grant writing in MediaProvider 488 */ 489 private suspend fun onMediaSelectionReady(selection: Set<Media>, deselection: Set<Media>) { 490 491 val configuration = configurationManager.configuration.first() 492 493 when (configuration.action) { 494 MediaStore.ACTION_PICK_IMAGES, 495 Intent.ACTION_GET_CONTENT -> 496 setResultForApp(selection, canSelectMultiple = configuration.selectionLimit > 1) 497 MediaStore.ACTION_USER_SELECT_IMAGES_FOR_APP -> { 498 val uid = 499 getIntent().getExtras()?.getInt(Intent.EXTRA_UID) 500 // If the permission controller did not provide a uid, there is no way to 501 // continue. 502 ?: throw IllegalStateException( 503 "Expected a uid to provided by PermissionController." 504 ) 505 updateGrantsForApp(selection, deselection, uid) 506 } 507 else -> {} 508 } 509 510 finish() 511 } 512 513 /** 514 * The selection must be returned to the calling app via [setResult] and [ClipData]. When the 515 * [MainActivity] is ending, this is part of the sequence of events to close the picker and 516 * provide the selected media uris to the caller. 517 * 518 * This work runs on the @Background [CoroutineDispatcher] to avoid any UI disruption. 519 * 520 * @param selection the prepared media that can be safely returned to the app. 521 * @param canSelectMultiple whether photopicker is in multi-select mode. 522 */ 523 private suspend fun setResultForApp(selection: Set<Media>, canSelectMultiple: Boolean) { 524 525 if (selection.size < 1) return 526 527 val resultData = Intent() 528 529 val uris: MutableList<Uri> = selection.map { it.mediaUri }.toMutableList() 530 531 if (!canSelectMultiple) { 532 // For Single selection set the Uri on the intent directly. 533 resultData.setData(uris.removeFirst()) 534 } else if (uris.isNotEmpty()) { 535 // For multi-selection, returned data needs to be attached via [ClipData] 536 val clipData = 537 ClipData( 538 /* label= */ null, 539 /* mimeTypes= */ selection.map { it.mimeType }.distinct().toTypedArray(), 540 /* item= */ ClipData.Item(uris.removeFirst()), 541 ) 542 543 // If there are any remaining items in the list, attach those as additional 544 // [ClipData.Item] 545 for (uri in uris) { 546 clipData.addItem(ClipData.Item(uri)) 547 } 548 resultData.setClipData(clipData) 549 } else { 550 // The selection is empty, and there is no data to return to the caller. 551 setResult(RESULT_CANCELED) 552 activityResultSet = RESULT_CANCELED 553 return 554 } 555 556 resultData.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 557 resultData.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) 558 559 setResult(RESULT_OK, resultData) 560 activityResultSet = RESULT_OK 561 dispatchSelectedMediaItemsStatusEvent(selection) 562 } 563 564 /** Dispatches an Event to log details of all the picked media items */ 565 private fun dispatchSelectedMediaItemsStatusEvent(selection: Set<Media>) { 566 val mediaStatus = Telemetry.MediaStatus.SELECTED 567 568 for (mediaItem in selection) { 569 dispatchReportPhotopickerMediaItemStatusEvent( 570 coroutineScope = lifecycleScope, 571 lazyEvents = events, 572 photopickerConfiguration = configurationManager.configuration.value, 573 mediaItem = mediaItem, 574 mediaStatus = mediaStatus, 575 ) 576 } 577 } 578 579 /** 580 * When Photopicker is in permission mode, the PermissionController is the calling application, 581 * and rather than returning a list of media uris to the caller, instead MediaGrants must be 582 * generated for the app uid provided by the PermissionController. (Which in this context is the 583 * app that has invoked the permission controller, and thus caused PermissionController to open 584 * photopicker). 585 * 586 * In addition to this, the preGranted items that are now de-selected by the user, the app 587 * should no longer hold MediaGrants for them. This method takes care of revoking these grants. 588 * 589 * This is part of the sequence of ending a Photopicker Session, and is done in place of 590 * returning data to the caller. 591 * 592 * @param selection The prepared media that is ready to be returned to the caller. 593 * @param deselection The media for which the read grants should be revoked. 594 * @param uid The uid of the calling application to issue media grants for. 595 */ 596 private suspend fun updateGrantsForApp( 597 currentSelection: Set<Media>, 598 currentDeSelection: Set<Media>, 599 uid: Int, 600 ) { 601 602 val selection = selection.get() 603 val deselectAllEnabled = 604 if (selection is GrantsAwareSelectionImpl) { 605 selection.isDeSelectAllEnabled 606 } else { 607 false 608 } 609 if (deselectAllEnabled) { 610 // removing all grants for preGranted items for this package. 611 MediaStore.revokeAllMediaReadForPackages(getApplicationContext(), uid) 612 } else { 613 // Removing grants for preGranted items that have now been de-selected by the user. 614 val urisForItemsToBeRevoked = currentDeSelection.map { it.mediaUri } 615 MediaStore.revokeMediaReadForPackages( 616 getApplicationContext(), 617 uid, 618 urisForItemsToBeRevoked, 619 ) 620 } 621 // Adding grants for items selected by the user. 622 val uris: List<Uri> = currentSelection.map { it.mediaUri } 623 MediaStore.grantMediaReadForPackage(getApplicationContext(), uid, uris) 624 625 // No need to send any data back to the PermissionController, just send an OK signal 626 // back to indicate the MediaGrants are available. 627 setResult(RESULT_OK) 628 activityResultSet = RESULT_OK 629 } 630 631 /** 632 * This will end the activity. Refer the current session to [com.android.documentsui] 633 * 634 * Note: Complete any pending logging or work before calling this method as this will end the 635 * process immediately. 636 */ 637 private fun referToDocumentsUi() { 638 // The incoming intent is not changed in any way when redirecting to DocumentsUi. 639 // The calling app launched [ACTION_GET_CONTENT] probably without knowing it would first 640 // come to Photopicker, so if Photopicker isn't going to handle the intent, just pass it 641 // along unmodified. 642 @Suppress("UnsafeIntentLaunch") val intent = getIntent() 643 intent?.apply { 644 addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) 645 addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) 646 setComponent(getDocumentssUiComponentName()) 647 } 648 startActivityAsUser(intent, processOwnerUserHandle) 649 finish() 650 } 651 652 /** 653 * Determines if this session should end and the user should be redirected to 654 * [com.android.documentsUi]. The evaluates the incoming [Intent] to see if Photopicker is 655 * running in [ACTION_GET_CONTENT], and if the mimetypes requested can be correctly handled by 656 * Photopicker. If the activity is not running in [ACTION_GET_CONTENT] this will always return 657 * false. 658 * 659 * A notable exception would be if Photopicker was started by DocumentsUi rather than the 660 * original app, in which case this method will return [false]. 661 * 662 * @return true if the activity is running [ACTION_GET_CONTENT] and Photopicker shouldn't handle 663 * the session. 664 */ 665 private fun shouldRerouteGetContentRequest(): Boolean { 666 val intent = getIntent() 667 668 return when { 669 Intent.ACTION_GET_CONTENT != intent.getAction() -> false 670 671 // GET_CONTENT for all (media and non-media) files opens DocumentsUi, but it still shows 672 // "Photo Picker app option. When the user clicks on "Photo Picker", the same intent 673 // which includes filters to show non-media files as well is forwarded to PhotoPicker. 674 // Make sure Photo Picker is opened when the intent is explicitly forwarded by 675 // documentsUi 676 isIntentReferredByDocumentsUi(getReferrer()) -> false 677 678 // Ensure Photopicker can handle the specified MIME types. 679 intent.canHandleGetContentIntentMimeTypes() -> false 680 else -> true 681 } 682 } 683 684 /** 685 * Resolves a [ComponentName] for DocumentsUi via [Intent.ACTION_OPEN_DOCUMENT] 686 * 687 * ACTION_OPEN_DOCUMENT is used to find DocumentsUi's component due to DocumentsUi being the 688 * default handler. 689 * 690 * @return the [ComponentName] for DocumentsUi's picker activity. 691 */ 692 private fun getDocumentssUiComponentName(): ComponentName? { 693 694 val intent = 695 Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 696 addCategory(Intent.CATEGORY_OPENABLE) 697 setType("*/*") 698 } 699 700 val componentName = intent.resolveActivity(getPackageManager()) 701 return componentName 702 } 703 704 /** 705 * Determines if the referrer uri came from [com.android.documentsui] 706 * 707 * @return true if the referrer [Uri] is from DocumentsUi. 708 */ 709 private fun isIntentReferredByDocumentsUi(referrer: Uri?): Boolean { 710 return referrer?.getHost() == getDocumentssUiComponentName()?.getPackageName() 711 } 712 } 713