1package com.xtremelabs.robolectric.shadows;
2
3import android.content.Context;
4import android.graphics.Typeface;
5import android.graphics.drawable.Drawable;
6import android.text.Layout;
7import android.text.SpannableStringBuilder;
8import android.text.TextPaint;
9import android.text.TextWatcher;
10import android.text.method.MovementMethod;
11import android.text.method.TransformationMethod;
12import android.text.style.URLSpan;
13import android.text.util.Linkify;
14import android.view.KeyEvent;
15import android.view.MotionEvent;
16import android.view.inputmethod.EditorInfo;
17import android.widget.TextView;
18import com.xtremelabs.robolectric.internal.Implementation;
19import com.xtremelabs.robolectric.internal.Implements;
20
21import java.util.ArrayList;
22import java.util.List;
23
24import static android.view.View.VISIBLE;
25import static com.xtremelabs.robolectric.Robolectric.shadowOf;
26import static com.xtremelabs.robolectric.Robolectric.shadowOf_;
27
28@SuppressWarnings({"UnusedDeclaration"})
29@Implements(TextView.class)
30public class ShadowTextView extends ShadowView {
31    private CharSequence text = "";
32    private CompoundDrawables compoundDrawablesImpl = new CompoundDrawables(0, 0, 0, 0);
33    private Integer textColorHexValue;
34    private Integer hintColorHexValue;
35    private float textSize = 14.0f;
36    private boolean autoLinkPhoneNumbers;
37    private int autoLinkMask;
38    private CharSequence hintText;
39    private CharSequence errorText;
40    private int compoundDrawablePadding;
41    private MovementMethod movementMethod;
42    private boolean linksClickable;
43    private int gravity;
44    private TextView.OnEditorActionListener onEditorActionListener;
45    private int imeOptions = EditorInfo.IME_NULL;
46    private int textAppearanceId;
47    private TransformationMethod transformationMethod;
48    private int inputType;
49    protected int selectionStart = 0;
50    protected int selectionEnd = 0;
51    private Typeface typeface;
52
53    private List<TextWatcher> watchers = new ArrayList<TextWatcher>();
54    private List<Integer> previousKeyCodes = new ArrayList<Integer>();
55    private List<KeyEvent> previousKeyEvents = new ArrayList<KeyEvent>();
56    private Layout layout;
57
58    @Override
59    public void applyAttributes() {
60        super.applyAttributes();
61        applyTextAttribute();
62        applyTextColorAttribute();
63        applyHintAttribute();
64        applyHintColorAttribute();
65        applyCompoundDrawablesWithIntrinsicBoundsAttributes();
66    }
67
68    @Implementation(i18nSafe = false)
69    public void setText(CharSequence text) {
70        if (text == null) {
71            text = "";
72        }
73
74        sendBeforeTextChanged(text);
75
76        CharSequence oldValue = this.text;
77        this.text = text;
78
79        sendOnTextChanged(oldValue);
80        sendAfterTextChanged();
81    }
82
83    @Implementation
84    public final void append(CharSequence text) {
85        boolean isSelectStartAtEnd = selectionStart == this.text.length();
86        boolean isSelectEndAtEnd = selectionEnd == this.text.length();
87        CharSequence oldValue = this.text;
88        StringBuffer sb = new StringBuffer(this.text);
89        sb.append(text);
90
91        sendBeforeTextChanged(sb.toString());
92        this.text = sb.toString();
93
94        if (isSelectStartAtEnd) {
95            selectionStart = this.text.length();
96        }
97        if (isSelectEndAtEnd) {
98            selectionEnd = this.text.length();
99        }
100
101        sendOnTextChanged(oldValue);
102        sendAfterTextChanged();
103    }
104
105    @Implementation
106    public void setText(int textResourceId) {
107        sendBeforeTextChanged(text);
108
109        CharSequence oldValue = this.text;
110        this.text = getResources().getText(textResourceId);
111
112        sendOnTextChanged(oldValue);
113        sendAfterTextChanged();
114    }
115
116    private void sendAfterTextChanged() {
117        for (TextWatcher watcher : watchers) {
118            watcher.afterTextChanged(new SpannableStringBuilder(getText()));
119        }
120    }
121
122    private void sendOnTextChanged(CharSequence oldValue) {
123        for (TextWatcher watcher : watchers) {
124            watcher.onTextChanged(text, 0, oldValue.length(), text.length());
125        }
126    }
127
128    private void sendBeforeTextChanged(CharSequence newValue) {
129        for (TextWatcher watcher : watchers) {
130            watcher.beforeTextChanged(this.text, 0, this.text.length(), newValue.length());
131        }
132    }
133
134    @Implementation
135    public CharSequence getText() {
136        return text;
137    }
138
139    @Implementation
140    public Typeface getTypeface() {
141        return typeface;
142    }
143
144    @Implementation
145    public void setTypeface(Typeface typeface) {
146        this.typeface = typeface;
147    }
148
149    @Implementation
150    public int length() {
151        return text.length();
152    }
153
154    @Implementation
155    public void setTextColor(int color) {
156        textColorHexValue = color;
157    }
158
159    @Implementation
160    public void setTextSize(float size) {
161        textSize = size;
162    }
163
164    @Implementation
165    public void setTextAppearance(Context context, int resid) {
166        textAppearanceId = resid;
167    }
168
169    @Implementation
170    public void setInputType(int type) {
171        this.inputType = type;
172    }
173
174    @Implementation
175    public int getInputType() {
176        return this.inputType;
177    }
178
179    @Implementation
180    public final void setHint(int resId) {
181        this.hintText = getResources().getText(resId);
182    }
183
184    @Implementation(i18nSafe = false)
185    public final void setHint(CharSequence hintText) {
186        this.hintText = hintText;
187    }
188
189    @Implementation
190    public CharSequence getHint() {
191        return hintText;
192    }
193
194    @Implementation
195    public final void setHintTextColor(int color) {
196        hintColorHexValue = color;
197    }
198
199    @Implementation
200    public final boolean getLinksClickable() {
201        return linksClickable;
202    }
203
204    @Implementation
205    public final void setLinksClickable(boolean whether) {
206        linksClickable = whether;
207    }
208
209    @Implementation
210    public final MovementMethod getMovementMethod() {
211        return movementMethod;
212    }
213
214    @Implementation
215    public final void setMovementMethod(MovementMethod movement) {
216        movementMethod = movement;
217    }
218
219    @Implementation
220    public URLSpan[] getUrls() {
221        String[] words = text.toString().split("\\s+");
222        List<URLSpan> urlSpans = new ArrayList<URLSpan>();
223        for (String word : words) {
224            if (word.startsWith("http://")) {
225                urlSpans.add(new URLSpan(word));
226            }
227        }
228        return urlSpans.toArray(new URLSpan[urlSpans.size()]);
229    }
230
231    @Implementation
232    public final void setAutoLinkMask(int mask) {
233        autoLinkMask = mask;
234
235        autoLinkPhoneNumbers = (mask & Linkify.PHONE_NUMBERS) != 0;
236    }
237
238    @Implementation
239    public void setCompoundDrawablesWithIntrinsicBounds(int left, int top, int right, int bottom) {
240        compoundDrawablesImpl = new CompoundDrawables(left, top, right, bottom);
241    }
242
243    @Implementation
244    public void setCompoundDrawablesWithIntrinsicBounds(Drawable left, Drawable top,
245                                                        Drawable right, Drawable bottom) {
246        compoundDrawablesImpl = new CompoundDrawables(left, top, right, bottom);
247    }
248
249    @Implementation
250    public void setCompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) {
251        compoundDrawablesImpl = new CompoundDrawables(left, top, right, bottom);
252    }
253
254    @Implementation
255    public Drawable[] getCompoundDrawables() {
256        if (compoundDrawablesImpl == null) {
257            return new Drawable[]{null, null, null, null};
258        }
259        return new Drawable[]{
260                compoundDrawablesImpl.leftDrawable,
261                compoundDrawablesImpl.topDrawable,
262                compoundDrawablesImpl.rightDrawable,
263                compoundDrawablesImpl.bottomDrawable
264        };
265    }
266
267    @Implementation
268    public void setCompoundDrawablePadding(int compoundDrawablePadding) {
269        this.compoundDrawablePadding = compoundDrawablePadding;
270    }
271
272    @Implementation
273    public int getCompoundDrawablePadding() {
274        return compoundDrawablePadding;
275    }
276
277    @Implementation
278    public boolean onKeyDown(int keyCode, KeyEvent event) {
279        previousKeyCodes.add(keyCode);
280        previousKeyEvents.add(event);
281        if (onKeyListener != null) {
282            return onKeyListener.onKey(realView, keyCode, event);
283        } else {
284            return false;
285        }
286    }
287
288    @Implementation
289    public boolean onKeyUp(int keyCode, KeyEvent event) {
290        previousKeyCodes.add(keyCode);
291        previousKeyEvents.add(event);
292        if (onKeyListener != null) {
293            return onKeyListener.onKey(realView, keyCode, event);
294        } else {
295            return false;
296        }
297    }
298
299    public int getPreviousKeyCode(int index) {
300        return previousKeyCodes.get(index);
301    }
302
303    public KeyEvent getPreviousKeyEvent(int index) {
304        return previousKeyEvents.get(index);
305    }
306
307    @Implementation
308    public int getGravity() {
309        return gravity;
310    }
311
312    @Implementation
313    public void setGravity(int gravity) {
314        this.gravity = gravity;
315    }
316
317
318    @Implementation
319    public int getImeOptions() {
320        return imeOptions;
321    }
322
323    @Implementation
324    public void setImeOptions(int imeOptions) {
325        this.imeOptions = imeOptions;
326    }
327
328    /**
329     * Returns the text string of this {@code TextView}.
330     * <p/>
331     * Robolectric extension.
332     */
333    @Override
334    public String innerText() {
335        return (text == null || getVisibility() != VISIBLE) ? "" : text.toString();
336    }
337
338    @Implementation
339    public void setError(CharSequence error) {
340      errorText = error;
341    }
342
343    @Implementation
344    public CharSequence getError() {
345      return errorText;
346    }
347
348    @Override
349    @Implementation
350    public boolean equals(Object o) {
351        return super.equals(shadowOf_(o));
352    }
353
354    @Override
355    @Implementation
356    public int hashCode() {
357        return super.hashCode();
358    }
359
360    public CompoundDrawables getCompoundDrawablesImpl() {
361        return compoundDrawablesImpl;
362    }
363
364    void setCompoundDrawablesImpl(CompoundDrawables compoundDrawablesImpl) {
365        this.compoundDrawablesImpl = compoundDrawablesImpl;
366    }
367
368    public Integer getTextColorHexValue() {
369        return textColorHexValue;
370    }
371
372    public int getTextAppearanceId() {
373        return textAppearanceId;
374    }
375
376    public Integer getHintColorHexValue() {
377        return hintColorHexValue;
378    }
379
380    @Implementation
381    public float getTextSize() {
382        return textSize;
383    }
384
385    public boolean isAutoLinkPhoneNumbers() {
386        return autoLinkPhoneNumbers;
387    }
388
389    private void applyTextAttribute() {
390        String text = attributeSet.getAttributeValue("android", "text");
391        if (text != null) {
392            if (text.startsWith("@string/")) {
393                int textResId = attributeSet.getAttributeResourceValue("android", "text", 0);
394                text = context.getResources().getString(textResId);
395            }
396            setText(text);
397        }
398    }
399
400    private void applyTextColorAttribute() {
401        String colorValue = attributeSet.getAttributeValue("android", "textColor");
402        if (colorValue != null) {
403            if (colorValue.startsWith("@color/") || colorValue.startsWith("@android:color/")) {
404                int colorResId = attributeSet.getAttributeResourceValue("android", "textColor", 0);
405                setTextColor(context.getResources().getColor(colorResId));
406            } else if (colorValue.startsWith("#")) {
407                int colorFromHex = (int) Long.valueOf(colorValue.replaceAll("#", ""), 16).longValue();
408                setTextColor(colorFromHex);
409            }
410        }
411    }
412
413    private void applyHintAttribute() {
414        String hint = attributeSet.getAttributeValue("android", "hint");
415        if (hint != null) {
416            if (hint.startsWith("@string/")) {
417                int textResId = attributeSet.getAttributeResourceValue("android", "hint", 0);
418                hint = context.getResources().getString(textResId);
419
420            }
421            setHint(hint);
422        }
423    }
424
425    private void applyHintColorAttribute() {
426        String colorValue = attributeSet.getAttributeValue("android", "hintColor");
427        if (colorValue != null) {
428            if (colorValue.startsWith("@color/") || colorValue.startsWith("@android:color/")) {
429                int colorResId = attributeSet.getAttributeResourceValue("android", "hintColor", 0);
430                setHintTextColor(context.getResources().getColor(colorResId));
431            } else if (colorValue.startsWith("#")) {
432                int colorFromHex = (int) Long.valueOf(colorValue.replaceAll("#", ""), 16).longValue();
433                setHintTextColor(colorFromHex);
434            }
435        }
436    }
437
438    private void applyCompoundDrawablesWithIntrinsicBoundsAttributes() {
439        setCompoundDrawablesWithIntrinsicBounds(
440                attributeSet.getAttributeResourceValue("android", "drawableLeft", 0),
441                attributeSet.getAttributeResourceValue("android", "drawableTop", 0),
442                attributeSet.getAttributeResourceValue("android", "drawableRight", 0),
443                attributeSet.getAttributeResourceValue("android", "drawableBottom", 0));
444    }
445
446    @Implementation
447    public void setOnEditorActionListener(android.widget.TextView.OnEditorActionListener onEditorActionListener) {
448        this.onEditorActionListener = onEditorActionListener;
449    }
450
451    public boolean triggerEditorAction(int imeAction) {
452        if (onEditorActionListener != null) {
453            return onEditorActionListener.onEditorAction((TextView) realView, imeAction, null);
454        }
455        return false;
456    }
457
458    @Implementation
459    public void setTransformationMethod(TransformationMethod transformationMethod) {
460        this.transformationMethod = transformationMethod;
461    }
462
463    @Implementation
464    public TransformationMethod getTransformationMethod() {
465        return transformationMethod;
466    }
467
468    @Implementation
469    public void addTextChangedListener(TextWatcher watcher) {
470        this.watchers.add(watcher);
471    }
472
473    @Implementation
474    public void removeTextChangedListener(TextWatcher watcher) {
475        this.watchers.remove(watcher);
476    }
477
478    @Implementation
479    public TextPaint getPaint() {
480        return new TextPaint();
481    }
482
483    @Implementation
484    public Layout getLayout() {
485        return this.layout;
486    }
487
488    public void setSelection(int index) {
489        setSelection(index, index);
490    }
491
492    public void setSelection(int start, int end) {
493        selectionStart = start;
494        selectionEnd = end;
495    }
496
497    @Implementation
498    public int getSelectionStart() {
499        return selectionStart;
500    }
501
502    @Implementation
503    public int getSelectionEnd() {
504        return selectionEnd;
505    }
506
507    @Implementation
508    public boolean onTouchEvent(MotionEvent event) {
509        boolean superResult = super.onTouchEvent(event);
510
511        if (movementMethod != null) {
512            boolean handled = movementMethod.onTouchEvent(null, null, event);
513
514            if (handled) {
515                return true;
516            }
517        }
518
519        return superResult;
520    }
521
522    /**
523     * @return the list of currently registered watchers/listeners
524     */
525    public List<TextWatcher> getWatchers() {
526        return watchers;
527    }
528
529    public void setLayout(Layout layout) {
530        this.layout = layout;
531    }
532
533    public static class CompoundDrawables {
534        public Drawable leftDrawable;
535        public Drawable topDrawable;
536        public Drawable rightDrawable;
537        public Drawable bottomDrawable;
538
539        public CompoundDrawables(Drawable left, Drawable top, Drawable right, Drawable bottom) {
540            leftDrawable = left;
541            topDrawable = top;
542            rightDrawable = right;
543            bottomDrawable = bottom;
544        }
545
546        public CompoundDrawables(int left, int top, int right, int bottom) {
547            leftDrawable = left != 0 ? ShadowDrawable.createFromResourceId(left) : null;
548            topDrawable = top != 0 ? ShadowDrawable.createFromResourceId(top) : null;
549            rightDrawable = right != 0 ? ShadowDrawable.createFromResourceId(right) : null;
550            bottomDrawable = bottom != 0 ? ShadowDrawable.createFromResourceId(bottom) : null;
551        }
552
553        @Override
554        public boolean equals(Object o) {
555            if (this == o) return true;
556            if (o == null || getClass() != o.getClass()) return false;
557
558            CompoundDrawables that = (CompoundDrawables) o;
559
560            if (getBottom() != that.getBottom()) return false;
561            if (getLeft() != that.getLeft()) return false;
562            if (getRight() != that.getRight()) return false;
563            if (getTop() != that.getTop()) return false;
564
565            return true;
566        }
567
568        @Override
569        public int hashCode() {
570            int result = getLeft();
571            result = 31 * result + getTop();
572            result = 31 * result + getRight();
573            result = 31 * result + getBottom();
574            return result;
575        }
576
577        @Override
578        public String toString() {
579            return "CompoundDrawables{" +
580                    "left=" + getLeft() +
581                    ", top=" + getTop() +
582                    ", right=" + getRight() +
583                    ", bottom=" + getBottom() +
584                    '}';
585        }
586
587        public int getLeft() {
588            return shadowOf(leftDrawable).getLoadedFromResourceId();
589        }
590
591        public int getTop() {
592            return shadowOf(topDrawable).getLoadedFromResourceId();
593        }
594
595        public int getRight() {
596            return shadowOf(rightDrawable).getLoadedFromResourceId();
597        }
598
599        public int getBottom() {
600            return shadowOf(bottomDrawable).getLoadedFromResourceId();
601        }
602    }
603}
604