mirror of
https://github.com/M66B/FairEmail.git
synced 2026-04-06 00:53:26 +02:00
463 lines
14 KiB
Java
463 lines
14 KiB
Java
/*
|
||
* 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 WatcherWrapper’s 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;
|
||
}
|
||
}
|
||
|
||
}
|