1 /*
<lambda>null2  * Copyright 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 androidx.compose.foundation.content
18 
19 import android.content.ClipData
20 import android.content.ClipDescription
21 import android.content.Intent
22 import android.net.Uri
23 import android.view.View
24 import androidx.compose.foundation.ExperimentalFoundationApi
25 import androidx.compose.foundation.TestActivity
26 import androidx.compose.foundation.content.internal.ReceiveContentConfiguration
27 import androidx.compose.foundation.content.internal.getReceiveContentConfiguration
28 import androidx.compose.foundation.layout.Box
29 import androidx.compose.foundation.layout.size
30 import androidx.compose.runtime.getValue
31 import androidx.compose.runtime.mutableStateOf
32 import androidx.compose.runtime.setValue
33 import androidx.compose.ui.Alignment
34 import androidx.compose.ui.ExperimentalComposeUiApi
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.geometry.Offset
37 import androidx.compose.ui.modifier.ModifierLocalModifierNode
38 import androidx.compose.ui.node.ModifierNodeElement
39 import androidx.compose.ui.platform.LocalView
40 import androidx.compose.ui.platform.firstUriOrNull
41 import androidx.compose.ui.test.junit4.createAndroidComposeRule
42 import androidx.compose.ui.unit.dp
43 import androidx.test.ext.junit.runners.AndroidJUnit4
44 import androidx.test.filters.MediumTest
45 import androidx.test.filters.SdkSuppress
46 import com.google.common.truth.Truth.assertThat
47 import org.junit.Rule
48 import org.junit.Test
49 import org.junit.runner.RunWith
50 
51 @MediumTest
52 @RunWith(AndroidJUnit4::class)
53 @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
54 class ReceiveContentTest {
55 
56     @get:Rule val rule = createAndroidComposeRule<TestActivity>()
57 
58     @Test
59     fun receiveContentConfiguration_isMergedBottomToTop() {
60         var calculatedReceiveContent: ReceiveContentConfiguration?
61         val listenerCalls = mutableListOf<Int>()
62         rule.setContent {
63             Box(
64                 modifier =
65                     Modifier.contentReceiver {
66                             listenerCalls += 3
67                             it
68                         }
69                         .contentReceiver {
70                             listenerCalls += 2
71                             it
72                         }
73                         .contentReceiver {
74                             listenerCalls += 1
75                             it
76                         }
77                         .then(
78                             TestElement {
79                                 calculatedReceiveContent = it.getReceiveContentConfiguration()
80                                 calculatedReceiveContent
81                                     ?.receiveContentListener
82                                     ?.onReceive(TransferableContent(createClipData()))
83                             }
84                         )
85             )
86         }
87 
88         rule.runOnIdle { assertThat(listenerCalls).isEqualTo(listOf(1, 2, 3)) }
89     }
90 
91     @Test
92     fun onReceiveCallbacks_passTheReturnedValue_toParentNode() {
93         var videoReceived: TransferableContent? = null
94         var audioReceived: TransferableContent? = null
95         var textReceived: TransferableContent? = null
96         rule.setContent {
97             Box(
98                 modifier =
99                     Modifier.contentReceiver { transferable ->
100                             videoReceived = transferable
101                             transferable.consume { it.uri?.toString()?.contains("video") ?: false }
102                         }
103                         .contentReceiver { transferable ->
104                             audioReceived = transferable
105                             transferable.consume { it.uri?.toString()?.contains("audio") ?: false }
106                         }
107                         .contentReceiver { transferable ->
108                             textReceived = transferable
109                             transferable.consume { it.text != null }
110                         }
111                         .then(
112                             TestElement {
113                                 it.getReceiveContentConfiguration()
114                                     ?.receiveContentListener
115                                     ?.onReceive(
116                                         TransferableContent(
117                                             createClipData {
118                                                 addText()
119                                                 addUri(Uri.parse("content://video"), "video/mp4")
120                                                 addUri(Uri.parse("content://audio"), "audio/ogg")
121                                             }
122                                         )
123                                     )
124                             }
125                         )
126             )
127         }
128 
129         rule.runOnIdle {
130             assertClipData(videoReceived!!.clipEntry.clipData)
131                 .isEqualToClipData(
132                     createClipData { addUri(Uri.parse("content://video"), "video/mp4") },
133                     ignoreClipDescription = true
134                 )
135             assertClipData(audioReceived!!.clipEntry.clipData)
136                 .isEqualToClipData(
137                     createClipData {
138                         addUri(Uri.parse("content://video"), "video/mp4")
139                         addUri(Uri.parse("content://audio"), "audio/ogg")
140                     },
141                     ignoreClipDescription = true
142                 )
143             assertClipData(textReceived!!.clipEntry.clipData)
144                 .isEqualToClipData(
145                     createClipData {
146                         addText()
147                         addUri(Uri.parse("content://video"), "video/mp4")
148                         addUri(Uri.parse("content://audio"), "audio/ogg")
149                     },
150                     ignoreClipDescription = true
151                 )
152         }
153     }
154 
155     @Test
156     fun receiveContentConfiguration_returnsNullIfNotDefined() {
157         var calculatedReceiveContent: ReceiveContentConfiguration? =
158             ReceiveContentConfiguration(ReceiveContentListener { null })
159         rule.setContent {
160             Box(
161                 modifier =
162                     Modifier.then(
163                         TestElement {
164                             calculatedReceiveContent = it.getReceiveContentConfiguration()
165                         }
166                     )
167             )
168         }
169 
170         rule.runOnIdle { assertThat(calculatedReceiveContent).isNull() }
171     }
172 
173     @Test
174     fun receiveContentConfiguration_returnsNullIfDefined_atSiblingNode() {
175         var calculatedReceiveContent: ReceiveContentConfiguration? =
176             ReceiveContentConfiguration(ReceiveContentListener { null })
177         rule.setContent {
178             Box {
179                 Box(
180                     modifier =
181                         Modifier.then(
182                             TestElement {
183                                 calculatedReceiveContent = it.getReceiveContentConfiguration()
184                             }
185                         )
186                 )
187                 Box(modifier = Modifier.contentReceiver { it })
188             }
189         }
190 
191         rule.runOnIdle { assertThat(calculatedReceiveContent).isNull() }
192     }
193 
194     @Test
195     fun receiveContentConfiguration_returnsNullIfDefined_atChildNode() {
196         var calculatedReceiveContent: ReceiveContentConfiguration? =
197             ReceiveContentConfiguration(ReceiveContentListener { null })
198         rule.setContent {
199             Box(
200                 modifier =
201                     Modifier.then(
202                         TestElement {
203                             calculatedReceiveContent = it.getReceiveContentConfiguration()
204                         }
205                     )
206             ) {
207                 Box(modifier = Modifier.contentReceiver { it })
208             }
209         }
210 
211         rule.runOnIdle { assertThat(calculatedReceiveContent).isNull() }
212     }
213 
214     @Test
215     fun detachedReceiveContent_disappearsFromMergedConfiguration() {
216         var getReceiveContentConfiguration: (() -> ReceiveContentConfiguration?)? = null
217         var attached by mutableStateOf(true)
218         val called = mutableListOf<Int>()
219         rule.setContent {
220             Box(
221                 modifier =
222                     Modifier.contentReceiver {
223                             called += 1
224                             it
225                         }
226                         .then(
227                             if (attached) {
228                                 Modifier.contentReceiver {
229                                     called += 2
230                                     it
231                                 }
232                             } else {
233                                 Modifier
234                             }
235                         )
236                         .contentReceiver {
237                             called += 3
238                             it
239                         }
240                         .then(
241                             TestElement {
242                                 getReceiveContentConfiguration = {
243                                     it.getReceiveContentConfiguration()
244                                 }
245                             }
246                         )
247             )
248         }
249 
250         rule.runOnIdle {
251             val receiveContentConfiguration = getReceiveContentConfiguration?.invoke()
252             assertThat(receiveContentConfiguration).isNotNull()
253             receiveContentConfiguration!!
254                 .receiveContentListener
255                 .onReceive(TransferableContent(createClipData()))
256             assertThat(called).isEqualTo(listOf(3, 2, 1))
257         }
258 
259         called.clear()
260         attached = false
261 
262         rule.runOnIdle {
263             val receiveContentConfiguration = getReceiveContentConfiguration?.invoke()
264             assertThat(receiveContentConfiguration).isNotNull()
265             receiveContentConfiguration!!
266                 .receiveContentListener
267                 .onReceive(TransferableContent(createClipData()))
268             assertThat(called).isEqualTo(listOf(3, 1))
269         }
270     }
271 
272     @Test
273     fun laterAttachedReceiveContent_appearsInMergedConfiguration() {
274         var getReceiveContentConfiguration: (() -> ReceiveContentConfiguration?)? = null
275         var attached by mutableStateOf(false)
276         val called = mutableListOf<Int>()
277 
278         rule.setContent {
279             Box(
280                 modifier =
281                     Modifier.contentReceiver {
282                             called += 1
283                             it
284                         }
285                         .then(
286                             if (attached) {
287                                 Modifier.contentReceiver {
288                                     called += 2
289                                     it
290                                 }
291                             } else {
292                                 Modifier
293                             }
294                         )
295                         .contentReceiver {
296                             called += 3
297                             it
298                         }
299                         .then(
300                             TestElement {
301                                 getReceiveContentConfiguration = {
302                                     it.getReceiveContentConfiguration()
303                                 }
304                             }
305                         )
306             )
307         }
308 
309         rule.runOnIdle {
310             val receiveContentConfiguration = getReceiveContentConfiguration?.invoke()
311             assertThat(receiveContentConfiguration).isNotNull()
312             receiveContentConfiguration!!
313                 .receiveContentListener
314                 .onReceive(TransferableContent(createClipData()))
315             assertThat(called).isEqualTo(listOf(3, 1))
316         }
317 
318         called.clear()
319         attached = true
320 
321         rule.runOnIdle {
322             val receiveContentConfiguration = getReceiveContentConfiguration?.invoke()
323             assertThat(receiveContentConfiguration).isNotNull()
324             receiveContentConfiguration!!
325                 .receiveContentListener
326                 .onReceive(TransferableContent(createClipData()))
327             assertThat(called).isEqualTo(listOf(3, 2, 1))
328         }
329     }
330 
331     @SdkSuppress(minSdkVersion = 24)
332     @Test
333     fun dragAndDrop_dropImplicitlyRequestsPermissions_once() {
334         lateinit var view: View
335         rule.setContent {
336             view = LocalView.current
337             Box(
338                 modifier =
339                     Modifier.size(200.dp)
340                         .contentReceiver { it }
341                         .size(100.dp)
342                         .contentReceiver { it }
343                         .size(50.dp)
344                         .contentReceiver { it }
345             )
346         }
347 
348         val draggingUri = Uri.parse("content://com.example/content.jpg")
349         testDragAndDrop(view, rule.density) {
350             drag(Offset(25.dp.toPx(), 25.dp.toPx()), draggingUri)
351             drop()
352         }
353 
354         rule.runOnIdle {
355             val requests = rule.activity.requestedDragAndDropPermissions
356             assertThat(requests.size).isEqualTo(1)
357             assertThat(requests.first().clipData.getItemAt(0).uri).isEqualTo(draggingUri)
358         }
359     }
360 
361     @Test
362     fun dragAndDropOnSingleNodeTriggersOnReceive() {
363         lateinit var view: View
364         var transferableContent: TransferableContent? = null
365         rule.setContent {
366             view = LocalView.current
367             Box(
368                 modifier =
369                     Modifier.size(100.dp).contentReceiver {
370                         transferableContent = it
371                         null // consume all
372                     }
373             )
374         }
375 
376         val draggingUri = Uri.parse("content://com.example/content.jpg")
377         testDragAndDrop(view, rule.density) {
378             drag(Offset(50.dp.toPx(), 50.dp.toPx()), draggingUri)
379             drop()
380         }
381 
382         rule.runOnIdle {
383             assertThat(transferableContent).isNotNull()
384             assertThat(transferableContent?.clipEntry?.firstUriOrNull()).isEqualTo(draggingUri)
385             assertThat(transferableContent?.source)
386                 .isEqualTo(TransferableContent.Source.DragAndDrop)
387         }
388     }
389 
390     @Test
391     fun dragAndDropOnSingleNode_withNotIncludedHintMediaType_triggersOnReceive() {
392         lateinit var view: View
393         var transferableContent: TransferableContent? = null
394         rule.setContent {
395             view = LocalView.current
396             Box(
397                 modifier =
398                     Modifier.size(100.dp).contentReceiver {
399                         transferableContent = it
400                         null // consume all
401                     }
402             )
403         }
404 
405         val draggingUri = Uri.parse("content://com.example/content.jpg")
406         testDragAndDrop(view, rule.density) {
407             drag(Offset(50.dp.toPx(), 50.dp.toPx()), draggingUri)
408             drop()
409         }
410 
411         rule.runOnIdle {
412             assertThat(transferableContent).isNotNull()
413             assertThat(transferableContent?.clipEntry?.firstUriOrNull()).isEqualTo(draggingUri)
414             assertThat(transferableContent?.source)
415                 .isEqualTo(TransferableContent.Source.DragAndDrop)
416         }
417     }
418 
419     @Test
420     fun dragAndDropOnNestedNode_triggersOnReceive_onAllNodes() {
421         lateinit var view: View
422         var childTransferableContent: TransferableContent? = null
423         var parentTransferableContent: TransferableContent? = null
424         rule.setContent {
425             view = LocalView.current
426             Box(
427                 modifier =
428                     Modifier.size(200.dp).contentReceiver {
429                         parentTransferableContent = it
430                         null
431                     }
432             ) {
433                 Box(
434                     modifier =
435                         Modifier.align(Alignment.Center).size(100.dp).contentReceiver {
436                             childTransferableContent = it
437                             it // don't consume anything
438                         }
439                 )
440             }
441         }
442 
443         val draggingUri = Uri.parse("content://com.example/content.jpg")
444         testDragAndDrop(view, rule.density) {
445             drag(Offset(100.dp.toPx(), 100.dp.toPx()), draggingUri)
446             drop()
447         }
448 
449         rule.runOnIdle {
450             assertThat(parentTransferableContent).isNotNull()
451             assertThat(parentTransferableContent?.clipEntry?.firstUriOrNull())
452                 .isEqualTo(draggingUri)
453             assertThat(parentTransferableContent?.source)
454                 .isEqualTo(TransferableContent.Source.DragAndDrop)
455 
456             assertThat(childTransferableContent).isNotNull()
457             assertThat(childTransferableContent?.clipEntry?.firstUriOrNull()).isEqualTo(draggingUri)
458             assertThat(childTransferableContent?.source)
459                 .isEqualTo(TransferableContent.Source.DragAndDrop)
460         }
461     }
462 
463     @Test
464     fun dragAndDropOnNestedNode_triggersOnReceive_onHoveringNodes() {
465         lateinit var view: View
466         var childTransferableContent: TransferableContent? = null
467         var parentTransferableContent: TransferableContent? = null
468         var grandParentTransferableContent: TransferableContent? = null
469         rule.setContent {
470             view = LocalView.current
471             Box(
472                 modifier =
473                     Modifier.size(200.dp).contentReceiver {
474                         grandParentTransferableContent = it
475                         null
476                     }
477             ) {
478                 Box(
479                     modifier =
480                         Modifier.align(Alignment.Center).size(100.dp).contentReceiver {
481                             parentTransferableContent = it
482                             it // don't consume anything
483                         }
484                 ) {
485                     Box(
486                         modifier =
487                             Modifier.align(Alignment.Center).size(50.dp).contentReceiver {
488                                 childTransferableContent = it
489                                 it // don't consume anything
490                             }
491                     )
492                 }
493             }
494         }
495 
496         val draggingUri = Uri.parse("content://com.example/content.jpg")
497         testDragAndDrop(view, rule.density) {
498             drag(Offset(60.dp.toPx(), 60.dp.toPx()), draggingUri)
499             drop()
500         }
501 
502         rule.runOnIdle {
503             assertThat(grandParentTransferableContent).isNotNull()
504             assertThat(parentTransferableContent).isNotNull()
505             assertThat(childTransferableContent).isNull() // child was not in hover region
506         }
507     }
508 
509     @Test
510     fun dragAndDrop_enterExitCallbacks_singleNode() {
511         lateinit var view: View
512         val calls = mutableListOf<String>()
513         rule.setContent {
514             view = LocalView.current
515             Box(
516                 modifier =
517                     Modifier.size(100.dp)
518                         .contentReceiver(
519                             object : ReceiveContentListener {
520                                 override fun onDragEnter() {
521                                     calls += "enter"
522                                 }
523 
524                                 override fun onDragExit() {
525                                     calls += "exit"
526                                 }
527 
528                                 override fun onReceive(
529                                     transferableContent: TransferableContent
530                                 ): TransferableContent? {
531                                     calls += "receive"
532                                     return null
533                                 }
534                             }
535                         )
536             )
537         }
538 
539         val draggingUri = Uri.parse("content://com.example/content.jpg")
540         testDragAndDrop(view, rule.density) {
541             drag(Offset(125.dp.toPx(), 125.dp.toPx()), draggingUri)
542             // enter
543             drag(Offset(90.dp.toPx(), 90.dp.toPx()), draggingUri)
544             // moves
545             drag(Offset(50.dp.toPx(), 50.dp.toPx()), draggingUri)
546             // exits
547             drag(Offset(101.dp.toPx(), 50.dp.toPx()), draggingUri)
548             // enters again
549             drag(Offset(99.dp.toPx(), 50.dp.toPx()), draggingUri)
550             drop()
551         }
552 
553         rule.runOnIdle { assertThat(calls).isEqualTo(listOf("enter", "exit", "enter", "receive")) }
554     }
555 
556     @Test
557     fun dragAndDrop_enterExitCallbacks_nestedNodes() {
558         lateinit var view: View
559         val calls = mutableListOf<String>()
560         rule.setContent {
561             view = LocalView.current
562             Box(
563                 modifier =
564                     Modifier.size(200.dp)
565                         .contentReceiver(
566                             object : ReceiveContentListener {
567                                 override fun onDragEnter() {
568                                     calls += "enter-1"
569                                 }
570 
571                                 override fun onDragExit() {
572                                     calls += "exit-1"
573                                 }
574 
575                                 override fun onReceive(
576                                     transferableContent: TransferableContent
577                                 ): TransferableContent = transferableContent
578                             }
579                         )
580             ) {
581                 Box(
582                     modifier =
583                         Modifier.align(Alignment.Center)
584                             .size(100.dp)
585                             .contentReceiver(
586                                 object : ReceiveContentListener {
587                                     override fun onDragEnter() {
588                                         calls += "enter-2"
589                                     }
590 
591                                     override fun onDragExit() {
592                                         calls += "exit-2"
593                                     }
594 
595                                     override fun onReceive(
596                                         transferableContent: TransferableContent
597                                     ): TransferableContent = transferableContent
598                                 }
599                             )
600                 ) {
601                     Box(
602                         modifier =
603                             Modifier.align(Alignment.Center)
604                                 .size(50.dp)
605                                 .contentReceiver(
606                                     object : ReceiveContentListener {
607                                         override fun onDragEnter() {
608                                             calls += "enter-3"
609                                         }
610 
611                                         override fun onDragExit() {
612                                             calls += "exit-3"
613                                         }
614 
615                                         override fun onReceive(
616                                             transferableContent: TransferableContent
617                                         ): TransferableContent = transferableContent
618                                     }
619                                 )
620                     )
621                 }
622             }
623         }
624 
625         val draggingUri = Uri.parse("content://com.example/content.jpg")
626         testDragAndDrop(view, rule.density) {
627             drag(Offset(225.dp.toPx(), 225.dp.toPx()), draggingUri)
628             // enter 1 and 2, skip 3
629             drag(Offset(60.dp.toPx(), 60.dp.toPx()), draggingUri)
630             // exits 2, stays in 1
631             drag(Offset(40.dp.toPx(), 40.dp.toPx()), draggingUri)
632             // enters 2 and 3
633             drag(Offset(100.dp.toPx(), 100.dp.toPx()), draggingUri)
634             // exits all of them at once
635             drag(Offset(201.dp.toPx(), 201.dp.toPx()), draggingUri)
636         }
637 
638         rule.runOnIdle {
639             assertThat(calls)
640                 .isEqualTo(
641                     listOf(
642                         "enter-1",
643                         "enter-2",
644                         "exit-2",
645                         "enter-2",
646                         "enter-3",
647                         "exit-1",
648                         "exit-2",
649                         "exit-3"
650                     )
651                 )
652         }
653     }
654 
655     @Test
656     fun dragAndDrop_startEndCallbacks_singleNode() {
657         lateinit var view: View
658         val calls = mutableListOf<String>()
659         rule.setContent {
660             view = LocalView.current
661             Box(
662                 modifier =
663                     Modifier.size(100.dp)
664                         .contentReceiver(
665                             object : ReceiveContentListener {
666                                 override fun onDragStart() {
667                                     calls += "start"
668                                 }
669 
670                                 override fun onDragEnd() {
671                                     calls += "end"
672                                 }
673 
674                                 override fun onReceive(
675                                     transferableContent: TransferableContent
676                                 ): TransferableContent? = null
677                             }
678                         )
679             )
680         }
681 
682         val draggingUri = Uri.parse("content://com.example/content.jpg")
683         testDragAndDrop(view, rule.density) {
684             drag(Offset(125.dp.toPx(), 125.dp.toPx()), draggingUri)
685             cancelDrag()
686         }
687 
688         rule.runOnIdle { assertThat(calls).isEqualTo(listOf("start", "end")) }
689 
690         calls.clear()
691 
692         testDragAndDrop(view, rule.density) {
693             drag(Offset(50.dp.toPx(), 50.dp.toPx()), draggingUri)
694             cancelDrag()
695         }
696 
697         rule.runOnIdle { assertThat(calls).isEqualTo(listOf("start", "end")) }
698     }
699 
700     @Test
701     fun dragAndDrop_startEndCallbacks_nestedNodes() {
702         lateinit var view: View
703         val calls = mutableListOf<String>()
704         rule.setContent {
705             view = LocalView.current
706             Box(
707                 modifier =
708                     Modifier.size(200.dp)
709                         .contentReceiver(
710                             object : ReceiveContentListener {
711                                 override fun onDragStart() {
712                                     calls += "start-1"
713                                 }
714 
715                                 override fun onDragEnd() {
716                                     calls += "end-1"
717                                 }
718 
719                                 override fun onReceive(
720                                     transferableContent: TransferableContent
721                                 ): TransferableContent = transferableContent
722                             }
723                         )
724             ) {
725                 Box(
726                     modifier =
727                         Modifier.align(Alignment.Center)
728                             .size(100.dp)
729                             .contentReceiver(
730                                 object : ReceiveContentListener {
731                                     override fun onDragStart() {
732                                         calls += "start-2"
733                                     }
734 
735                                     override fun onDragEnd() {
736                                         calls += "end-2"
737                                     }
738 
739                                     override fun onReceive(
740                                         transferableContent: TransferableContent
741                                     ): TransferableContent = transferableContent
742                                 }
743                             )
744                 ) {
745                     Box(
746                         modifier =
747                             Modifier.align(Alignment.Center)
748                                 .size(50.dp)
749                                 .contentReceiver(
750                                     object : ReceiveContentListener {
751                                         override fun onDragStart() {
752                                             calls += "start-3"
753                                         }
754 
755                                         override fun onDragEnd() {
756                                             calls += "end-3"
757                                         }
758 
759                                         override fun onReceive(
760                                             transferableContent: TransferableContent
761                                         ): TransferableContent = transferableContent
762                                     }
763                                 )
764                     )
765                 }
766             }
767         }
768 
769         val draggingUri = Uri.parse("content://com.example/content.jpg")
770         testDragAndDrop(view, rule.density) {
771             drag(Offset(225.dp.toPx(), 225.dp.toPx()), draggingUri)
772             cancelDrag()
773         }
774 
775         rule.runOnIdle {
776             assertThat(calls.take(3))
777                 .containsExactlyElementsIn(listOf("start-1", "start-2", "start-3"))
778             assertThat(calls.drop(3)).containsExactlyElementsIn(listOf("end-1", "end-2", "end-3"))
779         }
780     }
781 
782     private data class TestElement(val onNode: (TestNode) -> Unit) :
783         ModifierNodeElement<TestNode>() {
784         override fun create(): TestNode = TestNode(onNode)
785 
786         override fun update(node: TestNode) {
787             node.onNode = onNode
788         }
789     }
790 
791     private class TestNode(var onNode: (TestNode) -> Unit) :
792         Modifier.Node(), ModifierLocalModifierNode {
793 
794         override fun onAttach() {
795             onNode(this)
796         }
797     }
798 }
799 
createClipDatanull800 internal fun createClipData(
801     label: String = defaultLabel,
802     block: (ClipDataBuilder.() -> Unit)? = null
803 ): ClipData {
804     val builder = ClipDataBuilder()
805     return if (block != null) {
806         builder.block()
807         builder.build(label)
808     } else {
809         builder
810             .apply {
811                 addText()
812                 addUri()
813                 addHtmlText()
814                 addIntent()
815             }
816             .build(label)
817     }
818 }
819 
820 /**
821  * Helper scope to build ClipData objects for tests. This scope also builds a valid ClipDescription
822  * object according to supplied mimeTypes.
823  */
824 internal class ClipDataBuilder {
825     private val items = mutableListOf<ClipData.Item>()
826     private val mimeTypes = mutableSetOf<String>()
827 
addTextnull828     fun addText(
829         text: String = "plain text",
830         mimeType: String = ClipDescription.MIMETYPE_TEXT_PLAIN
831     ) {
832         items.add(ClipData.Item(text))
833         mimeTypes.add(mimeType)
834     }
835 
addHtmlTextnull836     fun addHtmlText(
837         text: String = "Html Content",
838         htmlText: String = "<p>Html Content</p>",
839         mimeType: String = ClipDescription.MIMETYPE_TEXT_HTML
840     ) {
841         items.add(ClipData.Item(text, htmlText))
842         mimeTypes.add(mimeType)
843     }
844 
addUrinull845     fun addUri(uri: Uri = defaultUri, mimeType: String = "image/png") {
846         items.add(ClipData.Item(uri))
847         mimeTypes.add(mimeType)
848     }
849 
addIntentnull850     fun addIntent(
851         intent: Intent = defaultIntent,
852         mimeType: String = ClipDescription.MIMETYPE_TEXT_INTENT
853     ) {
854         items.add(ClipData.Item(intent))
855         mimeTypes.add(mimeType)
856     }
857 
buildnull858     fun build(label: String = "label"): ClipData {
859         val clipDescription = ClipDescription(label, mimeTypes.toTypedArray())
860         val clipData = ClipData(clipDescription, items.first())
861         for (i in 1 until items.size) {
862             clipData.addItem(items[i])
863         }
864         return clipData
865     }
866 }
867 
868 private val defaultLabel = "label"
869 private val defaultIntent = Intent(Intent.ACTION_VIEW, Uri.parse("https://example.com"))
870 private val defaultUri = Uri.parse("content://com.example.app/image")
871 
872 private val MediaType.Companion.Video: MediaType
873     get() = MediaType("video/*")
874 
875 private val MediaType.Companion.Audio: MediaType
876     get() = MediaType("audio/*")
877