1 /*
2 * 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 package com.android.intentresolver.ui.viewmodel
17
18 import android.content.ComponentName
19 import android.content.Intent
20 import android.content.Intent.EXTRA_ALTERNATE_INTENTS
21 import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
22 import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION
23 import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER
24 import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
25 import android.content.Intent.EXTRA_CHOOSER_TARGETS
26 import android.content.Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER
27 import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS
28 import android.content.Intent.EXTRA_INITIAL_INTENTS
29 import android.content.Intent.EXTRA_INTENT
30 import android.content.Intent.EXTRA_METADATA_TEXT
31 import android.content.Intent.EXTRA_REPLACEMENT_EXTRAS
32 import android.content.Intent.EXTRA_TEXT
33 import android.content.Intent.EXTRA_TITLE
34 import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
35 import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
36 import android.content.IntentFilter
37 import android.content.IntentSender
38 import android.net.Uri
39 import android.os.Bundle
40 import android.service.chooser.ChooserAction
41 import android.service.chooser.ChooserTarget
42 import com.android.intentresolver.ChooserActivity
43 import com.android.intentresolver.ContentTypeHint
44 import com.android.intentresolver.R
45 import com.android.intentresolver.data.model.ChooserRequest
46 import com.android.intentresolver.ext.hasSendAction
47 import com.android.intentresolver.ext.ifMatch
48 import com.android.intentresolver.inject.ChooserServiceFlags
49 import com.android.intentresolver.ui.model.ActivityModel
50 import com.android.intentresolver.util.hasValidIcon
51 import com.android.intentresolver.validation.Validation
52 import com.android.intentresolver.validation.ValidationResult
53 import com.android.intentresolver.validation.types.IntentOrUri
54 import com.android.intentresolver.validation.types.array
55 import com.android.intentresolver.validation.types.value
56 import com.android.intentresolver.validation.validateFrom
57
58 private const val MAX_CHOOSER_ACTIONS = 5
59 private const val MAX_INITIAL_INTENTS = 2
60
maybeAddSendActionFlagsnull61 internal fun Intent.maybeAddSendActionFlags() =
62 ifMatch(Intent::hasSendAction) {
63 addFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
64 addFlags(FLAG_ACTIVITY_MULTIPLE_TASK)
65 }
66
readChooserRequestnull67 fun readChooserRequest(
68 model: ActivityModel,
69 flags: ChooserServiceFlags
70 ): ValidationResult<ChooserRequest> {
71 val extras = model.intent.extras ?: Bundle()
72 @Suppress("DEPRECATION")
73 return validateFrom(extras::get) {
74 val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags()
75
76 val isSendAction = targetIntent.hasSendAction()
77
78 val additionalTargets = readAlternateIntents() ?: emptyList()
79
80 val replacementExtras = optional(value<Bundle>(EXTRA_REPLACEMENT_EXTRAS))
81
82 val (customTitle, defaultTitleResource) =
83 if (isSendAction) {
84 ignored(
85 value<CharSequence>(EXTRA_TITLE),
86 "deprecated in P. You may wish to set a preview title by using EXTRA_TITLE " +
87 "property of the wrapped EXTRA_INTENT."
88 )
89 null to R.string.chooseActivity
90 } else {
91 val custom = optional(value<CharSequence>(EXTRA_TITLE))
92 custom to (custom?.let { 0 } ?: R.string.chooseActivity)
93 }
94
95 val initialIntents =
96 optional(array<Intent>(EXTRA_INITIAL_INTENTS))?.take(MAX_INITIAL_INTENTS)?.map {
97 it.maybeAddSendActionFlags()
98 }
99 ?: emptyList()
100
101 val chosenComponentSender =
102 optional(value<IntentSender>(EXTRA_CHOOSER_RESULT_INTENT_SENDER))
103 ?: optional(value<IntentSender>(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER))
104
105 val refinementIntentSender =
106 optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER))
107
108 val filteredComponents =
109 optional(array<ComponentName>(EXTRA_EXCLUDE_COMPONENTS)) ?: emptyList()
110
111 @Suppress("DEPRECATION")
112 val callerChooserTargets =
113 optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) ?: emptyList()
114
115 val retainInOnStop =
116 optional(value<Boolean>(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP)) ?: false
117
118 val sharedText = optional(value<CharSequence>(EXTRA_TEXT))
119
120 val chooserActions = readChooserActions() ?: emptyList()
121
122 val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION))
123
124 val additionalContentUri: Uri?
125 val focusedItemPos: Int
126 if (isSendAction && flags.chooserPayloadToggling()) {
127 additionalContentUri = optional(value<Uri>(Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI))
128 focusedItemPos = optional(value<Int>(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION)) ?: 0
129 } else {
130 additionalContentUri = null
131 focusedItemPos = 0
132 }
133
134 val contentTypeHint =
135 if (flags.chooserAlbumText()) {
136 when (optional(value<Int>(Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT))) {
137 Intent.CHOOSER_CONTENT_TYPE_ALBUM -> ContentTypeHint.ALBUM
138 else -> ContentTypeHint.NONE
139 }
140 } else {
141 ContentTypeHint.NONE
142 }
143
144 val metadataText =
145 if (flags.enableSharesheetMetadataExtra()) {
146 optional(value<CharSequence>(EXTRA_METADATA_TEXT))
147 } else {
148 null
149 }
150
151 ChooserRequest(
152 targetIntent = targetIntent,
153 targetAction = targetIntent.action,
154 isSendActionTarget = isSendAction,
155 targetType = targetIntent.type,
156 launchedFromPackage =
157 requireNotNull(model.launchedFromPackage) {
158 "launch.fromPackage was null, See Activity.getLaunchedFromPackage()"
159 },
160 title = customTitle,
161 defaultTitleResource = defaultTitleResource,
162 referrer = model.referrer,
163 filteredComponentNames = filteredComponents,
164 callerChooserTargets = callerChooserTargets,
165 chooserActions = chooserActions,
166 modifyShareAction = modifyShareAction,
167 shouldRetainInOnStop = retainInOnStop,
168 additionalTargets = additionalTargets,
169 replacementExtras = replacementExtras,
170 initialIntents = initialIntents,
171 chosenComponentSender = chosenComponentSender,
172 refinementIntentSender = refinementIntentSender,
173 sharedText = sharedText,
174 shareTargetFilter = targetIntent.toShareTargetFilter(),
175 additionalContentUri = additionalContentUri,
176 focusedItemPosition = focusedItemPos,
177 contentTypeHint = contentTypeHint,
178 metadataText = metadataText,
179 )
180 }
181 }
182
Validationnull183 fun Validation.readAlternateIntents(): List<Intent>? =
184 optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() }
185
Validationnull186 fun Validation.readChooserActions(): List<ChooserAction>? =
187 optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS))
188 ?.filter { hasValidIcon(it) }
189 ?.take(MAX_CHOOSER_ACTIONS)
190
toShareTargetFilternull191 private fun Intent.toShareTargetFilter(): IntentFilter? {
192 return type?.let {
193 IntentFilter().apply {
194 action?.also { addAction(it) }
195 addDataType(it)
196 }
197 }
198 }
199