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