Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit b382916

Browse files
committed
Test behavior instead of implementation. Made more of AndroidKeyProcessor guts private
1 parent fd37b4d commit b382916

File tree

4 files changed

+93
-106
lines changed

4 files changed

+93
-106
lines changed

shell/platform/android/io/flutter/embedding/android/AndroidKeyProcessor.java

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,12 @@
44

55
package io.flutter.embedding.android;
66

7-
import android.app.Activity;
8-
import android.content.Context;
9-
import android.content.ContextWrapper;
107
import android.util.Log;
118
import android.view.KeyCharacterMap;
129
import android.view.KeyEvent;
10+
import android.view.View;
1311
import androidx.annotation.NonNull;
1412
import androidx.annotation.Nullable;
15-
import androidx.annotation.VisibleForTesting;
1613
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
1714
import io.flutter.plugin.editing.TextInputPlugin;
1815
import java.util.AbstractMap.SimpleImmutableEntry;
@@ -41,40 +38,28 @@ public class AndroidKeyProcessor {
4138
@NonNull private final KeyEventChannel keyEventChannel;
4239
@NonNull private final TextInputPlugin textInputPlugin;
4340
private int combiningCharacter;
44-
@NonNull EventResponder eventResponder;
41+
@NonNull private EventResponder eventResponder;
4542

4643
/**
4744
* Constructor for AndroidKeyProcessor.
4845
*
49-
* @param context takes the application context so that this processor can find the activity for
50-
* re-dispatching of events that were not handled by the framework.
46+
* @param view takes the activity to use for re-dispatching of events that were not handled by the
47+
* framework.
5148
* @param keyEventChannel the event channel to listen to for new key events.
5249
* @param textInputPlugin a plugin, which, if set, is given key events before the framework is,
5350
* and if it has a valid input connection and is accepting text, then it will handle the event
5451
* and the framework will not receive it.
5552
*/
5653
public AndroidKeyProcessor(
57-
@NonNull Context context,
54+
@NonNull View view,
5855
@NonNull KeyEventChannel keyEventChannel,
5956
@NonNull TextInputPlugin textInputPlugin) {
6057
this.keyEventChannel = keyEventChannel;
6158
this.textInputPlugin = textInputPlugin;
62-
this.eventResponder = new EventResponder(context);
59+
this.eventResponder = new EventResponder(view);
6360
this.keyEventChannel.setEventResponseHandler(eventResponder);
6461
}
6562

66-
/**
67-
* Set the event responder for this key processor.
68-
*
69-
* <p>Typically used by the testing framework to inject mocks.
70-
*
71-
* @param eventResponder the event responder to use instead of the default responder.
72-
*/
73-
@VisibleForTesting
74-
public void setEventResponder(@NonNull EventResponder eventResponder) {
75-
this.eventResponder = eventResponder;
76-
}
77-
7863
/**
7964
* Called when a key up event is received by the {@link FlutterView}.
8065
*
@@ -186,16 +171,16 @@ private Character applyCombiningCharacterToBaseCharacter(int newCharacterCodePoi
186171
return complexCharacter;
187172
}
188173

189-
public static class EventResponder implements KeyEventChannel.EventResponseHandler {
174+
private static class EventResponder implements KeyEventChannel.EventResponseHandler {
190175
// The maximum number of pending events that are held before starting to
191176
// complain.
192177
private static final long MAX_PENDING_EVENTS = 1000;
193178
final Deque<Entry<Long, KeyEvent>> pendingEvents = new ArrayDeque<Entry<Long, KeyEvent>>();
194-
@NonNull private final Context context;
195-
@VisibleForTesting boolean dispatchingKeyEvent = false;
179+
@NonNull private final View view;
180+
boolean dispatchingKeyEvent = false;
196181

197-
public EventResponder(@NonNull Context context) {
198-
this.context = context;
182+
public EventResponder(@NonNull View view) {
183+
this.view = view;
199184
}
200185

201186
/**
@@ -264,31 +249,13 @@ public void addEvent(long id, @NonNull KeyEvent event) {
264249
*/
265250
public void dispatchKeyEvent(KeyEvent event) {
266251
// Since the framework didn't handle it, dispatch the key again.
267-
Activity activity = getActivity(context);
268-
if (activity != null) {
252+
if (view != null) {
269253
// Turn on dispatchingKeyEvent so that we don't dispatch to ourselves and
270254
// send it to the framework again.
271255
dispatchingKeyEvent = true;
272-
activity.dispatchKeyEvent(event);
256+
view.dispatchKeyEvent(event);
273257
dispatchingKeyEvent = false;
274258
}
275259
}
276-
277-
/**
278-
* Gets the nearest ancestor Activity for the given Context.
279-
*
280-
* @param context the context to look in for the activity.
281-
* @return null if no Activity found.
282-
*/
283-
private Activity getActivity(Context context) {
284-
if (context instanceof Activity) {
285-
return (Activity) context;
286-
}
287-
if (context instanceof ContextWrapper) {
288-
// Recurse up chain of base contexts until we find an Activity.
289-
return getActivity(((ContextWrapper) context).getBaseContext());
290-
}
291-
return null;
292-
}
293260
}
294261
}

