1 /*
<lambda>null2  * Copyright (C) 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.intentresolver.contentpreview.payloadtoggle.domain.update
18 
19 import android.content.ContentInterface
20 import android.content.Intent
21 import android.content.Intent.EXTRA_ALTERNATE_INTENTS
22 import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
23 import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION
24 import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER
25 import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
26 import android.content.Intent.EXTRA_CHOOSER_TARGETS
27 import android.content.Intent.EXTRA_INTENT
28 import android.content.Intent.EXTRA_METADATA_TEXT
29 import android.content.IntentSender
30 import android.net.Uri
31 import android.os.Bundle
32 import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED
33 import android.service.chooser.ChooserAction
34 import android.service.chooser.ChooserTarget
35 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
36 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
37 import com.android.intentresolver.inject.AdditionalContent
38 import com.android.intentresolver.inject.ChooserIntent
39 import com.android.intentresolver.inject.ChooserServiceFlags
40 import com.android.intentresolver.ui.viewmodel.readAlternateIntents
41 import com.android.intentresolver.ui.viewmodel.readChooserActions
42 import com.android.intentresolver.validation.Invalid
43 import com.android.intentresolver.validation.Valid
44 import com.android.intentresolver.validation.ValidationResult
45 import com.android.intentresolver.validation.log
46 import com.android.intentresolver.validation.types.array
47 import com.android.intentresolver.validation.types.value
48 import com.android.intentresolver.validation.validateFrom
49 import dagger.Binds
50 import dagger.Module
51 import dagger.hilt.InstallIn
52 import dagger.hilt.android.components.ViewModelComponent
53 import javax.inject.Inject
54 import kotlinx.coroutines.sync.Mutex
55 import kotlinx.coroutines.sync.withLock
56 
57 private const val TAG = "SelectionChangeCallback"
58 
59 /**
60  * Encapsulates payload change callback invocation to the sharing app; handles callback arguments
61  * and result format mapping.
62  */
63 fun interface SelectionChangeCallback {
64     suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate?
65 }
66 
67 class SelectionChangeCallbackImpl
68 @Inject
69 constructor(
70     @AdditionalContent private val uri: Uri,
71     @ChooserIntent private val chooserIntent: Intent,
72     private val contentResolver: ContentInterface,
73     private val flags: ChooserServiceFlags,
74 ) : SelectionChangeCallback {
75     private val mutex = Mutex()
76 
onSelectionChangednull77     override suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? =
78         mutex
79             .withLock {
80                 contentResolver.call(
81                     requireNotNull(uri.authority) { "URI authority can not be null" },
82                     ON_SELECTION_CHANGED,
83                     uri.toString(),
84                     Bundle().apply {
85                         putParcelable(
86                             EXTRA_INTENT,
87                             Intent(chooserIntent).apply { putExtra(EXTRA_INTENT, targetIntent) }
88                         )
89                     }
90                 )
91             }
bundlenull92             ?.let { bundle ->
93                 return when (val result = readCallbackResponse(bundle, flags)) {
94                     is Valid -> {
95                         result.warnings.forEach { it.log(TAG) }
96                         result.value
97                     }
98                     is Invalid -> {
99                         result.errors.forEach { it.log(TAG) }
100                         null
101                     }
102                 }
103             }
104 }
105 
readCallbackResponsenull106 private fun readCallbackResponse(
107     bundle: Bundle,
108     flags: ChooserServiceFlags
109 ): ValidationResult<ShareouselUpdate> {
110     return validateFrom(bundle::get) {
111         // An error is treated as an empty collection or null as the presence of a value indicates
112         // an intention to change the old value implying that the old value is obsolete (and should
113         // not be used).
114         val customActions =
115             bundle.readValueUpdate(EXTRA_CHOOSER_CUSTOM_ACTIONS) {
116                 readChooserActions() ?: emptyList()
117             }
118         val modifyShareAction =
119             bundle.readValueUpdate(EXTRA_CHOOSER_MODIFY_SHARE_ACTION) { key ->
120                 optional(value<ChooserAction>(key))
121             }
122         val alternateIntents =
123             bundle.readValueUpdate(EXTRA_ALTERNATE_INTENTS) {
124                 readAlternateIntents() ?: emptyList()
125             }
126         val callerTargets =
127             bundle.readValueUpdate(EXTRA_CHOOSER_TARGETS) { key ->
128                 optional(array<ChooserTarget>(key)) ?: emptyList()
129             }
130         val refinementIntentSender =
131             bundle.readValueUpdate(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER) { key ->
132                 optional(value<IntentSender>(key))
133             }
134         val resultIntentSender =
135             bundle.readValueUpdate(EXTRA_CHOOSER_RESULT_INTENT_SENDER) { key ->
136                 optional(value<IntentSender>(key))
137             }
138         val metadataText =
139             if (flags.enableSharesheetMetadataExtra()) {
140                 bundle.readValueUpdate(EXTRA_METADATA_TEXT) { key ->
141                     optional(value<CharSequence>(key))
142                 }
143             } else {
144                 ValueUpdate.Absent
145             }
146 
147         ShareouselUpdate(
148             customActions,
149             modifyShareAction,
150             alternateIntents,
151             callerTargets,
152             refinementIntentSender,
153             resultIntentSender,
154             metadataText,
155         )
156     }
157 }
158 
readValueUpdatenull159 private inline fun <reified T> Bundle.readValueUpdate(
160     key: String,
161     block: (String) -> T
162 ): ValueUpdate<T> =
163     if (containsKey(key)) {
164         ValueUpdate.Value(block(key))
165     } else {
166         ValueUpdate.Absent
167     }
168 
169 @Module
170 @InstallIn(ViewModelComponent::class)
171 interface SelectionChangeCallbackModule {
bindnull172     @Binds fun bind(impl: SelectionChangeCallbackImpl): SelectionChangeCallback
173 }
174