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