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