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