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