From 7f5a7b2a1c59cc4c86e05f7cc33479e96ac1cc46 Mon Sep 17 00:00:00 2001 From: Zoran Jovanovic Date: Mon, 16 Apr 2018 17:15:58 +0200 Subject: [PATCH 1/7] Add extensions for ClipData (#497) --- .../androidx/core/content/ClipDataTest.kt | 142 ++++++++++++++++++ .../java/androidx/core/content/ClipData.kt | 121 +++++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 src/androidTest/java/androidx/core/content/ClipDataTest.kt create mode 100644 src/main/java/androidx/core/content/ClipData.kt 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..8c328a13 --- /dev/null +++ b/src/androidTest/java/androidx/core/content/ClipDataTest.kt @@ -0,0 +1,142 @@ +/* + * 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 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 clipDataOf() { + 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) + } +} 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..4b379775 --- /dev/null +++ b/src/main/java/androidx/core/content/ClipData.kt @@ -0,0 +1,121 @@ +/* + * 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 [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() + forEach { + m.add(transform(it)) + } + 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 = 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)) + } + } + } + String::class.java.isAssignableFrom(T::class.java) -> + ClipData.newPlainText(label, l[0] as String).apply { + l.forEachIndexed { index, item -> + if (index > 0) addItem(ClipData.Item(item as String)) + } + } + 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 for $label and " + + "items: ${T::class.java.canonicalName}") +} From c7c9664119dbf1fe6ae179ba8754a4d59121e4f3 Mon Sep 17 00:00:00 2001 From: Zoran Jovanovic Date: Tue, 17 Apr 2018 19:26:55 +0200 Subject: [PATCH 2/7] updateApi for ClipData extensions (#497) --- api/current.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/current.txt b/api/current.txt index 8bced68b..0f9976cf 100644 --- a/api/current.txt +++ b/api/current.txt @@ -16,6 +16,15 @@ 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 operator java.util.Iterator iterator(android.content.ClipData); + } + public final class ContentValuesKt { ctor public ContentValuesKt(); method public static error.NonExistentClass contentValuesOf(kotlin.Pair... pairs); From d12f275a4687145ab2d6e416a1d4fe83e07759ea Mon Sep 17 00:00:00 2001 From: Zoran Jovanovic Date: Thu, 26 Apr 2018 12:01:14 +0200 Subject: [PATCH 3/7] ClipData: prefer calling real API (#497) Skip calling the extension to avoid overhead. --- src/main/java/androidx/core/content/ClipData.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/androidx/core/content/ClipData.kt b/src/main/java/androidx/core/content/ClipData.kt index 4b379775..474d5d26 100644 --- a/src/main/java/androidx/core/content/ClipData.kt +++ b/src/main/java/androidx/core/content/ClipData.kt @@ -71,8 +71,8 @@ operator fun ClipData.iterator() = object : Iterator { */ inline fun ClipData.map(transform: (ClipData.Item) -> T): List { var m = mutableListOf() - forEach { - m.add(transform(it)) + for (i in 0 until itemCount) { + m.add(transform(getItemAt(i))) } return m.toList() } From af7993e8838f7521be8ae248a8f160c223018c34 Mon Sep 17 00:00:00 2001 From: Zoran Jovanovic Date: Thu, 26 Apr 2018 12:02:35 +0200 Subject: [PATCH 4/7] ClipData: add Sequence property (#497) Simply missing from previous commits. --- api/current.txt | 1 + .../java/androidx/core/content/ClipDataTest.kt | 11 +++++++++++ src/main/java/androidx/core/content/ClipData.kt | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/api/current.txt b/api/current.txt index 0f9976cf..e2831fdb 100644 --- a/api/current.txt +++ b/api/current.txt @@ -22,6 +22,7 @@ package androidx.core.content { 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); } diff --git a/src/androidTest/java/androidx/core/content/ClipDataTest.kt b/src/androidTest/java/androidx/core/content/ClipDataTest.kt index 8c328a13..9564097b 100644 --- a/src/androidTest/java/androidx/core/content/ClipDataTest.kt +++ b/src/androidTest/java/androidx/core/content/ClipDataTest.kt @@ -108,6 +108,17 @@ class ClipDataTest { } } + @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")) diff --git a/src/main/java/androidx/core/content/ClipData.kt b/src/main/java/androidx/core/content/ClipData.kt index 474d5d26..f6794700 100644 --- a/src/main/java/androidx/core/content/ClipData.kt +++ b/src/main/java/androidx/core/content/ClipData.kt @@ -65,6 +65,12 @@ operator fun ClipData.iterator() = object : Iterator { 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. From 9ef31b0e18212ecf05f45eebcc37341955594372 Mon Sep 17 00:00:00 2001 From: Zoran Jovanovic Date: Tue, 22 May 2018 15:08:33 +0200 Subject: [PATCH 5/7] ClipData: check for CharSequence (#497) The original factory API uses CharSequence, so use it instead of String. --- src/main/java/androidx/core/content/ClipData.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/androidx/core/content/ClipData.kt b/src/main/java/androidx/core/content/ClipData.kt index f6794700..932a01a8 100644 --- a/src/main/java/androidx/core/content/ClipData.kt +++ b/src/main/java/androidx/core/content/ClipData.kt @@ -109,10 +109,10 @@ inline fun clipDataOf( } } } - String::class.java.isAssignableFrom(T::class.java) -> - ClipData.newPlainText(label, l[0] as String).apply { + 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 String)) + if (index > 0) addItem(ClipData.Item(item as CharSequence)) } } Intent::class.java.isAssignableFrom(T::class.java) -> From 7158a52a78ab7fcd49765eb167cccf242ef54bca Mon Sep 17 00:00:00 2001 From: Zoran Jovanovic Date: Tue, 22 May 2018 15:11:25 +0200 Subject: [PATCH 6/7] ClipData: extend test for invalid case (#497) ClipData cannot be created with types of ClipData.Item other than Uri, CharSequence and Intent. Test that IllegalArgumentException is thrown by clipDataOf. --- .../java/androidx/core/content/ClipDataTest.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/androidTest/java/androidx/core/content/ClipDataTest.kt b/src/androidTest/java/androidx/core/content/ClipDataTest.kt index 9564097b..b17e02a5 100644 --- a/src/androidTest/java/androidx/core/content/ClipDataTest.kt +++ b/src/androidTest/java/androidx/core/content/ClipDataTest.kt @@ -127,7 +127,7 @@ class ClipDataTest { assertThat(items).containsExactly("", "item1", "item2") } - @Test fun clipDataOf() { + @Test fun clipDataOfValid() { clip.addItem(ClipData.Item("item1")) clip.addItem(ClipData.Item("item2")) @@ -150,4 +150,10 @@ class ClipDataTest { val iI = cI.map { item -> item.intent } assertThat(iI).containsExactlyElementsIn(intents) } + + @Test fun clipDataOfInvalid() { + assertThrows { + clipDataOf(listOf(1, 2, 3), "ints") + }.hasMessageThat().isEqualTo("Illegal type: java.lang.Integer") + } } From fd5e104d3b805a6416d01cbae6c2f4c58985b3ec Mon Sep 17 00:00:00 2001 From: Zoran Jovanovic Date: Tue, 22 May 2018 15:12:34 +0200 Subject: [PATCH 7/7] ClipData: verify that list is not empty in clipDataOf (#497) ClipData.Item cannot be empty when creating a ClipData instance. --- .../java/androidx/core/content/ClipDataTest.kt | 4 ++++ src/main/java/androidx/core/content/ClipData.kt | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/androidTest/java/androidx/core/content/ClipDataTest.kt b/src/androidTest/java/androidx/core/content/ClipDataTest.kt index b17e02a5..36d1ad31 100644 --- a/src/androidTest/java/androidx/core/content/ClipDataTest.kt +++ b/src/androidTest/java/androidx/core/content/ClipDataTest.kt @@ -152,6 +152,10 @@ class ClipDataTest { } @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 index 932a01a8..968eae18 100644 --- a/src/main/java/androidx/core/content/ClipData.kt +++ b/src/main/java/androidx/core/content/ClipData.kt @@ -94,8 +94,10 @@ inline fun clipDataOf( l: List, label: String = "", cr: ContentResolver? = null -): ClipData = when { - Uri::class.java.isAssignableFrom(T::class.java) -> +): 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 -> @@ -109,19 +111,17 @@ inline fun clipDataOf( } } } - CharSequence::class.java.isAssignableFrom(T::class.java) -> + 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) -> + 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 for $label and " + - "items: ${T::class.java.canonicalName}") -} + else -> throw IllegalArgumentException("Illegal type: ${T::class.java.canonicalName}") + }