Signal-Android/app/src/main/java/org/thoughtcrime/securesms/components/ComposeText.java

416 wiersze
14 KiB
Java

package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Bundle;
import android.text.Annotation;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionDeleter;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.StringUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.List;
import java.util.Objects;
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
public class ComposeText extends EmojiEditText {
private CharSequence hint;
private SpannableString subHint;
private MentionRendererDelegate mentionRendererDelegate;
private MentionValidatorWatcher mentionValidatorWatcher;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
@Nullable private MentionQueryChangedListener mentionQueryChangedListener;
public ComposeText(Context context) {
super(context);
initialize();
}
public ComposeText(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public ComposeText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
/**
* Trims and returns text while preserving potential spans like {@link MentionAnnotation}.
*/
public @NonNull CharSequence getTextTrimmed() {
Editable text = getText();
if (text == null) {
return "";
}
return StringUtil.trimSequence(text);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getLayout() != null && !TextUtils.isEmpty(hint)) {
if (!TextUtils.isEmpty(subHint)) {
setHintWithChecks(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
.append("\n")
.append(ellipsizeToWidth(subHint)));
} else {
setHintWithChecks(ellipsizeToWidth(hint));
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onSelectionChanged(int selectionStart, int selectionEnd) {
super.onSelectionChanged(selectionStart, selectionEnd);
if (getText() != null) {
boolean selectionChanged = changeSelectionForPartialMentions(getText(), selectionStart, selectionEnd);
if (selectionChanged) {
return;
}
if (selectionStart == selectionEnd) {
doAfterCursorChange(getText());
} else {
updateQuery(null);
}
}
if (cursorPositionChangedListener != null) {
cursorPositionChangedListener.onCursorPositionChanged(selectionStart, selectionEnd);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (getText() != null && getLayout() != null) {
int checkpoint = canvas.save();
// Clip using same logic as TextView drawing
int maxScrollY = getLayout().getHeight() - getBottom() - getTop() - getCompoundPaddingBottom() - getCompoundPaddingTop();
float clipLeft = getCompoundPaddingLeft() + getScrollX();
float clipTop = (getScrollY() == 0) ? 0 : getExtendedPaddingTop() + getScrollY();
float clipRight = getRight() - getLeft() - getCompoundPaddingRight() + getScrollX();
float clipBottom = getBottom() - getTop() + getScrollY() - ((getScrollY() == maxScrollY) ? 0 : getExtendedPaddingBottom());
canvas.clipRect(clipLeft - 10, clipTop, clipRight + 10, clipBottom);
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
try {
mentionRendererDelegate.draw(canvas, getText(), getLayout());
} finally {
canvas.restoreToCount(checkpoint);
}
}
super.onDraw(canvas);
}
private CharSequence ellipsizeToWidth(CharSequence text) {
return TextUtils.ellipsize(text,
getPaint(),
getWidth() - getPaddingLeft() - getPaddingRight(),
TruncateAt.END);
}
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
this.hint = hint;
if (subHint != null) {
this.subHint = new SpannableString(subHint);
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
} else {
this.subHint = null;
}
if (this.subHint != null) {
setHintWithChecks(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
.append("\n")
.append(ellipsizeToWidth(this.subHint)));
} else {
setHintWithChecks(ellipsizeToWidth(this.hint));
}
setHintWithChecks(hint);
}
public void appendInvite(String invite) {
if (getText() == null) {
return;
}
if (!TextUtils.isEmpty(getText()) && !getText().toString().equals(" ")) {
append(" ");
}
append(invite);
setSelection(getText().length());
}
public void setCursorPositionChangedListener(@Nullable CursorPositionChangedListener listener) {
this.cursorPositionChangedListener = listener;
}
public void setMentionQueryChangedListener(@Nullable MentionQueryChangedListener listener) {
this.mentionQueryChangedListener = listener;
}
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
mentionValidatorWatcher.setMentionValidator(mentionValidator);
}
private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
public void setMessageSendType(MessageSendType messageSendType) {
final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
int inputType = getInputType();
if (isLandscape()) setImeActionLabel(getContext().getString(messageSendType.getComposeHintRes()), EditorInfo.IME_ACTION_SEND);
else setImeActionLabel(null, 0);
if (useSystemEmoji) {
inputType = (inputType & ~InputType.TYPE_MASK_VARIATION) | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE;
}
setImeOptions(imeOptions);
setHint(getContext().getString(messageSendType.getComposeHintRes()),
messageSendType.getSimName() != null
? getContext().getString(R.string.conversation_activity__from_sim_name, messageSendType.getSimName())
: null);
setInputType(inputType);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
if(SignalStore.settings().isEnterKeySends()) {
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
if (Build.VERSION.SDK_INT < 21) return inputConnection;
if (mediaListener == null) return inputConnection;
if (inputConnection == null) return null;
EditorInfoCompat.setContentMimeTypes(editorInfo, new String[] {"image/jpeg", "image/png", "image/gif"});
return InputConnectionCompat.createWrapper(inputConnection, editorInfo, new CommitContentListener(mediaListener));
}
public void setMediaListener(@Nullable InputPanel.MediaListener mediaListener) {
this.mediaListener = mediaListener;
}
public boolean hasMentions() {
Editable text = getText();
if (text != null) {
return !MentionAnnotation.getMentionAnnotations(text).isEmpty();
}
return false;
}
public @NonNull List<Mention> getMentions() {
return MentionAnnotation.getMentionsFromAnnotations(getText());
}
private void initialize() {
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
setImeOptions(getImeOptions() | 16777216);
}
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.conversation_mention_background_color));
addTextChangedListener(new MentionDeleter());
mentionValidatorWatcher = new MentionValidatorWatcher();
addTextChangedListener(mentionValidatorWatcher);
}
private void setHintWithChecks(@Nullable CharSequence newHint) {
if (getLayout() == null || Objects.equals(getHint(), newHint)) {
return;
}
setHint(newHint);
}
private boolean changeSelectionForPartialMentions(@NonNull Spanned spanned, int selectionStart, int selectionEnd) {
Annotation[] annotations = spanned.getSpans(0, spanned.length(), Annotation.class);
for (Annotation annotation : annotations) {
if (MentionAnnotation.isMentionAnnotation(annotation)) {
int spanStart = spanned.getSpanStart(annotation);
int spanEnd = spanned.getSpanEnd(annotation);
boolean startInMention = selectionStart > spanStart && selectionStart < spanEnd;
boolean endInMention = selectionEnd > spanStart && selectionEnd < spanEnd;
if (startInMention || endInMention) {
if (selectionStart == selectionEnd) {
setSelection(spanEnd, spanEnd);
} else {
int newStart = startInMention ? spanStart : selectionStart;
int newEnd = endInMention ? spanEnd : selectionEnd;
setSelection(newStart, newEnd);
}
return true;
}
}
}
return false;
}
private void doAfterCursorChange(@NonNull Editable text) {
if (enoughToFilter(text)) {
performFiltering(text);
} else {
updateQuery(null);
}
}
private void performFiltering(@NonNull Editable text) {
int end = getSelectionEnd();
int start = findQueryStart(text, end);
CharSequence query = text.subSequence(start, end);
updateQuery(query.toString());
}
private void updateQuery(@Nullable String query) {
if (mentionQueryChangedListener != null) {
mentionQueryChangedListener.onQueryChanged(query);
}
}
private boolean enoughToFilter(@NonNull Editable text) {
int end = getSelectionEnd();
if (end < 0) {
return false;
}
return findQueryStart(text, end) != -1;
}
public void replaceTextWithMention(@NonNull String displayName, @NonNull RecipientId recipientId) {
Editable text = getText();
if (text == null) {
return;
}
clearComposingText();
int end = getSelectionEnd();
int start = findQueryStart(text, end) - 1;
text.replace(start, end, createReplacementToken(displayName, recipientId));
}
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull RecipientId recipientId) {
SpannableStringBuilder builder = new SpannableStringBuilder().append(MENTION_STARTER);
if (text instanceof Spanned) {
SpannableString spannableString = new SpannableString(text + " ");
TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spannableString, 0);
builder.append(spannableString);
} else {
builder.append(text).append(" ");
}
builder.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(recipientId), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return builder;
}
private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition) {
if (inputCursorPosition == 0) {
return -1;
}
int delimiterSearchIndex = inputCursorPosition - 1;
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != MENTION_STARTER && text.charAt(delimiterSearchIndex) != ' ')) {
delimiterSearchIndex--;
}
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == MENTION_STARTER) {
return delimiterSearchIndex + 1;
}
return -1;
}
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
private static final String TAG = Log.tag(CommitContentListener.class);
private final InputPanel.MediaListener mediaListener;
private CommitContentListener(@NonNull InputPanel.MediaListener mediaListener) {
this.mediaListener = mediaListener;
}
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
if (Build.VERSION.SDK_INT >= 25 && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
Log.w(TAG, e);
return false;
}
}
if (inputContentInfo.getDescription().getMimeTypeCount() > 0) {
mediaListener.onMediaSelected(inputContentInfo.getContentUri(),
inputContentInfo.getDescription().getMimeType(0));
return true;
}
return false;
}
}
public interface CursorPositionChangedListener {
void onCursorPositionChanged(int start, int end);
}
public interface MentionQueryChangedListener {
void onQueryChanged(@Nullable String query);
}
}