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