shell/platform/android/io/flutter/embedding/android/FlutterView.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -849,8 +849,7 @@ public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
849849
this.flutterEngine.getPlatformViewsController());
850850
localizationPlugin = this.flutterEngine.getLocalizationPlugin();
851851
androidKeyProcessor =
852-
new AndroidKeyProcessor(
853-
getContext(), this.flutterEngine.getKeyEventChannel(), textInputPlugin);
852+
new AndroidKeyProcessor(this, this.flutterEngine.getKeyEventChannel(), textInputPlugin);
854853
androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
855854
accessibilityBridge =
856855
new AccessibilityBridge(

shell/platform/android/io/flutter/view/FlutterView.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ public void onPostResume() {
231231
mMouseCursorPlugin = null;
232232
}
233233
mLocalizationPlugin = new LocalizationPlugin(context, localizationChannel);
234-
androidKeyProcessor = new AndroidKeyProcessor(getContext(), keyEventChannel, mTextInputPlugin);
234+
androidKeyProcessor = new AndroidKeyProcessor(this, keyEventChannel, mTextInputPlugin);
235235
androidTouchProcessor = new AndroidTouchProcessor(flutterRenderer);
236236
mNativeView
237237
.getPluginRegistry()

shell/platform/android/test/io/flutter/embedding/android/AndroidKeyProcessorTest.java

Lines changed: 78 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,31 @@
11
package io.flutter.embedding.android;
22

3-
import static junit.framework.Assert.assertFalse;
4-
import static junit.framework.Assert.assertNotNull;
53
import static junit.framework.TestCase.assertEquals;
64
import static org.mockito.Mockito.any;
75
import static org.mockito.Mockito.mock;
8-
import static org.mockito.Mockito.spy;
96
import static org.mockito.Mockito.times;
107
import static org.mockito.Mockito.verify;
118
import static org.mockito.Mockito.when;
129

1310
import android.annotation.TargetApi;
14-
import android.app.Application;
15-
import android.content.Context;
1611
import android.view.KeyEvent;
12+
import android.view.View;
1713
import androidx.annotation.NonNull;
1814
import io.flutter.embedding.engine.FlutterEngine;
1915
import io.flutter.embedding.engine.FlutterJNI;
2016
import io.flutter.embedding.engine.systemchannels.KeyEventChannel;
2117
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
2218
import io.flutter.plugin.editing.TextInputPlugin;
2319
import io.flutter.util.FakeKeyEvent;
24-
import java.util.Map;
2520
import org.junit.Before;
2621
import org.junit.Test;
2722
import org.junit.runner.RunWith;
23+
import org.mockito.ArgumentCaptor;
2824
import org.mockito.Mock;
2925
import org.mockito.MockitoAnnotations;
26+
import org.mockito.invocation.InvocationOnMock;
27+
import org.mockito.stubbing.Answer;
3028
import org.robolectric.RobolectricTestRunner;
31-
import org.robolectric.RuntimeEnvironment;
3229
import org.robolectric.annotation.Config;
3330

3431
@Config(manifest = Config.NONE)
@@ -44,73 +41,97 @@ public void setUp() {
4441
}
4542

4643
@Test
47-
public void sendsKeyDownEventsToEventResponder() {
44+
public void respondsTrueWhenHandlingNewEvents() {
4845
FlutterEngine flutterEngine = mockFlutterEngine();
4946
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
50-
TextInputPlugin fakeTextInputPlugin = mock(TextInputPlugin.class);
47+
View fakeView = mock(View.class);
5148

5249
AndroidKeyProcessor processor =
53-
new AndroidKeyProcessor(
54-
RuntimeEnvironment.application, fakeKeyEventChannel, fakeTextInputPlugin);
50+
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
5551

56-
processor.onKeyDown(new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65));
57-
assertFalse(processor.eventResponder.dispatchingKeyEvent);
52+
boolean result = processor.onKeyDown(new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65));
53+
assertEquals(true, result);
5854
verify(fakeKeyEventChannel, times(1)).keyDown(any(KeyEventChannel.FlutterKeyEvent.class));
5955
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
60-
assertEquals(1, processor.eventResponder.pendingEvents.size());
61-
Map.Entry<Long, KeyEvent> firstPendingEvent =
62-
processor.eventResponder.pendingEvents.peekFirst();
63-
assertNotNull(firstPendingEvent);
64-
processor.eventResponder.onKeyEventHandled(firstPendingEvent.getKey());
65-
assertEquals(0, processor.eventResponder.pendingEvents.size());
56+
verify(fakeView, times(0)).dispatchKeyEvent(any(KeyEvent.class));
6657
}
6758

