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