1/*
2 * Copyright (C) 2016 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 com.android.setupwizardlib.view;
18
19import static com.google.common.truth.Truth.assertThat;
20
21import static org.junit.Assert.assertEquals;
22import static org.junit.Assert.assertFalse;
23import static org.junit.Assert.assertSame;
24import static org.junit.Assert.assertTrue;
25import static org.mockito.Matchers.eq;
26import static org.mockito.Mockito.doReturn;
27import static org.mockito.Mockito.mock;
28import static org.mockito.Mockito.spy;
29import static org.mockito.Mockito.verify;
30import static org.robolectric.RuntimeEnvironment.application;
31
32import android.annotation.SuppressLint;
33import android.content.Context;
34import android.content.ContextWrapper;
35import android.os.Build.VERSION;
36import android.os.Build.VERSION_CODES;
37import android.text.Annotation;
38import android.text.SpannableStringBuilder;
39import android.text.Spanned;
40import android.text.style.TextAppearanceSpan;
41import android.view.MotionEvent;
42
43import com.android.setupwizardlib.robolectric.SuwLibRobolectricTestRunner;
44import com.android.setupwizardlib.span.LinkSpan;
45import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
46import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
47
48import org.junit.Test;
49import org.junit.runner.RunWith;
50import org.robolectric.annotation.Config;
51
52import java.util.Arrays;
53
54@RunWith(SuwLibRobolectricTestRunner.class)
55@Config(sdk = { Config.OLDEST_SDK, Config.NEWEST_SDK })
56public class RichTextViewTest {
57
58    @Test
59    public void testLinkAnnotation() {
60        Annotation link = new Annotation("link", "foobar");
61        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
62        ssb.setSpan(link, 1, 2, 0 /* flags */);
63
64        RichTextView textView = new RichTextView(application);
65        textView.setText(ssb);
66
67        final CharSequence text = textView.getText();
68        assertTrue("Text should be spanned", text instanceof Spanned);
69
70        assertThat(textView.getMovementMethod()).isInstanceOf(TouchableLinkMovementMethod.class);
71
72        Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class);
73        assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length);
74
75        spans = ((Spanned) text).getSpans(0, text.length(), LinkSpan.class);
76        assertEquals("There should be one span " + Arrays.toString(spans), 1, spans.length);
77        assertTrue("The span should be a LinkSpan", spans[0] instanceof LinkSpan);
78        assertEquals("The LinkSpan should have id \"foobar\"",
79                "foobar", ((LinkSpan) spans[0]).getId());
80    }
81
82    @Test
83    public void testOnLinkClickListener() {
84        Annotation link = new Annotation("link", "foobar");
85        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
86        ssb.setSpan(link, 1, 2, 0 /* flags */);
87
88        RichTextView textView = new RichTextView(application);
89        textView.setText(ssb);
90
91        OnLinkClickListener listener = mock(OnLinkClickListener.class);
92        textView.setOnLinkClickListener(listener);
93
94        assertSame(listener, textView.getOnLinkClickListener());
95
96        CharSequence text = textView.getText();
97        LinkSpan[] spans = ((Spanned) text).getSpans(0, text.length(), LinkSpan.class);
98        spans[0].onClick(textView);
99
100        verify(listener).onLinkClick(eq(spans[0]));
101    }
102
103    @Test
104    public void testLegacyContextOnClickListener() {
105        // Click listener implemented by context should still be invoked for compatibility.
106        Annotation link = new Annotation("link", "foobar");
107        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
108        ssb.setSpan(link, 1, 2, 0 /* flags */);
109
110        TestContext context = spy(new TestContext(application));
111        RichTextView textView = new RichTextView(context);
112        textView.setText(ssb);
113
114        CharSequence text = textView.getText();
115        LinkSpan[] spans = ((Spanned) text).getSpans(0, text.length(), LinkSpan.class);
116        spans[0].onClick(textView);
117
118        verify(context).onClick(eq(spans[0]));
119    }
120
121    @Test
122    public void onTouchEvent_clickOnLinks_shouldReturnTrue() {
123        Annotation link = new Annotation("link", "foobar");
124        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
125        ssb.setSpan(link, 0, 2, 0 /* flags */);
126
127        RichTextView textView = new RichTextView(application);
128        textView.setText(ssb);
129
130        TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
131        textView.setMovementMethod(mockMovementMethod);
132
133        MotionEvent motionEvent =
134                MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
135        doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
136        doReturn(true).when(mockMovementMethod).isLastTouchEventHandled();
137        assertThat(textView.onTouchEvent(motionEvent)).isTrue();
138    }
139
140    @Test
141    public void onTouchEvent_clickOutsideLinks_shouldReturnFalse() {
142        Annotation link = new Annotation("link", "foobar");
143        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
144        ssb.setSpan(link, 0, 2, 0 /* flags */);
145
146        RichTextView textView = new RichTextView(application);
147        textView.setText(ssb);
148
149        TouchableLinkMovementMethod mockMovementMethod = mock(TouchableLinkMovementMethod.class);
150        textView.setMovementMethod(mockMovementMethod);
151
152        MotionEvent motionEvent =
153                MotionEvent.obtain(123, 22, MotionEvent.ACTION_DOWN, 0, 0, 0);
154        doReturn(motionEvent).when(mockMovementMethod).getLastTouchEvent();
155        doReturn(false).when(mockMovementMethod).isLastTouchEventHandled();
156        assertThat(textView.onTouchEvent(motionEvent)).isFalse();
157    }
158
159    @Test
160    public void testTextStyle() {
161        Annotation link = new Annotation("textAppearance", "foobar");
162        SpannableStringBuilder ssb = new SpannableStringBuilder("Hello world");
163        ssb.setSpan(link, 1, 2, 0 /* flags */);
164
165        RichTextView textView = new RichTextView(application);
166        textView.setText(ssb);
167
168        final CharSequence text = textView.getText();
169        assertTrue("Text should be spanned", text instanceof Spanned);
170
171        Object[] spans = ((Spanned) text).getSpans(0, text.length(), Annotation.class);
172        assertEquals("Annotation should be removed " + Arrays.toString(spans), 0, spans.length);
173
174        spans = ((Spanned) text).getSpans(0, text.length(), TextAppearanceSpan.class);
175        assertEquals("There should be one span " + Arrays.toString(spans), 1, spans.length);
176        assertTrue("The span should be a TextAppearanceSpan",
177                spans[0] instanceof TextAppearanceSpan);
178    }
179
180    @Test
181    public void testTextContainingLinksAreFocusable() {
182        Annotation testLink = new Annotation("link", "value");
183        SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder("Linked");
184        spannableStringBuilder.setSpan(testLink, 0, 3, 0);
185
186        RichTextView view = new RichTextView(application);
187        view.setText(spannableStringBuilder);
188
189        assertTrue("TextView should be focusable since it contains spans", view.isFocusable());
190    }
191
192
193    @SuppressLint("SetTextI18n")  // It's OK. This is just a test.
194    @Test
195    public void testTextContainingNoLinksAreNotFocusable() {
196        RichTextView textView = new RichTextView(application);
197        textView.setText("Thou shall not be focusable!");
198
199        assertFalse("TextView should not be focusable since it does not contain any span",
200                textView.isFocusable());
201    }
202
203
204    // Based on the text contents of the text view, the "focusable" property of the element
205    // should also be automatically changed.
206    @SuppressLint("SetTextI18n")  // It's OK. This is just a test.
207    @Test
208    public void testRichTextViewFocusChangesWithTextChange() {
209        RichTextView textView = new RichTextView(application);
210        textView.setText("Thou shall not be focusable!");
211
212        assertFalse(textView.isFocusable());
213        assertFalse(textView.isFocusableInTouchMode());
214
215        SpannableStringBuilder spannableStringBuilder =
216                new SpannableStringBuilder("I am focusable");
217        spannableStringBuilder.setSpan(new Annotation("link", "focus:on_me"), 0, 1, 0);
218        textView.setText(spannableStringBuilder);
219        assertTrue(textView.isFocusable());
220        if (VERSION.SDK_INT >= VERSION_CODES.N_MR1) {
221            assertTrue(textView.isFocusableInTouchMode());
222            assertFalse(textView.getRevealOnFocusHint());
223        } else {
224            assertFalse(textView.isFocusableInTouchMode());
225        }
226    }
227
228    public static class TestContext extends ContextWrapper implements LinkSpan.OnClickListener {
229
230        public TestContext(Context base) {
231            super(base);
232        }
233
234        @Override
235        public void onClick(LinkSpan span) {
236            // Ignore. Can be verified using Mockito
237        }
238    }
239}
240