68-
@Test
69-
public void unhandledKeyEventsAreSynthesized() {
59+
public void synthesizesEventsWhenKeyDownNotHandled() {
7060
FlutterEngine flutterEngine = mockFlutterEngine();
7161
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
72-
TextInputPlugin fakeTextInputPlugin = mock(TextInputPlugin.class);
73-
Application spiedApplication = spy(RuntimeEnvironment.application);
74-
Context spiedContext = spy(spiedApplication.getBaseContext());
75-
when(spiedApplication.getBaseContext()).thenReturn(spiedContext);
76-
62+
View fakeView = mock(View.class);
63+
ArgumentCaptor<KeyEventChannel.EventResponseHandler> handlerCaptor =
64+
ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class);
65+
verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture());
7766
AndroidKeyProcessor processor =
78-
new AndroidKeyProcessor(spiedApplication, fakeKeyEventChannel, fakeTextInputPlugin);
79-
AndroidKeyProcessor.EventResponder eventResponder =
80-
spy(new AndroidKeyProcessor.EventResponder(spiedContext));
81-
processor.setEventResponder(eventResponder);
82-
83-
KeyEvent event = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
84-
processor.onKeyDown(event);
85-
assertFalse(processor.eventResponder.dispatchingKeyEvent);
86-
assertEquals(1, processor.eventResponder.pendingEvents.size());
87-
Map.Entry<Long, KeyEvent> firstPendingEvent =
88-
processor.eventResponder.pendingEvents.peekFirst();
89-
assertNotNull(firstPendingEvent);
90-
processor.eventResponder.onKeyEventNotHandled(firstPendingEvent.getKey());
91-
assertEquals(0, processor.eventResponder.pendingEvents.size());
92-
verify(eventResponder).dispatchKeyEvent(event);
67+
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
68+
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> eventCaptor =
69+
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
70+
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_DOWN, 65);
71+
72+
boolean result = processor.onKeyDown(fakeKeyEvent);
73+
assertEquals(true, result);
74+
75+
// Capture the FlutterKeyEvent so we can find out its event ID to use when
76+
// faking our response.
77+
verify(fakeKeyEventChannel, times(1)).keyDown(eventCaptor.capture());
78+
boolean[] dispatchResult = {true};
79+
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
80+
.then(
81+
new Answer<Boolean>() {
82+
@Override
83+
public Boolean answer(InvocationOnMock invocation) throws Throwable {
84+
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
85+
assertEquals(fakeKeyEvent, event);
86+
dispatchResult[0] = processor.onKeyDown(event);
87+
return dispatchResult[0];
88+
}
89+
});
90+
91+
// Fake a response from the framework.
92+
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId);
93+
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
94+
assertEquals(false, dispatchResult[0]);
95+
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
9396
}
9497

