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