Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions jindong-compose/api/jindong-compose.api
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
public final class io/github/compose/jindong/HapticCapabilities {
public static final field $stable I
public fun <init> (ZZLio/github/compose/jindong/HapticPlatform;)V
public final fun component1 ()Z
public final fun component2 ()Z
public final fun component3 ()Lio/github/compose/jindong/HapticPlatform;
public final fun copy (ZZLio/github/compose/jindong/HapticPlatform;)Lio/github/compose/jindong/HapticCapabilities;
public static synthetic fun copy$default (Lio/github/compose/jindong/HapticCapabilities;ZZLio/github/compose/jindong/HapticPlatform;ILjava/lang/Object;)Lio/github/compose/jindong/HapticCapabilities;
public fun equals (Ljava/lang/Object;)Z
public final fun getHasAmplitudeControl ()Z
public final fun getPlatform ()Lio/github/compose/jindong/HapticPlatform;
public fun hashCode ()I
public final fun isSupported ()Z
public fun toString ()Ljava/lang/String;
}

public final class io/github/compose/jindong/HapticCapabilitiesKt {
public static final fun rememberHapticCapabilities (Landroidx/compose/runtime/Composer;I)Lio/github/compose/jindong/HapticCapabilities;
}

public final class io/github/compose/jindong/HapticPlatform : java/lang/Enum {
public static final field Android Lio/github/compose/jindong/HapticPlatform;
public static final field Ios Lio/github/compose/jindong/HapticPlatform;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lio/github/compose/jindong/HapticPlatform;
public static fun values ()[Lio/github/compose/jindong/HapticPlatform;
}

public final class io/github/compose/jindong/JindongKt {
public static final fun Jindong ([Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;I)V
}
Expand Down
34 changes: 34 additions & 0 deletions jindong-compose/api/jindong-compose.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,46 @@ open annotation class io.github.compose.jindong/JindongScopeMarker : kotlin/Anno
constructor <init>() // io.github.compose.jindong/JindongScopeMarker.<init>|<init>(){}[0]
}

final enum class io.github.compose.jindong/HapticPlatform : kotlin/Enum<io.github.compose.jindong/HapticPlatform> { // io.github.compose.jindong/HapticPlatform|null[0]
enum entry Android // io.github.compose.jindong/HapticPlatform.Android|null[0]
enum entry Ios // io.github.compose.jindong/HapticPlatform.Ios|null[0]

final val entries // io.github.compose.jindong/HapticPlatform.entries|#static{}entries[0]
final fun <get-entries>(): kotlin.enums/EnumEntries<io.github.compose.jindong/HapticPlatform> // io.github.compose.jindong/HapticPlatform.entries.<get-entries>|<get-entries>#static(){}[0]

final fun valueOf(kotlin/String): io.github.compose.jindong/HapticPlatform // io.github.compose.jindong/HapticPlatform.valueOf|valueOf#static(kotlin.String){}[0]
final fun values(): kotlin/Array<io.github.compose.jindong/HapticPlatform> // io.github.compose.jindong/HapticPlatform.values|values#static(){}[0]
}

abstract interface io.github.compose.jindong/JindongScope // io.github.compose.jindong/JindongScope|null[0]

final class io.github.compose.jindong/HapticCapabilities { // io.github.compose.jindong/HapticCapabilities|null[0]
constructor <init>(kotlin/Boolean, kotlin/Boolean, io.github.compose.jindong/HapticPlatform) // io.github.compose.jindong/HapticCapabilities.<init>|<init>(kotlin.Boolean;kotlin.Boolean;io.github.compose.jindong.HapticPlatform){}[0]

final val hasAmplitudeControl // io.github.compose.jindong/HapticCapabilities.hasAmplitudeControl|{}hasAmplitudeControl[0]
final fun <get-hasAmplitudeControl>(): kotlin/Boolean // io.github.compose.jindong/HapticCapabilities.hasAmplitudeControl.<get-hasAmplitudeControl>|<get-hasAmplitudeControl>(){}[0]
final val isSupported // io.github.compose.jindong/HapticCapabilities.isSupported|{}isSupported[0]
final fun <get-isSupported>(): kotlin/Boolean // io.github.compose.jindong/HapticCapabilities.isSupported.<get-isSupported>|<get-isSupported>(){}[0]
final val platform // io.github.compose.jindong/HapticCapabilities.platform|{}platform[0]
final fun <get-platform>(): io.github.compose.jindong/HapticPlatform // io.github.compose.jindong/HapticCapabilities.platform.<get-platform>|<get-platform>(){}[0]

final fun component1(): kotlin/Boolean // io.github.compose.jindong/HapticCapabilities.component1|component1(){}[0]
final fun component2(): kotlin/Boolean // io.github.compose.jindong/HapticCapabilities.component2|component2(){}[0]
final fun component3(): io.github.compose.jindong/HapticPlatform // io.github.compose.jindong/HapticCapabilities.component3|component3(){}[0]
final fun copy(kotlin/Boolean = ..., kotlin/Boolean = ..., io.github.compose.jindong/HapticPlatform = ...): io.github.compose.jindong/HapticCapabilities // io.github.compose.jindong/HapticCapabilities.copy|copy(kotlin.Boolean;kotlin.Boolean;io.github.compose.jindong.HapticPlatform){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // io.github.compose.jindong/HapticCapabilities.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // io.github.compose.jindong/HapticCapabilities.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // io.github.compose.jindong/HapticCapabilities.toString|toString(){}[0]
}

final val io.github.compose.jindong/io_github_compose_jindong_HapticCapabilities$stableprop // io.github.compose.jindong/io_github_compose_jindong_HapticCapabilities$stableprop|#static{}io_github_compose_jindong_HapticCapabilities$stableprop[0]

final fun (io.github.compose.jindong/JindongScope).io.github.compose.jindong.dsl/Delay(kotlin.time/Duration, androidx.compose.runtime/Composer?, kotlin/Int) // io.github.compose.jindong.dsl/Delay|Delay@io.github.compose.jindong.JindongScope(kotlin.time.Duration;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun (io.github.compose.jindong/JindongScope).io.github.compose.jindong.dsl/Haptic(kotlin.time/Duration, io.github.compose.jindong.core.model/HapticIntensity?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.github.compose.jindong.dsl/Haptic|Haptic@io.github.compose.jindong.JindongScope(kotlin.time.Duration;io.github.compose.jindong.core.model.HapticIntensity?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0]
final fun (io.github.compose.jindong/JindongScope).io.github.compose.jindong.dsl/Repeat(kotlin/Int, kotlin/Function3<io.github.compose.jindong/JindongScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // io.github.compose.jindong.dsl/Repeat|Repeat@io.github.compose.jindong.JindongScope(kotlin.Int;kotlin.Function3<io.github.compose.jindong.JindongScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun (io.github.compose.jindong/JindongScope).io.github.compose.jindong.dsl/RepeatWithIndex(kotlin/Int, kotlin/Function4<io.github.compose.jindong/JindongScope, kotlin/Int, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // io.github.compose.jindong.dsl/RepeatWithIndex|RepeatWithIndex@io.github.compose.jindong.JindongScope(kotlin.Int;kotlin.Function4<io.github.compose.jindong.JindongScope,kotlin.Int,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun (io.github.compose.jindong/JindongScope).io.github.compose.jindong.dsl/Sequence(kotlin/Function3<io.github.compose.jindong/JindongScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // io.github.compose.jindong.dsl/Sequence|Sequence@io.github.compose.jindong.JindongScope(kotlin.Function3<io.github.compose.jindong.JindongScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun io.github.compose.jindong/Jindong(kotlin/Array<out kotlin/Any?>..., kotlin/Function3<io.github.compose.jindong/JindongScope, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // io.github.compose.jindong/Jindong|Jindong(kotlin.Array<out|kotlin.Any?>...;kotlin.Function3<io.github.compose.jindong.JindongScope,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun io.github.compose.jindong/JindongProvider(kotlin/Function2<androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int) // io.github.compose.jindong/JindongProvider|JindongProvider(kotlin.Function2<androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
final fun io.github.compose.jindong/io_github_compose_jindong_HapticCapabilities$stableprop_getter(): kotlin/Int // io.github.compose.jindong/io_github_compose_jindong_HapticCapabilities$stableprop_getter|io_github_compose_jindong_HapticCapabilities$stableprop_getter(){}[0]
final fun io.github.compose.jindong/rememberHapticCapabilities(androidx.compose.runtime/Composer?, kotlin/Int): io.github.compose.jindong/HapticCapabilities // io.github.compose.jindong/rememberHapticCapabilities|rememberHapticCapabilities(androidx.compose.runtime.Composer?;kotlin.Int){}[0]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (C) 2026 compose-jindong
*
* 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 io.github.compose.jindong

internal actual fun hapticPlatform(): HapticPlatform = HapticPlatform.Android
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2026 compose-jindong
*
* 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 io.github.compose.jindong

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import io.github.compose.jindong.executor.LocalHapticExecutor

/**
* Platform a haptic capability reading was taken on.
*/
enum class HapticPlatform {
Android,
Ios,
}

/**
* Snapshot of the current device's haptic capabilities, read from the active [HapticExecutor].
*
* @property isSupported whether haptic feedback is available at all on this device.
* @property hasAmplitudeControl whether the actuator renders intermediate amplitudes. On Android
* this is `false` for ERM motors (every non-zero amplitude rounds up to 100%) and `true` for LRA
* motors. On iOS Core Haptics is always continuous, so this is always `true`; pair it with
* [platform] when the displayed label needs to distinguish "Yes/No" (Android) from
* "Continuous" (iOS).
* @property platform the platform this reading came from.
*/
data class HapticCapabilities(
val isSupported: Boolean,
val hasAmplitudeControl: Boolean,
val platform: HapticPlatform,
)

/**
* Reads the current device's [HapticCapabilities] from the haptic executor provided by
* [JindongProvider]. Must be called inside a `JindongProvider { }` scope.
*
* Example:
* ```
* JindongProvider {
* val capabilities = rememberHapticCapabilities()
* // capabilities.isSupported, capabilities.hasAmplitudeControl, capabilities.platform
* }
* ```
*/
@Composable
fun rememberHapticCapabilities(): HapticCapabilities {
val executor = LocalHapticExecutor.current
val platform = hapticPlatform()
return remember(executor, platform) {
HapticCapabilities(
isSupported = executor.isSupported,
hasAmplitudeControl = executor.hasAmplitudeControl,
platform = platform,
)
}
}

/**
* Returns the [HapticPlatform] the app is currently running on.
*/
internal expect fun hapticPlatform(): HapticPlatform
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2026 compose-jindong
*
* 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 io.github.compose.jindong

import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class HapticCapabilitiesTest :
FunSpec({
test("HapticCapabilities holds the supplied values") {
val capabilities = HapticCapabilities(
isSupported = true,
hasAmplitudeControl = false,
platform = HapticPlatform.Android,
)

assertSoftly(capabilities) {
isSupported shouldBe true
hasAmplitudeControl shouldBe false
platform shouldBe HapticPlatform.Android
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright (C) 2026 compose-jindong
*
* 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 io.github.compose.jindong

internal actual fun hapticPlatform(): HapticPlatform = HapticPlatform.Ios
5 changes: 5 additions & 0 deletions jindong-core/api/jindong-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,15 @@ public final class io/github/compose/jindong/core/element/VibrationElement : io/
public abstract interface class io/github/compose/jindong/core/executor/HapticExecutor {
public abstract fun execute (Lio/github/compose/jindong/core/model/HapticPattern;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun executeAsync (Lio/github/compose/jindong/core/model/HapticPattern;)Lio/github/compose/jindong/core/executor/HapticHandle;
public fun getHasAmplitudeControl ()Z
public abstract fun isSupported ()Z
public abstract fun release ()V
}

public final class io/github/compose/jindong/core/executor/HapticExecutor$DefaultImpls {
public static fun getHasAmplitudeControl (Lio/github/compose/jindong/core/executor/HapticExecutor;)Z
}

public final class io/github/compose/jindong/core/executor/HapticExecutor_androidKt {
public static final fun createHapticExecutor (Ljava/lang/Object;)Lio/github/compose/jindong/core/executor/HapticExecutor;
public static synthetic fun createHapticExecutor$default (Ljava/lang/Object;ILjava/lang/Object;)Lio/github/compose/jindong/core/executor/HapticExecutor;
Expand Down
2 changes: 2 additions & 0 deletions jindong-core/api/jindong-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ abstract interface io.github.compose.jindong.core.element/HapticElement { // io.
abstract interface io.github.compose.jindong.core.executor/HapticExecutor { // io.github.compose.jindong.core.executor/HapticExecutor|null[0]
abstract val isSupported // io.github.compose.jindong.core.executor/HapticExecutor.isSupported|{}isSupported[0]
abstract fun <get-isSupported>(): kotlin/Boolean // io.github.compose.jindong.core.executor/HapticExecutor.isSupported.<get-isSupported>|<get-isSupported>(){}[0]
open val hasAmplitudeControl // io.github.compose.jindong.core.executor/HapticExecutor.hasAmplitudeControl|{}hasAmplitudeControl[0]
open fun <get-hasAmplitudeControl>(): kotlin/Boolean // io.github.compose.jindong.core.executor/HapticExecutor.hasAmplitudeControl.<get-hasAmplitudeControl>|<get-hasAmplitudeControl>(){}[0]

abstract fun executeAsync(io.github.compose.jindong.core.model/HapticPattern): io.github.compose.jindong.core.executor/HapticHandle // io.github.compose.jindong.core.executor/HapticExecutor.executeAsync|executeAsync(io.github.compose.jindong.core.model.HapticPattern){}[0]
abstract fun release() // io.github.compose.jindong.core.executor/HapticExecutor.release|release(){}[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ internal class DefaultAndroidHapticExecutor(context: Context) : HapticExecutor {
vibrator.hasVibrator()
}

// ERM actuators round any non-zero amplitude up to 100%, so per-level intensity is indistinguishable;
// callers read this to warn that LIGHT/MEDIUM/STRONG/HIGH feel identical on such devices.
override val hasAmplitudeControl: Boolean by lazy {
vibrator.hasAmplitudeControl()
}

@RequiresPermission(Manifest.permission.VIBRATE)
override suspend fun execute(pattern: HapticPattern) {
if (!isSupported || pattern.events.isEmpty()) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ interface HapticExecutor {
*/
val isSupported: Boolean

/**
* Whether the actuator can render intermediate (non-binary) amplitudes.
*
* - **Android**: reflects `Vibrator.hasAmplitudeControl()`. ERM motors round every non-zero
* amplitude up to 100%, so they report `false` and effects like fall ramps are lost; LRA
* motors report `true`.
* - **iOS**: Core Haptics renders continuous intensity whenever haptics are supported, so this
* tracks [isSupported]. The "discrete vs. continuous" distinction is platform-specific; read
* the platform alongside this flag if the label matters (see the Compose `HapticCapabilities`).
*
* Defaults to `false` so existing third-party [HapticExecutor] implementations remain
* source- and binary-compatible without having to declare it.
*/
val hasAmplitudeControl: Boolean
Comment thread
coderabbitai[bot] marked this conversation as resolved.
get() = false

/**
* Executes haptic pattern. Cancellable via coroutine cancellation.
* When cancelled, the ongoing vibration is stopped immediately.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2026 compose-jindong
*
* 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 io.github.compose.jindong.core.executor

import io.github.compose.jindong.core.fake.FakeHapticExecutor
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe

class HapticExecutorCapabilityTest :
FunSpec({
test("hasAmplitudeControl defaults to true") {
FakeHapticExecutor().hasAmplitudeControl shouldBe true
}

test("hasAmplitudeControl can be overridden via constructor") {
FakeHapticExecutor(hasAmplitudeControl = false).hasAmplitudeControl shouldBe false
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import kotlinx.coroutines.delay
*/
class FakeHapticExecutor(
override val isSupported: Boolean = true,
override val hasAmplitudeControl: Boolean = true,
) : HapticExecutor {

private val _executedPatterns = mutableListOf<HapticPattern>()
Expand Down
Loading
Loading