Skip to content

Commit

Permalink
[TextInputLayout] Fix for TextInputLayout leak via AccessibilityManager.
Browse files Browse the repository at this point in the history
Resolves #2615

PiperOrigin-RevId: 449517571
  • Loading branch information
raajkumars authored and afohrman committed May 19, 2022
1 parent be1b38c commit 673cefc
Show file tree
Hide file tree
Showing 3 changed files with 82 additions and 17 deletions.
Expand Up @@ -44,6 +44,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.google.android.material.animation.AnimationUtils;
import com.google.android.material.textfield.TextInputLayout.BoxBackgroundMode;
Expand Down Expand Up @@ -71,8 +72,16 @@ class DropdownMenuEndIconDelegate extends EndIconDelegate {
}
};

private boolean editTextHasFocus;
private final TouchExplorationStateChangeListener touchExplorationStateChangeListener =
(boolean enabled) -> {
if (autoCompleteTextView != null && !isEditable(autoCompleteTextView)) {
ViewCompat.setImportantForAccessibility(
endIconView,
enabled ? IMPORTANT_FOR_ACCESSIBILITY_NO : IMPORTANT_FOR_ACCESSIBILITY_YES);
}
};

private boolean editTextHasFocus;
private boolean dropdownPopupDirty;
private boolean isEndIconChecked;
private long dropdownPopupActivatedAt = Long.MAX_VALUE;
Expand All @@ -89,15 +98,6 @@ void setUp() {
initAnimators();
accessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
accessibilityManager.addTouchExplorationStateChangeListener(enabled -> {
if (autoCompleteTextView != null && !isEditable(autoCompleteTextView)) {
ViewCompat.setImportantForAccessibility(
endIconView,
enabled ? IMPORTANT_FOR_ACCESSIBILITY_NO : IMPORTANT_FOR_ACCESSIBILITY_YES);
}
});
}
}

@SuppressLint("ClickableViewAccessibility") // There's an accessibility delegate that handles
Expand All @@ -113,6 +113,11 @@ void tearDown() {
}
}

@Override
public TouchExplorationStateChangeListener getTouchExplorationStateChangeListener() {
return touchExplorationStateChangeListener;
}

