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}