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 
17 package com.android.setupwizardlib.view;
18 
19 import android.content.Context;
20 import android.text.Annotation;
21 import android.text.SpannableString;
22 import android.text.Spanned;
23 import android.text.method.MovementMethod;
24 import android.text.style.ClickableSpan;
25 import android.text.style.TextAppearanceSpan;
26 import android.util.AttributeSet;
27 import android.util.Log;
28 import android.view.MotionEvent;
29 import android.widget.TextView;
30 import com.android.setupwizardlib.span.LinkSpan;
31 import com.android.setupwizardlib.span.LinkSpan.OnLinkClickListener;
32 import com.android.setupwizardlib.span.SpanHelper;
33 import com.android.setupwizardlib.view.TouchableMovementMethod.TouchableLinkMovementMethod;
34 
35 /**
36  * An extension of TextView that automatically replaces the annotation tags as specified in {@link
37  * SpanHelper#replaceSpan(android.text.Spannable, Object, Object)}
38  *
39  * <p>Note: The accessibility interaction for ClickableSpans (and therefore LinkSpans) are built
40  * into platform in O, although the interaction paradigm is different. (See b/17726921). In this
41  * platform version, the links are exposed in the Local Context Menu of TalkBack instead of
42  * accessible directly through swiping.
43  */
44 public class RichTextView extends TextView implements OnLinkClickListener {
45 
46   /* static section */
47 
48   private static final String TAG = "RichTextView";
49 
50   private static final String ANNOTATION_LINK = "link";
51   private static final String ANNOTATION_TEXT_APPEARANCE = "textAppearance";
52 
53   /**
54    * Replace &lt;annotation&gt; tags in strings to become their respective types. Currently 2 types
55    * are supported:
56    *
57    * <ol>
58    *   <li>&lt;annotation link="foobar"&gt; will create a {@link
59    *       com.android.setupwizardlib.span.LinkSpan} that broadcasts with the key "foobar"
60    *   <li>&lt;annotation textAppearance="TextAppearance.FooBar"&gt; will create a {@link
61    *       android.text.style.TextAppearanceSpan} with @style/TextAppearance.FooBar
62    * </ol>
63    */
getRichText(Context context, CharSequence text)64   public static CharSequence getRichText(Context context, CharSequence text) {
65     if (text instanceof Spanned) {
66       final SpannableString spannable = new SpannableString(text);
67       final Annotation[] spans = spannable.getSpans(0, spannable.length(), Annotation.class);
68       for (Annotation span : spans) {
69         final String key = span.getKey();
70         if (ANNOTATION_TEXT_APPEARANCE.equals(key)) {
71           String textAppearance = span.getValue();
72           final int style =
73               context
74                   .getResources()
75                   .getIdentifier(textAppearance, "style", context.getPackageName());
76           if (style == 0) {
77             Log.w(TAG, "Cannot find resource: " + style);
78           }
79           final TextAppearanceSpan textAppearanceSpan = new TextAppearanceSpan(context, style);
80           SpanHelper.replaceSpan(spannable, span, textAppearanceSpan);
81         } else if (ANNOTATION_LINK.equals(key)) {
82           LinkSpan link = new LinkSpan(span.getValue());
83           SpanHelper.replaceSpan(spannable, span, link);
84         }
85       }
86       return spannable;
87     }
88     return text;
89   }
90 
91   /* non-static section */
92 
93   private OnLinkClickListener mOnLinkClickListener;
94 
RichTextView(Context context)95   public RichTextView(Context context) {
96     super(context);
97   }
98 
RichTextView(Context context, AttributeSet attrs)99   public RichTextView(Context context, AttributeSet attrs) {
100     super(context, attrs);
101   }
102 
103   @Override
setText(CharSequence text, BufferType type)104   public void setText(CharSequence text, BufferType type) {
105     text = getRichText(getContext(), text);
106     // Set text first before doing anything else because setMovementMethod internally calls
107     // setText. This in turn ends up calling this method with mText as the first parameter
108     super.setText(text, type);
109     boolean hasLinks = hasLinks(text);
110 
111     if (hasLinks) {
112       // When a TextView has a movement method, it will set the view to clickable. This makes
113       // View.onTouchEvent always return true and consumes the touch event, essentially
114       // nullifying any return values of MovementMethod.onTouchEvent.
115       // To still allow propagating touch events to the parent when this view doesn't have
116       // links, we only set the movement method here if the text contains links.
117       setMovementMethod(TouchableLinkMovementMethod.getInstance());
118     } else {
119       setMovementMethod(null);
120     }
121     // ExploreByTouchHelper automatically enables focus for RichTextView
122     // even though it may not have any links. Causes problems during talkback
123     // as individual TextViews consume touch events and thereby reducing the focus window
124     // shown by Talkback. Disable focus if there are no links
125     setFocusable(hasLinks);
126     // Do not "reveal" (i.e. scroll to) this view when this view is focused. Since this view is
127     // focusable in touch mode, we may be focused when the screen is first shown, and starting
128     // a screen halfway scrolled down is confusing to the user.
129     setRevealOnFocusHint(false);
130     setFocusableInTouchMode(hasLinks);
131   }
132 
hasLinks(CharSequence text)133   private boolean hasLinks(CharSequence text) {
134     if (text instanceof Spanned) {
135       final ClickableSpan[] spans =
136           ((Spanned) text).getSpans(0, text.length(), ClickableSpan.class);
137       return spans.length > 0;
138     }
139     return false;
140   }
141 
142   @Override
143   @SuppressWarnings("ClickableViewAccessibility") // super.onTouchEvent is called
onTouchEvent(MotionEvent event)144   public boolean onTouchEvent(MotionEvent event) {
145     // Since View#onTouchEvent always return true if the view is clickable (which is the case
146     // when a TextView has a movement method), override the implementation to allow the movement
147     // method, if it implements TouchableMovementMethod, to say that the touch is not handled,
148     // allowing the event to bubble up to the parent view.
149     boolean superResult = super.onTouchEvent(event);
150     MovementMethod movementMethod = getMovementMethod();
151     if (movementMethod instanceof TouchableMovementMethod) {
152       TouchableMovementMethod touchableMovementMethod = (TouchableMovementMethod) movementMethod;
153       if (touchableMovementMethod.getLastTouchEvent() == event) {
154         return touchableMovementMethod.isLastTouchEventHandled();
155       }
156     }
157     return superResult;
158   }
159 
setOnLinkClickListener(OnLinkClickListener listener)160   public void setOnLinkClickListener(OnLinkClickListener listener) {
161     mOnLinkClickListener = listener;
162   }
163 
getOnLinkClickListener()164   public OnLinkClickListener getOnLinkClickListener() {
165     return mOnLinkClickListener;
166   }
167 
168   @Override
onLinkClick(LinkSpan span)169   public boolean onLinkClick(LinkSpan span) {
170     if (mOnLinkClickListener != null) {
171       return mOnLinkClickListener.onLinkClick(span);
172     }
173     return false;
174   }
175 }
176