From fb90c4a4ab23f2fd246c29c1dde4d6e155a2e38b Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 24 Dec 2018 01:17:31 -0500 Subject: Enhancement/ktlint (#179) * Add spotless dependency * Apply ktlint * Add license headers --- .../kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt | 18 ++++++++++++++++-- .../ca/allanwang/kau/kotlin/LazyResettableTest.kt | 18 ++++++++++++++++-- .../kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt | 18 ++++++++++++++++-- .../test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt | 21 +++++++++++++++++---- .../test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt | 17 ++++++++++++++++- 5 files changed, 81 insertions(+), 11 deletions(-) (limited to 'core/src/test/kotlin/ca/allanwang') diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt index 8ccdab3..c406901 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2018 Allan Wang + * + * 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 ca.allanwang.kau.kotlin import org.junit.Test @@ -48,5 +63,4 @@ class DebounceTest { Thread.sleep(30) assertEquals(10, i) } - -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt index 2025422..eaaaacb 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/LazyResettableTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2018 Allan Wang + * + * 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 ca.allanwang.kau.kotlin import org.junit.Before @@ -33,5 +48,4 @@ class LazyResettableTest { assertEquals(t1, t2, "Lazy resettable not returning same value after second call") assertNotEquals(t1, t3, "Lazy resettable not invalidated by registry") } - -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt index 1c40f57..4dc4a34 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/StreamsTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2018 Allan Wang + * + * 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 ca.allanwang.kau.kotlin import org.junit.Test @@ -38,5 +53,4 @@ class StreamsTest { items.kauRemoveIf { it == thePotato } //removal by equality assertEquals(result.size - 1, items.size, "Invalid list removal based on equality") } - -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt index 7eeffaf..1fbc38f 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt @@ -1,8 +1,23 @@ +/* + * Copyright 2018 Allan Wang + * + * 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 ca.allanwang.kau.kotlin import org.jetbrains.anko.doAsync import org.junit.Test -import java.util.* +import java.util.Random import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.test.assertTrue @@ -40,7 +55,6 @@ class ZipTest { assertTrue((0..10).toList().toTypedArray().contentEquals(results), "Basic zip results do not match") assertTrue(finish - start < 1000L, "Basic zip does not seem to be running asynchronously") latch.countDown() - } latch.await(1100, TimeUnit.MILLISECONDS) } @@ -66,5 +80,4 @@ class ZipTest { } latch.await(1100, TimeUnit.MILLISECONDS) } - -} \ No newline at end of file +} diff --git a/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt b/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt index ce2b757..1cce9ae 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2018 Allan Wang + * + * 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 ca.allanwang.kau.utils import android.graphics.Color @@ -22,4 +37,4 @@ class UtilsTest { assertEquals("22.466", 22.465920439.round(3)) assertEquals("22", 22f.round(3)) } -} \ No newline at end of file +} -- cgit v1.2.3 From 8447b1ae8ce89b3f1bbe79dbae8847d901831c12 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 24 Dec 2018 20:05:06 -0500 Subject: Enhancement/coroutines (#180) * Add coroutine dependency * Add coroutines to kprefactivity * Change base job to supervisor * Update coroutines for faq * Update changelog * Use preloading in media picker core * Make test logging internal * Remove anko --- README.md | 3 +- .../ca/allanwang/kau/about/AboutPanelDelegate.kt | 24 +++-- .../main/groovy/ca/allanwang/kau/Versions.groovy | 6 +- core/README.md | 4 +- core/build.gradle | 4 +- .../ca/allanwang/kau/internal/KauBaseActivity.kt | 30 +++++- .../src/main/kotlin/ca/allanwang/kau/kotlin/Zip.kt | 105 --------------------- .../src/main/kotlin/ca/allanwang/kau/logging/KL.kt | 7 +- .../kotlin/ca/allanwang/kau/utils/ActivityUtils.kt | 13 ++- .../kotlin/ca/allanwang/kau/utils/BundleUtils.kt | 55 ++++++++++- .../kotlin/ca/allanwang/kau/utils/ContextUtils.kt | 27 +++++- .../kotlin/ca/allanwang/kau/utils/FragmentUtils.kt | 3 +- .../main/kotlin/ca/allanwang/kau/xml/Changelog.kt | 21 ++--- core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt | 71 +++++++------- .../test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt | 83 ---------------- docs/Changelog.md | 6 ++ docs/Migration.md | 13 +++ .../allanwang/kau/kpref/activity/KPrefActivity.kt | 31 +++--- .../allanwang/kau/mediapicker/MediaPickerCore.kt | 33 ++----- sample/src/main/res/xml/kau_changelog.xml | 10 ++ .../ca/allanwang/kau/searchview/SearchView.kt | 2 +- 21 files changed, 250 insertions(+), 301 deletions(-) delete mode 100644 core/src/main/kotlin/ca/allanwang/kau/kotlin/Zip.kt delete mode 100644 core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt (limited to 'core/src/test/kotlin/ca/allanwang') diff --git a/README.md b/README.md index 4143ca8..6fe7590 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,9 @@ This means that you'll need to explicitly include each submodule you'd like to u * [Extension Functions](core#extension-functions) * [Lazy Resettable](core#lazy-resettable) * Includes -[`AppCompat`](https://developer.android.com/topic/libraries/support-library/index.html), +[`AndroidX Components`](https://developer.android.com/topic/libraries/support-library/index.html), [`Material Dialogs (core)`](https://github.com/afollestad/material-dialogs), [`Iconics`](https://github.com/mikepenz/Android-Iconics), -[`Anko`](https://github.com/Kotlin/anko), [`Kotlin stdlib`](https://kotlinlang.org/api/latest/jvm/stdlib/) ## [Core UI](core-ui#readme) diff --git a/about/src/main/kotlin/ca/allanwang/kau/about/AboutPanelDelegate.kt b/about/src/main/kotlin/ca/allanwang/kau/about/AboutPanelDelegate.kt index 35c1322..f77bcf2 100644 --- a/about/src/main/kotlin/ca/allanwang/kau/about/AboutPanelDelegate.kt +++ b/about/src/main/kotlin/ca/allanwang/kau/about/AboutPanelDelegate.kt @@ -35,8 +35,9 @@ import ca.allanwang.kau.utils.withMarginDecoration import ca.allanwang.kau.xml.kauParseFaq import com.mikepenz.aboutlibraries.Libs import com.mikepenz.fastadapter.IItem -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.launch /** * Created by Allan Wang on 2017-08-02. @@ -174,13 +175,16 @@ open class AboutPanelLibs : AboutPanelRecycler() { } override fun loadItems(activity: AboutActivityBase, position: Int) { - doAsync { - with(activity) { - items = - getLibraries(if (rClass == null) Libs(activity) else Libs(this, Libs.toStringArray(rClass.fields))) - .map(::LibraryIItem) + with(activity) { + launch { + items = async { + getLibraries( + if (rClass == null) Libs(activity) + else Libs(activity, Libs.toStringArray(rClass.fields)) + ).map(::LibraryIItem) + }.await() if (pageStatus[position] == 1) - uiThread { addItems(activity, position) } + addItems(activity, position) } } } @@ -202,8 +206,8 @@ open class AboutPanelFaqs : AboutPanelRecycler() { override fun loadItems(activity: AboutActivityBase, position: Int) { with(activity) { - kauParseFaq(configs.faqXmlRes, configs.faqParseNewLine) { - items = it.map(::FaqIItem) + launch { + items = async { kauParseFaq(configs.faqXmlRes, configs.faqParseNewLine) }.await().map(::FaqIItem) if (pageStatus[position] == 1) addItems(activity, position) } diff --git a/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy b/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy index a85fe97..e6f3cd7 100644 --- a/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy +++ b/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy @@ -26,12 +26,12 @@ class Versions { // https://kotlinlang.org/docs/reference/using-gradle.html static def kotlin = '1.3.11' + // https://github.com/Kotlin/kotlinx.coroutines/releases + static def coroutines = '1.0.1' + // https://github.com/mikepenz/AboutLibraries/releases static def aboutLibraries = '6.2.0' - // https://github.com/Kotlin/anko/releases - static def anko = '0.10.5' - // https://github.com/wasabeef/Blurry/releases static def blurry = '2.1.1' diff --git a/core/README.md b/core/README.md index 39899dd..b9a10f5 100644 --- a/core/README.md +++ b/core/README.md @@ -131,6 +131,9 @@ These variants are weakly held in the private `KotterknifeRegistry` object, and values through the `Kotterknife.reset` method. This is typically useful for Fragments, as they do not follow the same lifecycle as Activities and Views. +Note that this is useful for views that have ids in multiple layout files or in `id.xml` files. +Kotlin has another solution, [`kotlin-android-extensions`](https://kotlinlang.org/docs/tutorials/android-plugin.html), which is more convenient. + ## Ripple Canvas Ripple canvas provides a way to create simultaneous ripples against a background color. @@ -210,7 +213,6 @@ Include your email and subject, along with other optional configurations such as ## Extension Functions > "[Extensions](https://kotlinlang.org/docs/reference/extensions.html) provide the ability to extend a class with new functionality without having to inherit from the class" -
Note that since KAU depends on [ANKO](https://github.com/Kotlin/anko), all of the extensions in its core package is also in KAU. KAU's vast collection of extensions is one of its strongest features. There are too many to explain here, but you may check out the [utils package](https://github.com/AllanWang/KAU/tree/master/core/src/main/kotlin/ca/allanwang/kau/utils) diff --git a/core/build.gradle b/core/build.gradle index a57ee6e..3ac4f36 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -11,12 +11,12 @@ dependencies { api "androidx.constraintlayout:constraintlayout:${kau.constraintLayout}" api "com.google.android.material:material:${kau.googleMaterial}" + api "org.jetbrains.kotlinx:kotlinx-coroutines-android:${kau.coroutines}" + api "com.mikepenz:iconics-core:${kau.iconics}@aar" api "com.mikepenz:google-material-typeface:${kau.iconicsGoogle}.original@aar" api "com.afollestad.material-dialogs:core:${kau.materialDialog}" - - api "org.jetbrains.anko:anko-commons:${kau.anko}" } apply from: '../artifacts.gradle' diff --git a/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt b/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt index 85e711b..bf977f2 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt @@ -15,8 +15,14 @@ */ package ca.allanwang.kau.internal +import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlin.coroutines.CoroutineContext /** * Created by Allan Wang on 2017-08-01. @@ -26,11 +32,31 @@ import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult * Ensures that some singleton methods are called. * This is simply a convenience class; * you can always copy and paste this to your own class. + * + * This also implements [CoroutineScope] that adheres to the activity lifecycle. + * Note that by default, [SupervisorJob] is used, to avoid exceptions in one child from affecting that of another. + * The default job can be overridden within [defaultJob] */ -abstract class KauBaseActivity : AppCompatActivity() { +abstract class KauBaseActivity : AppCompatActivity(), CoroutineScope { + + open lateinit var job: Job + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + + open fun defaultJob(): Job = SupervisorJob() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + job = defaultJob() + } + + override fun onDestroy() { + job.cancel() + super.onDestroy() + } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) kauOnRequestPermissionsResult(permissions, grantResults) } -} +} \ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/kotlin/Zip.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/Zip.kt deleted file mode 100644 index b767b30..0000000 --- a/core/src/main/kotlin/ca/allanwang/kau/kotlin/Zip.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * 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 ca.allanwang.kau.kotlin - -import org.jetbrains.anko.doAsync -import java.util.concurrent.atomic.AtomicInteger - -/** - * Created by Allan Wang on 2017-08-06. - * - * Collection of zip methods that aim to replicate - * Reactive Zips - * For unit returning functions - * - * Typically, the functions will execute asynchronously and call their given callbacks when finished. - * Once all callbacks are called, the final onFinish callback will be executed. - * - * There is also a helper zipper to wrap synchronous functions with Anko's doAsync to achieve the same results - * - * Note that not wrapping synchronous functions will render these methods useless, - * as you can simply define an inline callback after all functions are finished - */ - -/** - * Callback which will only execute the first time - */ -open class ZipCallbackBase { - var completed: Boolean = false - - inline operator fun invoke(callback: () -> Unit) { - if (completed) return - completed = true - callback() - } -} - -class ZipCallback(val onReceived: (T) -> Unit) : ZipCallbackBase() { - operator fun invoke(result: T) = invoke { onReceived(result) } -} - -class ZipEmptyCallback(val onReceived: () -> Unit) : ZipCallbackBase() { - operator fun invoke() = invoke(onReceived) -} - -/** - * Given a default result, a series of tasks, and a finished callback, - * this method will run all tasks and wait until all tasks emit a response - * The response will then be sent back to the callback - * - * ALl tasks must invoke the task callback for [onFinished] to execute - */ -inline fun Collection<(ZipCallback) -> Unit>.zip( - defaultResult: T, - crossinline onFinished: (results: Array) -> Unit -) { - val result = Array(size) { defaultResult } - val countDown = AtomicInteger(size) - forEachIndexed { index, asyncFun -> - asyncFun(ZipCallback { - result[index] = it - if (countDown.decrementAndGet() <= 0) - onFinished(result) - }) - } -} - -/** - * Simplified zip method with no finished callback arguments - */ -inline fun Collection<(ZipEmptyCallback) -> Unit>.zip(crossinline onFinished: () -> Unit) { - val countDown = AtomicInteger(size) - forEach { asyncFun -> - asyncFun(ZipEmptyCallback { - if (countDown.decrementAndGet() <= 0) - onFinished() - }) - } -} - -/** - * Converts a collection of synchronous tasks to asynchronous tasks with a common callback - */ -inline fun Collection<() -> Unit>.zipAsync(crossinline onFinished: () -> Unit) { - map { synchronousFun -> - { callback: ZipEmptyCallback -> - doAsync { - synchronousFun() - callback() - }; Unit - } - }.zip(onFinished) -} diff --git a/core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt b/core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt index f92edb3..52e5415 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt @@ -22,4 +22,9 @@ import ca.allanwang.kau.BuildConfig * * Internal KAU logger */ -object KL : KauLogger("KAU", { BuildConfig.DEBUG }) +object KL : KauLogger("KAU", { BuildConfig.DEBUG }) { + internal inline fun test(message: () -> Any?) { + if (BuildConfig.DEBUG) + d { "Test1234 ${message()}" } + } +} diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt index a655e5b..3dd4bb9 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt @@ -28,13 +28,13 @@ import android.os.Build import android.os.Bundle import android.view.Menu import android.view.View +import android.view.ViewGroup import androidx.annotation.ColorInt import androidx.annotation.RequiresApi import androidx.annotation.StringRes import ca.allanwang.kau.R import com.google.android.material.snackbar.Snackbar import com.mikepenz.iconics.typeface.IIcon -import org.jetbrains.anko.contentView /** * Created by Allan Wang on 2017-06-21. @@ -160,6 +160,17 @@ inline fun Activity.showKeyboard() { currentFocus?.showKeyboard() } +/** + * Gets the view set by [Activity.setContentView] if it exists. + * + * Taken courtesy of Anko + * + * Previously, Anko was a dependency in KAU, but has been removed on 12/24/2018 + * as most of the methods weren't used + */ +inline val Activity.contentView: View? + get() = (findViewById(android.R.id.content) as? ViewGroup)?.getChildAt(0) + inline fun Activity.snackbar( text: String, duration: Int = Snackbar.LENGTH_LONG, diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/BundleUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/BundleUtils.kt index 314ca60..5b4b188 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/BundleUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/BundleUtils.kt @@ -20,10 +20,12 @@ import android.app.Activity import android.app.ActivityOptions import android.content.Context import android.os.Bundle +import android.os.Parcelable import android.util.Pair import android.view.View import androidx.annotation.AnimRes import ca.allanwang.kau.R +import java.io.Serializable /** * Created by Allan Wang on 10/12/17. @@ -36,6 +38,56 @@ infix fun Bundle.with(bundle: Bundle?): Bundle { return this } +/** + * Saves all bundle args based on their respective types. + * + * Taken courtesy of Anko + * + * Previously, Anko was a dependency in KAU, but has been removed on 12/24/2018 + * as most of the methods weren't used + */ +fun bundleOf(vararg params: kotlin.Pair): Bundle { + val b = Bundle() + for (p in params) { + val (k, v) = p + when (v) { + null -> b.putSerializable(k, null) + is Boolean -> b.putBoolean(k, v) + is Byte -> b.putByte(k, v) + is Char -> b.putChar(k, v) + is Short -> b.putShort(k, v) + is Int -> b.putInt(k, v) + is Long -> b.putLong(k, v) + is Float -> b.putFloat(k, v) + is Double -> b.putDouble(k, v) + is String -> b.putString(k, v) + is CharSequence -> b.putCharSequence(k, v) + is Parcelable -> b.putParcelable(k, v) + is Serializable -> b.putSerializable(k, v) + is BooleanArray -> b.putBooleanArray(k, v) + is ByteArray -> b.putByteArray(k, v) + is CharArray -> b.putCharArray(k, v) + is DoubleArray -> b.putDoubleArray(k, v) + is FloatArray -> b.putFloatArray(k, v) + is IntArray -> b.putIntArray(k, v) + is LongArray -> b.putLongArray(k, v) + is Array<*> -> { + @Suppress("UNCHECKED_CAST") + when { + v.isArrayOf() -> b.putParcelableArray(k, v as Array) + v.isArrayOf() -> b.putCharSequenceArray(k, v as Array) + v.isArrayOf() -> b.putStringArray(k, v as Array) + else -> throw KauException("Unsupported bundle component (${v.javaClass})") + } + } + is ShortArray -> b.putShortArray(k, v) + is Bundle -> b.putBundle(k, v) + else -> throw KauException("Unsupported bundle component (${v.javaClass})") + } + } + return b +} + /** * Adds transition bundle if context is activity and build is lollipop+ */ @@ -62,7 +114,8 @@ fun Bundle.withSceneTransitionAnimation(parent: View, data: Map) = @SuppressLint("NewApi") fun Bundle.withSceneTransitionAnimation(context: Context, data: Map) { if (context !is Activity || !buildIsLollipopAndUp) return - val options = ActivityOptions.makeSceneTransitionAnimation(context, + val options = ActivityOptions.makeSceneTransitionAnimation( + context, *data.map { (view, tag) -> Pair(view, tag) }.toTypedArray() ) putAll(options.toBundle()) diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt index 134126d..60ef236 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2018 Allan Wang + * Copyright 2017 Allan Wang * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,8 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.util.TypedValue import android.view.View import android.view.animation.AnimationUtils @@ -45,10 +47,33 @@ import androidx.core.content.ContextCompat import ca.allanwang.kau.R import ca.allanwang.kau.logging.KL import com.afollestad.materialdialogs.MaterialDialog +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlin.coroutines.CoroutineContext /** * Created by Allan Wang on 2017-06-03. */ +private object ContextHelper: CoroutineScope { + + val handler = Handler(Looper.getMainLooper()) + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main +} + +/** + * Most context items implement [CoroutineScope] by default. + * We will add a fallback just in case. + * It is expected that the scope returned always has the Android main dispatcher as part of the context. + */ +internal inline val Context.ctxCoroutine: CoroutineScope + get() = this as? CoroutineScope ?: ContextHelper + +fun Context.runOnUiThread(f: Context.() -> Unit) { + if (Looper.getMainLooper() === Looper.myLooper()) f() else ContextHelper.handler.post { f() } +} /** * Helper class to launch an activity from a context diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt index 1c97900..75dc1c1 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt @@ -16,12 +16,11 @@ package ca.allanwang.kau.utils import androidx.fragment.app.Fragment -import org.jetbrains.anko.bundleOf /** * Created by Allan Wang on 2017-07-02. */ -fun T.withArguments(vararg params: Pair): T { +fun T.withArguments(vararg params: Pair): T { arguments = bundleOf(*params) return this } diff --git a/core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt b/core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt index 6a75aa9..51e63f9 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt @@ -26,11 +26,12 @@ import androidx.annotation.LayoutRes import androidx.annotation.XmlRes import androidx.recyclerview.widget.RecyclerView import ca.allanwang.kau.R +import ca.allanwang.kau.utils.ctxCoroutine import ca.allanwang.kau.utils.materialDialog import ca.allanwang.kau.utils.use import com.afollestad.materialdialogs.MaterialDialog -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import org.xmlpull.v1.XmlPullParser /** @@ -39,15 +40,13 @@ import org.xmlpull.v1.XmlPullParser * Easy changelog loader */ fun Context.showChangelog(@XmlRes xmlRes: Int, @ColorInt textColor: Int? = null, customize: MaterialDialog.Builder.() -> Unit = {}) { - doAsync { - val items = parse(this@showChangelog, xmlRes) - uiThread { - materialDialog { - title(R.string.kau_changelog) - positiveText(R.string.kau_great) - adapter(ChangelogAdapter(items, textColor), null) - customize() - } + ctxCoroutine.launch { + val items = async { parse(this@showChangelog, xmlRes) }.await() + materialDialog { + title(R.string.kau_changelog) + positiveText(R.string.kau_great) + adapter(ChangelogAdapter(items, textColor), null) + customize() } } } diff --git a/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt b/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt index cb57216..73d7d6c 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt @@ -21,8 +21,6 @@ import android.text.Html import android.text.Spanned import androidx.annotation.XmlRes import ca.allanwang.kau.utils.use -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread import org.xmlpull.v1.XmlPullParser /** @@ -30,8 +28,8 @@ import org.xmlpull.v1.XmlPullParser */ /** - * Parse an xml asynchronously with two tags, Text and Text, - * and invoke the [callback] on the ui thread + * Parse an xml asynchronously with two tags, Text and Text. + * Note that this should executed in a background thread. */ @Suppress("DEPRECATION") fun Context.kauParseFaq( @@ -39,47 +37,44 @@ fun Context.kauParseFaq( /** * If \n is used, it will automatically be converted to
*/ - parseNewLine: Boolean = true, - callback: (items: List) -> Unit -) { - doAsync { - val items = mutableListOf() - resources.getXml(xmlRes).use { parser: XmlResourceParser -> - var eventType = parser.eventType - var question: Spanned? = null - var flag = -1 //-1, 0, 1 -> invalid, question, answer - while (eventType != XmlPullParser.END_DOCUMENT) { - if (eventType == XmlPullParser.START_TAG) { - flag = when (parser.name) { - "question" -> 0 - "answer" -> 1 - else -> -1 + parseNewLine: Boolean = true +): List { + val items = mutableListOf() + resources.getXml(xmlRes).use { parser: XmlResourceParser -> + var eventType = parser.eventType + var question: Spanned? = null + var flag = -1 //-1, 0, 1 -> invalid, question, answer + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + flag = when (parser.name) { + "question" -> 0 + "answer" -> 1 + else -> -1 + } + } else if (eventType == XmlPullParser.TEXT) { + when (flag) { + 0 -> { + question = Html.fromHtml(parser.text.replace("\n", if (parseNewLine) "
" else "")) + flag = -1 } - } else if (eventType == XmlPullParser.TEXT) { - when (flag) { - 0 -> { - question = Html.fromHtml(parser.text.replace("\n", if (parseNewLine) "
" else "")) - flag = -1 - } - 1 -> { - items.add( - FaqItem( - items.size + 1, - question - ?: throw IllegalArgumentException("KAU FAQ answer found without a question"), - Html.fromHtml(parser.text.replace("\n", if (parseNewLine) "
" else "")) - ) + 1 -> { + items.add( + FaqItem( + items.size + 1, + question + ?: throw IllegalArgumentException("KAU FAQ answer found without a question"), + Html.fromHtml(parser.text.replace("\n", if (parseNewLine) "
" else "")) ) - question = null - flag = -1 - } + ) + question = null + flag = -1 } } - eventType = parser.next() } + eventType = parser.next() } - uiThread { callback(items) } } + return items } data class FaqItem(val number: Int, val question: Spanned, val answer: Spanned) diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt deleted file mode 100644 index 1fbc38f..0000000 --- a/core/src/test/kotlin/ca/allanwang/kau/kotlin/ZipTest.kt +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2018 Allan Wang - * - * 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 ca.allanwang.kau.kotlin - -import org.jetbrains.anko.doAsync -import org.junit.Test -import java.util.Random -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.test.assertTrue - -/** - * Created by Allan Wang on 2017-08-06. - */ -class ZipTest { - - val debug = false - - fun p(text: String) { - if (debug) println(text) - } - - @Test - fun basic() { - val start = System.currentTimeMillis() - val latch = CountDownLatch(1) - val rnd = Random() - (0..10).map { - { callback: ZipCallback -> - doAsync { - val sleepTime = rnd.nextInt(100) + 200L - p("Task $it will sleep for ${sleepTime}ms") - Thread.sleep(sleepTime) - val finish = System.currentTimeMillis() - p("Task $it finished in ${finish - start}ms at $finish") - callback(it) - }; Unit - } - }.zip(-1) { results -> - val finish = System.currentTimeMillis() - println("Results ${results.contentToString()} received in ${finish - start}ms at $finish") - assertTrue((0..10).toList().toTypedArray().contentEquals(results), "Basic zip results do not match") - assertTrue(finish - start < 1000L, "Basic zip does not seem to be running asynchronously") - latch.countDown() - } - latch.await(1100, TimeUnit.MILLISECONDS) - } - - @Test - fun basicAsync() { - val start = System.currentTimeMillis() - val latch = CountDownLatch(1) - val rnd = Random() - (0..10).map { - { - val sleepTime = rnd.nextInt(100) + 200L - p("Task $it will sleep for ${sleepTime}ms") - Thread.sleep(sleepTime) - val finish = System.currentTimeMillis() - p("Task $it finished in ${finish - start}ms at $finish") - } - }.zipAsync { - val finish = System.currentTimeMillis() - println("Results received in ${finish - start}ms at $finish") - assertTrue(finish - start < 1000L, "BasicAsync does not seem to be wrapping the tasks asynchronously") - latch.countDown() - } - latch.await(1100, TimeUnit.MILLISECONDS) - } -} diff --git a/docs/Changelog.md b/docs/Changelog.md index 1e1b0d8..283759d 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -1,5 +1,11 @@ # Changelog +## v4.0.0-alpha02 +* :core: Remove anko dependency. Methods that used it now use coroutines; see the migration doc for minor changes +* :core: Add default CoroutineScope implementation to KauBaseActivity +* :core: Remove zip class. Coroutines and join can be used as an alternative +* :mediapicker: Use video preloading instead of full async loading + ## v4.0.0-alpha01 * Migrate to androidx. See migration for external dependency changes. * :core: Remove deprecation warning for Kotterknife diff --git a/docs/Migration.md b/docs/Migration.md index 8c5e016..cdaca4c 100644 --- a/docs/Migration.md +++ b/docs/Migration.md @@ -2,6 +2,19 @@ Below are some highlights on major refactoring/breaking changes +# v4.0.1-alpha02 + +* `kauParseFaq` is now synchronous. + +## Anko has been removed + +A lot of the methods are already implemented in KAU, and it was primarily imported for its `doAsync` methods. Now, they have been replaced with coroutines. +Some methods have been copied over: + +* import org.jetbrains.anko.runOnUiThread > import ca.allanwang.kau.utils.runOnUiThread +* import org.jetbrains.anko.contentView > import ca.allanwang.kau.utils.contentView +* import org.jetbrains.anko.bundleOf > import ca.allanwang.kau.utils.bundleOf + # v4.0.0-alpha01 This is the first introduction of androidx. The goal is to just do a migration with minimal changes. diff --git a/kpref-activity/src/main/kotlin/ca/allanwang/kau/kpref/activity/KPrefActivity.kt b/kpref-activity/src/main/kotlin/ca/allanwang/kau/kpref/activity/KPrefActivity.kt index 02b6e98..450bc6e 100644 --- a/kpref-activity/src/main/kotlin/ca/allanwang/kau/kpref/activity/KPrefActivity.kt +++ b/kpref-activity/src/main/kotlin/ca/allanwang/kau/kpref/activity/KPrefActivity.kt @@ -33,8 +33,8 @@ import ca.allanwang.kau.utils.statusBarColor import ca.allanwang.kau.utils.withLinearAdapter import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter import kotlinx.android.synthetic.main.kau_pref_activity.* -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import java.util.Stack abstract class KPrefActivity : KauBaseActivity(), KPrefActivityContract { @@ -104,19 +104,24 @@ abstract class KPrefActivity : KauBaseActivity(), KPrefActivityContract { builder: KPrefAdapterBuilder.() -> Unit, first: Boolean ) { - doAsync { - val items = KPrefAdapterBuilder(globalOptions) - builder(items) - kprefStack.push(toolbarTitleRes to items.list) + launch { + val items = async { + val items = KPrefAdapterBuilder(globalOptions) + builder(items) + kprefStack.push(toolbarTitleRes to items.list) + items.list + }.await() kau_recycler.itemAnimator = if (animate && !first) recyclerAnimatorNext else null - uiThread { - adapter.clear() - adapter.add(items.list.filter { it.core.visible() }) - toolbar.setTitle(toolbarTitleRes) - } + show(toolbarTitleRes, items) } } + private fun show(@StringRes toolbarTitleRes: Int, items: List) { + toolbar.setTitle(toolbarTitleRes) + adapter.clear() + adapter.add(items.filter { it.core.visible() }) + } + /** * Pops the stack and loads the next kpref list * Indices are not checked so ensure that this is possible first @@ -125,9 +130,7 @@ abstract class KPrefActivity : KauBaseActivity(), KPrefActivityContract { kprefStack.pop() val (title, list) = kprefStack.peek() kau_recycler.itemAnimator = if (animate) recyclerAnimatorPrev else null - adapter.clear() - adapter.add(list.filter { it.core.visible() }) - toolbar.setTitle(title) + show(title, list) } /** diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaPickerCore.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaPickerCore.kt index abdc266..7004967 100644 --- a/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaPickerCore.kt +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaPickerCore.kt @@ -49,10 +49,8 @@ import com.mikepenz.fastadapter.adapters.ItemAdapter import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.IIcon -import org.jetbrains.anko.doAsync +import kotlinx.coroutines.CancellationException import java.io.File -import java.util.concurrent.ExecutionException -import java.util.concurrent.Future /** * Created by Allan Wang on 2017-07-23. @@ -121,7 +119,6 @@ abstract class MediaPickerCore>( lateinit var glide: RequestManager private var hasPreloaded = false - private var prefetcher: Future<*>? = null val adapter = ItemAdapter() @@ -137,7 +134,7 @@ abstract class MediaPickerCore>( fun initializeRecycler(recycler: RecyclerView) { val adapterHeader = ItemAdapter() - val fulladapter = fastAdapter(adapterHeader, adapter) + val fulladapter = fastAdapter>(adapterHeader, adapter) adapterHeader.add(mediaActions.map { MediaActionItem(it, mediaType) }) recycler.apply { val manager = object : GridLayoutManager(context, computeColumnCount(context)) { @@ -146,7 +143,6 @@ abstract class MediaPickerCore>( } } setItemViewCacheSize(CACHE_SIZE) - isDrawingCacheEnabled = true layoutManager = manager adapter = fulladapter setHasFixedSize(true) @@ -195,18 +191,14 @@ abstract class MediaPickerCore>( addItems(models.map { converter(it) }) if (!hasPreloaded && mediaType == MediaType.VIDEO) { hasPreloaded = true - prefetcher = doAsync { - models.subList(0, Math.min(models.size, 50)).map { it.data }.forEach { - val target = glide.load(it) - .applyMediaOptions(this@MediaPickerCore) - .submit() - try { - target.get() - } catch (ignored: InterruptedException) { - } catch (ignored: ExecutionException) { - } finally { - glide.clear(target) - } + val preloads = models.subList(0, Math.min(models.size, 50)).map { + glide.load(it.data) + .applyMediaOptions(this@MediaPickerCore) + .preload() + } + job.invokeOnCompletion { + if (it is CancellationException) { + preloads.forEach(glide::clear) } } } @@ -242,11 +234,6 @@ abstract class MediaPickerCore>( open fun onStatusChange(loaded: Boolean) {} - override fun onDestroy() { - prefetcher?.cancel(true) - super.onDestroy() - } - /** * Method used to retrieve uri data for API 19+ * See diff --git a/sample/src/main/res/xml/kau_changelog.xml b/sample/src/main/res/xml/kau_changelog.xml index 6c0fea1..3c49078 100644 --- a/sample/src/main/res/xml/kau_changelog.xml +++ b/sample/src/main/res/xml/kau_changelog.xml @@ -6,6 +6,16 @@ --> + + + + + + + + + + diff --git a/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt index 11880cd..169e914 100644 --- a/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt +++ b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt @@ -53,6 +53,7 @@ import ca.allanwang.kau.utils.hideKeyboard import ca.allanwang.kau.utils.invisibleIf import ca.allanwang.kau.utils.isVisible import ca.allanwang.kau.utils.parentViewGroup +import ca.allanwang.kau.utils.runOnUiThread import ca.allanwang.kau.utils.setIcon import ca.allanwang.kau.utils.setMarginTop import ca.allanwang.kau.utils.showKeyboard @@ -65,7 +66,6 @@ import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.iconics.typeface.IIcon import kotlinx.android.synthetic.main.kau_search_view.view.* -import org.jetbrains.anko.runOnUiThread /** * Created by Allan Wang on 2017-06-23. -- cgit v1.2.3 From d850474b0a82ee00d094990d9bd3392ae8cd9575 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 24 Dec 2018 22:00:41 -0500 Subject: Simplify color utils and fix tests, resolves #156 --- .../kotlin/ca/allanwang/kau/ui/views/CutoutView.kt | 2 +- .../ca/allanwang/kau/utils/UtilsAndroidTest.kt | 50 ++++++++++++++++++++++ .../kotlin/ca/allanwang/kau/xml/FaqTest.kt | 28 +++--------- .../kotlin/ca/allanwang/kau/utils/ColorUtils.kt | 31 +++++++------- .../kotlin/ca/allanwang/kau/utils/UtilsTest.kt | 18 ++++++++ 5 files changed, 91 insertions(+), 38 deletions(-) create mode 100644 core/src/androidTest/kotlin/ca/allanwang/kau/utils/UtilsAndroidTest.kt (limited to 'core/src/test/kotlin/ca/allanwang') diff --git a/core-ui/src/main/kotlin/ca/allanwang/kau/ui/views/CutoutView.kt b/core-ui/src/main/kotlin/ca/allanwang/kau/ui/views/CutoutView.kt index 6da65a3..474d40a 100644 --- a/core-ui/src/main/kotlin/ca/allanwang/kau/ui/views/CutoutView.kt +++ b/core-ui/src/main/kotlin/ca/allanwang/kau/ui/views/CutoutView.kt @@ -90,7 +90,7 @@ class CutoutView @JvmOverloads constructor( if (attrs != null) { val a = context.obtainStyledAttributes(attrs, R.styleable.CutoutView, 0, 0) if (a.hasValue(R.styleable.CutoutView_font)) - paint.typeface = context.getFont(a.getString(R.styleable.CutoutView_font)) + paint.typeface = context.getFont(a.getString(R.styleable.CutoutView_font)!!) foregroundColor = a.getColor(R.styleable.CutoutView_foregroundColor, foregroundColor) text = a.getString(R.styleable.CutoutView_android_text) ?: text minHeight = a.getDimension(R.styleable.CutoutView_android_minHeight, minHeight) diff --git a/core/src/androidTest/kotlin/ca/allanwang/kau/utils/UtilsAndroidTest.kt b/core/src/androidTest/kotlin/ca/allanwang/kau/utils/UtilsAndroidTest.kt new file mode 100644 index 0000000..665e0b2 --- /dev/null +++ b/core/src/androidTest/kotlin/ca/allanwang/kau/utils/UtilsAndroidTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2018 Allan Wang + * + * 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 ca.allanwang.kau.utils + +import android.graphics.Color +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Test +import org.junit.runner.RunWith +import kotlin.test.assertEquals + +/** + * Created by Allan Wang on 2018-12-24. + * + * Misc test code that are dependent on android + */ +@RunWith(AndroidJUnit4::class) +class UtilsAndroidTest { + + @Test + fun colorBlend() { + assertEquals(0x22446688, 0x11335577.blendWith(0x33557799, 0.5f), "Failed to blend with 50% ratio") + assertEquals(0x11335577, 0x11335577.blendWith(0x33557799, 0.0f), "Failed to blend with 0% ratio") + assertEquals(0x33557799, 0x22446688.blendWith(0x33557799, 1.0f), "Failed to blend with 100% ratio") + } + + @Test + fun lighten() { + assertEquals(Color.WHITE, Color.WHITE.lighten(0.35f), "Should not be able to further lighten white") + assertEquals(0xFFEEAAEA.toInt(), 0xFFDD55D5.toInt().lighten(0.5f), "Failed to lighten color by 50%") + } + + @Test + fun darken() { + assertEquals(Color.BLACK, Color.BLACK.darken(0.35f), "Should not be able to further darken black") + assertEquals(0xFF224424.toInt(), 0xFF448848.toInt().darken(0.5f), "Failed to darken color by 50%") + } +} diff --git a/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt b/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt index 2d02dbc..7ec2a41 100644 --- a/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt +++ b/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt @@ -36,27 +36,11 @@ class FaqTest { @Test fun simpleTest() { - context.kauParseFaq(R.xml.test_faq) { data -> - assertEquals(2, data.size, "FAQ size is incorrect") - assertEquals("1. This is a question", data.first().question.toString(), "First question does not match") - assertEquals("This is an answer", data.first().answer.toString(), "First answer does not match") - assertEquals( - "2. This is another question", - data.last().question.toString(), - "Second question does not match" - ) - assertEquals("This is another answer", data.last().answer.toString(), "Second answer does not match") - } - } - - @Test - fun withoutNumbering() { - context.kauParseFaq(R.xml.test_faq, false) { data -> - assertEquals(2, data.size, "FAQ size is incorrect") - assertEquals("This is a question", data.first().question.toString(), "First question does not match") - assertEquals("This is an answer", data.first().answer.toString(), "First answer does not match") - assertEquals("This is another question", data.last().question.toString(), "Second question does not match") - assertEquals("This is another answer", data.last().answer.toString(), "Second answer does not match") - } + val data = context.kauParseFaq(R.xml.test_faq, false) + assertEquals(2, data.size, "FAQ size is incorrect") + assertEquals("This is a question", data.first().question.toString(), "First question does not match") + assertEquals("This is an answer", data.first().answer.toString(), "First answer does not match") + assertEquals("This is another question", data.last().question.toString(), "Second question does not match") + assertEquals("This is another answer", data.last().answer.toString(), "Second answer does not match") } } diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt index 9e1832f..bbb8953 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt @@ -118,25 +118,28 @@ fun Int.blendWith(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) ratio: } @ColorInt -fun Int.withAlpha(@IntRange(from = 0L, to = 255L) alpha: Int): Int = - Color.argb(alpha, Color.red(this), Color.green(this), Color.blue(this)) +fun Int.withAlpha(@IntRange(from = 0, to = 0xFF) alpha: Int): Int = + this and 0x00FFFFFF or (alpha shl 24) @ColorInt -fun Int.withMinAlpha(@IntRange(from = 0L, to = 255L) alpha: Int): Int = - Color.argb(Math.max(alpha, Color.alpha(this)), Color.red(this), Color.green(this), Color.blue(this)) +fun Int.withMinAlpha(@IntRange(from = 0, to = 0xFF) alpha: Int): Int = + withAlpha(Math.max(alpha, this ushr 24)) @ColorInt -fun Int.lighten(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int { +private inline fun Int.colorFactor(rgbFactor: (Int) -> Float): Int { val (red, green, blue) = intArrayOf(Color.red(this), Color.green(this), Color.blue(this)) - .map { (it * (1f - factor) + 255f * factor).toInt() } + .map { rgbFactor(it).toInt() } return Color.argb(Color.alpha(this), red, green, blue) } @ColorInt -fun Int.darken(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int { - val (red, green, blue) = intArrayOf(Color.red(this), Color.green(this), Color.blue(this)) - .map { (it * (1f - factor)).toInt() } - return Color.argb(Color.alpha(this), red, green, blue) +fun Int.lighten(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int = colorFactor { + (it * (1f - factor) + 255f * factor) +} + +@ColorInt +fun Int.darken(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int = colorFactor { + it * (1f - factor) } @ColorInt @@ -147,13 +150,11 @@ fun Int.colorToBackground(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f fun Int.colorToForeground(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int = if (isColorDark) lighten(factor) else darken(factor) -@Throws(IllegalArgumentException::class) fun String.toColor(): Int { - val toParse: String - if (startsWith("#") && length == 4) - toParse = "#${this[1]}${this[1]}${this[2]}${this[2]}${this[3]}${this[3]}" + val toParse: String = if (startsWith("#") && length == 4) + "#${this[1]}${this[1]}${this[2]}${this[2]}${this[3]}${this[3]}" else - toParse = this + this return Color.parseColor(toParse) } diff --git a/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt b/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt index 1cce9ae..b9c200a 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/utils/UtilsTest.kt @@ -31,6 +31,24 @@ class UtilsTest { assertEquals("#ffffff", Color.WHITE.toHexString(withAlpha = false, withHexPrefix = true).toLowerCase()) } + @Test + fun colorWithAlpha() { + val origColor = 0xFF123456.toInt() + assertEquals(0x00123456, origColor.withAlpha(0), "Failed to convert with alpha 0") + assertEquals(0x50123456, origColor.withAlpha(80), "Failed to convert with alpha 80") + assertEquals(0xFF123456.toInt(), origColor.withAlpha(255), "Failed to convert with alpha 255") + assertEquals(0xFF123456.toInt(), origColor.withAlpha(0xFF), "Failed to convert with alpha 0xFF") + assertEquals(Color.TRANSPARENT, Color.BLACK.withAlpha(0), "Failed to convert black to transparent") + } + + @Test + fun colorWithMinAlpha() { + val origColor = 0x80123456.toInt() + assertEquals(origColor, origColor.withMinAlpha(0), "Failed to convert with min alpha 0") + assertEquals(0xFA123456.toInt(), origColor.withMinAlpha(0xFA), "Failed to convert with min alpha 0xFA") + assertEquals(Color.BLUE, Color.BLUE.withMinAlpha(89), "Failed to convert blue with min alpha 89") + } + @Test fun rounding() { assertEquals("1.23", 1.23456f.round(2)) -- cgit v1.2.3 From d61ff7cb4f43d71d2170cdd25ceab2e3edcb81fc Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Fri, 4 Jan 2019 23:03:50 -0500 Subject: Coroutine tests (#185) * Add some coroutine tests for implicit cancellation * Create util test and new helper methods * Remove coroutinescope extension from withcontext * Update dependencies --- .../main/groovy/ca/allanwang/kau/Versions.groovy | 8 +-- .../kotlin/ca/allanwang/kau/utils/ContextUtils.kt | 27 ---------- .../ca/allanwang/kau/utils/CoroutineUtils.kt | 60 ++++++++++++++++++++++ .../ca/allanwang/kau/kotlin/CoroutineTest.kt | 58 +++++++++++++++++++++ docs/Changelog.md | 1 + sample/src/main/res/xml/kau_changelog.xml | 2 +- 6 files changed, 125 insertions(+), 31 deletions(-) create mode 100644 core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt create mode 100644 core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt (limited to 'core/src/test/kotlin/ca/allanwang') diff --git a/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy b/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy index e6f3cd7..4afec82 100644 --- a/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy +++ b/buildSrc/src/main/groovy/ca/allanwang/kau/Versions.groovy @@ -27,7 +27,7 @@ class Versions { static def kotlin = '1.3.11' // https://github.com/Kotlin/kotlinx.coroutines/releases - static def coroutines = '1.0.1' + static def coroutines = '1.1.0' // https://github.com/mikepenz/AboutLibraries/releases static def aboutLibraries = '6.2.0' @@ -72,6 +72,8 @@ class Versions { static def playPublishPlugin = '1.2.2' // https://github.com/KeepSafe/dexcount-gradle-plugin/releases - static def dexCountPlugin = '0.8.3' - static def gitVersionPlugin = '0.4.4' + static def dexCountPlugin = '0.8.5' + + // https://github.com/gladed/gradle-android-git-version/releases + static def gitVersionPlugin = '0.4.7' } \ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt index 09ad4ea..fc8049d 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt @@ -26,7 +26,6 @@ import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle -import android.os.Handler import android.os.Looper import android.util.TypedValue import android.view.View @@ -47,36 +46,10 @@ import androidx.core.content.ContextCompat import ca.allanwang.kau.R import ca.allanwang.kau.logging.KL import com.afollestad.materialdialogs.MaterialDialog -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.android.asCoroutineDispatcher -import kotlin.coroutines.CoroutineContext /** * Created by Allan Wang on 2017-06-03. */ -object ContextHelper : CoroutineScope { - - val looper = Looper.getMainLooper() - - val handler = Handler(looper) - - /** - * Creating dispatcher from main handler to avoid IO - * See https://github.com/Kotlin/kotlinx.coroutines/issues/878 - */ - val dispatcher = handler.asCoroutineDispatcher("kau-main") - - override val coroutineContext: CoroutineContext get() = dispatcher -} - -/** - * Most context items implement [CoroutineScope] by default. - * We will add a fallback just in case. - * It is expected that the scope returned always has the Android main dispatcher as part of the context. - */ -internal inline val Context.ctxCoroutine: CoroutineScope - get() = this as? CoroutineScope ?: ContextHelper - fun Context.runOnUiThread(f: Context.() -> Unit) { if (ContextHelper.looper === Looper.myLooper()) f() else ContextHelper.handler.post { f() } } diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt new file mode 100644 index 0000000..032c407 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt @@ -0,0 +1,60 @@ +package ca.allanwang.kau.utils + +import android.content.Context +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.android.asCoroutineDispatcher +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +object ContextHelper : CoroutineScope { + + val looper = Looper.getMainLooper() + + val handler = Handler(looper) + + /** + * Creating dispatcher from main handler to avoid IO + * See https://github.com/Kotlin/kotlinx.coroutines/issues/878 + */ + val dispatcher = handler.asCoroutineDispatcher("kau-main") + + override val coroutineContext: CoroutineContext get() = dispatcher +} + +/** + * Most context items implement [CoroutineScope] by default. + * We will add a fallback just in case. + * It is expected that the scope returned always has the Android main dispatcher as part of the context. + */ +internal inline val Context.ctxCoroutine: CoroutineScope + get() = this as? CoroutineScope ?: ContextHelper + +/** + * Calls [launch] with an explicit dispatcher for Android's main thread + */ +fun CoroutineScope.launchMain( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit +) = launch(ContextHelper.dispatcher + context, start, block) + +/** + * Calls [async] with an explicit dispatcher for Android's main thread + */ +fun CoroutineScope.asyncMain( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + block: suspend CoroutineScope.() -> Unit +) = async(ContextHelper.dispatcher + context, start, block) + +/** + * Calls [withContext] with an explicit dispatcher for Android's main thread + */ +suspend fun withMainContext(block: suspend CoroutineScope.() -> T) = + withContext(ContextHelper.dispatcher, block) diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt new file mode 100644 index 0000000..91d0174 --- /dev/null +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt @@ -0,0 +1,58 @@ +package ca.allanwang.kau.kotlin + +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +/** + * Tests geared towards coroutines + */ +class CoroutineTest { + + /** + * If a job is cancelled, then a switch to a new context will not run + */ + @Test + fun implicitCancellationBefore() { + val job = Job() + var id = 0 + try { + runBlocking(job) { + id++ + job.cancel() + withContext(Dispatchers.IO) { + fail("Context switch should not be reached") + } + } + } catch (ignore: CancellationException) { + } finally { + assertEquals(1, id, "Launcher never executed") + } + } + + /** + * If a job is cancelled, then a switch from a new context will not run + */ + @Test + fun implicitCancellationAfter() { + val job = Job() + var id = 0 + try { + runBlocking(job) { + withContext(Dispatchers.IO) { + id++ + job.cancel() + } + fail("Post context switch should not be reached") + } + } catch (ignore: CancellationException) { + } finally { + assertEquals(1, id, "Context switch never executed") + } + } +} \ No newline at end of file diff --git a/docs/Changelog.md b/docs/Changelog.md index e6d51d2..7291753 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -5,6 +5,7 @@ * :core: Add default CoroutineScope implementation to KauBaseActivity * :core: Remove zip class. Coroutines and join can be used as an alternative * :core: Delete flyweight implementation. Kotlin already has getOrPut +* :core: Introduce ContextHelper, where you can get the default looper, handler, and dispatcher for Android * :mediapicker: Use video preloading instead of full async loading ## v4.0.0-alpha01 diff --git a/sample/src/main/res/xml/kau_changelog.xml b/sample/src/main/res/xml/kau_changelog.xml index 0283d7f..570a0b9 100644 --- a/sample/src/main/res/xml/kau_changelog.xml +++ b/sample/src/main/res/xml/kau_changelog.xml @@ -11,10 +11,10 @@ + - -- cgit v1.2.3 From 72d6461a9055929aa5774f1982527c7b9ff406cd Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Fri, 4 Jan 2019 23:06:29 -0500 Subject: Remove frost tag from media method, resolves #186 --- .../kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt | 1 - .../kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt | 15 +++++++++++++++ .../kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt | 17 ++++++++++++++++- .../kotlin/ca/allanwang/kau/mediapicker/MediaUtils.kt | 9 ++++++--- 4 files changed, 37 insertions(+), 5 deletions(-) (limited to 'core/src/test/kotlin/ca/allanwang') diff --git a/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt b/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt index be23d45..cb8f4bf 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/internal/KauBaseActivity.kt @@ -20,7 +20,6 @@ import androidx.appcompat.app.AppCompatActivity import ca.allanwang.kau.permissions.kauOnRequestPermissionsResult import ca.allanwang.kau.utils.ContextHelper import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlin.coroutines.CoroutineContext diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt index 032c407..57a9921 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/CoroutineUtils.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2019 Allan Wang + * + * 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 ca.allanwang.kau.utils import android.content.Context diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt index 91d0174..1e86305 100644 --- a/core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt +++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/CoroutineTest.kt @@ -1,3 +1,18 @@ +/* + * Copyright 2019 Allan Wang + * + * 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 ca.allanwang.kau.kotlin import kotlinx.coroutines.CancellationException @@ -55,4 +70,4 @@ class CoroutineTest { assertEquals(1, id, "Context switch never executed") } } -} \ No newline at end of file +} diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaUtils.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaUtils.kt index 32e64e4..ff6784b 100644 --- a/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaUtils.kt +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/mediapicker/MediaUtils.kt @@ -42,14 +42,17 @@ internal fun Activity.finish(data: ArrayList) { else finish() } +/** + * Creates a folder named [prefix] as well as a new file with the prefix, current time, and extension. + */ @Throws(IOException::class) fun createMediaFile(prefix: String, extension: String): File { val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) val imageFileName = "${prefix}_${timeStamp}_" val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) - val frostDir = File(storageDir, prefix) - if (!frostDir.exists()) frostDir.mkdirs() - return File.createTempFile(imageFileName, extension, frostDir) + val prefixDir = File(storageDir, prefix) + if (!prefixDir.exists()) prefixDir.mkdirs() + return File.createTempFile(imageFileName, extension, prefixDir) } @Throws(IOException::class) -- cgit v1.2.3