Files
FairEmail/app/src/main/java/androidx/emoji2/text/SpannableBuilder.java
2023-04-01 09:20:35 +02:00

463 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.emoji2.text;
import android.annotation.SuppressLint;
import android.os.Build;
import android.text.Editable;
import android.text.SpanWatcher;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextWatcher;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* When setSpan functions is called on EmojiSpannableBuilder, it checks if the mObject is instance
* of the DynamicLayout$ChangeWatcher. if so, it wraps it into another listener mObject
* (WatcherWrapper) that implements the same interfaces.
* <p>
* During a span change event WatcherWrappers functions are fired, it checks if the span is an
* EmojiSpan, and prevents the ChangeWatcher being fired for that span. WatcherWrapper informs
* ChangeWatcher only once at the end of the edit. Important point is, the block operation is
* applied only for EmojiSpans. Therefore any other span change operation works the same way as in
* the framework.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public final class SpannableBuilder extends eu.faircode.email.SpannableStringBuilderEx {
/**
* DynamicLayout$ChangeWatcher class.
*/
private final @NonNull Class<?> mWatcherClass;
/**
* All WatcherWrappers.
*/
private final @NonNull List<WatcherWrapper> mWatchers = new ArrayList<>();
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
SpannableBuilder(@NonNull Class<?> watcherClass) {
Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
mWatcherClass = watcherClass;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text) {
super(text);
Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
mWatcherClass = watcherClass;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
SpannableBuilder(@NonNull Class<?> watcherClass, @NonNull CharSequence text, int start,
int end) {
super(text, start, end);
Preconditions.checkNotNull(watcherClass, "watcherClass cannot be null");
mWatcherClass = watcherClass;
}
/**
* @hide
*/
@NonNull
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public static SpannableBuilder create(@NonNull Class<?> clazz, @NonNull CharSequence text) {
return new SpannableBuilder(clazz, text);
}
/**
* Checks whether the mObject is instance of the DynamicLayout$ChangeWatcher.
*
* @param object mObject to be checked
*
* @return true if mObject is instance of the DynamicLayout$ChangeWatcher.
*/
private boolean isWatcher(@Nullable Object object) {
return object != null && isWatcher(object.getClass());
}
/**
* Checks whether the class is DynamicLayout$ChangeWatcher.
*
* @param clazz class to be checked
*
* @return true if class is DynamicLayout$ChangeWatcher.
*/
private boolean isWatcher(@NonNull Class<?> clazz) {
return mWatcherClass == clazz;
}
@SuppressLint("UnknownNullness")
@Override
public CharSequence subSequence(int start, int end) {
return new SpannableBuilder(mWatcherClass, this, start, end);
}
/**
* If the span being added is instance of DynamicLayout$ChangeWatcher, wrap the watcher in
* another internal watcher that will prevent EmojiSpan events to be fired to DynamicLayout. Set
* this new mObject as the span.
*/
@Override
public void setSpan(@Nullable Object what, int start, int end, int flags) {
if (isWatcher(what)) {
final WatcherWrapper span = new WatcherWrapper(what);
mWatchers.add(span);
what = span;
}
super.setSpan(what, start, end, flags);
}
/**
* If previously a DynamicLayout$ChangeWatcher was wrapped in a WatcherWrapper, return the
* correct Object that the client has set.
*/
@SuppressLint("UnknownNullness")
@SuppressWarnings("unchecked")
@Override
public <T> T[] getSpans(int queryStart, int queryEnd, @NonNull Class<T> kind) {
if (isWatcher(kind)) {
final WatcherWrapper[] spans = super.getSpans(queryStart, queryEnd,
WatcherWrapper.class);
final T[] result = (T[]) Array.newInstance(kind, spans.length);
for (int i = 0; i < spans.length; i++) {
result[i] = (T) spans[i].mObject;
}
return result;
}
return super.getSpans(queryStart, queryEnd, kind);
}
/**
* If the client wants to remove the DynamicLayout$ChangeWatcher span, remove the WatcherWrapper
* instead.
*/
@Override
public void removeSpan(@Nullable Object what) {
final WatcherWrapper watcher;
if (isWatcher(what)) {
watcher = getWatcherFor(what);
if (watcher != null) {
what = watcher;
}
} else {
watcher = null;
}
super.removeSpan(what);
if (watcher != null) {
mWatchers.remove(watcher);
}
}
/**
* Return the correct start for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int getSpanStart(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
tag = watcher;
}
}
return super.getSpanStart(tag);
}
/**
* Return the correct end for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int getSpanEnd(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
tag = watcher;
}
}
return super.getSpanEnd(tag);
}
/**
* Return the correct flags for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int getSpanFlags(@Nullable Object tag) {
if (isWatcher(tag)) {
final WatcherWrapper watcher = getWatcherFor(tag);
if (watcher != null) {
tag = watcher;
}
}
return super.getSpanFlags(tag);
}
/**
* Return the correct transition for the DynamicLayout$ChangeWatcher span.
*/
@Override
public int nextSpanTransition(int start, int limit, @Nullable Class type) {
if (type == null || isWatcher(type)) {
type = WatcherWrapper.class;
}
return super.nextSpanTransition(start, limit, type);
}
/**
* Find the WatcherWrapper for a given DynamicLayout$ChangeWatcher.
*
* @param object DynamicLayout$ChangeWatcher mObject
*
* @return WatcherWrapper that wraps the mObject.
*/
private WatcherWrapper getWatcherFor(Object object) {
for (int i = 0; i < mWatchers.size(); i++) {
WatcherWrapper watcher = mWatchers.get(i);
if (watcher.mObject == object) {
return watcher;
}
}
return null;
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void beginBatchEdit() {
blockWatchers();
}
/**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
public void endBatchEdit() {
unblockwatchers();
fireWatchers();
}
/**
* Block all watcher wrapper events.
*/
private void blockWatchers() {
for (int i = 0; i < mWatchers.size(); i++) {
mWatchers.get(i).blockCalls();
}
}
/**
* Unblock all watcher wrapper events.
*/
private void unblockwatchers() {
for (int i = 0; i < mWatchers.size(); i++) {
mWatchers.get(i).unblockCalls();
}
}
/**
* Unblock all watcher wrapper events. Called by editing operations, namely
* {@link SpannableStringBuilder#replace(int, int, CharSequence)}.
*/
private void fireWatchers() {
for (int i = 0; i < mWatchers.size(); i++) {
mWatchers.get(i).onTextChanged(this, 0, this.length(), this.length());
}
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb) {
blockWatchers();
super.replace(start, end, tb);
unblockwatchers();
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder replace(int start, int end, CharSequence tb, int tbstart,
int tbend) {
blockWatchers();
super.replace(start, end, tb, tbstart, tbend);
unblockwatchers();
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder insert(int where, CharSequence tb) {
super.insert(where, tb);
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder insert(int where, CharSequence tb, int start, int end) {
super.insert(where, tb, start, end);
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder delete(int start, int end) {
super.delete(start, end);
return this;
}
@NonNull
@Override
public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text) {
super.append(text);
return this;
}
@NonNull
@Override
public SpannableStringBuilder append(char text) {
super.append(text);
return this;
}
@NonNull
@Override
public SpannableStringBuilder append(@SuppressLint("UnknownNullness") CharSequence text,
int start,
int end) {
super.append(text, start, end);
return this;
}
@SuppressLint("UnknownNullness")
@Override
public SpannableStringBuilder append(CharSequence text, Object what, int flags) {
super.append(text, what, flags);
return this;
}
/**
* Wraps a DynamicLayout$ChangeWatcher in order to prevent firing of events to DynamicLayout.
*/
private static class WatcherWrapper implements TextWatcher, SpanWatcher {
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Object mObject;
private final AtomicInteger mBlockCalls = new AtomicInteger(0);
WatcherWrapper(Object object) {
this.mObject = object;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
((TextWatcher) mObject).beforeTextChanged(s, start, count, after);
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
((TextWatcher) mObject).onTextChanged(s, start, before, count);
}
@Override
public void afterTextChanged(Editable s) {
((TextWatcher) mObject).afterTextChanged(s);
}
/**
* Prevent the onSpanAdded calls to DynamicLayout$ChangeWatcher if in a replace operation
* (mBlockCalls is set) and the span that is added is an EmojiSpan.
*/
@Override
public void onSpanAdded(Spannable text, Object what, int start, int end) {
if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
return;
}
((SpanWatcher) mObject).onSpanAdded(text, what, start, end);
}
/**
* Prevent the onSpanRemoved calls to DynamicLayout$ChangeWatcher if in a replace operation
* (mBlockCalls is set) and the span that is added is an EmojiSpan.
*/
@Override
public void onSpanRemoved(Spannable text, Object what, int start, int end) {
if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
return;
}
((SpanWatcher) mObject).onSpanRemoved(text, what, start, end);
}
/**
* Prevent the onSpanChanged calls to DynamicLayout$ChangeWatcher if in a replace operation
* (mBlockCalls is set) and the span that is added is an EmojiSpan.
*/
@Override
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart,
int nend) {
if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
return;
}
// workaround for platform bug fixed in Android P
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// b/67926915 start cannot be determined, fallback to reflow from start instead
// of causing an exception.
// emoji2 bug b/216891011
if (ostart > oend) {
ostart = 0;
}
if (nstart > nend) {
nstart = 0;
}
}
((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend);
}
final void blockCalls() {
mBlockCalls.incrementAndGet();
}
final void unblockCalls() {
mBlockCalls.decrementAndGet();
}
private boolean isEmojiSpan(final Object span) {
return span instanceof EmojiSpan;
}
}
}