Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement haptic feedback #59

Merged
merged 11 commits into from
Nov 1, 2023
Merged
8 changes: 5 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
</intent>
</queries>

<uses-permission android:name="android.permission.VIBRATE" />

<application
android:name=".SpeakTouchApplication"
android:allowBackup="true"
Expand All @@ -16,7 +18,8 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.SpeakTouch"
tools:targetApi="31">
tools:targetApi="31"
android:icon="@null">

<service
android:name=".service.SpeakTouchService"
Expand All @@ -26,8 +29,7 @@
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />

<!-- <category android:name="android.accessibilityservice.category.FEEDBACK_AUDIBLE" />-->
<!-- <category android:name="android.accessibilityservice.category.FEEDBACK_HAPTIC" />-->
<category android:name="android.accessibilityservice.category.FEEDBACK_HAPTIC" />
<category android:name="android.accessibilityservice.category.FEEDBACK_SPOKEN" />
</intent-filter>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Handle haptic feedback.
*
* Copyright (C) 2023 Irineu A. Silva.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.neo.speaktouch.intercepter

import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.neo.speaktouch.intercepter.interfece.Interceptor
import com.neo.speaktouch.utils.VibrationUtil

class HapticInterceptor(
private val vibration: VibrationUtil
) : Interceptor {

override fun handle(event: AccessibilityEvent) {

if (!vibration.hasVibrator()) return

if (event.eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
handleFocusVibration(event.source ?: return)
}
}

private fun handleFocusVibration(nodeInfo: AccessibilityNodeInfo) {

if (nodeInfo.isClickable) {
vibration.vibrateEffectHeavyClick()
return
}

vibration.vibrateEffectTick()

return
}

override fun finish() = vibration.finish()
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ class SpeechInterceptor(
speak(reader.getContent(nodeInfo))
}

fun shutdown() {
override fun finish() {

Timber.i("shutdown")
Timber.i("finish")

textToSpeech.shutdown()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ import android.view.accessibility.AccessibilityEvent

interface Interceptor {
fun handle(event: AccessibilityEvent)
fun finish() = Unit
}
19 changes: 13 additions & 6 deletions app/src/main/java/com/neo/speaktouch/service/SpeakTouchService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ package com.neo.speaktouch.service
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent
import com.neo.speaktouch.intercepter.FocusInterceptor
import com.neo.speaktouch.intercepter.HapticInterceptor
import com.neo.speaktouch.intercepter.SpeechInterceptor
import com.neo.speaktouch.intercepter.interfece.Interceptor
import com.neo.speaktouch.utils.extension.getInstance
import com.neo.speaktouch.utils.extension.getLog
import com.neo.speaktouch.utils.VibrationUtil
import timber.log.Timber

class SpeakTouchService : AccessibilityService() {
Expand All @@ -35,23 +36,29 @@ class SpeakTouchService : AccessibilityService() {
super.onCreate()

interceptors.add(FocusInterceptor())

interceptors.add(SpeechInterceptor.getInstance(this))

interceptors.add(
HapticInterceptor(
vibration = VibrationUtil(this),
)
)
}

override fun onAccessibilityEvent(event: AccessibilityEvent) {

Timber.d(event.getLog())

interceptors.forEach { it.handle(event) }
interceptors.forEach {
it.handle(event)
}
}

override fun onDestroy() {
super.onDestroy()

interceptors
.getInstance<SpeechInterceptor>()
.shutdown()

interceptors.forEach(Interceptor::finish)
interceptors.clear()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Legacy effects equivalent to android.os.VibrationEffect.
*
* Copyright (C) 2023 Irineu A. Silva.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.neo.speaktouch.utils

sealed class LegacyVibrationEffect(val pattern: LongArray) {
object HeavyClick : LegacyVibrationEffect(
pattern = longArrayOf(
0, 50, 50
)
)

object Tick : LegacyVibrationEffect(
pattern = longArrayOf(
0, 30
)
)
}
112 changes: 112 additions & 0 deletions app/src/main/java/com/neo/speaktouch/utils/VibrationUtil.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Vibration utility wrapper.
*
* Copyright (C) 2023 Irineu A. Silva.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.neo.speaktouch.utils

import android.content.Context
import android.os.Build
Irineu333 marked this conversation as resolved.
Show resolved Hide resolved
import android.os.VibrationAttributes
import android.os.VibrationEffect
import android.os.Vibrator

class VibrationUtil(private val vibrator: Vibrator) {

@Suppress("deprecation")
constructor(context: Context) : this(
context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
)

fun vibrateEffectHeavyClick() {

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
vibrator.vibrate(
VibrationEffect.createPredefined(
VibrationEffect.EFFECT_HEAVY_CLICK
),
VibrationAttributes.createForUsage(
VibrationAttributes.USAGE_ACCESSIBILITY
)
)

return
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
vibrator.vibrate(
VibrationEffect.createPredefined(
VibrationEffect.EFFECT_HEAVY_CLICK
)
)

return
}

vibrateLegacy(
LegacyVibrationEffect.HeavyClick
)
}

fun vibrateEffectTick() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
vibrator.vibrate(
VibrationEffect.createPredefined(
VibrationEffect.EFFECT_TICK
),
VibrationAttributes.createForUsage(
VibrationAttributes.USAGE_ACCESSIBILITY
)
)

return
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
vibrator.vibrate(
VibrationEffect.createPredefined(
VibrationEffect.EFFECT_TICK
)
)

return
}

vibrateLegacy(
LegacyVibrationEffect.Tick,
)
}

@Suppress("deprecation")
fun vibrateLegacy(
legacyVibrationEffect: LegacyVibrationEffect,
repeat: Int = DEFAULT_REPEAT,
) {
vibrator.vibrate(
legacyVibrationEffect.pattern,
repeat
)
}

fun hasVibrator() = vibrator.hasVibrator()

fun finish() = vibrator.cancel()

companion object {
private const val DEFAULT_REPEAT = -1
}
}

21 changes: 21 additions & 0 deletions app/src/main/java/com/neo/speaktouch/utils/extension/IntArray.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Extensions for IntArray.
*
* Copyright (C) 2023 Irineu A. Silva.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

package com.neo.speaktouch.utils.extension

fun IntArray.toLongArray() = LongArray(size) { this[it].toLong() }
2 changes: 1 addition & 1 deletion app/src/main/res/xml-v26/accessibility_service_config.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackSpoken"
android:accessibilityFeedbackType="feedbackSpoken|feedbackHaptic"
android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode|flagRetrieveInteractiveWindows|flagEnableAccessibilityVolume"
android:canRequestTouchExplorationMode="true"
android:canRetrieveWindowContent="true"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/xml-v31/accessibility_service_config.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackSpoken"
android:accessibilityFeedbackType="feedbackSpoken|feedbackHaptic"
android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode|flagRetrieveInteractiveWindows|flagEnableAccessibilityVolume"
android:canRequestTouchExplorationMode="true"
android:canRetrieveWindowContent="true"
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/xml/accessibility_service_config.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeAllMask"
android:accessibilityFeedbackType="feedbackSpoken"
android:accessibilityFeedbackType="feedbackSpoken|feedbackHaptic"
android:accessibilityFlags="flagDefault|flagRequestTouchExplorationMode|flagRetrieveInteractiveWindows"
android:canRequestTouchExplorationMode="true"
android:canRetrieveWindowContent="true"
Expand Down