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