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