/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.intentresolver.ui import android.app.Activity import android.app.compat.CompatChanges import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.IntentSender import android.service.chooser.ChooserResult import android.service.chooser.ChooserResult.CHOOSER_RESULT_COPY import android.service.chooser.ChooserResult.CHOOSER_RESULT_EDIT import android.service.chooser.ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT import android.service.chooser.ChooserResult.CHOOSER_RESULT_UNKNOWN import android.service.chooser.ChooserResult.ResultType import android.util.Log import com.android.intentresolver.inject.Background import com.android.intentresolver.inject.Main import com.android.intentresolver.ui.model.ShareAction import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.qualifiers.ActivityContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext private const val TAG = "ShareResultSender" /** Reports the result of a share to another process across binder, via an [IntentSender] */ interface ShareResultSender { /** Reports user selection of an activity to launch from the provided choices. */ fun onComponentSelected(component: ComponentName, directShare: Boolean, crossProfile: Boolean) /** Reports user invocation of a built-in system action. See [ShareAction]. */ fun onActionSelected(action: ShareAction) } @AssistedFactory interface ShareResultSenderFactory { fun create(callerUid: Int, chosenComponentSender: IntentSender): ShareResultSenderImpl } /** Dispatches Intents via IntentSender */ fun interface IntentSenderDispatcher { fun dispatchIntent(intentSender: IntentSender, intent: Intent) } class ShareResultSenderImpl( @Main private val scope: CoroutineScope, @Background val backgroundDispatcher: CoroutineDispatcher, private val callerUid: Int, private val resultSender: IntentSender, private val intentDispatcher: IntentSenderDispatcher ) : ShareResultSender { @AssistedInject constructor( @ActivityContext context: Context, @Main scope: CoroutineScope, @Background backgroundDispatcher: CoroutineDispatcher, @Assisted callerUid: Int, @Assisted chosenComponentSender: IntentSender, ) : this( scope, backgroundDispatcher, callerUid, chosenComponentSender, IntentSenderDispatcher { sender, intent -> sender.dispatchIntent(context, intent) } ) override fun onComponentSelected( component: ComponentName, directShare: Boolean, crossProfile: Boolean ) { Log.i(TAG, "onComponentSelected: $component directShare=$directShare cross=$crossProfile") scope.launch { val intent = createChosenComponentIntent(component, directShare, crossProfile) intent?.let { intentDispatcher.dispatchIntent(resultSender, it) } } } override fun onActionSelected(action: ShareAction) { Log.i(TAG, "onActionSelected: $action") scope.launch { if (chooserResultSupported(callerUid)) { @ResultType val chosenAction = shareActionToChooserResult(action) val intent: Intent = createSelectedActionIntent(chosenAction) intentDispatcher.dispatchIntent(resultSender, intent) } else { Log.i(TAG, "Not sending SelectedAction") } } } private suspend fun createChosenComponentIntent( component: ComponentName, direct: Boolean, crossProfile: Boolean, ): Intent? { if (chooserResultSupported(callerUid)) { if (crossProfile) { Log.i(TAG, "Redacting package from cross-profile ${Intent.EXTRA_CHOOSER_RESULT}") return Intent() .putExtra( Intent.EXTRA_CHOOSER_RESULT, ChooserResult(CHOOSER_RESULT_UNKNOWN, null, direct) ) } else { // Add extra with component name for backwards compatibility. val intent: Intent = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, component) // Add ChooserResult value for Android V+ intent.putExtra( Intent.EXTRA_CHOOSER_RESULT, ChooserResult(CHOOSER_RESULT_SELECTED_COMPONENT, component, direct) ) return intent } } else { if (crossProfile) { // We can only send cross-profile results in the new ChooserResult format. Log.i(TAG, "Omitting selection callback for cross-profile target") return null } else { val intent: Intent = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, component) Log.i(TAG, "Not including ${Intent.EXTRA_CHOOSER_RESULT}") return intent } } } @ResultType private fun shareActionToChooserResult(action: ShareAction) = when (action) { ShareAction.SYSTEM_COPY -> CHOOSER_RESULT_COPY ShareAction.SYSTEM_EDIT -> CHOOSER_RESULT_EDIT ShareAction.APPLICATION_DEFINED -> CHOOSER_RESULT_UNKNOWN } private fun createSelectedActionIntent(@ResultType result: Int): Intent { return Intent().putExtra(Intent.EXTRA_CHOOSER_RESULT, ChooserResult(result, null, false)) } private suspend fun chooserResultSupported(uid: Int): Boolean { return withContext(backgroundDispatcher) { // background -> Binder call to system_server CompatChanges.isChangeEnabled(ChooserResult.SEND_CHOOSER_RESULT, uid) } } } private fun IntentSender.dispatchIntent(context: Context, intent: Intent) { try { sendIntent( /* context = */ context, /* code = */ Activity.RESULT_OK, /* intent = */ intent, /* onFinished = */ null, /* handler = */ null ) } catch (e: IntentSender.SendIntentException) { Log.e(TAG, "Failed to send intent to IntentSender", e) } }