diff --git a/api/current.txt b/api/current.txt index 8bced68b..e2831fdb 100644 --- a/api/current.txt +++ b/api/current.txt @@ -16,6 +16,16 @@ package androidx.core.animation { package androidx.core.content { + public final class ClipDataKt { + ctor public ClipDataKt(); + method public static operator boolean contains(android.content.ClipData, android.content.ClipData.Item item); + method public static void forEach(android.content.ClipData, kotlin.jvm.functions.Function1 action); + method public static void forEachIndexed(android.content.ClipData, kotlin.jvm.functions.Function2 action); + method public static operator android.content.ClipData.Item get(android.content.ClipData, int index); + method public static kotlin.sequences.Sequence getItems(android.content.ClipData); + method public static operator java.util.Iterator iterator(android.content.ClipData); + } + public final class ContentValuesKt { ctor public ContentValuesKt(); method public static error.NonExistentClass contentValuesOf(kotlin.Pair... pairs); diff --git a/src/androidTest/java/androidx/core/content/ClipDataTest.kt b/src/androidTest/java/androidx/core/content/ClipDataTest.kt new file mode 100644 index 00000000..36d1ad31 --- /dev/null +++ b/src/androidTest/java/androidx/core/content/ClipDataTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2018 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 androidx.core.content + +import android.content.ClipData +import android.content.Intent +import android.net.Uri +import android.support.test.InstrumentationRegistry +import androidx.testutils.assertThrows +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertSame +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertTrue +import org.junit.Test + +class ClipDataTest { + private val clip = ClipData.newPlainText("", "") + private val context = InstrumentationRegistry.getContext() + + @Test fun get() { + val item = ClipData.Item("") + clip.addItem(item) + + // ClipData.newPlainText will instantiate + // an Intent within its initial Item + // so item.equals(clip[0]) will return false + assertEquals(item.text, clip[0].text) + assertEquals(item.uri, clip[0].uri) + assertEquals(item.htmlText, clip[0].htmlText) + val intent = clip[0].intent + if (intent != null) { + assertNotSame(intent, item.intent) + } + + assertSame(item, clip[1]) + } + + @Test fun contains() { + val item1 = ClipData.Item("") + clip.addItem(item1) + assertTrue(item1 in clip) + + val item2 = ClipData.Item("") + clip.addItem(item2) + assertTrue(item2 in clip) + } + + @Test fun forEach() { + val item1 = ClipData.Item("") + clip.addItem(item1) + val item2 = ClipData.Item("") + clip.addItem(item2) + + val items = mutableListOf() + clip.forEach { + items += it + } + assertEquals(3, items.size) + assertThat(items).containsAllOf(item1, item2) + } + + @Test fun forEachIndexed() { + val item1 = ClipData.Item("") + clip.addItem(item1) + val item2 = ClipData.Item("") + clip.addItem(item2) + + val items = mutableListOf() + clip.forEachIndexed { index, item -> + assertEquals(index, items.size) + items += item + } + assertEquals(3, items.size) + assertThat(items).containsAllOf(item1, item2) + } + + @Test fun iterator() { + val item1 = ClipData.Item("") + clip.addItem(item1) + val item2 = ClipData.Item("") + clip.addItem(item2) + + val iterator = clip.iterator() + assertTrue(iterator.hasNext()) + iterator.next() + assertSame(item1, iterator.next()) + assertTrue(iterator.hasNext()) + assertSame(item2, iterator.next()) + assertFalse(iterator.hasNext()) + assertThrows { + iterator.next() + } + } + + @Test fun items() { + val itemsList = listOf(ClipData.Item(""), ClipData.Item("")) + itemsList.forEach { clip.addItem(it) } + + clip.items.forEachIndexed { index, item -> + if (index != 0) { + assertSame(itemsList[index - 1], item) + } + } + } + + @Test fun map() { + clip.addItem(ClipData.Item("item1")) + clip.addItem(ClipData.Item("item2")) + + val items = clip.map { item -> item.text } + assertThat(items).containsExactly("", "item1", "item2") + } + + @Test fun clipDataOfValid() { + clip.addItem(ClipData.Item("item1")) + clip.addItem(ClipData.Item("item2")) + + val l = listOf("", "item1", "item2") + val c = clipDataOf(l, "strings") + val items = c.map { item -> item.text } + assertThat(items).containsExactlyElementsIn(l) + + val uris = listOf(Uri.parse("content://uri1"), + Uri.parse("content://uri2"), + Uri.parse("content://uri3")) + val cU = clipDataOf(uris, "uris") + val iU = cU.map { item -> item.uri } + assertThat(iU).containsExactlyElementsIn(uris) + + val intents = listOf(Intent("com.androidx.action", Uri.parse("content://uri1")), + Intent("com.androidx.action", Uri.parse("content://uri2")), + Intent("com.androidx.action", Uri.parse("content://uri3"))) + val cI = clipDataOf(intents, "intents") + val iI = cI.map { item -> item.intent } + assertThat(iI).containsExactlyElementsIn(intents) + } + + @Test fun clipDataOfInvalid() { + assertThrows { + clipDataOf(listOf(), "empty") + }.hasMessageThat().isEqualTo("Illegal argument, list cannot be empty.") + + assertThrows { + clipDataOf(listOf(1, 2, 3), "ints") + }.hasMessageThat().isEqualTo("Illegal type: java.lang.Integer") + } +} diff --git a/src/main/java/androidx/core/content/ClipData.kt b/src/main/java/androidx/core/content/ClipData.kt new file mode 100644 index 00000000..968eae18 --- /dev/null +++ b/src/main/java/androidx/core/content/ClipData.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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. + */ + +@file:Suppress("NOTHING_TO_INLINE") + +package androidx.core.content + +import android.content.ClipData +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri + +/** + * Returns the ClipData.Item at [index]. + * + * @throws IndexOutOfBoundsException if index is less than 0 or greater than or equal to the count. + */ +inline operator fun ClipData.get(index: Int): ClipData.Item = getItemAt(index) + +/** Returns `true` if [item] is found in this ClipData. */ +operator fun ClipData.contains(item: ClipData.Item): Boolean { + @Suppress("LoopToCallChain") + for (index in 0 until itemCount) { + if (getItemAt(index) == item) { + return true + } + } + return false +} + +/** Performs the given action on each item in this ClipData. */ +inline fun ClipData.forEach(action: (item: ClipData.Item) -> Unit) { + for (index in 0 until itemCount) { + action(getItemAt(index)) + } +} + +/** Performs the given action on each item in this ClipData, providing its sequential index. */ +inline fun ClipData.forEachIndexed(action: (index: Int, item: ClipData.Item) -> Unit) { + for (index in 0 until itemCount) { + action(index, getItemAt(index)) + } +} + +/** + * Returns an [Iterator] over the items in this ClipData. + * (NOTE: ClipData doesn't allow removal of items.) + **/ +operator fun ClipData.iterator() = object : Iterator { + private var index = 0 + override fun hasNext() = index < itemCount + override fun next() = getItemAt(index++) ?: throw IndexOutOfBoundsException() +} + +/** Returns a [Sequence] over the items in this ClipData. */ +val ClipData.items: Sequence + get() = object : Sequence { + override fun iterator() = this@items.iterator() + } + +/** + * Returns a [List] containing the results of applying the given transform function to each + * item in this ClipData. + */ +inline fun ClipData.map(transform: (ClipData.Item) -> T): List { + var m = mutableListOf() + for (i in 0 until itemCount) { + m.add(transform(getItemAt(i))) + } + return m.toList() +} + +/** + * Returns a new [ClipData] with given list of items. + * NOTE: HtmlText ClipData not supported. + * + * @throws IllegalArgumentException When the list doesn't contain items of type supported + * by [ClipData]. +*/ +inline fun clipDataOf( + l: List, + label: String = "", + cr: ContentResolver? = null +): ClipData = if (l.isEmpty()) { + throw IllegalArgumentException("Illegal argument, list cannot be empty.") + } else when { + Uri::class.java.isAssignableFrom(T::class.java) -> + if (cr == null) { + ClipData.newRawUri(label, l[0] as Uri).apply { + l.forEachIndexed { index, item -> + if (index > 0) addItem(ClipData.Item(item as Uri)) + } + } + } else { + ClipData.newUri(cr, label, l[0] as Uri).apply { + l.forEachIndexed { index, item -> + if (index > 0) addItem(ClipData.Item(item as Uri)) + } + } + } + CharSequence::class.java.isAssignableFrom(T::class.java) -> + ClipData.newPlainText(label, l[0] as CharSequence).apply { + l.forEachIndexed { index, item -> + if (index > 0) addItem(ClipData.Item(item as CharSequence)) + } + } + Intent::class.java.isAssignableFrom(T::class.java) -> + ClipData.newIntent(label, l[0] as Intent).apply { + l.forEachIndexed { index, item -> + if (index > 0) addItem(ClipData.Item(item as Intent)) + } + } + else -> throw IllegalArgumentException("Illegal type: ${T::class.java.canonicalName}") + }