From d6649924bfd84aebf506f5662a387dc0fb1572f3 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Thu, 21 Mar 2024 19:21:28 -0700 Subject: Payload selection callback: explicitely encode absent property values. Add a sealed type to encode either a value or the absense of it in the payload selection change callback result. Bug: 302691505 Test: atest IntentResolver-tests-unit Test: manual functionaliryt test using ShareTest app Change-Id: I3b736df3fbf62b1841506f2c41324841d2a3d617 --- .../interactor/UpdateTargetIntentInteractor.kt | 8 +-- .../payloadtoggle/domain/model/ShareouselUpdate.kt | 14 ++--- .../payloadtoggle/domain/model/ValueUpdate.kt | 37 +++++++++++++ .../domain/update/SelectionChangeCallback.kt | 56 +++++++++++++++---- .../interactor/ChooserRequestUpdateInteractor.kt | 62 +++++----------------- 5 files changed, 109 insertions(+), 68 deletions(-) create mode 100644 java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt index 3ce9aaff..06e28cba 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt @@ -26,6 +26,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.Cus import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.toCustomActionModel +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.onValue import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import javax.inject.Inject @@ -56,9 +57,10 @@ constructor( .mapLatest { record -> selectionCallback.onSelectionChanged(record.intent) } .filterNotNull() .collect { updates -> - val actions = updates.customActions ?: emptyList() - intentRepository.customActions.value = - actions.map { it.toCustomActionModel(pendingIntentSender) } + updates.customActions.onValue { actions -> + intentRepository.customActions.value = + actions.map { it.toCustomActionModel(pendingIntentSender) } + } chooserParamsUpdateRepository.setUpdates(updates) } } diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt index 41a34d1a..821e88a5 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt @@ -24,11 +24,11 @@ import android.service.chooser.ChooserTarget /** Sharing session updates provided by the sharing app from the payload change callback */ data class ShareouselUpdate( // for all properties, null value means no change - val customActions: List? = null, - val modifyShareAction: ChooserAction? = null, - val alternateIntents: List? = null, - val callerTargets: List? = null, - val refinementIntentSender: IntentSender? = null, - val resultIntentSender: IntentSender? = null, - val metadataText: CharSequence? = null, + val customActions: ValueUpdate> = ValueUpdate.Absent, + val modifyShareAction: ValueUpdate = ValueUpdate.Absent, + val alternateIntents: ValueUpdate> = ValueUpdate.Absent, + val callerTargets: ValueUpdate> = ValueUpdate.Absent, + val refinementIntentSender: ValueUpdate = ValueUpdate.Absent, + val resultIntentSender: ValueUpdate = ValueUpdate.Absent, + val metadataText: ValueUpdate = ValueUpdate.Absent, ) diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt new file mode 100644 index 00000000..bad4eebe --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt @@ -0,0 +1,37 @@ +/* + * Copyright 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.contentpreview.payloadtoggle.domain.model + +/** Represents an either updated value or the absence of it */ +sealed interface ValueUpdate { + data class Value(val value: T) : ValueUpdate + data object Absent : ValueUpdate +} + +/** Return encapsulated value if this instance represent Value or `default` if Absent */ +fun ValueUpdate.getOrDefault(default: T): T = + when (this) { + is ValueUpdate.Value -> value + is ValueUpdate.Absent -> default + } + +/** Executes the `block` with encapsulated value if this instance represents Value */ +inline fun ValueUpdate.onValue(block: (T) -> Unit) { + if (this is ValueUpdate.Value) { + block(value) + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt index e7644dc5..20af264a 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt @@ -18,6 +18,8 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.update import android.content.ContentInterface import android.content.Intent +import android.content.Intent.EXTRA_ALTERNATE_INTENTS +import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER @@ -31,6 +33,7 @@ import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTIO import android.service.chooser.ChooserAction import android.service.chooser.ChooserTarget import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate import com.android.intentresolver.inject.AdditionalContent import com.android.intentresolver.inject.ChooserIntent import com.android.intentresolver.inject.ChooserServiceFlags @@ -88,7 +91,10 @@ constructor( } ?.let { bundle -> return when (val result = readCallbackResponse(bundle, flags)) { - is Valid -> result.value + is Valid -> { + result.warnings.forEach { it.log(TAG) } + result.value + } is Invalid -> { result.errors.forEach { it.log(TAG) } null @@ -102,18 +108,40 @@ private fun readCallbackResponse( flags: ChooserServiceFlags ): ValidationResult { return validateFrom(bundle::get) { - val customActions = readChooserActions() - val modifyShareAction = optional(value(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) - val alternateIntents = readAlternateIntents() - val callerTargets = optional(array(EXTRA_CHOOSER_TARGETS)) + // An error is treated as an empty collection or null as the presence of a value indicates + // an intention to change the old value implying that the old value is obsolete (and should + // not be used). + val customActions = + bundle.readValueUpdate(EXTRA_CHOOSER_CUSTOM_ACTIONS) { + readChooserActions() ?: emptyList() + } + val modifyShareAction = + bundle.readValueUpdate(EXTRA_CHOOSER_MODIFY_SHARE_ACTION) { key -> + optional(value(key)) + } + val alternateIntents = + bundle.readValueUpdate(EXTRA_ALTERNATE_INTENTS) { + readAlternateIntents() ?: emptyList() + } + val callerTargets = + bundle.readValueUpdate(EXTRA_CHOOSER_TARGETS) { key -> + optional(array(key)) ?: emptyList() + } val refinementIntentSender = - optional(value(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) - val resultIntentSender = optional(value(EXTRA_CHOOSER_RESULT_INTENT_SENDER)) + bundle.readValueUpdate(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER) { key -> + optional(value(key)) + } + val resultIntentSender = + bundle.readValueUpdate(EXTRA_CHOOSER_RESULT_INTENT_SENDER) { key -> + optional(value(key)) + } val metadataText = if (flags.enableSharesheetMetadataExtra()) { - optional(value(EXTRA_METADATA_TEXT)) + bundle.readValueUpdate(EXTRA_METADATA_TEXT) { key -> + optional(value(key)) + } } else { - null + ValueUpdate.Absent } ShareouselUpdate( @@ -128,6 +156,16 @@ private fun readCallbackResponse( } } +private inline fun Bundle.readValueUpdate( + key: String, + block: (String) -> T +): ValueUpdate = + if (containsKey(key)) { + ValueUpdate.Value(block(key)) + } else { + ValueUpdate.Absent + } + @Module @InstallIn(ViewModelComponent::class) interface SelectionChangeCallbackModule { diff --git a/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt index 4afe46b0..37213403 100644 --- a/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt +++ b/java/src/com/android/intentresolver/v2/domain/interactor/ChooserRequestUpdateInteractor.kt @@ -17,12 +17,10 @@ package com.android.intentresolver.v2.domain.interactor import android.content.Intent -import android.content.IntentSender -import android.service.chooser.ChooserAction -import android.service.chooser.ChooserTarget import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ChooserParamsUpdateRepository import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.TargetIntentRepository import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate +import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault import com.android.intentresolver.v2.ui.model.ChooserRequest import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -63,61 +61,27 @@ constructor( } private fun updateTargetIntent(targetIntent: Intent) { - chooserRequestRepository.update { current -> - current.updatedWith(targetIntent = targetIntent) - } + chooserRequestRepository.update { current -> current.copy(targetIntent = targetIntent) } } private fun updateChooserParameters(update: ShareouselUpdate) { chooserRequestRepository.update { current -> - current.updatedWith( - callerChooserTargets = update.callerTargets, - modifyShareAction = update.modifyShareAction, - additionalTargets = update.alternateIntents, - chosenComponentSender = update.resultIntentSender, - refinementIntentSender = update.refinementIntentSender, - metadataText = update.metadataText, + current.copy( + callerChooserTargets = + update.callerTargets.getOrDefault(current.callerChooserTargets), + modifyShareAction = + update.modifyShareAction.getOrDefault(current.modifyShareAction), + additionalTargets = update.alternateIntents.getOrDefault(current.additionalTargets), + chosenComponentSender = + update.resultIntentSender.getOrDefault(current.chosenComponentSender), + refinementIntentSender = + update.refinementIntentSender.getOrDefault(current.refinementIntentSender), + metadataText = update.metadataText.getOrDefault(current.metadataText), ) } } } -private fun ChooserRequest.updatedWith( - targetIntent: Intent? = null, - callerChooserTargets: List? = null, - modifyShareAction: ChooserAction? = null, - additionalTargets: List? = null, - chosenComponentSender: IntentSender? = null, - refinementIntentSender: IntentSender? = null, - metadataText: CharSequence? = null, -) = - ChooserRequest( - targetIntent ?: this.targetIntent, - this.targetAction, - this.isSendActionTarget, - this.targetType, - this.launchedFromPackage, - this.title, - this.defaultTitleResource, - this.referrer, - this.filteredComponentNames, - callerChooserTargets ?: this.callerChooserTargets, - this.chooserActions, - modifyShareAction ?: this.modifyShareAction, - this.shouldRetainInOnStop, - additionalTargets ?: this.additionalTargets, - this.replacementExtras, - this.initialIntents, - chosenComponentSender ?: this.chosenComponentSender, - refinementIntentSender ?: this.refinementIntentSender, - this.sharedText, - this.shareTargetFilter, - this.additionalContentUri, - this.focusedItemPosition, - this.contentTypeHint, - metadataText ?: this.metadataText, - ) - @AssistedFactory @ViewModelScoped interface ChooserRequestUpdateInteractorFactory { -- cgit v1.2.3-59-g8ed1b