LinkAccessibilityHelper.java revision d832154e333a3a45b5faecd518b664ddd297183c
1d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam/*
2d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Copyright (C) 2016 The Android Open Source Project
3d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *
4d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Licensed under the Apache License, Version 2.0 (the "License");
5d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * you may not use this file except in compliance with the License.
6d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * You may obtain a copy of the License at
7d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *
8d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *      http://www.apache.org/licenses/LICENSE-2.0
9d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *
10d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * Unless required by applicable law or agreed to in writing, software
11d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * distributed under the License is distributed on an "AS IS" BASIS,
12d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * See the License for the specific language governing permissions and
14d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * limitations under the License.
15d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam */
16d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
17d832154e333a3a45b5faecd518b664ddd297183cMaurice Lampackage com.android.setupwizardlib.util;
18d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
19d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.graphics.Rect;
20d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.os.Bundle;
21d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
22d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.support.v4.widget.ExploreByTouchHelper;
23d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.Layout;
24d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.Spanned;
25d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.text.style.ClickableSpan;
26d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.util.Log;
27d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.view.accessibility.AccessibilityEvent;
28d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport android.widget.TextView;
29d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
30d832154e333a3a45b5faecd518b664ddd297183cMaurice Lamimport java.util.List;
31d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
32d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam/**
33d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * An accessibility delegate that allows {@link android.text.style.ClickableSpan} to be focused and
34d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * clicked by accessibility services.
35d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *
36d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * <p />Sample usage:
37d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * <pre>
38d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * LinkAccessibilityHelper mAccessibilityHelper;
39d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *
40d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * private void init() {
41d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *     mAccessibilityHelper = new LinkAccessibilityHelper(myTextView);
42d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *     ViewCompat.setAccessibilityDelegate(myTextView, mLinkHelper);
43d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * }
44d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *
45d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * {@literal @}Override
46d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * protected boolean dispatchHoverEvent({@literal @}NonNull MotionEvent event) {
47d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *     if (mAccessibilityHelper != null && mAccessibilityHelper.dispatchHoverEvent(event)) {
48d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *         return true;
49d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *     }
50d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *     return super.dispatchHoverEvent(event);
51d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * }
52d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * </pre>
53d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam *
54d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * @see com.android.setupwizardlib.view.RichTextView
55d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam * @see android.support.v4.widget.ExploreByTouchHelper
56d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam */
57d832154e333a3a45b5faecd518b664ddd297183cMaurice Lampublic class LinkAccessibilityHelper extends ExploreByTouchHelper {
58d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
59d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private static final String TAG = "LinkAccessibilityHelper";
60d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
61d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private final TextView mView;
62d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private final Rect mTempRect = new Rect();
63d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
64d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    public LinkAccessibilityHelper(TextView view) {
65d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        super(view);
66d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        mView = view;
67d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
68d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
69d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    @Override
70d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    protected int getVirtualViewAt(float x, float y) {
71d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        final CharSequence text = mView.getText();
72d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (text instanceof Spanned) {
73d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final Spanned spannedText = (Spanned) text;
74d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final int offset = getOffsetForPosition(mView, x, y);
75d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            ClickableSpan[] linkSpans = spannedText.getSpans(offset, offset, ClickableSpan.class);
76d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            if (linkSpans.length == 1) {
77d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                ClickableSpan linkSpan = linkSpans[0];
78d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                return spannedText.getSpanStart(linkSpan);
79d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            }
80d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
81d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return INVALID_ID;
82d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
83d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
84d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    @Override
85d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    protected void getVisibleVirtualViews(List<Integer> virtualViewIds) {
86d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        final CharSequence text = mView.getText();
87d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (text instanceof Spanned) {
88d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final Spanned spannedText = (Spanned) text;
89d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            ClickableSpan[] linkSpans = spannedText.getSpans(0, spannedText.length(),
90d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                    ClickableSpan.class);
91d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            for (ClickableSpan span : linkSpans) {
92d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                virtualViewIds.add(spannedText.getSpanStart(span));
93d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            }
94d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
95d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
96d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
97d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    @Override
98d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
99d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        final ClickableSpan span = getSpanForOffset(virtualViewId);
100d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (span != null) {
101d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            event.setContentDescription(getTextForSpan(span));
102d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        } else {
103d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
104d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            event.setContentDescription(mView.getText());
105d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
106d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
107d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
108d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    @Override
109d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    protected void onPopulateNodeForVirtualView(int virtualViewId,
110d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            AccessibilityNodeInfoCompat info) {
111d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        final ClickableSpan span = getSpanForOffset(virtualViewId);
112d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (span != null) {
113d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            info.setContentDescription(getTextForSpan(span));
114d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        } else {
115d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
116d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            info.setContentDescription(mView.getText());
117d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
118d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        info.setFocusable(true);
119d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        info.setClickable(true);
120d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        getBoundsForSpan(span, mTempRect);
121d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (!mTempRect.isEmpty()) {
122d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            info.setBoundsInParent(getBoundsForSpan(span, mTempRect));
123d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        } else {
124d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            Log.e(TAG, "LinkSpan bounds is empty for: " + virtualViewId);
125d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            mTempRect.set(0, 0, 1, 1);
126d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            info.setBoundsInParent(mTempRect);
127d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
128d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
129d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
130d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
131d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    @Override
132d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
133d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            Bundle arguments) {
134d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (action == AccessibilityNodeInfoCompat.ACTION_CLICK) {
135d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            ClickableSpan span = getSpanForOffset(virtualViewId);
136d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            if (span != null) {
137d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                span.onClick(mView);
138d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                return true;
139d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            } else {
140d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                Log.e(TAG, "LinkSpan is null for offset: " + virtualViewId);
141d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            }
142d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
143d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return false;
144d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
145d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
146d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private ClickableSpan getSpanForOffset(int offset) {
147d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        CharSequence text = mView.getText();
148d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (text instanceof Spanned) {
149d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            Spanned spannedText = (Spanned) text;
150d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            ClickableSpan[] spans = spannedText.getSpans(offset, offset, ClickableSpan.class);
151d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            if (spans.length == 1) {
152d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                return spans[0];
153d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            }
154d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
155d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return null;
156d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
157d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
158d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private CharSequence getTextForSpan(ClickableSpan span) {
159d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        CharSequence text = mView.getText();
160d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (text instanceof Spanned) {
161d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            Spanned spannedText = (Spanned) text;
162d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            return spannedText.subSequence(spannedText.getSpanStart(span),
163d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                    spannedText.getSpanEnd(span));
164d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
165d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return text;
166d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
167d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
168d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    // Find the bounds of a span. If it spans multiple lines, it will only return the bounds for the
169d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    // section on the first line.
170d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private Rect getBoundsForSpan(ClickableSpan span, Rect outRect) {
171d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        CharSequence text = mView.getText();
172d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        outRect.setEmpty();
173d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (text instanceof Spanned) {
174d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            Spanned spannedText = (Spanned) text;
175d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final int spanStart = spannedText.getSpanStart(span);
176d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final int spanEnd = spannedText.getSpanEnd(span);
177d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final Layout layout = mView.getLayout();
178d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final float xStart = layout.getPrimaryHorizontal(spanStart);
179d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final float xEnd = layout.getPrimaryHorizontal(spanEnd);
180d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final int lineStart = layout.getLineForOffset(spanStart);
181d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            final int lineEnd = layout.getLineForOffset(spanEnd);
182d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            layout.getLineBounds(lineStart, outRect);
183d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            outRect.left = (int) xStart;
184d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            if (lineEnd == lineStart) {
185d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam                outRect.right = (int) xEnd;
186d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            } // otherwise just leave it at the end of the start line
187d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
188d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            // Offset for padding
189d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam            outRect.offset(mView.getTotalPaddingLeft(), mView.getTotalPaddingTop());
190d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        }
191d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return outRect;
192d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
193d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
194d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    // Compat implementation of TextView#getOffsetForPosition().
195d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
196d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private static int getOffsetForPosition(TextView view, float x, float y) {
197d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        if (view.getLayout() == null) return -1;
198d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        final int line = getLineAtCoordinate(view, y);
199d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return getOffsetAtCoordinate(view, line, x);
200d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
201d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
202d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private static float convertToLocalHorizontalCoordinate(TextView view, float x) {
203d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        x -= view.getTotalPaddingLeft();
204d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        // Clamp the position to inside of the view.
205d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        x = Math.max(0.0f, x);
206d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        x = Math.min(view.getWidth() - view.getTotalPaddingRight() - 1, x);
207d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        x += view.getScrollX();
208d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return x;
209d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
210d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
211d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private static int getLineAtCoordinate(TextView view, float y) {
212d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        y -= view.getTotalPaddingTop();
213d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        // Clamp the position to inside of the view.
214d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        y = Math.max(0.0f, y);
215d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        y = Math.min(view.getHeight() - view.getTotalPaddingBottom() - 1, y);
216d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        y += view.getScrollY();
217d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return view.getLayout().getLineForVertical((int) y);
218d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
219d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam
220d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    private static int getOffsetAtCoordinate(TextView view, int line, float x) {
221d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        x = convertToLocalHorizontalCoordinate(view, x);
222d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam        return view.getLayout().getOffsetForHorizontal(line, x);
223d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam    }
224d832154e333a3a45b5faecd518b664ddd297183cMaurice Lam}
225