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}