1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.compose.ui.platform
18 
19 import android.graphics.Rect
20 import androidx.compose.ui.semantics.SemanticsNode
21 import androidx.compose.ui.semantics.SemanticsProperties
22 import androidx.compose.ui.text.TextLayoutResult
23 import androidx.compose.ui.text.style.ResolvedTextDirection
24 import androidx.compose.ui.util.fastRoundToInt
25 import java.text.BreakIterator
26 import java.util.Locale
27 
28 /**
29  * This class contains the implementation of text segment iterators for accessibility support.
30  *
31  * Note: We want to be able to iterator over [SemanticsProperties.ContentDescription] of any
32  * component.
33  */
34 internal class AccessibilityIterators {
35 
36     interface TextSegmentIterator {
37         /** Given the current position, returning the start and end of next element in an array. */
followingnull38         fun following(current: Int): IntArray?
39 
40         /**
41          * Given the current position, returning the start and end of previous element in an array.
42          */
43         fun preceding(current: Int): IntArray?
44     }
45 
46     abstract class AbstractTextSegmentIterator : TextSegmentIterator {
47 
48         protected lateinit var text: String
49 
50         private val segment = IntArray(2)
51 
52         open fun initialize(text: String) {
53             this.text = text
54         }
55 
56         protected fun getRange(start: Int, end: Int): IntArray? {
57             if (start < 0 || end < 0 || start == end) {
58                 return null
59             }
60             segment[0] = start
61             segment[1] = end
62             return segment
63         }
64     }
65 
66     open class CharacterTextSegmentIterator private constructor(locale: Locale) :
67         AbstractTextSegmentIterator() {
68         companion object {
69             private var instance: CharacterTextSegmentIterator? = null
70 
getInstancenull71             fun getInstance(locale: Locale): CharacterTextSegmentIterator {
72                 if (instance == null) {
73                     instance = CharacterTextSegmentIterator(locale)
74                 }
75                 return instance as CharacterTextSegmentIterator
76             }
77         }
78 
79         private lateinit var impl: BreakIterator
80 
81         init {
82             onLocaleChanged(locale)
83             // TODO(yingleiw): register callback for locale change
84             // ViewRootImpl.addConfigCallback(this);
85         }
86 
initializenull87         override fun initialize(text: String) {
88             super.initialize(text)
89             impl.setText(text)
90         }
91 
followingnull92         override fun following(current: Int): IntArray? {
93             val textLength = text.length
94             if (textLength <= 0) {
95                 return null
96             }
97             if (current >= textLength) {
98                 return null
99             }
100             var start = current
101             if (start < 0) {
102                 start = 0
103             }
104             while (!impl.isBoundary(start)) {
105                 start = impl.following(start)
106                 if (start == BreakIterator.DONE) {
107                     return null
108                 }
109             }
110             val end = impl.following(start)
111             if (end == BreakIterator.DONE) {
112                 return null
113             }
114             return getRange(start, end)
115         }
116 
precedingnull117         override fun preceding(current: Int): IntArray? {
118             val textLength = text.length
119             if (textLength <= 0) {
120                 return null
121             }
122             if (current <= 0) {
123                 return null
124             }
125             var end = current
126             if (end > textLength) {
127                 end = textLength
128             }
129             while (!impl.isBoundary(end)) {
130                 end = impl.preceding(end)
131                 if (end == BreakIterator.DONE) {
132                     return null
133                 }
134             }
135             val start = impl.preceding(end)
136             if (start == BreakIterator.DONE) {
137                 return null
138             }
139             return getRange(start, end)
140         }
141 
142         // TODO(yingleiw): callback for locale change
143         /*
144         @Override
145         public void onConfigurationChanged(Configuration globalConfig) {
146             final Locale locale = globalConfig.getLocales().get(0);
147             if (locale == null) {
148                 return;
149             }
150             if (!mLocale.equals(locale)) {
151                 mLocale = locale;
152                 onLocaleChanged(locale);
153             }
154         }
155         */
156 
onLocaleChangednull157         private fun onLocaleChanged(locale: Locale) {
158             impl = BreakIterator.getCharacterInstance(locale)
159         }
160     }
161 
162     class WordTextSegmentIterator private constructor(locale: Locale) :
163         AbstractTextSegmentIterator() {
164         companion object {
165             private var instance: WordTextSegmentIterator? = null
166 
getInstancenull167             fun getInstance(locale: Locale): WordTextSegmentIterator {
168                 if (instance == null) {
169                     instance = WordTextSegmentIterator(locale)
170                 }
171                 return instance as WordTextSegmentIterator
172             }
173         }
174 
175         private lateinit var impl: BreakIterator
176 
177         init {
178             onLocaleChanged(locale)
179             // TODO: register callback for locale change
180             // ViewRootImpl.addConfigCallback(this);
181         }
182 
initializenull183         override fun initialize(text: String) {
184             super.initialize(text)
185             impl.setText(text)
186         }
187 
onLocaleChangednull188         private fun onLocaleChanged(locale: Locale) {
189             impl = BreakIterator.getWordInstance(locale)
190         }
191 
followingnull192         override fun following(current: Int): IntArray? {
193             val textLength = text.length
194             if (textLength <= 0) {
195                 return null
196             }
197             if (current >= text.length) {
198                 return null
199             }
200             var start = current
201             if (start < 0) {
202                 start = 0
203             }
204             while (!isLetterOrDigit(start) && !isStartBoundary(start)) {
205                 start = impl.following(start)
206                 if (start == BreakIterator.DONE) {
207                     return null
208                 }
209             }
210             val end = impl.following(start)
211             if (end == BreakIterator.DONE || !isEndBoundary(end)) {
212                 return null
213             }
214             return getRange(start, end)
215         }
216 
precedingnull217         override fun preceding(current: Int): IntArray? {
218             val textLength = text.length
219             if (textLength <= 0) {
220                 return null
221             }
222             if (current <= 0) {
223                 return null
224             }
225             var end = current
226             if (end > textLength) {
227                 end = textLength
228             }
229             while (end > 0 && !isLetterOrDigit(end - 1) && !isEndBoundary(end)) {
230                 end = impl.preceding(end)
231                 if (end == BreakIterator.DONE) {
232                     return null
233                 }
234             }
235             val start = impl.preceding(end)
236             if (start == BreakIterator.DONE || !isStartBoundary(start)) {
237                 return null
238             }
239             return getRange(start, end)
240         }
241 
isStartBoundarynull242         private fun isStartBoundary(index: Int): Boolean {
243             return isLetterOrDigit(index) && (index == 0 || !isLetterOrDigit(index - 1))
244         }
245 
isEndBoundarynull246         private fun isEndBoundary(index: Int): Boolean {
247             return (index > 0 && isLetterOrDigit(index - 1)) &&
248                 (index == text.length || !isLetterOrDigit(index))
249         }
250 
isLetterOrDigitnull251         private fun isLetterOrDigit(index: Int): Boolean {
252             if (index >= 0 && index < text.length) {
253                 val codePoint = text.codePointAt(index)
254                 return Character.isLetterOrDigit(codePoint)
255             }
256             return false
257         }
258     }
259 
260     class ParagraphTextSegmentIterator private constructor() : AbstractTextSegmentIterator() {
261         companion object {
262             private var instance: ParagraphTextSegmentIterator? = null
263 
getInstancenull264             fun getInstance(): ParagraphTextSegmentIterator {
265                 if (instance == null) {
266                     instance = ParagraphTextSegmentIterator()
267                 }
268                 return instance as ParagraphTextSegmentIterator
269             }
270         }
271 
followingnull272         override fun following(current: Int): IntArray? {
273             val textLength = text.length
274             if (textLength <= 0) {
275                 return null
276             }
277             if (current >= textLength) {
278                 return null
279             }
280             var start = current
281             if (start < 0) {
282                 start = 0
283             }
284             while (start < textLength && text[start] == '\n' && !isStartBoundary(start)) {
285                 start++
286             }
287             if (start >= textLength) {
288                 return null
289             }
290             var end = start + 1
291             while (end < textLength && !isEndBoundary(end)) {
292                 end++
293             }
294             return getRange(start, end)
295         }
296 
precedingnull297         override fun preceding(current: Int): IntArray? {
298             val textLength = text.length
299             if (textLength <= 0) {
300                 return null
301             }
302             if (current <= 0) {
303                 return null
304             }
305             var end = current
306             if (end > textLength) {
307                 end = textLength
308             }
309             while (end > 0 && text[end - 1] == '\n' && !isEndBoundary(end)) {
310                 end--
311             }
312             if (end <= 0) {
313                 return null
314             }
315             var start = end - 1
316             while (start > 0 && !isStartBoundary(start)) {
317                 start--
318             }
319             return getRange(start, end)
320         }
321 
isStartBoundarynull322         private fun isStartBoundary(index: Int): Boolean {
323             return (text[index] != '\n' && (index == 0 || text[index - 1] == '\n'))
324         }
325 
isEndBoundarynull326         private fun isEndBoundary(index: Int): Boolean {
327             return (index > 0 &&
328                 text[index - 1] != '\n' &&
329                 (index == text.length || text[index] == '\n'))
330         }
331     }
332 
333     class LineTextSegmentIterator private constructor() : AbstractTextSegmentIterator() {
334         companion object {
335             private var lineInstance: LineTextSegmentIterator? = null
336             private val DirectionStart = ResolvedTextDirection.Rtl
337             private val DirectionEnd = ResolvedTextDirection.Ltr
338 
getInstancenull339             fun getInstance(): LineTextSegmentIterator {
340                 if (lineInstance == null) {
341                     lineInstance = LineTextSegmentIterator()
342                 }
343                 return lineInstance as LineTextSegmentIterator
344             }
345         }
346 
347         private lateinit var layoutResult: TextLayoutResult
348 
initializenull349         fun initialize(text: String, layoutResult: TextLayoutResult) {
350             this.text = text
351             this.layoutResult = layoutResult
352         }
353 
followingnull354         override fun following(current: Int): IntArray? {
355             val textLength = text.length
356             if (textLength <= 0) {
357                 return null
358             }
359             if (current >= text.length) {
360                 return null
361             }
362             val nextLine =
363                 if (current < 0) {
364                     layoutResult.getLineForOffset(0)
365                 } else {
366                     val currentLine = layoutResult.getLineForOffset(current)
367                     if (getLineEdgeIndex(currentLine, DirectionStart) == current) {
368                         currentLine
369                     } else {
370                         currentLine + 1
371                     }
372                 }
373             if (nextLine >= layoutResult.lineCount) {
374                 return null
375             }
376             val start = getLineEdgeIndex(nextLine, DirectionStart)
377             val end = getLineEdgeIndex(nextLine, DirectionEnd) + 1
378             return getRange(start, end)
379         }
380 
precedingnull381         override fun preceding(current: Int): IntArray? {
382             val textLength = text.length
383             if (textLength <= 0) {
384                 return null
385             }
386             if (current <= 0) {
387                 return null
388             }
389             val previousLine =
390                 if (current > text.length) {
391                     layoutResult.getLineForOffset(text.length)
392                 } else {
393                     val currentLine = layoutResult.getLineForOffset(current)
394                     if (getLineEdgeIndex(currentLine, DirectionEnd) + 1 == current) {
395                         currentLine
396                     } else {
397                         currentLine - 1
398                     }
399                 }
400             if (previousLine < 0) {
401                 return null
402             }
403             val start = getLineEdgeIndex(previousLine, DirectionStart)
404             val end = getLineEdgeIndex(previousLine, DirectionEnd) + 1
405             return getRange(start, end)
406         }
407 
getLineEdgeIndexnull408         private fun getLineEdgeIndex(lineNumber: Int, direction: ResolvedTextDirection): Int {
409             val lineStart = layoutResult.getLineStart(lineNumber)
410             val paragraphDirection = layoutResult.getParagraphDirection(lineStart)
411             return if (direction != paragraphDirection) {
412                 layoutResult.getLineStart(lineNumber)
413             } else {
414                 layoutResult.getLineEnd(lineNumber) - 1
415             }
416         }
417     }
418 
419     // TODO(b/27505408): A11y movement by granularity page not working in edittext.
420     class PageTextSegmentIterator private constructor() : AbstractTextSegmentIterator() {
421         companion object {
422             private var pageInstance: PageTextSegmentIterator? = null
423             private val DirectionStart = ResolvedTextDirection.Rtl
424             private val DirectionEnd = ResolvedTextDirection.Ltr
425 
getInstancenull426             fun getInstance(): PageTextSegmentIterator {
427                 if (pageInstance == null) {
428                     pageInstance = PageTextSegmentIterator()
429                 }
430                 return pageInstance as PageTextSegmentIterator
431             }
432         }
433 
434         private lateinit var layoutResult: TextLayoutResult
435         private lateinit var node: SemanticsNode
436 
437         private var tempRect = Rect()
438 
initializenull439         fun initialize(text: String, layoutResult: TextLayoutResult, node: SemanticsNode) {
440             this.text = text
441             this.layoutResult = layoutResult
442             this.node = node
443         }
444 
followingnull445         override fun following(current: Int): IntArray? {
446             val textLength = text.length
447             if (textLength <= 0) {
448                 return null
449             }
450             if (current >= text.length) {
451                 return null
452             }
453             val pageHeight: Int
454             try {
455                 pageHeight = node.boundsInRoot.height.fastRoundToInt()
456                 // TODO(b/153198816): check whether we still get this exception when R is in.
457             } catch (e: IllegalStateException) {
458                 return null
459             }
460 
461             val start = 0.coerceAtLeast(current)
462 
463             val currentLine = layoutResult.getLineForOffset(start)
464             val currentLineTop = layoutResult.getLineTop(currentLine)
465             // TODO: Please help me translate the below where mView is the TextView
466             //  final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
467             //                    - mView.getTotalPaddingBottom();
468             val nextPageStartY = currentLineTop + pageHeight
469             val lastLineTop = layoutResult.getLineTop(layoutResult.lineCount - 1)
470             val currentPageEndLine =
471                 if (nextPageStartY < lastLineTop)
472                     layoutResult.getLineForVerticalPosition(nextPageStartY) - 1
473                 else layoutResult.lineCount - 1
474 
475             val end = getLineEdgeIndex(currentPageEndLine, DirectionEnd) + 1
476 
477             return getRange(start, end)
478         }
479 
precedingnull480         override fun preceding(current: Int): IntArray? {
481             val textLength = text.length
482             if (textLength <= 0) {
483                 return null
484             }
485             if (current <= 0) {
486                 return null
487             }
488             val pageHeight: Int
489             try {
490                 pageHeight = node.boundsInRoot.height.fastRoundToInt()
491                 // TODO(b/153198816): check whether we still get this exception when R is in.
492             } catch (e: IllegalStateException) {
493                 return null
494             }
495 
496             val end = text.length.coerceAtMost(current)
497 
498             val currentLine = layoutResult.getLineForOffset(end)
499             val currentLineTop = layoutResult.getLineTop(currentLine)
500             // TODO: It won't work for text with padding yet.
501             //  Please help me translate the below where mView is the TextView
502             //  final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
503             //                    - mView.getTotalPaddingBottom();
504             val previousPageEndY = currentLineTop - pageHeight
505             var currentPageStartLine =
506                 if (previousPageEndY > 0) layoutResult.getLineForVerticalPosition(previousPageEndY)
507                 else 0
508             // If we're at the end of text, we're at the end of the current line rather than the
509             // start of the next line, so we should move up one fewer lines than we would otherwise.
510             if (end == text.length && (currentPageStartLine < currentLine)) {
511                 currentPageStartLine += 1
512             }
513 
514             val start = getLineEdgeIndex(currentPageStartLine, DirectionStart)
515 
516             return getRange(start, end)
517         }
518 
getLineEdgeIndexnull519         private fun getLineEdgeIndex(lineNumber: Int, direction: ResolvedTextDirection): Int {
520             val lineStart = layoutResult.getLineStart(lineNumber)
521             val paragraphDirection = layoutResult.getParagraphDirection(lineStart)
522             return if (direction != paragraphDirection) {
523                 layoutResult.getLineStart(lineNumber)
524             } else {
525                 layoutResult.getLineEnd(lineNumber) - 1
526             }
527         }
528     }
529 }
530