Fix NPE and remove global focus listener when tearing down the view (… (#27671)
* Fix NPE and remove global focus listener when tearing down the view (#27655)
* Update ci.yaml to enable the tests
Co-authored-by: Emmanuel Garcia <egarciad@google.com>
diff --git a/.ci.yaml b/.ci.yaml
index 869db0a..6a1cece 100644
--- a/.ci.yaml
+++ b/.ci.yaml
@@ -6,6 +6,7 @@
# More information at:
# * https://github.com/flutter/cocoon/blob/master/CI_YAML.md
enabled_branches:
+ - flutter-2.4-candidate.8
- master
- dev
- beta
diff --git a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
index 0097154..d46f534 100644
--- a/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
+++ b/shell/platform/android/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorView.java
@@ -74,23 +74,41 @@
return false;
}
+ @Nullable @VisibleForTesting ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener;
+
/**
- * Adds a focus change listener that notifies when the current view or any of its descendant views
+ * Sets a focus change listener that notifies when the current view or any of its descendant views
* have received focus.
*
- * @param focusListener The focus listener.
+ * <p>If there's an active focus listener, it will first remove the current listener, and then add
+ * the new one.
+ *
+ * @param userFocusListener A user provided focus listener.
*/
- public void addOnFocusChangeListener(@NonNull OnFocusChangeListener focusListener) {
+ public void setOnDescendantFocusChangeListener(@NonNull OnFocusChangeListener userFocusListener) {
+ unsetOnDescendantFocusChangeListener();
+
final View mutatorView = this;
- final ViewTreeObserver observer = this.getViewTreeObserver();
- if (observer.isAlive()) {
- observer.addOnGlobalFocusChangeListener(
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer.isAlive() && activeFocusListener == null) {
+ activeFocusListener =
new ViewTreeObserver.OnGlobalFocusChangeListener() {
@Override
public void onGlobalFocusChanged(View oldFocus, View newFocus) {
- focusListener.onFocusChange(mutatorView, childHasFocus(mutatorView));
+ userFocusListener.onFocusChange(mutatorView, childHasFocus(mutatorView));
}
- });
+ };
+ observer.addOnGlobalFocusChangeListener(activeFocusListener);
+ }
+ }
+
+ /** Unsets any active focus listener. */
+ public void unsetOnDescendantFocusChangeListener() {
+ final ViewTreeObserver observer = getViewTreeObserver();
+ if (observer.isAlive() && activeFocusListener != null) {
+ final ViewTreeObserver.OnGlobalFocusChangeListener currFocusListener = activeFocusListener;
+ activeFocusListener = null;
+ observer.removeOnGlobalFocusChangeListener(currFocusListener);
}
}
diff --git a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
index 8d59a26..781328b 100644
--- a/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
+++ b/shell/platform/android/io/flutter/plugin/platform/PlatformViewsController.java
@@ -61,7 +61,7 @@
// The texture registry maintaining the textures into which the embedded views will be rendered.
private TextureRegistry textureRegistry;
- private TextInputPlugin textInputPlugin;
+ @Nullable private TextInputPlugin textInputPlugin;
// The system channel used to communicate with the framework about platform views.
private PlatformViewsChannel platformViewsChannel;
@@ -164,8 +164,8 @@
platformViews.remove(viewId);
platformView.dispose();
}
-
if (parentView != null) {
+ parentView.unsetOnDescendantFocusChangeListener();
((ViewGroup) parentView.getParent()).removeView(parentView);
platformViewParent.remove(viewId);
}
@@ -748,11 +748,11 @@
new FlutterMutatorView(
context, context.getResources().getDisplayMetrics().density, androidTouchProcessor);
- parentView.addOnFocusChangeListener(
+ parentView.setOnDescendantFocusChangeListener(
(view, hasFocus) -> {
if (hasFocus) {
platformViewsChannel.invokeViewFocused(viewId);
- } else {
+ } else if (textInputPlugin != null) {
textInputPlugin.clearPlatformViewClient(viewId);
}
});
diff --git a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java
index 37b9023..9a5e46b 100644
--- a/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java
+++ b/shell/platform/android/test/io/flutter/embedding/engine/mutatorsstack/FlutterMutatorViewTest.java
@@ -143,7 +143,7 @@
};
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
- view.addOnFocusChangeListener(focusListener);
+ view.setOnDescendantFocusChangeListener(focusListener);
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
@@ -172,7 +172,7 @@
};
final OnFocusChangeListener focusListener = mock(OnFocusChangeListener.class);
- view.addOnFocusChangeListener(focusListener);
+ view.setOnDescendantFocusChangeListener(focusListener);
final ArgumentCaptor<ViewTreeObserver.OnGlobalFocusChangeListener> focusListenerCaptor =
ArgumentCaptor.forClass(ViewTreeObserver.OnGlobalFocusChangeListener.class);
@@ -193,6 +193,61 @@
return viewTreeObserver;
}
};
- view.addOnFocusChangeListener(mock(OnFocusChangeListener.class));
+ view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
+ }
+
+ @Test
+ public void setOnDescendantFocusChangeListener_keepsSingleListener() {
+ final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
+ when(viewTreeObserver.isAlive()).thenReturn(true);
+
+ final FlutterMutatorView view =
+ new FlutterMutatorView(RuntimeEnvironment.systemContext) {
+ @Override
+ public ViewTreeObserver getViewTreeObserver() {
+ return viewTreeObserver;
+ }
+ };
+
+ assertNull(view.activeFocusListener);
+
+ view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
+ assertNotNull(view.activeFocusListener);
+
+ final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
+ view.activeFocusListener;
+
+ view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
+ assertNotNull(view.activeFocusListener);
+
+ verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
+ }
+
+ @Test
+ public void unsetOnDescendantFocusChangeListener_removesActiveListener() {
+ final ViewTreeObserver viewTreeObserver = mock(ViewTreeObserver.class);
+ when(viewTreeObserver.isAlive()).thenReturn(true);
+
+ final FlutterMutatorView view =
+ new FlutterMutatorView(RuntimeEnvironment.systemContext) {
+ @Override
+ public ViewTreeObserver getViewTreeObserver() {
+ return viewTreeObserver;
+ }
+ };
+
+ assertNull(view.activeFocusListener);
+
+ view.setOnDescendantFocusChangeListener(mock(OnFocusChangeListener.class));
+ assertNotNull(view.activeFocusListener);
+
+ final ViewTreeObserver.OnGlobalFocusChangeListener activeFocusListener =
+ view.activeFocusListener;
+
+ view.unsetOnDescendantFocusChangeListener();
+ assertNull(view.activeFocusListener);
+
+ view.unsetOnDescendantFocusChangeListener();
+ verify(viewTreeObserver, times(1)).removeOnGlobalFocusChangeListener(activeFocusListener);
}
}