1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appcompat.widget;
18 
19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
20 import static androidx.appcompat.widget.AppCompatReceiveContentHelper.maybeHandleDragEventViaPerformReceiveContent;
21 import static androidx.appcompat.widget.AppCompatReceiveContentHelper.maybeHandleMenuActionViaPerformReceiveContent;
22 
23 import android.content.Context;
24 import android.content.res.ColorStateList;
25 import android.graphics.PorterDuff;
26 import android.graphics.drawable.Drawable;
27 import android.os.Build;
28 import android.text.Editable;
29 import android.text.method.KeyListener;
30 import android.util.AttributeSet;
31 import android.view.ActionMode;
32 import android.view.DragEvent;
33 import android.view.inputmethod.EditorInfo;
34 import android.view.inputmethod.InputConnection;
35 import android.view.inputmethod.InputMethodManager;
36 import android.view.textclassifier.TextClassifier;
37 import android.widget.EditText;
38 import android.widget.TextView;
39 
40 import androidx.annotation.DrawableRes;
41 import androidx.annotation.RequiresApi;
42 import androidx.annotation.RestrictTo;
43 import androidx.annotation.UiThread;
44 import androidx.appcompat.R;
45 import androidx.core.view.ContentInfoCompat;
46 import androidx.core.view.OnReceiveContentListener;
47 import androidx.core.view.OnReceiveContentViewBehavior;
48 import androidx.core.view.TintableBackgroundView;
49 import androidx.core.view.ViewCompat;
50 import androidx.core.view.inputmethod.EditorInfoCompat;
51 import androidx.core.view.inputmethod.InputConnectionCompat;
52 import androidx.core.widget.TextViewCompat;
53 import androidx.core.widget.TextViewOnReceiveContentListener;
54 import androidx.core.widget.TintableCompoundDrawablesView;
55 import androidx.resourceinspection.annotation.AppCompatShadowedAttributes;
56 
57 import org.jspecify.annotations.NonNull;
58 import org.jspecify.annotations.Nullable;
59 
60 /**
61  * A {@link EditText} which supports compatible features on older versions of the platform,
62  * including:
63  * <ul>
64  *     <li>Allows dynamic tint of its background via the background tint methods in
65  *     {@link androidx.core.view.ViewCompat}.</li>
66  *     <li>Allows setting of the background tint using {@link R.attr#backgroundTint} and
67  *     {@link R.attr#backgroundTintMode}.</li>
68  *     <li>Allows setting a custom {@link OnReceiveContentListener listener} to handle
69  *     insertion of content (e.g. pasting text or an image from the clipboard). This listener
70  *     provides the opportunity to implement app-specific handling such as creating an attachment
71  *     when an image is pasted.</li>
72  * </ul>
73  *
74  * <p>This will automatically be used when you use {@link EditText} in your layouts
75  * and the top-level activity / dialog is provided by
76  * <a href="{@docRoot}topic/libraries/support-library/packages.html#v7-appcompat">appcompat</a>.
77  * You should only need to manually use this class when writing custom views.</p>
78  */
79 @AppCompatShadowedAttributes
80 public class AppCompatEditText extends EditText implements TintableBackgroundView,
81         OnReceiveContentViewBehavior, EmojiCompatConfigurationView, TintableCompoundDrawablesView {
82 
83     private final AppCompatBackgroundHelper mBackgroundTintHelper;
84     private final AppCompatTextHelper mTextHelper;
85     private final AppCompatTextClassifierHelper mTextClassifierHelper;
86     private final TextViewOnReceiveContentListener mDefaultOnReceiveContentListener;
87     private final @NonNull AppCompatEmojiEditTextHelper mAppCompatEmojiEditTextHelper;
88     private @Nullable SuperCaller mSuperCaller;
89 
AppCompatEditText(@onNull Context context)90     public AppCompatEditText(@NonNull Context context) {
91         this(context, null);
92     }
93 
AppCompatEditText(@onNull Context context, @Nullable AttributeSet attrs)94     public AppCompatEditText(@NonNull Context context, @Nullable AttributeSet attrs) {
95         this(context, attrs, R.attr.editTextStyle);
96     }
97 
AppCompatEditText( @onNull Context context, @Nullable AttributeSet attrs, int defStyleAttr)98     public AppCompatEditText(
99             @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
100         super(TintContextWrapper.wrap(context), attrs, defStyleAttr);
101 
102         ThemeUtils.checkAppCompatTheme(this, getContext());
103 
104         mBackgroundTintHelper = new AppCompatBackgroundHelper(this);
105         mBackgroundTintHelper.loadFromAttributes(attrs, defStyleAttr);
106 
107         mTextHelper = new AppCompatTextHelper(this);
108         mTextHelper.loadFromAttributes(attrs, defStyleAttr);
109         mTextHelper.applyCompoundDrawablesTints();
110 
111         mTextClassifierHelper = new AppCompatTextClassifierHelper(this);
112 
113         mDefaultOnReceiveContentListener = new TextViewOnReceiveContentListener();
114         mAppCompatEmojiEditTextHelper = new AppCompatEmojiEditTextHelper(this);
115         mAppCompatEmojiEditTextHelper.loadFromAttributes(attrs, defStyleAttr);
116         initEmojiKeyListener(mAppCompatEmojiEditTextHelper);
117     }
118 
119     /**
120      * Call from the constructor to safely add KeyListener for emoji2.
121      *
122      * This will always call super methods to avoid leaking a partially constructed this to
123      * overrides of non-final methods.
124      *
125      * @param appCompatEmojiEditTextHelper emojicompat helper
126      */
initEmojiKeyListener(AppCompatEmojiEditTextHelper appCompatEmojiEditTextHelper)127     void initEmojiKeyListener(AppCompatEmojiEditTextHelper appCompatEmojiEditTextHelper) {
128         // setKeyListener will cause a reset both focusable and the inputType to the most basic
129         // style for the key listener. Since we're calling this from the View constructor, this
130         // will cause both focusable and inputType to reset from the XML attributes.
131         // See: b/191061070 and b/188049943 for details
132         //
133         // We will only reset this during ctor invocation, and default to the platform behavior
134         // for later calls to setKeyListener, to emulate the exact behavior that a regular
135         // EditText would provide.
136         //
137         // Since we're calling non-final methods from a ctor (setKeyListener, setRawInputType,
138         // setFocusable) move this out of AppCompatEmojiEditTextHelper and into the respective
139         // views to ensure we only call the super methods during construction  (b/208480173).
140         KeyListener currentKeyListener = getKeyListener();
141         if (appCompatEmojiEditTextHelper.isEmojiCapableKeyListener(currentKeyListener)) {
142             boolean wasFocusable = super.isFocusable();
143             boolean wasClickable = super.isClickable();
144             boolean wasLongClickable = super.isLongClickable();
145             int inputType = super.getInputType();
146             KeyListener wrappedKeyListener = appCompatEmojiEditTextHelper.getKeyListener(
147                     currentKeyListener);
148             // don't call parent setKeyListener if it's not wrapped
149             if (wrappedKeyListener == currentKeyListener) return;
150             super.setKeyListener(wrappedKeyListener);
151             // reset the input type and focusable attributes after calling setKeyListener
152             super.setRawInputType(inputType);
153             super.setFocusable(wasFocusable);
154             super.setClickable(wasClickable);
155             super.setLongClickable(wasLongClickable);
156         }
157     }
158 
159     /**
160      * Return the text that the view is displaying. If an editable text has not been set yet, this
161      * will return null.
162      */
163     @Override
getText()164     public @Nullable Editable getText() {
165         if (Build.VERSION.SDK_INT >= 28) {
166             return super.getText();
167         }
168         // A bug pre-P makes getText() crash if called before the first setText due to a cast, so
169         // retrieve the editable text.
170         return super.getEditableText();
171     }
172 
173     @Override
setBackgroundResource(@rawableRes int resId)174     public void setBackgroundResource(@DrawableRes int resId) {
175         super.setBackgroundResource(resId);
176         if (mBackgroundTintHelper != null) {
177             mBackgroundTintHelper.onSetBackgroundResource(resId);
178         }
179     }
180 
181     @Override
setBackgroundDrawable(@ullable Drawable background)182     public void setBackgroundDrawable(@Nullable Drawable background) {
183         super.setBackgroundDrawable(background);
184         if (mBackgroundTintHelper != null) {
185             mBackgroundTintHelper.onSetBackgroundDrawable(background);
186         }
187     }
188 
189     /**
190      * This should be accessed via
191      * {@link androidx.core.view.ViewCompat#setBackgroundTintList(android.view.View, ColorStateList)}
192      *
193      */
194     @RestrictTo(LIBRARY_GROUP_PREFIX)
195     @Override
setSupportBackgroundTintList(@ullable ColorStateList tint)196     public void setSupportBackgroundTintList(@Nullable ColorStateList tint) {
197         if (mBackgroundTintHelper != null) {
198             mBackgroundTintHelper.setSupportBackgroundTintList(tint);
199         }
200     }
201 
202     /**
203      * This should be accessed via
204      * {@link androidx.core.view.ViewCompat#getBackgroundTintList(android.view.View)}
205      *
206      */
207     @RestrictTo(LIBRARY_GROUP_PREFIX)
208     @Override
getSupportBackgroundTintList()209     public @Nullable ColorStateList getSupportBackgroundTintList() {
210         return mBackgroundTintHelper != null
211                 ? mBackgroundTintHelper.getSupportBackgroundTintList() : null;
212     }
213 
214     /**
215      * This should be accessed via
216      * {@link androidx.core.view.ViewCompat#setBackgroundTintMode(android.view.View, PorterDuff.Mode)}
217      *
218      */
219     @RestrictTo(LIBRARY_GROUP_PREFIX)
220     @Override
setSupportBackgroundTintMode(PorterDuff.@ullable Mode tintMode)221     public void setSupportBackgroundTintMode(PorterDuff.@Nullable Mode tintMode) {
222         if (mBackgroundTintHelper != null) {
223             mBackgroundTintHelper.setSupportBackgroundTintMode(tintMode);
224         }
225     }
226 
227     /**
228      * This should be accessed via
229      * {@link androidx.core.view.ViewCompat#getBackgroundTintMode(android.view.View)}
230      *
231      */
232     @RestrictTo(LIBRARY_GROUP_PREFIX)
233     @Override
getSupportBackgroundTintMode()234     public PorterDuff.@Nullable Mode getSupportBackgroundTintMode() {
235         return mBackgroundTintHelper != null
236                 ? mBackgroundTintHelper.getSupportBackgroundTintMode() : null;
237     }
238 
239     @Override
drawableStateChanged()240     protected void drawableStateChanged() {
241         super.drawableStateChanged();
242         if (mBackgroundTintHelper != null) {
243             mBackgroundTintHelper.applySupportBackgroundTint();
244         }
245         if (mTextHelper != null) {
246             mTextHelper.applyCompoundDrawablesTints();
247         }
248     }
249 
250     @Override
setTextAppearance(Context context, int resId)251     public void setTextAppearance(Context context, int resId) {
252         super.setTextAppearance(context, resId);
253         if (mTextHelper != null) {
254             mTextHelper.onSetTextAppearance(context, resId);
255         }
256     }
257 
258     /**
259      * If a {@link ViewCompat#setOnReceiveContentListener listener is set}, the returned
260      * {@link InputConnection} will use it to handle calls to {@link InputConnection#commitContent}.
261      *
262      * {@inheritDoc}
263      */
264     @Override
onCreateInputConnection(@onNull EditorInfo outAttrs)265     public @Nullable InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
266         InputConnection ic = super.onCreateInputConnection(outAttrs);
267         mTextHelper.populateSurroundingTextIfNeeded(this, ic, outAttrs);
268         ic = AppCompatHintHelper.onCreateInputConnection(ic, outAttrs, this);
269 
270         // On SDK 30 and below, we manually configure the InputConnection here to use
271         // ViewCompat.performReceiveContent. On S and above, the platform's BaseInputConnection
272         // implementation calls View.performReceiveContent by default.
273         if (ic != null && Build.VERSION.SDK_INT <= 30) {
274             String[] mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this);
275             if (mimeTypes != null) {
276                 EditorInfoCompat.setContentMimeTypes(outAttrs, mimeTypes);
277                 ic = InputConnectionCompat.createWrapper(this, ic, outAttrs);
278             }
279         }
280         return mAppCompatEmojiEditTextHelper.onCreateInputConnection(ic, outAttrs);
281     }
282 
283     /**
284      * See
285      * {@link TextViewCompat#setCustomSelectionActionModeCallback(TextView, ActionMode.Callback)}
286      */
287     @Override
setCustomSelectionActionModeCallback( ActionMode.@ullable Callback actionModeCallback)288     public void setCustomSelectionActionModeCallback(
289             ActionMode.@Nullable Callback actionModeCallback) {
290         super.setCustomSelectionActionModeCallback(
291                 TextViewCompat.wrapCustomSelectionActionModeCallback(this, actionModeCallback));
292     }
293 
294     @Override
getCustomSelectionActionModeCallback()295     public ActionMode.@Nullable Callback getCustomSelectionActionModeCallback() {
296         return TextViewCompat.unwrapCustomSelectionActionModeCallback(
297                 super.getCustomSelectionActionModeCallback());
298     }
299 
300     @Override
onDetachedFromWindow()301     protected void onDetachedFromWindow() {
302         super.onDetachedFromWindow();
303         if (Build.VERSION.SDK_INT >= 30 && Build.VERSION.SDK_INT < 33) {
304             final InputMethodManager imm = (InputMethodManager) getContext().getSystemService(
305                     Context.INPUT_METHOD_SERVICE);
306             // Calling isActive() here implied a checkFocus() call to update the active served
307             // view for input method. This is a backport for mServedView was detached, but the
308             // next served view gets mistakenly cleared as well.
309             // https://android.googlesource.com/platform/frameworks/base/+/734613a500fb
310             imm.isActive(this);
311         }
312     }
313 
314     @UiThread
315     @RequiresApi(26)
getSuperCaller()316     private @NonNull SuperCaller getSuperCaller() {
317         if (mSuperCaller == null) {
318             mSuperCaller = new SuperCaller();
319         }
320         return mSuperCaller;
321     }
322 
323     /**
324      * Sets the {@link TextClassifier} for this TextView.
325      */
326     @Override
327     @RequiresApi(api = 26)
setTextClassifier(@ullable TextClassifier textClassifier)328     public void setTextClassifier(@Nullable TextClassifier textClassifier) {
329         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P || mTextClassifierHelper == null) {
330             getSuperCaller().setTextClassifier(textClassifier);
331             return;
332         }
333         mTextClassifierHelper.setTextClassifier(textClassifier);
334     }
335 
336     /**
337      * Returns the {@link TextClassifier} used by this TextView.
338      * If no TextClassifier has been set, this TextView uses the default set by the
339      * {@link android.view.textclassifier.TextClassificationManager}.
340      */
341     @Override
342     @RequiresApi(api = 26)
getTextClassifier()343     public @NonNull TextClassifier getTextClassifier() {
344         // The null check is necessary because getTextClassifier is called when we are invoking
345         // the super class's constructor.
346         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P || mTextClassifierHelper == null) {
347             return getSuperCaller().getTextClassifier();
348         }
349         return mTextClassifierHelper.getTextClassifier();
350     }
351 
352     @Override
onDragEvent(@uppressWarnings"MissingNullability") DragEvent event)353     public boolean onDragEvent(@SuppressWarnings("MissingNullability") DragEvent event) {
354         if (maybeHandleDragEventViaPerformReceiveContent(this, event)) {
355             return true;
356         }
357         return super.onDragEvent(event);
358     }
359 
360     /**
361      * If a {@link ViewCompat#setOnReceiveContentListener listener is set}, uses it to execute the
362      * "Paste" and "Paste as plain text" menu actions.
363      *
364      * {@inheritDoc}
365      */
366     @Override
onTextContextMenuItem(int id)367     public boolean onTextContextMenuItem(int id) {
368         if (maybeHandleMenuActionViaPerformReceiveContent(this, id)) {
369             return true;
370         }
371         return super.onTextContextMenuItem(id);
372     }
373 
374     /**
375      * Implements the default behavior for receiving content, which coerces all content to text
376      * and inserts into the view.
377      *
378      * <p>IMPORTANT: This method is provided to enable custom widgets that extend this class
379      * to customize the default behavior for receiving content. Apps wishing to provide custom
380      * behavior for receiving content should not override this method, but rather should set
381      * a listener via {@link ViewCompat#setOnReceiveContentListener}. App code wishing to inject
382      * content into this view should not call this method directly, but rather should invoke
383      * {@link ViewCompat#performReceiveContent}.
384      *
385      * @param payload The content to insert and related metadata.
386      *
387      * @return The portion of the passed-in content that was not handled (may be all, some, or none
388      * of the passed-in content).
389      */
390     @Override
onReceiveContent(@onNull ContentInfoCompat payload)391     public @Nullable ContentInfoCompat onReceiveContent(@NonNull ContentInfoCompat payload) {
392         return mDefaultOnReceiveContentListener.onReceiveContent(this, payload);
393     }
394 
395     /**
396      * Adds EmojiCompat KeyListener to correctly edit multi-codepoint emoji when they've been
397      * converted to spans.
398      *
399      * {@inheritDoc}
400      */
401     @Override
setKeyListener(@ullable KeyListener keyListener)402     public void setKeyListener(@Nullable KeyListener keyListener) {
403         super.setKeyListener(mAppCompatEmojiEditTextHelper.getKeyListener(keyListener));
404     }
405 
406     @Override
setEmojiCompatEnabled(boolean enabled)407     public void setEmojiCompatEnabled(boolean enabled) {
408         mAppCompatEmojiEditTextHelper.setEnabled(enabled);
409     }
410 
411     @Override
isEmojiCompatEnabled()412     public boolean isEmojiCompatEnabled() {
413         return mAppCompatEmojiEditTextHelper.isEnabled();
414     }
415 
416     @Override
setCompoundDrawables(@ullable Drawable left, @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom)417     public void setCompoundDrawables(@Nullable Drawable left, @Nullable Drawable top,
418             @Nullable Drawable right, @Nullable Drawable bottom) {
419         super.setCompoundDrawables(left, top, right, bottom);
420         if (mTextHelper != null) {
421             mTextHelper.onSetCompoundDrawables();
422         }
423     }
424 
425     @Override
setCompoundDrawablesRelative(@ullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom)426     public void setCompoundDrawablesRelative(@Nullable Drawable start, @Nullable Drawable top,
427             @Nullable Drawable end, @Nullable Drawable bottom) {
428         super.setCompoundDrawablesRelative(start, top, end, bottom);
429         if (mTextHelper != null) {
430             mTextHelper.onSetCompoundDrawables();
431         }
432     }
433 
434     /**
435      * This should be accessed via
436      * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintList(TextView)}
437      *
438      * @return the tint applied to the compound drawables
439      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint
440      * @see #setSupportCompoundDrawablesTintList(ColorStateList)
441      *
442      */
443     @Override
444     @RestrictTo(LIBRARY_GROUP_PREFIX)
getSupportCompoundDrawablesTintList()445     public @Nullable ColorStateList getSupportCompoundDrawablesTintList() {
446         return mTextHelper.getCompoundDrawableTintList();
447     }
448 
449     /**
450      * This should be accessed via {@link
451      * androidx.core.widget.TextViewCompat#setCompoundDrawableTintList(TextView, ColorStateList)}
452      *
453      * Applies a tint to the compound drawables. Does not modify the current tint mode, which is
454      * {@link PorterDuff.Mode#SRC_IN} by default.
455      * <p>
456      * Subsequent calls to {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)} and
457      * related methods will automatically mutate the drawables and apply the specified tint and tint
458      * mode using {@link Drawable#setTintList(ColorStateList)}.
459      *
460      * @param tintList the tint to apply, may be {@code null} to clear tint
461      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTint
462      * @see #getSupportCompoundDrawablesTintList()
463      *
464      */
465     @Override
466     @RestrictTo(LIBRARY_GROUP_PREFIX)
setSupportCompoundDrawablesTintList(@ullable ColorStateList tintList)467     public void setSupportCompoundDrawablesTintList(@Nullable ColorStateList tintList) {
468         mTextHelper.setCompoundDrawableTintList(tintList);
469         mTextHelper.applyCompoundDrawablesTints();
470     }
471 
472     /**
473      * This should be accessed via
474      * {@link androidx.core.widget.TextViewCompat#getCompoundDrawableTintMode(TextView)}
475      *
476      * Returns the blending mode used to apply the tint to the compound drawables, if specified.
477      *
478      * @return the blending mode used to apply the tint to the compound drawables
479      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode
480      * @see #setSupportCompoundDrawablesTintMode(PorterDuff.Mode)
481      *
482      */
483     @Override
484     @RestrictTo(LIBRARY_GROUP_PREFIX)
getSupportCompoundDrawablesTintMode()485     public PorterDuff.@Nullable Mode getSupportCompoundDrawablesTintMode() {
486         return mTextHelper.getCompoundDrawableTintMode();
487     }
488 
489     /**
490      * This should be accessed via {@link
491      * androidx.core.widget.TextViewCompat#setCompoundDrawableTintMode(TextView, PorterDuff.Mode)}
492      *
493      * Specifies the blending mode used to apply the tint specified by
494      * {@link #setSupportCompoundDrawablesTintList(ColorStateList)} to the compound drawables. The
495      * default mode is {@link PorterDuff.Mode#SRC_IN}.
496      *
497      * @param tintMode the blending mode used to apply the tint, may be {@code null} to clear tint
498      * @attr ref androidx.appcompat.R.styleable#AppCompatTextView_drawableTintMode
499      * @see #setSupportCompoundDrawablesTintList(ColorStateList)
500      *
501      */
502     @Override
503     @RestrictTo(LIBRARY_GROUP_PREFIX)
setSupportCompoundDrawablesTintMode(PorterDuff.@ullable Mode tintMode)504     public void setSupportCompoundDrawablesTintMode(PorterDuff.@Nullable Mode tintMode) {
505         mTextHelper.setCompoundDrawableTintMode(tintMode);
506         mTextHelper.applyCompoundDrawablesTints();
507     }
508 
509     @RequiresApi(api = 26)
510     class SuperCaller {
511 
getTextClassifier()512         public @Nullable TextClassifier getTextClassifier() {
513             return AppCompatEditText.super.getTextClassifier();
514         }
515 
setTextClassifier(TextClassifier textClassifier)516         public void setTextClassifier(TextClassifier textClassifier) {
517             AppCompatEditText.super.setTextClassifier(textClassifier);
518         }
519     }
520 }
521