95-
@Test
96-
public void sendsKeyUpEventsToEventResponder() {
98+
public void synthesizesEventsWhenKeyUpNotHandled() {
9799
FlutterEngine flutterEngine = mockFlutterEngine();
98100
KeyEventChannel fakeKeyEventChannel = flutterEngine.getKeyEventChannel();
99-
TextInputPlugin fakeTextInputPlugin = mock(TextInputPlugin.class);
100-
101+
View fakeView = mock(View.class);
102+
ArgumentCaptor<KeyEventChannel.EventResponseHandler> handlerCaptor =
103+
ArgumentCaptor.forClass(KeyEventChannel.EventResponseHandler.class);
104+
verify(fakeKeyEventChannel).setEventResponseHandler(handlerCaptor.capture());
101105
AndroidKeyProcessor processor =
102-
new AndroidKeyProcessor(
103-
RuntimeEnvironment.application, fakeKeyEventChannel, fakeTextInputPlugin);
104-
105-
processor.onKeyUp(new FakeKeyEvent(KeyEvent.ACTION_UP, 65));
106-
assertFalse(processor.eventResponder.dispatchingKeyEvent);
107-
verify(fakeKeyEventChannel, times(0)).keyDown(any(KeyEventChannel.FlutterKeyEvent.class));
108-
verify(fakeKeyEventChannel, times(1)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
109-
Map.Entry<Long, KeyEvent> firstPendingEvent =
110-
processor.eventResponder.pendingEvents.peekFirst();
111-
assertNotNull(firstPendingEvent);
112-
processor.eventResponder.onKeyEventHandled(firstPendingEvent.getKey());
113-
assertEquals(0, processor.eventResponder.pendingEvents.size());
106+
new AndroidKeyProcessor(fakeView, fakeKeyEventChannel, mock(TextInputPlugin.class));
107+
ArgumentCaptor<KeyEventChannel.FlutterKeyEvent> eventCaptor =
108+
ArgumentCaptor.forClass(KeyEventChannel.FlutterKeyEvent.class);
109+
FakeKeyEvent fakeKeyEvent = new FakeKeyEvent(KeyEvent.ACTION_UP, 65);
110+
111+
boolean result = processor.onKeyUp(fakeKeyEvent);
112+
assertEquals(true, result);
113+
114+
// Capture the FlutterKeyEvent so we can find out its event ID to use when
115+
// faking our response.
116+
verify(fakeKeyEventChannel, times(1)).keyUp(eventCaptor.capture());
117+
boolean[] dispatchResult = {true};
118+
when(fakeView.dispatchKeyEvent(any(KeyEvent.class)))
119+
.then(
120+
new Answer<Boolean>() {
121+
@Override
122+
public Boolean answer(InvocationOnMock invocation) throws Throwable {
123+
KeyEvent event = (KeyEvent) invocation.getArguments()[0];
124+
assertEquals(fakeKeyEvent, event);
125+
dispatchResult[0] = processor.onKeyUp(event);
126+
return dispatchResult[0];
127+
}
128+
});
129+
130+
// Fake a response from the framework.
131+
handlerCaptor.getValue().onKeyEventNotHandled(eventCaptor.getValue().eventId);
132+
verify(fakeView, times(1)).dispatchKeyEvent(fakeKeyEvent);
133+
assertEquals(false, dispatchResult[0]);
134+
verify(fakeKeyEventChannel, times(0)).keyUp(any(KeyEventChannel.FlutterKeyEvent.class));
114135
}
115136

116137
@NonNull

0 commit comments

Comments
 (0)