diff options
4 files changed, 334 insertions, 75 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt index 003f6884..6f4f5167 100644 --- a/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/PayloadToggleInteractor.kt @@ -17,8 +17,10 @@ package com.android.intentresolver.contentpreview import android.content.Intent +import android.content.IntentSender import android.net.Uri import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget import android.util.Log import android.util.SparseArray import java.io.Closeable @@ -43,7 +45,7 @@ private const val TAG = "PayloadToggleInteractor" @OptIn(ExperimentalCoroutinesApi::class) class PayloadToggleInteractor( - // must use single-thread dispatcher (or we should enforce it with a lock) + // TODO: a single-thread dispatcher is currently expected. iterate on the synchronization logic. private val scope: CoroutineScope, private val initiallySharedUris: List<Uri>, private val focusedUriIdx: Int, @@ -51,7 +53,7 @@ class PayloadToggleInteractor( private val cursorReaderProvider: suspend () -> CursorReader, private val uriMetadataReader: (Uri) -> FileInfo, private val targetIntentModifier: (List<Item>) -> Intent, - private val selectionCallback: (Intent) -> CallbackResult?, + private val selectionCallback: (Intent) -> ShareouselUpdate?, ) { private var cursorDataRef = CompletableDeferred<CursorData?>() private val records = LinkedList<Record>() @@ -183,7 +185,7 @@ class PayloadToggleInteractor( val (reader, selectionTracker) = waitForCursorData() ?: return if (!reader.hasMoreBefore) return - val newItems = reader.readPageBefore().toRecords() + val newItems = reader.readPageBefore().toItems() selectionTracker.onStartItemsAdded(newItems) for (i in newItems.size() - 1 downTo 0) { records.add( @@ -224,7 +226,7 @@ class PayloadToggleInteractor( val (reader, selectionTracker) = waitForCursorData() ?: return if (!reader.hasMoreAfter) return - val newItems = reader.readPageAfter().toRecords() + val newItems = reader.readPageAfter().toItems() selectionTracker.onEndItemsAdded(newItems) for (i in 0 until newItems.size()) { val key = newItems.keyAt(i) @@ -254,7 +256,7 @@ class PayloadToggleInteractor( } } - private fun SparseArray<Uri>.toRecords(): SparseArray<Item> { + private fun SparseArray<Uri>.toItems(): SparseArray<Item> { val items = SparseArray<Item>(size()) for (i in 0 until size()) { val key = keyAt(i) @@ -335,7 +337,14 @@ class PayloadToggleInteractor( val isSelected = MutableStateFlow(false) } - data class CallbackResult(val customActions: List<ChooserAction>?) + data class ShareouselUpdate( + // for all properties, null value means no change + val customActions: List<ChooserAction>? = null, + val modifyShareAction: ChooserAction? = null, + val alternateIntents: List<Intent>? = null, + val callerTargets: List<ChooserTarget>? = null, + val refinementIntentSender: IntentSender? = null, + ) private data class CursorData( val reader: CursorReader, diff --git a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt index 4e2e37b8..5c916882 100644 --- a/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt +++ b/java/src/com/android/intentresolver/contentpreview/SelectionChangeCallback.kt @@ -18,13 +18,25 @@ package com.android.intentresolver.contentpreview import android.content.ContentInterface import android.content.Intent -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_TARGETS import android.content.Intent.EXTRA_INTENT +import android.content.IntentSender import android.net.Uri import android.os.Bundle import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED import android.service.chooser.ChooserAction -import com.android.intentresolver.contentpreview.PayloadToggleInteractor.CallbackResult +import android.service.chooser.ChooserTarget +import com.android.intentresolver.contentpreview.PayloadToggleInteractor.ShareouselUpdate +import com.android.intentresolver.v2.ui.viewmodel.readAlternateIntents +import com.android.intentresolver.v2.ui.viewmodel.readChooserActions +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.types.array +import com.android.intentresolver.v2.validation.types.value +import com.android.intentresolver.v2.validation.validateFrom + +private const val TAG = "SelectionChangeCallback" /** * Encapsulates payload change callback invocation to the sharing app; handles callback arguments @@ -34,8 +46,8 @@ class SelectionChangeCallback( private val uri: Uri, private val chooserIntent: Intent, private val contentResolver: ContentInterface, -) : (Intent) -> CallbackResult? { - fun onSelectionChanged(targetIntent: Intent): CallbackResult? = +) : (Intent) -> ShareouselUpdate? { + fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? = contentResolver .call( requireNotNull(uri.authority) { "URI authority can not be null" }, @@ -49,20 +61,35 @@ class SelectionChangeCallback( } ) ?.let { bundle -> - val actions = - if (bundle.containsKey(EXTRA_CHOOSER_CUSTOM_ACTIONS)) { - bundle - .getParcelableArray( - EXTRA_CHOOSER_CUSTOM_ACTIONS, - ChooserAction::class.java - ) - ?.filterNotNull() - ?: emptyList() + readCallbackResponse(bundle).let { validation -> + if (validation.isSuccess()) { + validation.value } else { + validation.reportToLogcat(TAG) null } - CallbackResult(actions) + } } override fun invoke(targetIntent: Intent) = onSelectionChanged(targetIntent) + + private fun readCallbackResponse(bundle: Bundle): ValidationResult<ShareouselUpdate> { + return validateFrom(bundle::get) { + val customActions = readChooserActions() + val modifyShareAction = + optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) + val alternateIntents = readAlternateIntents() + val callerTargets = optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) + val refinementIntentSender = + optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) + + ShareouselUpdate( + customActions, + modifyShareAction, + alternateIntents, + callerTargets, + refinementIntentSender, + ) + } + } } diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt index 8fe1dba5..91eed408 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt @@ -28,7 +28,6 @@ import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS import android.content.Intent.EXTRA_INITIAL_INTENTS import android.content.Intent.EXTRA_INTENT import android.content.Intent.EXTRA_METADATA_TEXT -import android.content.Intent.EXTRA_REFERRER import android.content.Intent.EXTRA_REPLACEMENT_EXTRAS import android.content.Intent.EXTRA_TEXT import android.content.Intent.EXTRA_TITLE @@ -49,6 +48,7 @@ import com.android.intentresolver.v2.ext.hasSendAction import com.android.intentresolver.v2.ext.ifMatch import com.android.intentresolver.v2.ui.model.ActivityModel import com.android.intentresolver.v2.ui.model.ChooserRequest +import com.android.intentresolver.v2.validation.Validation import com.android.intentresolver.v2.validation.ValidationResult import com.android.intentresolver.v2.validation.types.IntentOrUri import com.android.intentresolver.v2.validation.types.array @@ -75,9 +75,7 @@ fun readChooserRequest( val isSendAction = targetIntent.hasSendAction() - val additionalTargets = - optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() } - ?: emptyList() + val additionalTargets = readAlternateIntents() ?: emptyList() val replacementExtras = optional(value<Bundle>(EXTRA_REPLACEMENT_EXTRAS)) @@ -119,16 +117,10 @@ fun readChooserRequest( val sharedText = optional(value<CharSequence>(EXTRA_TEXT)) - val chooserActions = - optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS)) - ?.filter { hasValidIcon(it) } - ?.take(MAX_CHOOSER_ACTIONS) - ?: emptyList() + val chooserActions = readChooserActions() ?: emptyList() val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) - val referrerFillIn = Intent().putExtra(EXTRA_REFERRER, launch.referrer) - val additionalContentUri: Uri? val focusedItemPos: Int if (isSendAction && flags.chooserPayloadToggling()) { @@ -188,6 +180,14 @@ fun readChooserRequest( } } +fun Validation.readAlternateIntents(): List<Intent>? = + optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() } + +fun Validation.readChooserActions(): List<ChooserAction>? = + optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS)) + ?.filter { hasValidIcon(it) } + ?.take(MAX_CHOOSER_ACTIONS) + private fun Intent.toShareTargetFilter(): IntentFilter? { return type?.let { IntentFilter().apply { diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/SelectionChangeCallbackTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/SelectionChangeCallbackTest.kt index 110448bb..40f2ab26 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/SelectionChangeCallbackTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/SelectionChangeCallbackTest.kt @@ -17,17 +17,25 @@ package com.android.intentresolver.contentpreview import android.app.PendingIntent +import android.content.ComponentName import android.content.ContentInterface import android.content.Intent import android.content.Intent.ACTION_CHOOSER +import android.content.Intent.ACTION_SEND import android.content.Intent.ACTION_SEND_MULTIPLE +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_TARGETS import android.content.Intent.EXTRA_INTENT import android.content.Intent.EXTRA_STREAM import android.graphics.drawable.Icon import android.net.Uri import android.os.Bundle +import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED import android.service.chooser.ChooserAction +import android.service.chooser.ChooserTarget import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.android.intentresolver.any @@ -35,15 +43,15 @@ import com.android.intentresolver.argumentCaptor import com.android.intentresolver.capture import com.android.intentresolver.mock import com.android.intentresolver.whenever +import com.google.common.truth.Correspondence +import com.google.common.truth.Correspondence.BinaryPredicate import com.google.common.truth.Truth.assertThat +import com.google.common.truth.Truth.assertWithMessage import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.times import org.mockito.Mockito.verify -// TODO: replace with the new API AdditionalContentContract$MethodNames#ON_SELECTION_CHANGED -private const val MethodName = "onSelectionChanged" - @RunWith(AndroidJUnit4::class) class SelectionChangeCallbackTest { private val uri = Uri.parse("content://org.pkg/content-provider") @@ -52,7 +60,74 @@ class SelectionChangeCallbackTest { private val context = InstrumentationRegistry.getInstrumentation().context @Test - fun testCallbackProducesChooserIntentArgument() { + fun testPayloadChangeCallbackContact() { + val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) + + val u1 = createUri(1) + val u2 = createUri(2) + val targetIntent = + Intent(ACTION_SEND_MULTIPLE).apply { + val uris = + ArrayList<Uri>().apply { + add(u1) + add(u2) + } + putExtra(EXTRA_STREAM, uris) + type = "image/jpg" + } + testSubject.onSelectionChanged(targetIntent) + + val authorityCaptor = argumentCaptor<String>() + val methodCaptor = argumentCaptor<String>() + val argCaptor = argumentCaptor<String>() + val extraCaptor = argumentCaptor<Bundle>() + verify(contentResolver, times(1)) + .call( + capture(authorityCaptor), + capture(methodCaptor), + capture(argCaptor), + capture(extraCaptor) + ) + assertWithMessage("Wrong additional content provider authority") + .that(authorityCaptor.value) + .isEqualTo(uri.authority) + assertWithMessage("Wrong additional content provider #call() method name") + .that(methodCaptor.value) + .isEqualTo(ON_SELECTION_CHANGED) + assertWithMessage("Wrong additional content provider argument value") + .that(argCaptor.value) + .isEqualTo(uri.toString()) + val extraBundle = extraCaptor.value + assertWithMessage("Additional content provider #call() should have a non-null extras arg.") + .that(extraBundle) + .isNotNull() + requireNotNull(extraBundle) + val argChooserIntent = extraBundle.getParcelable(EXTRA_INTENT, Intent::class.java) + assertWithMessage("#call() extras arg. should contain Intent#EXTRA_INTENT") + .that(argChooserIntent) + .isNotNull() + requireNotNull(argChooserIntent) + assertWithMessage("#call() extras arg's Intent#EXTRA_INTENT should be a Chooser intent") + .that(argChooserIntent.action) + .isEqualTo(chooserIntent.action) + val argTargetIntent = argChooserIntent.getParcelableExtra(EXTRA_INTENT, Intent::class.java) + assertWithMessage( + "A chooser intent passed into #call() method should contain updated target intent" + ) + .that(argTargetIntent) + .isNotNull() + requireNotNull(argTargetIntent) + assertWithMessage("Incorrect target intent") + .that(argTargetIntent.action) + .isEqualTo(targetIntent.action) + assertWithMessage("Incorrect target intent") + .that(argTargetIntent.getParcelableArrayListExtra(EXTRA_STREAM, Uri::class.java)) + .containsExactly(u1, u2) + .inOrder() + } + + @Test + fun testPayloadChangeCallbackUpdatesCustomActions() { val a1 = ChooserAction.Builder( Icon.createWithContentUri(createUri(10)), @@ -84,50 +159,198 @@ class SelectionChangeCallbackTest { val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) - val u1 = createUri(1) - val u2 = createUri(2) - val targetIntent = - Intent(ACTION_SEND_MULTIPLE).apply { - val uris = - ArrayList<Uri>().apply { - add(u1) - add(u2) - } - putExtra(EXTRA_STREAM, uris) - type = "image/jpg" - } + val targetIntent = Intent(ACTION_SEND_MULTIPLE) val result = testSubject.onSelectionChanged(targetIntent) - assertThat(result).isNotNull() - assertThat(result?.customActions).hasSize(2) - assertThat(result?.customActions?.get(0)?.icon).isEqualTo(a1.icon) - assertThat(result?.customActions?.get(0)?.label).isEqualTo(a1.label) - assertThat(result?.customActions?.get(1)?.icon).isEqualTo(a2.icon) - assertThat(result?.customActions?.get(1)?.label).isEqualTo(a2.label) + assertWithMessage("Callback result should not be null").that(result).isNotNull() + requireNotNull(result) + assertWithMessage("Unexpected custom actions") + .that(result.customActions?.map { it.icon to it.label }) + .containsExactly(a1.icon to a1.label, a2.icon to a2.label) + .inOrder() - val authorityCaptor = argumentCaptor<String>() - val methodCaptor = argumentCaptor<String>() - val argCaptor = argumentCaptor<String>() - val extraCaptor = argumentCaptor<Bundle>() - verify(contentResolver, times(1)) - .call( - capture(authorityCaptor), - capture(methodCaptor), - capture(argCaptor), - capture(extraCaptor) + assertThat(result.modifyShareAction).isNull() + assertThat(result.alternateIntents).isNull() + assertThat(result.callerTargets).isNull() + assertThat(result.refinementIntentSender).isNull() + } + + @Test + fun testPayloadChangeCallbackUpdatesReselectionAction() { + val modifyShare = + ChooserAction.Builder( + Icon.createWithContentUri(createUri(10)), + "Modify Share", + PendingIntent.getBroadcast( + context, + 1, + Intent("test"), + PendingIntent.FLAG_IMMUTABLE + ) + ) + .build() + whenever(contentResolver.call(any<String>(), any(), any(), any())) + .thenReturn( + Bundle().apply { putParcelable(EXTRA_CHOOSER_MODIFY_SHARE_ACTION, modifyShare) } ) - assertThat(authorityCaptor.value).isEqualTo(uri.authority) - assertThat(methodCaptor.value).isEqualTo(MethodName) - assertThat(argCaptor.value).isEqualTo(uri.toString()) - val extraBundle = extraCaptor.value - assertThat(extraBundle).isNotNull() - val argChooserIntent = extraBundle.getParcelable(EXTRA_INTENT, Intent::class.java) - assertThat(argChooserIntent).isNotNull() - assertThat(argChooserIntent?.action).isEqualTo(chooserIntent.action) - val argTargetIntent = argChooserIntent?.getParcelableExtra(EXTRA_INTENT, Intent::class.java) - assertThat(argTargetIntent?.action).isEqualTo(targetIntent.action) - assertThat(argTargetIntent?.getParcelableArrayListExtra(EXTRA_STREAM, Uri::class.java)) - .containsExactly(u1, u2) + + val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) + + val targetIntent = Intent(ACTION_SEND) + val result = testSubject.onSelectionChanged(targetIntent) + assertWithMessage("Callback result should not be null").that(result).isNotNull() + requireNotNull(result) + assertWithMessage("Unexpected modify share action: wrong icon") + .that(result.modifyShareAction?.icon) + .isEqualTo(modifyShare.icon) + assertWithMessage("Unexpected modify share action: wrong label") + .that(result.modifyShareAction?.label) + .isEqualTo(modifyShare.label) + + assertThat(result.customActions).isNull() + assertThat(result.alternateIntents).isNull() + assertThat(result.callerTargets).isNull() + assertThat(result.refinementIntentSender).isNull() + } + + @Test + fun testPayloadChangeCallbackUpdatesAlternateIntents() { + val alternateIntents = + arrayOf( + Intent(ACTION_SEND_MULTIPLE).apply { + addCategory("test") + type = "" + } + ) + whenever(contentResolver.call(any<String>(), any(), any(), any())) + .thenReturn( + Bundle().apply { putParcelableArray(EXTRA_ALTERNATE_INTENTS, alternateIntents) } + ) + + val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) + + val targetIntent = Intent(ACTION_SEND) + val result = testSubject.onSelectionChanged(targetIntent) + assertWithMessage("Callback result should not be null").that(result).isNotNull() + requireNotNull(result) + assertWithMessage("Wrong number of alternate intents") + .that(result.alternateIntents) + .hasSize(1) + assertWithMessage("Wrong alternate intent: action") + .that(result.alternateIntents?.get(0)?.action) + .isEqualTo(alternateIntents[0].action) + assertWithMessage("Wrong alternate intent: categories") + .that(result.alternateIntents?.get(0)?.categories) + .containsExactlyElementsIn(alternateIntents[0].categories) + assertWithMessage("Wrong alternate intent: mime type") + .that(result.alternateIntents?.get(0)?.type) + .isEqualTo(alternateIntents[0].type) + + assertThat(result.customActions).isNull() + assertThat(result.modifyShareAction).isNull() + assertThat(result.callerTargets).isNull() + assertThat(result.refinementIntentSender).isNull() + } + + @Test + fun testPayloadChangeCallbackUpdatesCallerTargets() { + val t1 = + ChooserTarget( + "Target 1", + Icon.createWithContentUri(createUri(1)), + 0.99f, + ComponentName("org.pkg.app", ".ClassA"), + null + ) + val t2 = + ChooserTarget( + "Target 2", + Icon.createWithContentUri(createUri(1)), + 1f, + ComponentName("org.pkg.app", ".ClassB"), + null + ) + whenever(contentResolver.call(any<String>(), any(), any(), any())) + .thenReturn( + Bundle().apply { putParcelableArray(EXTRA_CHOOSER_TARGETS, arrayOf(t1, t2)) } + ) + + val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) + + val targetIntent = Intent(ACTION_SEND) + val result = testSubject.onSelectionChanged(targetIntent) + assertWithMessage("Callback result should not be null").that(result).isNotNull() + requireNotNull(result) + assertWithMessage("Wrong caller targets") + .that(result.callerTargets) + .comparingElementsUsing( + Correspondence.from( + BinaryPredicate<ChooserTarget?, ChooserTarget> { actual, expected -> + expected.componentName == actual?.componentName && + expected.title == actual?.title && + expected.icon == actual?.icon && + expected.score == actual?.score + }, + "" + ) + ) + .containsExactly(t1, t2) .inOrder() + + assertThat(result.customActions).isNull() + assertThat(result.modifyShareAction).isNull() + assertThat(result.alternateIntents).isNull() + assertThat(result.refinementIntentSender).isNull() + } + + @Test + fun testPayloadChangeCallbackUpdatesRefinementIntentSender() { + val broadcast = + PendingIntent.getBroadcast(context, 1, Intent("test"), PendingIntent.FLAG_IMMUTABLE) + + whenever(contentResolver.call(any<String>(), any(), any(), any())) + .thenReturn( + Bundle().apply { + putParcelable(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, broadcast.intentSender) + } + ) + + val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) + + val targetIntent = Intent(ACTION_SEND) + val result = testSubject.onSelectionChanged(targetIntent) + assertWithMessage("Callback result should not be null").that(result).isNotNull() + requireNotNull(result) + assertThat(result.customActions).isNull() + assertThat(result.modifyShareAction).isNull() + assertThat(result.alternateIntents).isNull() + assertThat(result.callerTargets).isNull() + assertThat(result.refinementIntentSender).isNotNull() + } + + @Test + fun testPayloadChangeCallbackProvidesInvalidData_invalidDataIgnored() { + whenever(contentResolver.call(any<String>(), any(), any(), any())) + .thenReturn( + Bundle().apply { + putParcelableArrayList(EXTRA_CHOOSER_CUSTOM_ACTIONS, ArrayList<ChooserAction>()) + putParcelable(EXTRA_CHOOSER_MODIFY_SHARE_ACTION, createUri(1)) + putParcelableArrayList(EXTRA_ALTERNATE_INTENTS, ArrayList<Intent>()) + putParcelableArrayList(EXTRA_CHOOSER_TARGETS, ArrayList<ChooserTarget>()) + putParcelable(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, createUri(2)) + } + ) + + val testSubject = SelectionChangeCallback(uri, chooserIntent, contentResolver) + + val targetIntent = Intent(ACTION_SEND) + val result = testSubject.onSelectionChanged(targetIntent) + assertWithMessage("Callback result should not be null").that(result).isNotNull() + requireNotNull(result) + assertThat(result.customActions).isNull() + assertThat(result.modifyShareAction).isNull() + assertThat(result.alternateIntents).isNull() + assertThat(result.callerTargets).isNull() + assertThat(result.refinementIntentSender).isNull() } } |