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.ComponentName
20 import android.content.ContentInterface
21 import android.content.Intent
22 import android.content.Intent.EXTRA_ALTERNATE_INTENTS
23 import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
24 import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION
25 import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER
26 import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
27 import android.content.Intent.EXTRA_CHOOSER_TARGETS
28 import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS
29 import android.content.Intent.EXTRA_INTENT
30 import android.content.Intent.EXTRA_METADATA_TEXT
31 import android.content.IntentSender
32 import android.net.Uri
33 import android.os.Bundle
34 import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED
35 import android.service.chooser.ChooserAction
36 import android.service.chooser.ChooserTarget
37 import com.android.intentresolver.Flags.shareouselUpdateExcludeComponentsExtra
38 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
39 import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
40 import com.android.intentresolver.inject.AdditionalContent
41 import com.android.intentresolver.inject.ChooserIntent
42 import com.android.intentresolver.ui.viewmodel.readAlternateIntents
43 import com.android.intentresolver.ui.viewmodel.readChooserActions
44 import com.android.intentresolver.validation.Invalid
45 import com.android.intentresolver.validation.Valid
46 import com.android.intentresolver.validation.ValidationResult
47 import com.android.intentresolver.validation.log
48 import com.android.intentresolver.validation.types.array
49 import com.android.intentresolver.validation.types.value
50 import com.android.intentresolver.validation.validateFrom
51 import dagger.Binds
52 import dagger.Module
53 import dagger.hilt.InstallIn
54 import dagger.hilt.android.components.ViewModelComponent
55 import javax.inject.Inject
56 import kotlinx.coroutines.sync.Mutex
57 import kotlinx.coroutines.sync.withLock
58
59 private const val TAG = "SelectionChangeCallback"
60
61 /**
62 * Encapsulates payload change callback invocation to the sharing app; handles callback arguments
63 * and result format mapping.
64 */
65 fun interface SelectionChangeCallback {
66 suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate?
67 }
68
69 class SelectionChangeCallbackImpl
70 @Inject
71 constructor(
72 @AdditionalContent private val uri: Uri,
73 @ChooserIntent private val chooserIntent: Intent,
74 private val contentResolver: ContentInterface,
75 ) : SelectionChangeCallback {
76 private val mutex = Mutex()
77
onSelectionChangednull78 override suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? =
79 mutex
80 .withLock {
81 contentResolver.call(
82 requireNotNull(uri.authority) { "URI authority can not be null" },
83 ON_SELECTION_CHANGED,
84 uri.toString(),
85 Bundle().apply {
86 putParcelable(
87 EXTRA_INTENT,
88 Intent(chooserIntent).apply { putExtra(EXTRA_INTENT, targetIntent) }
89 )
90 }
91 )
92 }
bundlenull93 ?.let { bundle ->
94 return when (val result = readCallbackResponse(bundle)) {
95 is Valid -> {
96 result.warnings.forEach { it.log(TAG) }
97 result.value
98 }
99 is Invalid -> {
100 result.errors.forEach { it.log(TAG) }
101 null
102 }
103 }
104 }
105 }
106
readCallbackResponsenull107 private fun readCallbackResponse(
108 bundle: Bundle,
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 bundle.readValueUpdate(EXTRA_METADATA_TEXT) { key ->
140 optional(value<CharSequence>(key))
141 }
142 val excludedComponents: ValueUpdate<List<ComponentName>> =
143 if (shareouselUpdateExcludeComponentsExtra()) {
144 bundle.readValueUpdate(EXTRA_EXCLUDE_COMPONENTS) { key ->
145 optional(array<ComponentName>(key)) ?: emptyList()
146 }
147 } else {
148 ValueUpdate.Absent
149 }
150
151 ShareouselUpdate(
152 customActions,
153 modifyShareAction,
154 alternateIntents,
155 callerTargets,
156 refinementIntentSender,
157 resultIntentSender,
158 metadataText,
159 excludedComponents,
160 )
161 }
162 }
163
readValueUpdatenull164 private inline fun <reified T> Bundle.readValueUpdate(
165 key: String,
166 block: (String) -> T
167 ): ValueUpdate<T> =
168 if (containsKey(key)) {
169 ValueUpdate.Value(block(key))
170 } else {
171 ValueUpdate.Absent
172 }
173
174 @Module
175 @InstallIn(ViewModelComponent::class)
176 interface SelectionChangeCallbackModule {
bindnull177 @Binds fun bind(impl: SelectionChangeCallbackImpl): SelectionChangeCallback
178 }
179