@Override
int getIconDrawableResId() {
// For lollipop+, the arrow icon changes orientation based on dropdown popup, otherwise it
Expand Down
Expand Up @@ -30,6 +30,7 @@
import static com.google.android.material.textfield.TextInputLayout.END_ICON_PASSWORD_TOGGLE;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
Expand All @@ -43,7 +44,9 @@
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
Expand All @@ -57,6 +60,8 @@
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.view.MarginLayoutParamsCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityManagerCompat;
import androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
import androidx.core.widget.TextViewCompat;
import com.google.android.material.internal.CheckableImageButton;
import com.google.android.material.internal.TextWatcherAdapter;
Expand Down Expand Up @@ -97,6 +102,8 @@ class EndCompoundLayout extends LinearLayout {
private boolean hintExpanded;

private EditText editText;
@Nullable private final AccessibilityManager accessibilityManager;
@Nullable private TouchExplorationStateChangeListener touchExplorationStateChangeListener;

private final TextWatcher editTextWatcher =
new TextWatcherAdapter() {
Expand Down Expand Up @@ -137,6 +144,9 @@ public void onEditTextAttached(@NonNull TextInputLayout textInputLayout) {
EndCompoundLayout(TextInputLayout textInputLayout, TintTypedArray a) {
super(textInputLayout.getContext());

accessibilityManager =
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);

this.textInputLayout = textInputLayout;

setVisibility(GONE);
Expand Down Expand Up @@ -170,6 +180,18 @@ public void onEditTextAttached(@NonNull TextInputLayout textInputLayout) {
addView(errorIconView);

textInputLayout.addOnEditTextAttachedListener(onEditTextAttachedListener);
addOnAttachStateChangeListener(
new OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View ignored) {
addTouchExplorationStateChangeListenerIfNeeded();
}

@Override
public void onViewDetachedFromWindow(View ignored) {
removeTouchExplorationStateChangeListenerIfNeeded();
}
});
}

private CheckableImageButton createIconView(
Expand Down Expand Up @@ -324,7 +346,7 @@ void setEndIconMode(@EndIconMode int endIconMode) {
if (this.endIconMode == endIconMode) {
return;
}
getEndIconDelegate().tearDown();
tearDownDelegate(getEndIconDelegate());
int previousEndIconMode = this.endIconMode;
this.endIconMode = endIconMode;
dispatchOnEndIconChanged(previousEndIconMode);
Expand All @@ -334,7 +356,7 @@ void setEndIconMode(@EndIconMode int endIconMode) {
setEndIconContentDescription(delegate.getIconContentDescriptionResId());
setEndIconCheckable(delegate.isIconCheckable());
if (delegate.isBoxBackgroundModeSupported(textInputLayout.getBoxBackgroundMode())) {
delegate.setUp();
setUpDelegate(delegate);
} else {
throw new IllegalStateException(
"The current box background mode "
Expand Down Expand Up @@ -373,6 +395,35 @@ void refreshIconState(boolean force) {
}
}

private void setUpDelegate(@NonNull EndIconDelegate delegate) {
delegate.setUp();

touchExplorationStateChangeListener = delegate.getTouchExplorationStateChangeListener();
addTouchExplorationStateChangeListenerIfNeeded();
}

private void tearDownDelegate(@NonNull EndIconDelegate delegate) {
removeTouchExplorationStateChangeListenerIfNeeded();
touchExplorationStateChangeListener = null;
delegate.tearDown();
}

private void addTouchExplorationStateChangeListenerIfNeeded() {
if (touchExplorationStateChangeListener != null
&& accessibilityManager != null
&& ViewCompat.isAttachedToWindow(this)) {
AccessibilityManagerCompat.addTouchExplorationStateChangeListener(
accessibilityManager, touchExplorationStateChangeListener);
}
}

private void removeTouchExplorationStateChangeListenerIfNeeded() {
if (touchExplorationStateChangeListener != null && accessibilityManager != null) {
AccessibilityManagerCompat.removeTouchExplorationStateChangeListener(
accessibilityManager, touchExplorationStateChangeListener);
}
}

private int getIconResId(EndIconDelegate delegate) {
int customIconResId = endIconDelegates.customEndIconDrawableId;
return customIconResId == 0 ? delegate.getIconDrawableResId() : customIconResId;
Expand Down
Expand Up @@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.view.accessibility.AccessibilityManagerCompat.TouchExplorationStateChangeListener;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import com.google.android.material.internal.CheckableImageButton;
import com.google.android.material.textfield.TextInputLayout.BoxBackgroundMode;
Expand Down Expand Up @@ -130,29 +131,37 @@ boolean isBoxBackgroundModeSupported(@BoxBackgroundMode int boxBackgroundMode) {
void onSuffixVisibilityChanged(boolean visible) {}

/**
* Overrides this method to provides an {@link OnClickListener} to handle click events of the end
* Override this method to provide an {@link OnClickListener} to handle click events of the end
* icon.
*/
OnClickListener getOnIconClickListener() {
return null;
}

/**
* Overrides this method to provides an {@link OnFocusChangeListener} to handle focus change
* events of the edit text.
* Override this method to provide an {@link OnFocusChangeListener} to handle focus change events
* of the edit text.
*/
OnFocusChangeListener getOnEditTextFocusChangeListener() {
return null;
}

/**
* Overrides this method to provides an {@link OnFocusChangeListener} to handle focus change
* events of the end icon.
* Override this method to provide an {@link OnFocusChangeListener} to handle focus change events
* of the end icon.
*/
OnFocusChangeListener getOnIconViewFocusChangeListener() {
return null;
}

/**
* Override this method to provide a {@link TouchExplorationStateChangeListener} to handle touch
* exploration state changes of the end icon.
*/
TouchExplorationStateChangeListener getTouchExplorationStateChangeListener() {
return null;
}

/** This method will be called when the edit text of the text input layout is attached. */
void onEditTextAttached(@Nullable EditText editText) {}

Expand Down

0 comments on commit 673cefc

Please sign in to comment.