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.ui
18
19 import android.app.Activity
20 import android.app.compat.CompatChanges
21 import android.content.ComponentName
22 import android.content.Context
23 import android.content.Intent
24 import android.content.IntentSender
25 import android.service.chooser.ChooserResult
26 import android.service.chooser.ChooserResult.CHOOSER_RESULT_COPY
27 import android.service.chooser.ChooserResult.CHOOSER_RESULT_EDIT
28 import android.service.chooser.ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT
29 import android.service.chooser.ChooserResult.CHOOSER_RESULT_UNKNOWN
30 import android.service.chooser.ChooserResult.ResultType
31 import android.util.Log
32 import com.android.intentresolver.inject.Background
33 import com.android.intentresolver.inject.Main
34 import com.android.intentresolver.ui.model.ShareAction
35 import dagger.assisted.Assisted
36 import dagger.assisted.AssistedFactory
37 import dagger.assisted.AssistedInject
38 import dagger.hilt.android.qualifiers.ActivityContext
39 import kotlinx.coroutines.CoroutineDispatcher
40 import kotlinx.coroutines.CoroutineScope
41 import kotlinx.coroutines.launch
42 import kotlinx.coroutines.withContext
43
44 private const val TAG = "ShareResultSender"
45
46 /** Reports the result of a share to another process across binder, via an [IntentSender] */
47 interface ShareResultSender {
48 /** Reports user selection of an activity to launch from the provided choices. */
49 fun onComponentSelected(component: ComponentName, directShare: Boolean, crossProfile: Boolean)
50
51 /** Reports user invocation of a built-in system action. See [ShareAction]. */
52 fun onActionSelected(action: ShareAction)
53 }
54
55 @AssistedFactory
56 interface ShareResultSenderFactory {
createnull57 fun create(callerUid: Int, chosenComponentSender: IntentSender): ShareResultSenderImpl
58 }
59
60 /** Dispatches Intents via IntentSender */
61 fun interface IntentSenderDispatcher {
62 fun dispatchIntent(intentSender: IntentSender, intent: Intent)
63 }
64
65 class ShareResultSenderImpl(
66 @Main private val scope: CoroutineScope,
67 @Background val backgroundDispatcher: CoroutineDispatcher,
68 private val callerUid: Int,
69 private val resultSender: IntentSender,
70 private val intentDispatcher: IntentSenderDispatcher
71 ) : ShareResultSender {
72 @AssistedInject
73 constructor(
74 @ActivityContext context: Context,
75 @Main scope: CoroutineScope,
76 @Background backgroundDispatcher: CoroutineDispatcher,
77 @Assisted callerUid: Int,
78 @Assisted chosenComponentSender: IntentSender,
79 ) : this(
80 scope,
81 backgroundDispatcher,
82 callerUid,
83 chosenComponentSender,
sendernull84 IntentSenderDispatcher { sender, intent -> sender.dispatchIntent(context, intent) }
85 )
86
onComponentSelectednull87 override fun onComponentSelected(
88 component: ComponentName,
89 directShare: Boolean,
90 crossProfile: Boolean
91 ) {
92 Log.i(TAG, "onComponentSelected: $component directShare=$directShare cross=$crossProfile")
93 scope.launch {
94 val intent = createChosenComponentIntent(component, directShare, crossProfile)
95 intent?.let { intentDispatcher.dispatchIntent(resultSender, it) }
96 }
97 }
98
onActionSelectednull99 override fun onActionSelected(action: ShareAction) {
100 Log.i(TAG, "onActionSelected: $action")
101 scope.launch {
102 if (chooserResultSupported(callerUid)) {
103 @ResultType val chosenAction = shareActionToChooserResult(action)
104 val intent: Intent = createSelectedActionIntent(chosenAction)
105 intentDispatcher.dispatchIntent(resultSender, intent)
106 } else {
107 Log.i(TAG, "Not sending SelectedAction")
108 }
109 }
110 }
111
createChosenComponentIntentnull112 private suspend fun createChosenComponentIntent(
113 component: ComponentName,
114 direct: Boolean,
115 crossProfile: Boolean,
116 ): Intent? {
117 if (chooserResultSupported(callerUid)) {
118 if (crossProfile) {
119 Log.i(TAG, "Redacting package from cross-profile ${Intent.EXTRA_CHOOSER_RESULT}")
120 return Intent()
121 .putExtra(
122 Intent.EXTRA_CHOOSER_RESULT,
123 ChooserResult(CHOOSER_RESULT_UNKNOWN, null, direct)
124 )
125 } else {
126 // Add extra with component name for backwards compatibility.
127 val intent: Intent = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, component)
128
129 // Add ChooserResult value for Android V+
130 intent.putExtra(
131 Intent.EXTRA_CHOOSER_RESULT,
132 ChooserResult(CHOOSER_RESULT_SELECTED_COMPONENT, component, direct)
133 )
134 return intent
135 }
136 } else {
137 if (crossProfile) {
138 // We can only send cross-profile results in the new ChooserResult format.
139 Log.i(TAG, "Omitting selection callback for cross-profile target")
140 return null
141 } else {
142 val intent: Intent = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, component)
143 Log.i(TAG, "Not including ${Intent.EXTRA_CHOOSER_RESULT}")
144 return intent
145 }
146 }
147 }
148
149 @ResultType
shareActionToChooserResultnull150 private fun shareActionToChooserResult(action: ShareAction) =
151 when (action) {
152 ShareAction.SYSTEM_COPY -> CHOOSER_RESULT_COPY
153 ShareAction.SYSTEM_EDIT -> CHOOSER_RESULT_EDIT
154 ShareAction.APPLICATION_DEFINED -> CHOOSER_RESULT_UNKNOWN
155 }
156
createSelectedActionIntentnull157 private fun createSelectedActionIntent(@ResultType result: Int): Intent {
158 return Intent().putExtra(Intent.EXTRA_CHOOSER_RESULT, ChooserResult(result, null, false))
159 }
160
chooserResultSupportednull161 private suspend fun chooserResultSupported(uid: Int): Boolean {
162 return withContext(backgroundDispatcher) {
163 // background -> Binder call to system_server
164 CompatChanges.isChangeEnabled(ChooserResult.SEND_CHOOSER_RESULT, uid)
165 }
166 }
167 }
168
dispatchIntentnull169 private fun IntentSender.dispatchIntent(context: Context, intent: Intent) {
170 try {
171 sendIntent(
172 /* context = */ context,
173 /* code = */ Activity.RESULT_OK,
174 /* intent = */ intent,
175 /* onFinished = */ null,
176 /* handler = */ null
177 )
178 } catch (e: IntentSender.SendIntentException) {
179 Log.e(TAG, "Failed to send intent to IntentSender", e)
180 }
181 }
182