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}