diff --git a/android-bindings/src/main/kotlin/net/aquadc/properties/android/LooperExecutorFactory.kt b/android-bindings/src/main/kotlin/net/aquadc/properties/android/LooperExecutorFactory.kt deleted file mode 100644 index d4543827..00000000 --- a/android-bindings/src/main/kotlin/net/aquadc/properties/android/LooperExecutorFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.aquadc.properties.android - -import android.os.Handler -import android.os.Looper -import java.util.concurrent.Executor - -/** - * Creates [Executor] for given Android [Looper] [Thread]. - * @see net.aquadc.properties.internal.executorForThread - */ -class LooperExecutorFactory : (Thread) -> Executor? { - override fun invoke(p1: Thread): Executor? { - val my = Looper.myLooper() - ?: return null - - val handler = Handler(my) - return Executor { handler.post(it) } - } -} diff --git a/properties/build.gradle b/properties/build.gradle index 1a407490..5cd6fd69 100644 --- a/properties/build.gradle +++ b/properties/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin' dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + compileOnly project(':fake-android') testCompile group: 'junit', name: 'junit', version: '4.12' } diff --git a/properties/src/main/kotlin/net/aquadc/properties/executor/android.kt b/properties/src/main/kotlin/net/aquadc/properties/executor/android.kt new file mode 100644 index 00000000..955e1692 --- /dev/null +++ b/properties/src/main/kotlin/net/aquadc/properties/executor/android.kt @@ -0,0 +1,17 @@ +package net.aquadc.properties.executor + +import android.os.Handler +import java.util.concurrent.Executor + +/** + * [Executor] implementation based on Android's [Handler]. + * Will cause [NoClassDefFoundError] if called out of Android. + */ +class HandlerAsExecutor( + private val handler: Handler +) : Executor { + + override fun execute(command: Runnable) = + check(handler.post(command)) + +} diff --git a/properties/src/main/kotlin/net/aquadc/properties/executor/fx.kt b/properties/src/main/kotlin/net/aquadc/properties/executor/fx.kt new file mode 100644 index 00000000..13620d7e --- /dev/null +++ b/properties/src/main/kotlin/net/aquadc/properties/executor/fx.kt @@ -0,0 +1,14 @@ +package net.aquadc.properties.executor + +import javafx.application.Platform +import java.util.concurrent.Executor + +/** + * Wraps [Platform] to run JavaFX Application Thread. + */ +object FxApplicationThreadExecutor : Executor { + + override fun execute(command: Runnable) = + Platform.runLater(command) + +} diff --git a/properties/src/main/kotlin/net/aquadc/properties/executor/util.kt b/properties/src/main/kotlin/net/aquadc/properties/executor/util.kt new file mode 100644 index 00000000..75fb31a6 --- /dev/null +++ b/properties/src/main/kotlin/net/aquadc/properties/executor/util.kt @@ -0,0 +1,56 @@ +package net.aquadc.properties.executor + +import android.os.Handler +import android.os.Looper +import javafx.application.Platform +import java.util.concurrent.* + + +internal object ScheduledDaemonHolder { + internal val scheduledDaemon = + ScheduledThreadPoolExecutor(1, ThreadFactory { Thread(it).also { it.isDaemon = true } }) +} + +internal object PlatformExecutors { + private val executors = ConcurrentHashMap(4) + private val executorFactories: Array<() -> Executor?> + + init { + val facs = ArrayList<() -> Executor?>(2) + + try { + Looper.myLooper() // ensure class available + facs.add { + Looper.myLooper()?.let { myLooper -> HandlerAsExecutor(Handler(myLooper)) } + } + } catch (ignored: NoClassDefFoundError) {} + + try { + Platform.isFxApplicationThread() // ensure class available + facs.add { + if (Platform.isFxApplicationThread()) FxApplicationThreadExecutor else null + } + } catch (ignored: NoClassDefFoundError) {} + + executorFactories = facs.toTypedArray() + } + + internal fun executorForCurrentThread(): Executor { + val thread = Thread.currentThread() + + val ex = executors[thread] + if (ex != null) return ex + + val newEx = createForCurrentThread() + + // if putIfAbsent returns non-null value, the executor was set concurrently, + // use it and throw away our then + return executors.putIfAbsent(thread, newEx) ?: newEx + } + + private fun createForCurrentThread(): Executor { + executorFactories.forEach { it()?.let { return it } } + throw UnsupportedOperationException("Can't execute task on $this") + } + +} diff --git a/properties/src/main/kotlin/net/aquadc/properties/internal/ConcDebouncedProperty.kt b/properties/src/main/kotlin/net/aquadc/properties/internal/ConcDebouncedProperty.kt index 1c590e64..2040db4b 100644 --- a/properties/src/main/kotlin/net/aquadc/properties/internal/ConcDebouncedProperty.kt +++ b/properties/src/main/kotlin/net/aquadc/properties/internal/ConcDebouncedProperty.kt @@ -2,10 +2,11 @@ package net.aquadc.properties.internal import net.aquadc.properties.ChangeListener import net.aquadc.properties.Property +import net.aquadc.properties.executor.PlatformExecutors +import net.aquadc.properties.executor.ScheduledDaemonHolder import java.util.concurrent.CopyOnWriteArrayList +import java.util.concurrent.Executor import java.util.concurrent.ScheduledFuture -import java.util.concurrent.ScheduledThreadPoolExecutor -import java.util.concurrent.ThreadFactory import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicReferenceFieldUpdater @@ -34,23 +35,13 @@ class ConcDebouncedProperty( it.first } - Pair(reallyOld, scheduled.schedule(NotifyOnCorrectThread(listeners, reallyOld, new), delay, unit)) - } - } - } - - /** - * Why not a lambda? This class's private modifier helps ProGuard find out that outer is not used. - * Btw, this not helps much: if `debounced` used anywhere, outer will be kept. - */ - private class NotifyOnCorrectThread( - private val listeners: List>>, - private val old: T, - private val new: T - ) : Runnable { - override fun run() { - listeners.forEach { - PlatformExecutors.executorForThread(it.first).execute { it.second(old, new) } + Pair(reallyOld, + ScheduledDaemonHolder.scheduledDaemon.schedule({ + listeners.forEach { + it.first.execute { it.second(reallyOld, new) } + } + }, delay, unit) + ) } } } @@ -58,11 +49,9 @@ class ConcDebouncedProperty( override fun getValue(): T = original.getValue() - private val listeners = CopyOnWriteArrayList>>() + private val listeners = CopyOnWriteArrayList>>() override fun addChangeListener(onChange: (old: T, new: T) -> Unit) { - val thread = Thread.currentThread() - PlatformExecutors.executorForThread(thread) // ensure such executor exists - listeners.add(thread to onChange) + listeners.add(PlatformExecutors.executorForCurrentThread() to onChange) } override fun removeChangeListener(onChange: (old: T, new: T) -> Unit) { listeners.firstOrNull { it.second == onChange }?.let { listeners.remove(it) } @@ -75,8 +64,6 @@ class ConcDebouncedProperty( @Suppress("NOTHING_TO_INLINE", "UNCHECKED_CAST") private inline fun pendingUpdater() = pendingUpdater as AtomicReferenceFieldUpdater, Pair>?> - - internal val scheduled = ScheduledThreadPoolExecutor(1, ThreadFactory { Thread(it).also { it.isDaemon = true } }) } } diff --git a/properties/src/main/kotlin/net/aquadc/properties/internal/UnsDebouncedProperty.kt b/properties/src/main/kotlin/net/aquadc/properties/internal/UnsDebouncedProperty.kt index 0c730d0a..f8b6346a 100644 --- a/properties/src/main/kotlin/net/aquadc/properties/internal/UnsDebouncedProperty.kt +++ b/properties/src/main/kotlin/net/aquadc/properties/internal/UnsDebouncedProperty.kt @@ -1,6 +1,8 @@ package net.aquadc.properties.internal import net.aquadc.properties.Property +import net.aquadc.properties.executor.PlatformExecutors +import net.aquadc.properties.executor.ScheduledDaemonHolder import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit @@ -14,7 +16,7 @@ class UnsDebouncedProperty( private val unit: TimeUnit ) : UnsListeners() { - private val executor = PlatformExecutors.executorForThread(Thread.currentThread()) + private val executor = PlatformExecutors.executorForCurrentThread() private var pending: Pair>? = null init { @@ -29,7 +31,7 @@ class UnsDebouncedProperty( it.first } - pending = Pair(reallyOld, ConcDebouncedProperty.scheduled.schedule({ + pending = Pair(reallyOld, ScheduledDaemonHolder.scheduledDaemon.schedule({ executor.execute { valueChanged(reallyOld, new) } diff --git a/properties/src/main/kotlin/net/aquadc/properties/internal/concPropUtils.kt b/properties/src/main/kotlin/net/aquadc/properties/internal/concPropUtils.kt index 2c7d6d51..ba66c564 100644 --- a/properties/src/main/kotlin/net/aquadc/properties/internal/concPropUtils.kt +++ b/properties/src/main/kotlin/net/aquadc/properties/internal/concPropUtils.kt @@ -1,10 +1,6 @@ package net.aquadc.properties.internal -import javafx.application.Platform -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executor import java.util.concurrent.atomic.AtomicReferenceFieldUpdater -import kotlin.collections.ArrayList internal inline fun AtomicReferenceFieldUpdater.update(zis: T, update: (V) -> V) { @@ -45,38 +41,3 @@ internal inline fun Array.withoutNulls(): Array { return newArray as Array } - -object PlatformExecutors { - private val executors = ConcurrentHashMap() - private val executorFactories: Array<(Thread) -> Executor?> - - init { - val facs = ArrayList<(Thread) -> Executor?>(2) - - try { - facs.add(Class.forName("net.aquadc.properties.android.LooperExecutorFactory").newInstance() as (Thread) -> Executor?) - } catch (ignored: ClassNotFoundException) {} - - try { - Platform.isFxApplicationThread() // ensure class available - facs.add { if (Platform.isFxApplicationThread()) Executor(Platform::runLater) else null } - } catch (ignored: NoClassDefFoundError) {} - - executorFactories = facs.toTypedArray() - } - - internal fun executorForThread(thread: Thread): Executor { - val ex = executors[thread] - if (ex == null) { - val newEx = thread.createExecutor() - executors.putIfAbsent(thread, newEx) - } - return executors[thread]!! - } - - private fun Thread.createExecutor(): Executor { - executorFactories.forEach { it(this)?.let { return it } } - throw UnsupportedOperationException("Can't schedule task on $this") - } - -} \ No newline at end of file