1 /*
2  * Copyright (C) 2011 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.text;
18 
19 
20 import android.view.View;
21 
22 import java.nio.CharBuffer;
23 
24 /**
25  * Some objects that implement {@link TextDirectionHeuristic}. Use these with
26  * the {@link BidiFormatter#unicodeWrap unicodeWrap()} methods in {@link BidiFormatter}.
27  * Also notice that these direction heuristics correspond to the same types of constants
28  * provided in the {@link android.view.View} class for {@link android.view.View#setTextDirection
29  * setTextDirection()}, such as {@link android.view.View#TEXT_DIRECTION_RTL}.
30  * <p>To support versions lower than {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
31  * you can use the support library's {@link android.support.v4.text.TextDirectionHeuristicsCompat}
32  * class.
33  *
34  */
35 public class TextDirectionHeuristics {
36 
37     /**
38      * Always decides that the direction is left to right.
39      */
40     public static final TextDirectionHeuristic LTR =
41         new TextDirectionHeuristicInternal(null /* no algorithm */, false);
42 
43     /**
44      * Always decides that the direction is right to left.
45      */
46     public static final TextDirectionHeuristic RTL =
47         new TextDirectionHeuristicInternal(null /* no algorithm */, true);
48 
49     /**
50      * Determines the direction based on the first strong directional character, including bidi
51      * format chars, falling back to left to right if it finds none. This is the default behavior
52      * of the Unicode Bidirectional Algorithm.
53      */
54     public static final TextDirectionHeuristic FIRSTSTRONG_LTR =
55         new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, false);
56 
57     /**
58      * Determines the direction based on the first strong directional character, including bidi
59      * format chars, falling back to right to left if it finds none. This is similar to the default
60      * behavior of the Unicode Bidirectional Algorithm, just with different fallback behavior.
61      */
62     public static final TextDirectionHeuristic FIRSTSTRONG_RTL =
63         new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, true);
64 
65     /**
66      * If the text contains any strong right to left non-format character, determines that the
67      * direction is right to left, falling back to left to right if it finds none.
68      */
69     public static final TextDirectionHeuristic ANYRTL_LTR =
70         new TextDirectionHeuristicInternal(AnyStrong.INSTANCE_RTL, false);
71 
72     /**
73      * Force the paragraph direction to the Locale direction. Falls back to left to right.
74      */
75     public static final TextDirectionHeuristic LOCALE = TextDirectionHeuristicLocale.INSTANCE;
76 
77     /**
78      * State constants for taking care about true / false / unknown
79      */
80     private static final int STATE_TRUE = 0;
81     private static final int STATE_FALSE = 1;
82     private static final int STATE_UNKNOWN = 2;
83 
84     /* Returns STATE_TRUE for strong RTL characters, STATE_FALSE for strong LTR characters, and
85      * STATE_UNKNOWN for everything else.
86      */
isRtlCodePoint(int codePoint)87     private static int isRtlCodePoint(int codePoint) {
88         switch (Character.getDirectionality(codePoint)) {
89             case Character.DIRECTIONALITY_LEFT_TO_RIGHT:
90                 return STATE_FALSE;
91             case Character.DIRECTIONALITY_RIGHT_TO_LEFT:
92             case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC:
93                 return STATE_TRUE;
94             case Character.DIRECTIONALITY_UNDEFINED:
95                 // Unassigned characters still have bidi direction, defined at:
96                 // http://www.unicode.org/Public/UCD/latest/ucd/extracted/DerivedBidiClass.txt
97 
98                 if ((0x0590 <= codePoint && codePoint <= 0x08FF) ||
99                         (0xFB1D <= codePoint && codePoint <= 0xFDCF) ||
100                         (0xFDF0 <= codePoint && codePoint <= 0xFDFF) ||
101                         (0xFE70 <= codePoint && codePoint <= 0xFEFF) ||
102                         (0x10800 <= codePoint && codePoint <= 0x10FFF) ||
103                         (0x1E800 <= codePoint && codePoint <= 0x1EFFF)) {
104                     // Unassigned RTL character
105                     return STATE_TRUE;
106                 } else if (
107                         // Potentially-unassigned Default_Ignorable. Ranges are from unassigned
108                         // characters that have Unicode property Other_Default_Ignorable_Code_Point
109                         // plus some enlargening to cover bidi isolates and simplify checks.
110                         (0x2065 <= codePoint && codePoint <= 0x2069) ||
111                         (0xFFF0 <= codePoint && codePoint <= 0xFFF8) ||
112                         (0xE0000 <= codePoint && codePoint <= 0xE0FFF) ||
113                         // Non-character
114                         (0xFDD0 <= codePoint && codePoint <= 0xFDEF) ||
115                         ((codePoint & 0xFFFE) == 0xFFFE) ||
116                         // Currency symbol
117                         (0x20A0 <= codePoint && codePoint <= 0x20CF) ||
118                         // Unpaired surrogate
119                         (0xD800 <= codePoint && codePoint <= 0xDFFF)) {
120                     return STATE_UNKNOWN;
121                 } else {
122                     // Unassigned LTR character
123                     return STATE_FALSE;
124                 }
125             default:
126                 return STATE_UNKNOWN;
127         }
128     }
129 
130     /**
131      * Computes the text direction based on an algorithm.  Subclasses implement
132      * {@link #defaultIsRtl} to handle cases where the algorithm cannot determine the
133      * direction from the text alone.
134      */
135     private static abstract class TextDirectionHeuristicImpl implements TextDirectionHeuristic {
136         private final TextDirectionAlgorithm mAlgorithm;
137 
TextDirectionHeuristicImpl(TextDirectionAlgorithm algorithm)138         public TextDirectionHeuristicImpl(TextDirectionAlgorithm algorithm) {
139             mAlgorithm = algorithm;
140         }
141 
142         /**
143          * Return true if the default text direction is rtl.
144          */
defaultIsRtl()145         abstract protected boolean defaultIsRtl();
146 
147         @Override
isRtl(char[] array, int start, int count)148         public boolean isRtl(char[] array, int start, int count) {
149             return isRtl(CharBuffer.wrap(array), start, count);
150         }
151 
152         @Override
isRtl(CharSequence cs, int start, int count)153         public boolean isRtl(CharSequence cs, int start, int count) {
154             if (cs == null || start < 0 || count < 0 || cs.length() - count < start) {
155                 throw new IllegalArgumentException();
156             }
157             if (mAlgorithm == null) {
158                 return defaultIsRtl();
159             }
160             return doCheck(cs, start, count);
161         }
162 
doCheck(CharSequence cs, int start, int count)163         private boolean doCheck(CharSequence cs, int start, int count) {
164             switch(mAlgorithm.checkRtl(cs, start, count)) {
165                 case STATE_TRUE:
166                     return true;
167                 case STATE_FALSE:
168                     return false;
169                 default:
170                     return defaultIsRtl();
171             }
172         }
173     }
174 
175     private static class TextDirectionHeuristicInternal extends TextDirectionHeuristicImpl {
176         private final boolean mDefaultIsRtl;
177 
TextDirectionHeuristicInternal(TextDirectionAlgorithm algorithm, boolean defaultIsRtl)178         private TextDirectionHeuristicInternal(TextDirectionAlgorithm algorithm,
179                 boolean defaultIsRtl) {
180             super(algorithm);
181             mDefaultIsRtl = defaultIsRtl;
182         }
183 
184         @Override
defaultIsRtl()185         protected boolean defaultIsRtl() {
186             return mDefaultIsRtl;
187         }
188     }
189 
190     /**
191      * Interface for an algorithm to guess the direction of a paragraph of text.
192      */
193     private static interface TextDirectionAlgorithm {
194         /**
195          * Returns whether the range of text is RTL according to the algorithm.
196          */
checkRtl(CharSequence cs, int start, int count)197         int checkRtl(CharSequence cs, int start, int count);
198     }
199 
200     /**
201      * Algorithm that uses the first strong directional character to determine the paragraph
202      * direction. This is the standard Unicode Bidirectional Algorithm (steps P2 and P3), with the
203      * exception that if no strong character is found, UNKNOWN is returned.
204      */
205     private static class FirstStrong implements TextDirectionAlgorithm {
206         @Override
checkRtl(CharSequence cs, int start, int count)207         public int checkRtl(CharSequence cs, int start, int count) {
208             int result = STATE_UNKNOWN;
209             int openIsolateCount = 0;
210             for (int cp, i = start, end = start + count;
211                     i < end && result == STATE_UNKNOWN;
212                     i += Character.charCount(cp)) {
213                 cp = Character.codePointAt(cs, i);
214                 if (0x2066 <= cp && cp <= 0x2068) { // Opening isolates
215                     openIsolateCount += 1;
216                 } else if (cp == 0x2069) { // POP DIRECTIONAL ISOLATE (PDI)
217                     if (openIsolateCount > 0) openIsolateCount -= 1;
218                 } else if (openIsolateCount == 0) {
219                     // Only consider the characters outside isolate pairs
220                     result = isRtlCodePoint(cp);
221                 }
222             }
223             return result;
224         }
225 
FirstStrong()226         private FirstStrong() {
227         }
228 
229         public static final FirstStrong INSTANCE = new FirstStrong();
230     }
231 
232     /**
233      * Algorithm that uses the presence of any strong directional character of the type indicated
234      * in the constructor parameter to determine the direction of text.
235      *
236      * Characters inside isolate pairs are skipped.
237      */
238     private static class AnyStrong implements TextDirectionAlgorithm {
239         private final boolean mLookForRtl;
240 
241         @Override
checkRtl(CharSequence cs, int start, int count)242         public int checkRtl(CharSequence cs, int start, int count) {
243             boolean haveUnlookedFor = false;
244             int openIsolateCount = 0;
245             for (int cp, i = start, end = start + count; i < end; i += Character.charCount(cp)) {
246                 cp = Character.codePointAt(cs, i);
247                 if (0x2066 <= cp && cp <= 0x2068) { // Opening isolates
248                     openIsolateCount += 1;
249                 } else if (cp == 0x2069) { // POP DIRECTIONAL ISOLATE (PDI)
250                     if (openIsolateCount > 0) openIsolateCount -= 1;
251                 } else if (openIsolateCount == 0) {
252                     // Only consider the characters outside isolate pairs
253                     switch (isRtlCodePoint(cp)) {
254                         case STATE_TRUE:
255                             if (mLookForRtl) {
256                                 return STATE_TRUE;
257                             }
258                             haveUnlookedFor = true;
259                             break;
260                         case STATE_FALSE:
261                             if (!mLookForRtl) {
262                                 return STATE_FALSE;
263                             }
264                             haveUnlookedFor = true;
265                             break;
266                         default:
267                             break;
268                     }
269                 }
270             }
271             if (haveUnlookedFor) {
272                 return mLookForRtl ? STATE_FALSE : STATE_TRUE;
273             }
274             return STATE_UNKNOWN;
275         }
276 
AnyStrong(boolean lookForRtl)277         private AnyStrong(boolean lookForRtl) {
278             this.mLookForRtl = lookForRtl;
279         }
280 
281         public static final AnyStrong INSTANCE_RTL = new AnyStrong(true);
282         public static final AnyStrong INSTANCE_LTR = new AnyStrong(false);
283     }
284 
285     /**
286      * Algorithm that uses the Locale direction to force the direction of a paragraph.
287      */
288     private static class TextDirectionHeuristicLocale extends TextDirectionHeuristicImpl {
289 
TextDirectionHeuristicLocale()290         public TextDirectionHeuristicLocale() {
291             super(null);
292         }
293 
294         @Override
defaultIsRtl()295         protected boolean defaultIsRtl() {
296             final int dir = TextUtils.getLayoutDirectionFromLocale(java.util.Locale.getDefault());
297             return (dir == View.LAYOUT_DIRECTION_RTL);
298         }
299 
300         public static final TextDirectionHeuristicLocale INSTANCE =
301                 new TextDirectionHeuristicLocale();
302     }
303 }
304