1/*
2 * Copyright (C) 2017 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 */
16package android.support.text.emoji.widget;
17
18import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
19
20import android.support.annotation.NonNull;
21import android.support.annotation.Nullable;
22import android.support.annotation.RestrictTo;
23import android.support.text.emoji.EmojiSpan;
24import android.support.v4.util.Preconditions;
25import android.text.Editable;
26import android.text.SpanWatcher;
27import android.text.Spannable;
28import android.text.SpannableStringBuilder;
29import android.text.TextWatcher;
30
31import java.lang.reflect.Array;
32import java.util.ArrayList;
33import java.util.List;
34import java.util.concurrent.atomic.AtomicInteger;
35
36/**
37 * When setSpan functions is called on EmojiSpannableBuilder, it checks if the mObject is instance
38 * of the DynamicLayout$ChangeWatcher. if so, it wraps it into another listener mObject
39 * (WatcherWrapper) that implements the same interfaces.
40 * <p>
41 * During a span change event WatcherWrapper’s functions are fired, it checks if the span is an
42 * EmojiSpan, and prevents the ChangeWatcher being fired for that span. WatcherWrapper informs
43 * ChangeWatcher only once at the end of the edit. Important point is, the block operation is
44 * applied only for EmojiSpans. Therefore any other span change operation works the same way as in
45 * the framework.
46 *
47 * @hide
48 * @see EmojiEditableFactory
49 */
50@RestrictTo(LIBRARY_GROUP)
51public final class SpannableBuilder extends SpannableStringBuilder {
52    /**
53     * DynamicLayout$ChangeWatcher class.
54     */
55    private final Class<?> mWatcherClass;
56
57    /**
58     * All WatcherWrappers.
59     */
60    private final List<WatcherWrapper> mWatchers = new ArrayList<>();
61
62    /**
63     * @hide
64     */
65    @RestrictTo(LIBRARY_GROUP)
66    SpannableBuilder(@NonNull Class<?> watcherClass) {
67        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
68        mWatcherClass = watcherClass;
69    }
70
71    /**
72     * @hide
73     */
74    @RestrictTo(LIBRARY_GROUP)
75    SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text) {
76        super(text);
77        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
78        mWatcherClass = watcherClass;
79    }
80
81    /**
82     * @hide
83     */
84    @RestrictTo(LIBRARY_GROUP)
85    SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text, int start,
86            int end) {
87        super(text, start, end);
88        Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
89        mWatcherClass = watcherClass;
90    }
91
92    /**
93     * @hide
94     */
95    @RestrictTo(LIBRARY_GROUP)
96    static SpannableBuilder create(@NonNull Class<?> clazz, @NonNull CharSequence text) {
97        return new SpannableBuilder(clazz, text);
98    }
99
100    /**
101     * Checks whether the mObject is instance of the DynamicLayout$ChangeWatcher.
102     *
103     * @param object mObject to be checked
104     *
105     * @return true if mObject is instance of the DynamicLayout$ChangeWatcher.
106     */
107    private boolean isWatcher(@Nullable Object object) {
108        return object != null && isWatcher(object.getClass());
109    }
110
111    /**
112     * Checks whether the class is DynamicLayout$ChangeWatcher.
113     *
114     * @param clazz class to be checked
115     *
116     * @return true if class is DynamicLayout$ChangeWatcher.
117     */
118    private boolean isWatcher(@NonNull Class<?> clazz) {
119        return mWatcherClass == clazz;
120    }
121
122    @Override
123    public CharSequence subSequence(int start, int end) {
124        return new SpannableBuilder(mWatcherClass, this, start, end);
125    }
126
127    /**
128     * If the span being added is instance of DynamicLayout$ChangeWatcher, wrap the watcher in
129     * another internal watcher that will prevent EmojiSpan events to be fired to DynamicLayout. Set
130     * this new mObject as the span.
131     */
132    @Override
133    public void setSpan(Object what, int start, int end, int flags) {
134        if (isWatcher(what)) {
135            final WatcherWrapper span = new WatcherWrapper(what);
136            mWatchers.add(span);
137            what = span;
138        }
139        super.setSpan(what, start, end, flags);
140    }
141
142    /**
143     * If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the
144     * correct Object that the client has set.
145     */
146    @SuppressWarnings("unchecked")
147    @Override
148    public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) {
149        if (isWatcher(kind)) {
150            final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd,
151                    WatcherWrapper.class);
152            final T[] result = (T[]) Array.newInstance(kind, spans.length);
153            for (int i = 0; i < spans.length; i++) {
154                result[i] = (T) spans[i].mObject;
155            }
156            return result;
157        }
158        return super.getSpans(queryStart, queryEnd, kind);
159    }
160
161    /**
162     * If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper
163     * instead.
164     */
165    @Override
166    public void removeSpan(Object what) {
167        final WatcherWrapper watcher;
168        if (isWatcher(what)) {
169            watcher = getWatcherFor(what);
170            if (watcher != null) {
171                what = watcher;
172            }
173        } else {
174            watcher = null;
175        }
176
177        super.removeSpan(what);
178
179        if (watcher != null) {
180            mWatchers.remove(watcher);
181        }
182    }
183
184    /**
185     * Return the correct start for the DynamicLayout$ChangeWatcher span.
186     */
187    @Override
188    public int getSpanStart(Object tag) {
189        if (isWatcher(tag)) {
190            final WatcherWrapper watcher = getWatcherFor(tag);
191            if (watcher != null) {
192                tag = watcher;
193            }
194        }
195        return super.getSpanStart(tag);
196    }
197
198    /**
199     * Return the correct end for the DynamicLayout$ChangeWatcher span.
200     */
201    @Override
202    public int getSpanEnd(Object tag) {
203        if (isWatcher(tag)) {
204            final WatcherWrapper watcher = getWatcherFor(tag);
205            if (watcher != null) {
206                tag = watcher;
207            }
208        }
209        return super.getSpanEnd(tag);
210    }
211
212    /**
213     * Return the correct flags for the DynamicLayout$ChangeWatcher span.
214     */
215    @Override
216    public int getSpanFlags(Object tag) {
217        if (isWatcher(tag)) {
218            final WatcherWrapper watcher = getWatcherFor(tag);
219            if (watcher != null) {
220                tag = watcher;
221            }
222        }
223        return super.getSpanFlags(tag);
224    }
225
226    /**
227     * Return the correct transition for the DynamicLayout$ChangeWatcher span.
228     */
229    @Override
230    public int nextSpanTransition(int start, int limit, Class type) {
231        if (isWatcher(type)) {
232            type = WatcherWrapper.class;
233        }
234        return super.nextSpanTransition(start, limit, type);
235    }
236
237    /**
238     * Find the WatcherWrapper for a given DynamicLayout$ChangeWatcher.
239     *
240     * @param object DynamicLayout$ChangeWatcher mObject
241     *
242     * @return WatcherWrapper that wraps the mObject.
243     */
244    private WatcherWrapper getWatcherFor(Object object) {
245        for (int i = 0; i < mWatchers.size(); i++) {
246            WatcherWrapper watcher = mWatchers.get(i);
247            if (watcher.mObject == object) {
248                return watcher;
249            }
250        }
251        return null;
252    }
253
254    /**
255     * @hide
256     */
257    @RestrictTo(LIBRARY_GROUP)
258    public void beginBatchEdit() {
259        blockWatchers();
260    }
261
262    /**
263     * @hide
264     */
265    @RestrictTo(LIBRARY_GROUP)
266    public void endBatchEdit() {
267        unblockwatchers();
268        fireWatchers();
269    }
270
271    /**
272     * Block all watcher wrapper events.
273     */
274    private void blockWatchers() {
275        for (int i = 0; i < mWatchers.size(); i++) {
276            mWatchers.get(i).blockCalls();
277        }
278    }
279
280    /**
281     * Unblock all watcher wrapper events.
282     */
283    private void unblockwatchers() {
284        for (int i = 0; i < mWatchers.size(); i++) {
285            mWatchers.get(i).unblockCalls();
286        }
287    }
288
289    /**
290     * Unblock all watcher wrapper events. Called by editing operations, namely
291     * {@link SpannableStringBuilder#replace(int, int, CharSequence)}.
292     */
293    private void fireWatchers() {
294        for (int i = 0; i < mWatchers.size(); i++) {
295            mWatchers.get(i).onTextChanged(this, 0, this.length(), this.length());
296        }
297    }
298
299    @Override
300    public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
301        blockWatchers();
302        super.replace(start, end, tb);
303        unblockwatchers();
304        return this;
305    }
306
307    @Override
308    public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
309            int tbend) {
310        blockWatchers();
311        super.replace(start, end, tb, tbstart, tbend);
312        unblockwatchers();
313        return this;
314    }
315
316    @Override
317    public SpannableStringBuilder insert(int where, CharSequence tb) {
318        super.insert(where, tb);
319        return this;
320    }
321
322    @Override
323    public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
324        super.insert(where, tb, start, end);
325        return this;
326    }
327
328    @Override
329    public SpannableStringBuilder delete(int start, int end) {
330        super.delete(start, end);
331        return this;
332    }
333
334    @Override
335    public SpannableStringBuilder append(CharSequence text) {
336        super.append(text);
337        return this;
338    }
339
340    @Override
341    public SpannableStringBuilder append(char text) {
342        super.append(text);
343        return this;
344    }
345
346    @Override
347    public SpannableStringBuilder append(CharSequence text, int start, int end) {
348        super.append(text, start, end);
349        return this;
350    }
351
352    @Override
353    public SpannableStringBuilder append(CharSequence text, Object what, int flags) {
354        super.append(text, what, flags);
355        return this;
356    }
357
358    /**
359     * Wraps a DynamicLayout$ChangeWatcher in order to prevent firing of events to DynamicLayout.
360     */
361    private static class WatcherWrapper implements TextWatcher, SpanWatcher {
362        private final Object mObject;
363        private final AtomicInteger mBlockCalls = new AtomicInteger(0);
364
365        WatcherWrapper(Object object) {
366            this.mObject = object;
367        }
368
369        @Override
370        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
371            ((TextWatcher) mObject).beforeTextChanged(s, start, count, after);
372        }
373
374        @Override
375        public void onTextChanged(CharSequence s, int start, int before, int count) {
376            ((TextWatcher) mObject).onTextChanged(s, start, before, count);
377        }
378
379        @Override
380        public void afterTextChanged(Editable s) {
381            ((TextWatcher) mObject).afterTextChanged(s);
382        }
383
384        /**
385         * Prevent the onSpanAdded calls to DynamicLayout$ChangeWatcher if in a replace operation
386         * (mBlockCalls is set) and the span that is added is an EmojiSpan.
387         */
388        @Override
389        public void onSpanAdded(Spannable text, Object what, int start, int end) {
390            if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
391                return;
392            }
393            ((SpanWatcher) mObject).onSpanAdded(text, what, start, end);
394        }
395
396        /**
397         * Prevent the onSpanRemoved calls to DynamicLayout$ChangeWatcher if in a replace operation
398         * (mBlockCalls is set) and the span that is added is an EmojiSpan.
399         */
400        @Override
401        public void onSpanRemoved(Spannable text, Object what, int start, int end) {
402            if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
403                return;
404            }
405            ((SpanWatcher) mObject).onSpanRemoved(text, what, start, end);
406        }
407
408        /**
409         * Prevent the onSpanChanged calls to DynamicLayout$ChangeWatcher if in a replace operation
410         * (mBlockCalls is set) and the span that is added is an EmojiSpan.
411         */
412        @Override
413        public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart,
414                int nend) {
415            if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
416                return;
417            }
418            ((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend);
419        }
420
421        final void blockCalls() {
422            mBlockCalls.incrementAndGet();
423        }
424
425        final void unblockCalls() {
426            mBlockCalls.decrementAndGet();
427        }
428
429        private boolean isEmojiSpan(final Object span) {
430            return span instanceof EmojiSpan;
431        }
432    }
433
434}
435