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