EditMessage.java

  1package eu.siacs.conversations.ui.widget;
  2
  3import android.content.Context;
  4import android.content.SharedPreferences;
  5import android.os.Build;
  6import android.os.Bundle;
  7import android.os.Handler;
  8import android.preference.PreferenceManager;
  9import android.text.Editable;
 10import android.text.InputFilter;
 11import android.text.InputType;
 12import android.text.Spanned;
 13import android.util.AttributeSet;
 14import android.view.KeyEvent;
 15import android.view.inputmethod.EditorInfo;
 16import android.view.inputmethod.InputConnection;
 17
 18import androidx.core.view.inputmethod.EditorInfoCompat;
 19import androidx.core.view.inputmethod.InputConnectionCompat;
 20import androidx.core.view.inputmethod.InputContentInfoCompat;
 21
 22import java.util.concurrent.ExecutorService;
 23import java.util.concurrent.Executors;
 24
 25import eu.siacs.conversations.Config;
 26import eu.siacs.conversations.R;
 27import eu.siacs.conversations.ui.util.QuoteHelper;
 28
 29public class EditMessage extends EmojiWrapperEditText {
 30
 31    private static final InputFilter SPAN_FILTER = (source, start, end, dest, dstart, dend) -> source instanceof Spanned ? source.toString() : source;
 32    private final ExecutorService executor = Executors.newSingleThreadExecutor();
 33    protected Handler mTypingHandler = new Handler();
 34    protected KeyboardListener keyboardListener;
 35    private OnCommitContentListener mCommitContentListener = null;
 36    private String[] mimeTypes = null;
 37    private boolean isUserTyping = false;
 38    private final Runnable mTypingTimeout = new Runnable() {
 39        @Override
 40        public void run() {
 41            if (isUserTyping && keyboardListener != null) {
 42                keyboardListener.onTypingStopped();
 43                isUserTyping = false;
 44            }
 45        }
 46    };
 47    private boolean lastInputWasTab = false;
 48
 49    public EditMessage(Context context, AttributeSet attrs) {
 50        super(context, attrs);
 51    }
 52
 53    public EditMessage(Context context) {
 54        super(context);
 55    }
 56
 57    @Override
 58    public boolean onKeyDown(final int keyCode, final KeyEvent e) {
 59        final boolean isCtrlPressed = e.isCtrlPressed();
 60        if (keyCode == KeyEvent.KEYCODE_ENTER && !e.isShiftPressed()) {
 61            lastInputWasTab = false;
 62            if (keyboardListener != null && keyboardListener.onEnterPressed(isCtrlPressed)) {
 63                return true;
 64            }
 65        } else if (keyCode == KeyEvent.KEYCODE_TAB && !e.isAltPressed() && !isCtrlPressed) {
 66            if (keyboardListener != null && keyboardListener.onTabPressed(this.lastInputWasTab)) {
 67                lastInputWasTab = true;
 68                return true;
 69            }
 70        } else {
 71            lastInputWasTab = false;
 72        }
 73        return super.onKeyDown(keyCode, e);
 74    }
 75
 76    @Override
 77    public int getAutofillType() {
 78        return AUTOFILL_TYPE_NONE;
 79    }
 80
 81
 82    @Override
 83    public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
 84        super.onTextChanged(text, start, lengthBefore, lengthAfter);
 85        lastInputWasTab = false;
 86        if (this.mTypingHandler != null && this.keyboardListener != null) {
 87            executor.execute(() -> triggerKeyboardEvents(text.length()));
 88        }
 89    }
 90
 91    private void triggerKeyboardEvents(final int length) {
 92        final KeyboardListener listener = this.keyboardListener;
 93        if (listener == null) {
 94            return;
 95        }
 96        this.mTypingHandler.removeCallbacks(mTypingTimeout);
 97        this.mTypingHandler.postDelayed(mTypingTimeout, Config.TYPING_TIMEOUT * 1000);
 98        if (!isUserTyping && length > 0) {
 99            this.isUserTyping = true;
100            listener.onTypingStarted();
101        } else if (length == 0) {
102            this.isUserTyping = false;
103            listener.onTextDeleted();
104        }
105        listener.onTextChanged();
106    }
107
108    public void setKeyboardListener(KeyboardListener listener) {
109        this.keyboardListener = listener;
110        if (listener != null) {
111            this.isUserTyping = false;
112        }
113    }
114
115    @Override
116    public boolean onTextContextMenuItem(int id) {
117        if (id == android.R.id.paste) {
118            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
119                return super.onTextContextMenuItem(android.R.id.pasteAsPlainText);
120            } else {
121                Editable editable = getEditableText();
122                InputFilter[] filters = editable.getFilters();
123                InputFilter[] tempFilters = new InputFilter[filters != null ? filters.length + 1 : 1];
124                if (filters != null) {
125                    System.arraycopy(filters, 0, tempFilters, 1, filters.length);
126                }
127                tempFilters[0] = SPAN_FILTER;
128                editable.setFilters(tempFilters);
129                try {
130                    return super.onTextContextMenuItem(id);
131                } finally {
132                    editable.setFilters(filters);
133                }
134            }
135        } else {
136            return super.onTextContextMenuItem(id);
137        }
138    }
139
140    public void setRichContentListener(String[] mimeTypes, OnCommitContentListener listener) {
141        this.mimeTypes = mimeTypes;
142        this.mCommitContentListener = listener;
143    }
144
145    public void insertAsQuote(String text) {
146        text = QuoteHelper.replaceAltQuoteCharsInText(text);
147        text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2").replaceAll("(^|\n)([^" + QuoteHelper.QUOTE_CHAR + "])", "$1> $2").replaceAll("\n$", "");
148        Editable editable = getEditableText();
149        int position = getSelectionEnd();
150        if (position == -1) position = editable.length();
151        if (position > 0 && editable.charAt(position - 1) != '\n') {
152            editable.insert(position++, "\n");
153        }
154        editable.insert(position, text);
155        position += text.length();
156        editable.insert(position++, "\n");
157        if (position < editable.length() && editable.charAt(position) != '\n') {
158            editable.insert(position, "\n");
159        }
160        setSelection(position);
161    }
162
163    @Override
164    public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
165        final InputConnection ic = super.onCreateInputConnection(editorInfo);
166
167        if (mimeTypes != null && mCommitContentListener != null && ic != null) {
168            EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
169            return InputConnectionCompat.createWrapper(ic, editorInfo, (inputContentInfo, flags, opts) -> EditMessage.this.mCommitContentListener.onCommitContent(inputContentInfo, flags, opts, mimeTypes));
170        } else {
171            return ic;
172        }
173    }
174
175    public void refreshIme() {
176        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getContext());
177        final boolean usingEnterKey = p.getBoolean("display_enter_key", getResources().getBoolean(R.bool.display_enter_key));
178        final boolean enterIsSend = p.getBoolean("enter_is_send", getResources().getBoolean(R.bool.enter_is_send));
179
180        if (usingEnterKey && enterIsSend) {
181            setInputType(getInputType() & (~InputType.TYPE_TEXT_FLAG_MULTI_LINE));
182            setInputType(getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
183        } else if (usingEnterKey) {
184            setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
185            setInputType(getInputType() & (~InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE));
186        } else {
187            setInputType(getInputType() | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
188            setInputType(getInputType() | InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE);
189        }
190    }
191
192    public interface OnCommitContentListener {
193        boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] mimeTypes);
194    }
195
196    public interface KeyboardListener {
197        boolean onEnterPressed(boolean isCtrlPressed);
198
199        void onTypingStarted();
200
201        void onTypingStopped();
202
203        void onTextDeleted();
204
205        void onTextChanged();
206
207        boolean onTabPressed(boolean repeated);
208    }
209}