1+ package io.snabble.sdk.ui
2+
3+ import android.accessibilityservice.AccessibilityServiceInfo
4+ import android.annotation.SuppressLint
5+ import android.content.Context
6+ import android.view.View
7+ import android.view.ViewGroup
8+ import android.view.accessibility.AccessibilityEvent
9+ import android.view.accessibility.AccessibilityManager
10+ import android.view.accessibility.AccessibilityNodeInfo
11+ import android.widget.TextView
12+ import androidx.annotation.IntDef
13+ import androidx.annotation.StringRes
14+ import androidx.core.view.AccessibilityDelegateCompat
15+ import androidx.core.view.ViewCompat
16+ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
17+ import androidx.core.view.isVisible
18+ import io.snabble.sdk.Snabble
19+
20+ typealias AccessibilityEventListener = (host: ViewGroup ? ,
21+ child: View ? ,
22+ event: AccessibilityEvent ) -> Any?
23+
24+ class AccessibilityToolBox (private val target : View ): AccessibilityDelegateCompat() {
25+ private val eventListeners = mutableMapOf<Int , AccessibilityEventListener >()
26+ private var clickActionLabel: String? = null
27+ private var longClickActionLabel: String? = null
28+ val isTalkBackActive
29+ get() = target.context.isTalkBackActive
30+ var phoneticDict: Map <String , String > = emptyMap()
31+ set(value) {
32+ field = value
33+ (target.contentDescription?.toString() ? : (target as ? TextView )?.text?.toString())?.let { currentValue ->
34+ var newValue = currentValue
35+ field.entries.forEach { (from, to) ->
36+ newValue = newValue.replace(from, to)
37+ }
38+ if (newValue != currentValue) {
39+ target.contentDescription = newValue
40+ }
41+ }
42+ }
43+ private var onInitializeAccessibilityNodeInfo: ((info: AccessibilityNodeInfoCompat )-> Unit )? = null
44+ fun onInitializeAccessibilityNodeInfo (block : (info: AccessibilityNodeInfoCompat )-> Unit ) {
45+ onInitializeAccessibilityNodeInfo = block
46+ }
47+
48+ override fun onRequestSendAccessibilityEvent (
49+ host : ViewGroup ? ,
50+ child : View ? ,
51+ event : AccessibilityEvent ?
52+ ): Boolean {
53+ val listener = event?.let { eventListeners[event.eventType] }
54+ listener?.invoke(host, child, event)
55+ return super .onRequestSendAccessibilityEvent(host, child, event)
56+ }
57+
58+ override fun onInitializeAccessibilityNodeInfo (
59+ host : View ,
60+ info : AccessibilityNodeInfoCompat
61+ ) {
62+ super .onInitializeAccessibilityNodeInfo(host, info)
63+ clickActionLabel?.let {
64+ info.addAction(
65+ AccessibilityNodeInfoCompat .AccessibilityActionCompat (
66+ AccessibilityNodeInfoCompat .ACTION_CLICK ,
67+ clickActionLabel
68+ )
69+ )
70+ }
71+ longClickActionLabel?.let {
72+ info.addAction(
73+ AccessibilityNodeInfoCompat .AccessibilityActionCompat (
74+ AccessibilityNodeInfoCompat .ACTION_LONG_CLICK ,
75+ longClickActionLabel
76+ )
77+ )
78+ }
79+ onInitializeAccessibilityNodeInfo?.invoke(info)
80+ }
81+
82+ fun onAccessibilityEvent (@EventType event : Int , block : AccessibilityEventListener ) {
83+ eventListeners[event] = block
84+ }
85+
86+ fun setLongClickAction (label : String , onLongClick : (() -> Any )? = null) {
87+ longClickActionLabel = label
88+ onLongClick?.let {
89+ target.setOnLongClickListener {
90+ onLongClick()
91+ true
92+ }
93+ }
94+ }
95+
96+ fun setLongClickAction (@StringRes action : Int , onLongClick : (() -> Any )? = null) =
97+ setLongClickAction(target.context.getString(action), onLongClick)
98+
99+ fun setClickAction (label : String , onClick : (() -> Any )? = null) {
100+ clickActionLabel = label
101+ onClick?.let {
102+ target.setOnClickListener {
103+ onClick()
104+ }
105+ }
106+ }
107+
108+ fun setClickAction (@StringRes action : Int , onLongClick : (() -> Any )? = null) =
109+ setClickAction(target.context.getString(action), onLongClick)
110+ }
111+
112+ @SuppressLint(" InlinedApi" )
113+ @IntDef(
114+ flag = true ,
115+ value = [
116+ AccessibilityEvent .TYPE_VIEW_CLICKED , AccessibilityEvent .TYPE_VIEW_LONG_CLICKED ,
117+ AccessibilityEvent .TYPE_VIEW_SELECTED , AccessibilityEvent .TYPE_VIEW_FOCUSED ,
118+ AccessibilityEvent .TYPE_VIEW_TEXT_CHANGED , AccessibilityEvent .TYPE_WINDOW_STATE_CHANGED ,
119+ AccessibilityEvent .TYPE_NOTIFICATION_STATE_CHANGED , AccessibilityEvent .TYPE_VIEW_HOVER_ENTER ,
120+ AccessibilityEvent .TYPE_VIEW_HOVER_EXIT , AccessibilityEvent .TYPE_TOUCH_EXPLORATION_GESTURE_START ,
121+ AccessibilityEvent .TYPE_TOUCH_EXPLORATION_GESTURE_END , AccessibilityEvent .TYPE_WINDOW_CONTENT_CHANGED ,
122+ AccessibilityEvent .TYPE_VIEW_SCROLLED , AccessibilityEvent .TYPE_VIEW_TEXT_SELECTION_CHANGED ,
123+ AccessibilityEvent .TYPE_ANNOUNCEMENT , AccessibilityEvent .TYPE_VIEW_ACCESSIBILITY_FOCUSED ,
124+ AccessibilityEvent .TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED ,
125+ AccessibilityEvent .TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY ,
126+ AccessibilityEvent .TYPE_GESTURE_DETECTION_START , AccessibilityEvent .TYPE_GESTURE_DETECTION_END ,
127+ AccessibilityEvent .TYPE_TOUCH_INTERACTION_START , AccessibilityEvent .TYPE_TOUCH_INTERACTION_END ,
128+ AccessibilityEvent .TYPE_WINDOWS_CHANGED , AccessibilityEvent .TYPE_VIEW_CONTEXT_CLICKED ,
129+ AccessibilityEvent .TYPE_ASSIST_READING_CONTEXT
130+ ]
131+ )
132+ @Retention(AnnotationRetention .SOURCE )
133+ annotation class EventType
134+
135+ fun View.accessibility (block : AccessibilityToolBox .() -> Any ) {
136+ var toolbox = getTag(R .id.snabble_accessibility_toolbox) as ? AccessibilityToolBox
137+ if (toolbox == null ) {
138+ toolbox = AccessibilityToolBox (this )
139+ setTag(R .id.snabble_accessibility_toolbox, toolbox)
140+ ViewCompat .setAccessibilityDelegate(this , toolbox)
141+ }
142+ block(toolbox)
143+ }
144+
145+ fun View.setClickDescription (stringId : Int , vararg formatArgs : Any ) {
146+ setClickDescription(context.getString(stringId, formatArgs))
147+ }
148+
149+ fun View.setClickDescription (description : String ) {
150+ ViewCompat .setAccessibilityDelegate(this , object : AccessibilityDelegateCompat () {
151+ override fun onInitializeAccessibilityNodeInfo (v : View , info : AccessibilityNodeInfoCompat ) {
152+ super .onInitializeAccessibilityNodeInfo(v, info)
153+ info.addAction(
154+ AccessibilityNodeInfoCompat .AccessibilityActionCompat (
155+ AccessibilityNodeInfoCompat .ACTION_CLICK ,
156+ description
157+ )
158+ )
159+ }
160+ })
161+ }
162+
163+ fun TextView.cleanUpDescription () {
164+ contentDescription = text.toString().replace(" ..." , " …" ).replace(" snabble" , " snäbble" )
165+ }
166+
167+ val Context .isTalkBackActive: Boolean
168+ get() {
169+ val am = getSystemService(Context .ACCESSIBILITY_SERVICE ) as ? AccessibilityManager
170+ val voiceServices = am?.getEnabledAccessibilityServiceList(AccessibilityServiceInfo .FEEDBACK_SPOKEN ).orEmpty()
171+ val isTouchExplorationEnabled = am?.isTouchExplorationEnabled ? : false
172+ return voiceServices.isNotEmpty() && isTouchExplorationEnabled
173+ }
174+
175+ fun View.focusForAccessibility () {
176+ performAccessibilityAction(AccessibilityNodeInfo .ACTION_ACCESSIBILITY_FOCUS , null )
177+ sendAccessibilityEvent(AccessibilityEvent .TYPE_VIEW_SELECTED )
178+ }
179+
180+ fun orderViewsForAccessibility (vararg views : View ? ) {
181+ views
182+ .filterNotNull()
183+ .filter { it.isVisible }
184+ .forEachWindow { before, current, after ->
185+ ViewCompat .setAccessibilityDelegate(current, object : AccessibilityDelegateCompat () {
186+ override fun onInitializeAccessibilityNodeInfo (
187+ host : View ,
188+ info : AccessibilityNodeInfoCompat
189+ ) {
190+ super .onInitializeAccessibilityNodeInfo(host, info)
191+ info.setTraversalBefore(after)
192+ info.setTraversalAfter(before)
193+ }
194+ })
195+ }
196+ }
197+
198+ /* *
199+ * Iterate over an Iterable the iterator param last can be only `null` when the size is 1. The last
200+ * parameter can only be `null` when the size is less then 2, otherwise both params are not `null`.
201+ */
202+ fun <T > Iterable<T>.forEachWindow (iterator : (last: T ? , current: T , next: T ? ) -> Unit ) {
203+ val list = toList()
204+ when {
205+ list.size == 1 ->
206+ iterator(null , first(), null )
207+ list.size == 2 ->
208+ iterator(first(), last(), null )
209+ list.size >= 2 -> {
210+ for (i in 1 .. count() - 2 ) {
211+ iterator(list[i - 1 ], list[i], list[i + 1 ])
212+ }
213+ }
214+ }
215+ }
216+
217+ object AccessibilityPreferences {
218+ private const val KEY_SUPPRESS_SCANNER_HINT = " suppress_scanner_hint"
219+ private val sharedPreferences = Snabble .application.getSharedPreferences(" accessibility" , Context .MODE_PRIVATE )
220+ var suppressScannerHint: Boolean
221+ get() = sharedPreferences.getBoolean(KEY_SUPPRESS_SCANNER_HINT , false )
222+ set(seen) {
223+ sharedPreferences
224+ .edit()
225+ .putBoolean(KEY_SUPPRESS_SCANNER_HINT , seen)
226+ .apply ()
227+ }
228+ }
0 commit comments