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