1 /*
2  * Copyright (C) 2013 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.cts;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNull;
22 import static org.junit.Assert.assertTrue;
23 
24 import android.icu.util.ULocale;
25 import android.text.BidiFormatter;
26 import android.text.SpannableString;
27 import android.text.Spanned;
28 import android.text.TextDirectionHeuristics;
29 import android.text.style.RelativeSizeSpan;
30 
31 import androidx.test.filters.SmallTest;
32 import androidx.test.runner.AndroidJUnit4;
33 
34 import org.junit.Test;
35 import org.junit.runner.RunWith;
36 
37 import java.util.Locale;
38 
39 @SmallTest
40 @RunWith(AndroidJUnit4.class)
41 public class BidiFormatterTest {
42 
43     private static final BidiFormatter LTR_FMT = BidiFormatter.getInstance(false /* LTR context */);
44     private static final BidiFormatter RTL_FMT = BidiFormatter.getInstance(true /* RTL context */);
45 
46     private static final BidiFormatter LTR_FMT_EXIT_RESET =
47             new BidiFormatter.Builder(false /* LTR context */).stereoReset(false).build();
48     private static final BidiFormatter RTL_FMT_EXIT_RESET =
49             new BidiFormatter.Builder(true /* RTL context */).stereoReset(false).build();
50 
51     private static final String EN = "abba";
52     private static final String HE = "\u05E0\u05E1";
53 
54     private static final String LRM = "\u200E";
55     private static final String RLM = "\u200F";
56     private static final String LRE = "\u202A";
57     private static final String RLE = "\u202B";
58     private static final String PDF = "\u202C";
59 
60     @Test
testIsRtlContext()61     public void testIsRtlContext() {
62         assertEquals(false, LTR_FMT.isRtlContext());
63         assertEquals(true, RTL_FMT.isRtlContext());
64 
65         assertEquals(false, BidiFormatter.getInstance(Locale.ENGLISH).isRtlContext());
66         assertEquals(true, BidiFormatter.getInstance(true).isRtlContext());
67     }
68 
69     @Test
testCachedInstances()70     public void testCachedInstances() {
71         // Test that we get the same cached static instances for simple cases
72         BidiFormatter defaultFormatterInstance = BidiFormatter.getInstance();
73         assertTrue(defaultFormatterInstance == LTR_FMT || defaultFormatterInstance == RTL_FMT);
74 
75         assertEquals(LTR_FMT, BidiFormatter.getInstance(false));
76         assertEquals(RTL_FMT, BidiFormatter.getInstance(true));
77 
78         assertEquals(LTR_FMT, BidiFormatter.getInstance(false));
79         assertEquals(RTL_FMT, BidiFormatter.getInstance(Locale.forLanguageTag("ar")));
80     }
81 
82     @Test
testBuilderIsRtlContext()83     public void testBuilderIsRtlContext() {
84         assertEquals(false, new BidiFormatter.Builder(false).build().isRtlContext());
85         assertEquals(true, new BidiFormatter.Builder(true).build().isRtlContext());
86     }
87 
88     @Test
testIsRtl()89     public void testIsRtl() {
90         assertEquals(true, BidiFormatter.getInstance(true).isRtl(HE));
91         assertEquals(true, BidiFormatter.getInstance(false).isRtl(HE));
92 
93         assertEquals(false, BidiFormatter.getInstance(true).isRtl(EN));
94         assertEquals(false, BidiFormatter.getInstance(false).isRtl(EN));
95     }
96 
97     @Test
testUnicodeWrap()98     public void testUnicodeWrap() {
99         // Make sure an input of null doesn't crash anything.
100         assertNull(LTR_FMT.unicodeWrap(null));
101 
102         // Uniform directionality in opposite context.
103         assertEquals("uniform dir opposite to LTR context",
104                 RLE + "." + HE + "." + PDF + LRM,
105                 LTR_FMT_EXIT_RESET.unicodeWrap("." + HE + "."));
106         assertEquals("uniform dir opposite to LTR context, stereo reset",
107                 LRM + RLE + "." + HE + "." + PDF + LRM,
108                 LTR_FMT.unicodeWrap("." + HE + "."));
109         assertEquals("uniform dir opposite to LTR context, stereo reset, no isolation",
110                 RLE + "." + HE + "." + PDF,
111                 LTR_FMT.unicodeWrap("." + HE + ".", false));
112         assertEquals("neutral treated as opposite to LTR context",
113                 RLE + "." + PDF + LRM,
114                 LTR_FMT_EXIT_RESET.unicodeWrap(".", TextDirectionHeuristics.RTL));
115         assertEquals("uniform dir opposite to RTL context",
116                 LRE + "." + EN + "." + PDF + RLM,
117                 RTL_FMT_EXIT_RESET.unicodeWrap("." + EN + "."));
118         assertEquals("uniform dir opposite to RTL context, stereo reset",
119                 RLM + LRE + "." + EN + "." + PDF + RLM,
120                 RTL_FMT.unicodeWrap("." + EN + "."));
121         assertEquals("uniform dir opposite to RTL context, stereo reset, no isolation",
122                 LRE + "." + EN + "." + PDF,
123                 RTL_FMT.unicodeWrap("." + EN + ".", false));
124         assertEquals("neutral treated as opposite to RTL context",
125                 LRE + "." + PDF + RLM,
126                 RTL_FMT_EXIT_RESET.unicodeWrap(".", TextDirectionHeuristics.LTR));
127 
128         // We test mixed-directionality cases only with an explicit overall directionality parameter
129         // because the estimation logic is outside the sphere of BidiFormatter, and different
130         // estimators will treat them differently.
131 
132         // Overall directionality matching context, but with opposite exit directionality.
133         assertEquals("exit dir opposite to LTR context",
134                 EN + HE + LRM,
135                 LTR_FMT_EXIT_RESET.unicodeWrap(EN + HE, TextDirectionHeuristics.LTR));
136         assertEquals("exit dir opposite to LTR context, stereo reset",
137                 EN + HE + LRM,
138                 LTR_FMT.unicodeWrap(EN + HE, TextDirectionHeuristics.LTR));
139         assertEquals("exit dir opposite to LTR context, stereo reset, no isolation",
140                 EN + HE,
141                 LTR_FMT.unicodeWrap(EN + HE, TextDirectionHeuristics.LTR, false));
142 
143         assertEquals("exit dir opposite to RTL context",
144                 HE + EN + RLM,
145                 RTL_FMT_EXIT_RESET.unicodeWrap(HE + EN, TextDirectionHeuristics.RTL));
146         assertEquals("exit dir opposite to RTL context, stereo reset",
147                 HE + EN + RLM,
148                 RTL_FMT.unicodeWrap(HE + EN, TextDirectionHeuristics.RTL));
149         assertEquals("exit dir opposite to RTL context, stereo reset, no isolation",
150                 HE + EN,
151                 RTL_FMT.unicodeWrap(HE + EN, TextDirectionHeuristics.RTL, false));
152 
153         // Overall directionality matching context, but with opposite entry directionality.
154         assertEquals("entry dir opposite to LTR context",
155                 HE + EN,
156                 LTR_FMT_EXIT_RESET.unicodeWrap(HE + EN, TextDirectionHeuristics.LTR));
157         assertEquals("entry dir opposite to LTR context, stereo reset",
158                 LRM + HE + EN,
159                 LTR_FMT.unicodeWrap(HE + EN, TextDirectionHeuristics.LTR));
160         assertEquals("entry dir opposite to LTR context, stereo reset, no isolation",
161                 HE + EN,
162                 LTR_FMT.unicodeWrap(HE + EN, TextDirectionHeuristics.LTR, false));
163 
164         assertEquals("entry dir opposite to RTL context",
165                 EN + HE,
166                 RTL_FMT_EXIT_RESET.unicodeWrap(EN + HE, TextDirectionHeuristics.RTL));
167         assertEquals("entry dir opposite to RTL context, stereo reset",
168                 RLM + EN + HE,
169                 RTL_FMT.unicodeWrap(EN + HE, TextDirectionHeuristics.RTL));
170         assertEquals("entry dir opposite to RTL context, stereo reset, no isolation",
171                 EN + HE,
172                 RTL_FMT.unicodeWrap(EN + HE, TextDirectionHeuristics.RTL, false));
173 
174         // Overall directionality matching context, but with opposite entry and exit directionality.
175         assertEquals("entry and exit dir opposite to LTR context",
176                 HE + EN + HE + LRM,
177                 LTR_FMT_EXIT_RESET.unicodeWrap(HE + EN + HE, TextDirectionHeuristics.LTR));
178         assertEquals("entry and exit dir opposite to LTR context, stereo reset",
179                 LRM + HE + EN + HE + LRM,
180                 LTR_FMT.unicodeWrap(HE + EN + HE, TextDirectionHeuristics.LTR));
181         assertEquals("entry and exit dir opposite to LTR context, no isolation",
182                 HE + EN + HE,
183                 LTR_FMT_EXIT_RESET.unicodeWrap(HE + EN + HE, TextDirectionHeuristics.LTR, false));
184 
185         assertEquals("entry and exit dir opposite to RTL context",
186                 EN + HE + EN + RLM,
187                 RTL_FMT_EXIT_RESET.unicodeWrap(EN + HE + EN, TextDirectionHeuristics.RTL));
188         assertEquals("entry and exit dir opposite to RTL context, no isolation",
189                 EN + HE + EN,
190                 RTL_FMT_EXIT_RESET.unicodeWrap(EN + HE + EN, TextDirectionHeuristics.RTL, false));
191 
192         // Entry and exit directionality matching context, but with opposite overall directionality.
193         assertEquals("overall dir (but not entry or exit dir) opposite to LTR context",
194                 RLE + EN + HE + EN + PDF + LRM,
195                 LTR_FMT_EXIT_RESET.unicodeWrap(EN + HE + EN, TextDirectionHeuristics.RTL));
196         assertEquals("overall dir (but not entry or exit dir) opposite to LTR context, stereo reset",
197                 LRM + RLE + EN + HE + EN + PDF + LRM,
198                 LTR_FMT.unicodeWrap(EN + HE + EN, TextDirectionHeuristics.RTL));
199         assertEquals("overall dir (but not entry or exit dir) opposite to LTR context, no isolation",
200                 RLE + EN + HE + EN + PDF,
201                 LTR_FMT_EXIT_RESET.unicodeWrap(EN + HE + EN, TextDirectionHeuristics.RTL, false));
202 
203         assertEquals("overall dir (but not entry or exit dir) opposite to RTL context",
204                 LRE + HE + EN + HE + PDF + RLM,
205                 RTL_FMT_EXIT_RESET.unicodeWrap(HE + EN + HE, TextDirectionHeuristics.LTR));
206         assertEquals("overall dir (but not entry or exit dir) opposite to RTL context, stereo reset",
207                 RLM + LRE + HE + EN + HE + PDF + RLM,
208                 RTL_FMT.unicodeWrap(HE + EN + HE, TextDirectionHeuristics.LTR));
209         assertEquals("overall dir (but not entry or exit dir) opposite to RTL context, no isolation",
210                 LRE + HE + EN + HE + PDF,
211                 RTL_FMT_EXIT_RESET.unicodeWrap(HE + EN + HE, TextDirectionHeuristics.LTR, false));
212     }
213 
214     @Test
testGetStereoReset()215     public void testGetStereoReset() {
216         assertTrue(LTR_FMT.getStereoReset());
217         assertTrue(RTL_FMT.getStereoReset());
218         assertFalse(LTR_FMT_EXIT_RESET.getStereoReset());
219         assertFalse(RTL_FMT_EXIT_RESET.getStereoReset());
220     }
221 
222     @Test
testBuilder_construction()223     public void testBuilder_construction() {
224         final BidiFormatter defaultFmt = new BidiFormatter.Builder().build();
225         // Test that the default locale and the BidiFormatter's locale have the same direction.
226         assertEquals(ULocale.getDefault().isRightToLeft(), defaultFmt.isRtlContext());
227 
228         final BidiFormatter ltrFmt = new BidiFormatter.Builder(false).build();
229         assertFalse(ltrFmt.isRtlContext());
230 
231         final BidiFormatter rtlFmt = new BidiFormatter.Builder(true).build();
232         assertTrue(rtlFmt.isRtlContext());
233 
234         final BidiFormatter englishFmt = new BidiFormatter.Builder(Locale.ENGLISH).build();
235         assertFalse(englishFmt.isRtlContext());
236 
237         final BidiFormatter arabicFmt =
238                 new BidiFormatter.Builder(Locale.forLanguageTag("ar")).build();
239         assertTrue(arabicFmt.isRtlContext());
240     }
241 
242     @Test
testBuilder_setTextDirectionHeuristic()243     public void testBuilder_setTextDirectionHeuristic() {
244         final BidiFormatter defaultFmt = new BidiFormatter.Builder().build();
245         assertFalse(defaultFmt.isRtl(EN + HE + EN));
246 
247         final BidiFormatter modifiedFmt = new BidiFormatter.Builder().setTextDirectionHeuristic(
248                 TextDirectionHeuristics.ANYRTL_LTR).build();
249         assertTrue(modifiedFmt.isRtl(EN + HE + EN));
250     }
251 
252     @Test
testCharSequenceApis()253     public void testCharSequenceApis() {
254         final CharSequence CS_HE = new SpannableString(HE);
255         assertEquals(true, BidiFormatter.getInstance(true).isRtl(CS_HE));
256 
257         final SpannableString CS_EN_HE = new SpannableString(EN + HE);
258         final Object RELATIVE_SIZE_SPAN = new RelativeSizeSpan(1.2f);
259         CS_EN_HE.setSpan(RELATIVE_SIZE_SPAN, 0, EN.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
260 
261         Spanned wrapped;
262         Object[] spans;
263 
264         wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE);
265         assertEquals(EN + HE + LRM, wrapped.toString());
266         spans = wrapped.getSpans(0, wrapped.length(), Object.class);
267         assertEquals(1, spans.length);
268         assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
269         assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
270         assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
271 
272         wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE, TextDirectionHeuristics.LTR);
273         assertEquals(EN + HE + LRM, wrapped.toString());
274         spans = wrapped.getSpans(0, wrapped.length(), Object.class);
275         assertEquals(1, spans.length);
276         assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
277         assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
278         assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
279 
280         wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE, false);
281         assertEquals(EN + HE, wrapped.toString());
282         spans = wrapped.getSpans(0, wrapped.length(), Object.class);
283         assertEquals(1, spans.length);
284         assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
285         assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
286         assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
287 
288         wrapped = (Spanned) LTR_FMT.unicodeWrap(CS_EN_HE, TextDirectionHeuristics.LTR, false);
289         assertEquals(EN + HE, wrapped.toString());
290         spans = wrapped.getSpans(0, wrapped.length(), Object.class);
291         assertEquals(1, spans.length);
292         assertEquals(RELATIVE_SIZE_SPAN, spans[0]);
293         assertEquals(0, wrapped.getSpanStart(RELATIVE_SIZE_SPAN));
294         assertEquals(EN.length(), wrapped.getSpanEnd(RELATIVE_SIZE_SPAN));
295     }
296 }
297