1 /* 2 * Copyright (C) 2012 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 android.view; 18 19 import android.compat.annotation.UnsupportedAppUsage; 20 import android.content.res.Configuration; 21 import android.os.Build; 22 23 import java.text.BreakIterator; 24 import java.util.Locale; 25 26 /** 27 * This class contains the implementation of text segment iterators 28 * for accessibility support. 29 * 30 * Note: Such iterators are needed in the view package since we want 31 * to be able to iterator over content description of any view. 32 * 33 * @hide 34 */ 35 public final class AccessibilityIterators { 36 37 /** 38 * @hide 39 */ 40 public static interface TextSegmentIterator { following(int current)41 public int[] following(int current); preceding(int current)42 public int[] preceding(int current); 43 } 44 45 /** 46 * @hide 47 */ 48 public static abstract class AbstractTextSegmentIterator implements TextSegmentIterator { 49 50 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) AbstractTextSegmentIterator()51 public AbstractTextSegmentIterator() { 52 } 53 54 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) 55 protected String mText; 56 57 private final int[] mSegment = new int[2]; 58 initialize(String text)59 public void initialize(String text) { 60 mText = text; 61 } 62 getRange(int start, int end)63 protected int[] getRange(int start, int end) { 64 if (start < 0 || end < 0 || start == end) { 65 return null; 66 } 67 mSegment[0] = start; 68 mSegment[1] = end; 69 return mSegment; 70 } 71 } 72 73 static class CharacterTextSegmentIterator extends AbstractTextSegmentIterator 74 implements ViewRootImpl.ConfigChangedCallback { 75 private static CharacterTextSegmentIterator sInstance; 76 77 private Locale mLocale; 78 79 protected BreakIterator mImpl; 80 getInstance(Locale locale)81 public static CharacterTextSegmentIterator getInstance(Locale locale) { 82 if (sInstance == null) { 83 sInstance = new CharacterTextSegmentIterator(locale); 84 } 85 return sInstance; 86 } 87 CharacterTextSegmentIterator(Locale locale)88 private CharacterTextSegmentIterator(Locale locale) { 89 mLocale = locale; 90 onLocaleChanged(locale); 91 ViewRootImpl.addConfigCallback(this); 92 } 93 94 @Override initialize(String text)95 public void initialize(String text) { 96 super.initialize(text); 97 mImpl.setText(text); 98 } 99 100 @Override following(int offset)101 public int[] following(int offset) { 102 final int textLegth = mText.length(); 103 if (textLegth <= 0) { 104 return null; 105 } 106 if (offset >= textLegth) { 107 return null; 108 } 109 int start = offset; 110 if (start < 0) { 111 start = 0; 112 } 113 while (!mImpl.isBoundary(start)) { 114 start = mImpl.following(start); 115 if (start == BreakIterator.DONE) { 116 return null; 117 } 118 } 119 final int end = mImpl.following(start); 120 if (end == BreakIterator.DONE) { 121 return null; 122 } 123 return getRange(start, end); 124 } 125 126 @Override preceding(int offset)127 public int[] preceding(int offset) { 128 final int textLegth = mText.length(); 129 if (textLegth <= 0) { 130 return null; 131 } 132 if (offset <= 0) { 133 return null; 134 } 135 int end = offset; 136 if (end > textLegth) { 137 end = textLegth; 138 } 139 while (!mImpl.isBoundary(end)) { 140 end = mImpl.preceding(end); 141 if (end == BreakIterator.DONE) { 142 return null; 143 } 144 } 145 final int start = mImpl.preceding(end); 146 if (start == BreakIterator.DONE) { 147 return null; 148 } 149 return getRange(start, end); 150 } 151 152 @Override onConfigurationChanged(Configuration globalConfig)153 public void onConfigurationChanged(Configuration globalConfig) { 154 final Locale locale = globalConfig.getLocales().get(0); 155 if (locale == null) { 156 return; 157 } 158 if (!mLocale.equals(locale)) { 159 mLocale = locale; 160 onLocaleChanged(locale); 161 } 162 } 163 onLocaleChanged(Locale locale)164 protected void onLocaleChanged(Locale locale) { 165 mImpl = BreakIterator.getCharacterInstance(locale); 166 } 167 } 168 169 static class WordTextSegmentIterator extends CharacterTextSegmentIterator { 170 private static WordTextSegmentIterator sInstance; 171 getInstance(Locale locale)172 public static WordTextSegmentIterator getInstance(Locale locale) { 173 if (sInstance == null) { 174 sInstance = new WordTextSegmentIterator(locale); 175 } 176 return sInstance; 177 } 178 WordTextSegmentIterator(Locale locale)179 private WordTextSegmentIterator(Locale locale) { 180 super(locale); 181 } 182 183 @Override onLocaleChanged(Locale locale)184 protected void onLocaleChanged(Locale locale) { 185 mImpl = BreakIterator.getWordInstance(locale); 186 } 187 188 @Override following(int offset)189 public int[] following(int offset) { 190 final int textLegth = mText.length(); 191 if (textLegth <= 0) { 192 return null; 193 } 194 if (offset >= mText.length()) { 195 return null; 196 } 197 int start = offset; 198 if (start < 0) { 199 start = 0; 200 } 201 while (!isLetterOrDigit(start) && !isStartBoundary(start)) { 202 start = mImpl.following(start); 203 if (start == BreakIterator.DONE) { 204 return null; 205 } 206 } 207 final int end = mImpl.following(start); 208 if (end == BreakIterator.DONE || !isEndBoundary(end)) { 209 return null; 210 } 211 return getRange(start, end); 212 } 213 214 @Override preceding(int offset)215 public int[] preceding(int offset) { 216 final int textLegth = mText.length(); 217 if (textLegth <= 0) { 218 return null; 219 } 220 if (offset <= 0) { 221 return null; 222 } 223 int end = offset; 224 if (end > textLegth) { 225 end = textLegth; 226 } 227 while (end > 0 && !isLetterOrDigit(end - 1) && !isEndBoundary(end)) { 228 end = mImpl.preceding(end); 229 if (end == BreakIterator.DONE) { 230 return null; 231 } 232 } 233 final int start = mImpl.preceding(end); 234 if (start == BreakIterator.DONE || !isStartBoundary(start)) { 235 return null; 236 } 237 return getRange(start, end); 238 } 239 isStartBoundary(int index)240 private boolean isStartBoundary(int index) { 241 return isLetterOrDigit(index) 242 && (index == 0 || !isLetterOrDigit(index - 1)); 243 } 244 isEndBoundary(int index)245 private boolean isEndBoundary(int index) { 246 return (index > 0 && isLetterOrDigit(index - 1)) 247 && (index == mText.length() || !isLetterOrDigit(index)); 248 } 249 isLetterOrDigit(int index)250 private boolean isLetterOrDigit(int index) { 251 if (index >= 0 && index < mText.length()) { 252 final int codePoint = mText.codePointAt(index); 253 return Character.isLetterOrDigit(codePoint); 254 } 255 return false; 256 } 257 } 258 259 static class ParagraphTextSegmentIterator extends AbstractTextSegmentIterator { 260 private static ParagraphTextSegmentIterator sInstance; 261 getInstance()262 public static ParagraphTextSegmentIterator getInstance() { 263 if (sInstance == null) { 264 sInstance = new ParagraphTextSegmentIterator(); 265 } 266 return sInstance; 267 } 268 269 @Override following(int offset)270 public int[] following(int offset) { 271 final int textLength = mText.length(); 272 if (textLength <= 0) { 273 return null; 274 } 275 if (offset >= textLength) { 276 return null; 277 } 278 int start = offset; 279 if (start < 0) { 280 start = 0; 281 } 282 while (start < textLength && mText.charAt(start) == '\n' 283 && !isStartBoundary(start)) { 284 start++; 285 } 286 if (start >= textLength) { 287 return null; 288 } 289 int end = start + 1; 290 while (end < textLength && !isEndBoundary(end)) { 291 end++; 292 } 293 return getRange(start, end); 294 } 295 296 @Override preceding(int offset)297 public int[] preceding(int offset) { 298 final int textLength = mText.length(); 299 if (textLength <= 0) { 300 return null; 301 } 302 if (offset <= 0) { 303 return null; 304 } 305 int end = offset; 306 if (end > textLength) { 307 end = textLength; 308 } 309 while(end > 0 && mText.charAt(end - 1) == '\n' && !isEndBoundary(end)) { 310 end--; 311 } 312 if (end <= 0) { 313 return null; 314 } 315 int start = end - 1; 316 while (start > 0 && !isStartBoundary(start)) { 317 start--; 318 } 319 return getRange(start, end); 320 } 321 isStartBoundary(int index)322 private boolean isStartBoundary(int index) { 323 return (mText.charAt(index) != '\n' 324 && (index == 0 || mText.charAt(index - 1) == '\n')); 325 } 326 isEndBoundary(int index)327 private boolean isEndBoundary(int index) { 328 return (index > 0 && mText.charAt(index - 1) != '\n' 329 && (index == mText.length() || mText.charAt(index) == '\n')); 330 } 331 } 332 } 333