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.span;
18
19import android.content.Context;
20import android.content.ContextWrapper;
21import android.graphics.Typeface;
22import android.os.Build;
23import android.support.annotation.Nullable;
24import android.text.Selection;
25import android.text.Spannable;
26import android.text.TextPaint;
27import android.text.style.ClickableSpan;
28import android.util.Log;
29import android.view.View;
30import android.widget.TextView;
31
32/**
33 * A clickable span that will listen for click events and send it back to the context. To use this
34 * class, implement {@link OnLinkClickListener} in your TextView, or use
35 * {@link com.android.setupwizardlib.view.RichTextView#setOnClickListener(View.OnClickListener)}.
36 *
37 * <p />Note on accessibility: For TalkBack to be able to traverse and interact with the links, you
38 * should use {@code LinkAccessibilityHelper} in your {@code TextView} subclass. Optionally you can
39 * also use {@code RichTextView}, which includes link support.
40 */
41public class LinkSpan extends ClickableSpan {
42
43    /*
44     * Implementation note: When the orientation changes, TextView retains a reference to this span
45     * instead of writing it to a parcel (ClickableSpan is not Parcelable). If this class has any
46     * reference to the containing Activity (i.e. the activity context, or any views in the
47     * activity), it will cause memory leak.
48     */
49
50    /* static section */
51
52    private static final String TAG = "LinkSpan";
53
54    private static final Typeface TYPEFACE_MEDIUM =
55            Typeface.create("sans-serif-medium", Typeface.NORMAL);
56
57    /**
58     * @deprecated Use {@link OnLinkClickListener}
59     */
60    @Deprecated
61    public interface OnClickListener {
62        void onClick(LinkSpan span);
63    }
64
65    /**
66     * Listener that is invoked when a link span is clicked. If the containing view of this span
67     * implements this interface, this will be invoked when the link is clicked.
68     */
69    public interface OnLinkClickListener {
70
71        /**
72         * Called when a link has been clicked.
73         *
74         * @param span The span that was clicked.
75         * @return True if the click was handled, stopping further propagation of the click event.
76         */
77        boolean onLinkClick(LinkSpan span);
78    }
79
80    /* non-static section */
81
82    private final String mId;
83
84    public LinkSpan(String id) {
85        mId = id;
86    }
87
88    @Override
89    public void onClick(View view) {
90        if (dispatchClick(view)) {
91            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
92                // Prevent the touch event from bubbling up to the parent views.
93                view.cancelPendingInputEvents();
94            }
95        } else {
96            Log.w(TAG, "Dropping click event. No listener attached.");
97        }
98        if (view instanceof TextView) {
99            // Remove the highlight effect when the click happens by clearing the selection
100            CharSequence text = ((TextView) view).getText();
101            if (text instanceof Spannable) {
102                Selection.setSelection((Spannable) text, 0);
103            }
104        }
105    }
106
107    private boolean dispatchClick(View view) {
108        boolean handled = false;
109        if (view instanceof OnLinkClickListener) {
110            handled = ((OnLinkClickListener) view).onLinkClick(this);
111        }
112        if (!handled) {
113            final OnClickListener listener = getLegacyListenerFromContext(view.getContext());
114            if (listener != null) {
115                listener.onClick(this);
116                handled = true;
117            }
118        }
119        return handled;
120    }
121
122    /**
123     * @deprecated Deprecated together with {@link OnClickListener}
124     */
125    @Nullable
126    @Deprecated
127    private OnClickListener getLegacyListenerFromContext(@Nullable Context context) {
128        while (true) {
129            if (context instanceof OnClickListener) {
130                return (OnClickListener) context;
131            } else if (context instanceof ContextWrapper) {
132                // Unwrap any context wrapper, in base the base context implements onClickListener.
133                // ContextWrappers cannot have circular base contexts, so at some point this will
134                // reach the one of the other cases and return.
135                context = ((ContextWrapper) context).getBaseContext();
136            } else {
137                return null;
138            }
139        }
140    }
141
142    @Override
143    public void updateDrawState(TextPaint drawState) {
144        super.updateDrawState(drawState);
145        drawState.setUnderlineText(false);
146        drawState.setTypeface(TYPEFACE_MEDIUM);
147    }
148
149    public String getId() {
150        return mId;
151    }
152}
153