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