This repository has been archived by the owner on Feb 22, 2023. It is now read-only.
/
InputAwareWebView.java
189 lines (168 loc) · 7.12 KB
/
InputAwareWebView.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.webviewflutter;
import static android.content.Context.INPUT_METHOD_SERVICE;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.webkit.WebView;
/**
* A WebView subclass that mirrors the same implementation hacks that the system WebView does in
* order to correctly create an InputConnection.
*
* <p>These hacks are only needed in Android versions below N and exist to create an InputConnection
* on the WebView's dedicated input, or IME, thread. The majority of this proxying logic is in
* {@link #checkInputConnectionProxy}.
*
* <p>See also {@link ThreadedInputConnectionProxyAdapterView}.
*/
final class InputAwareWebView extends WebView {
private static final String TAG = "InputAwareWebView";
private View threadedInputConnectionProxyView;
private ThreadedInputConnectionProxyAdapterView proxyAdapterView;
private View containerView;
InputAwareWebView(Context context, View containerView) {
super(context);
this.containerView = containerView;
}
void setContainerView(View containerView) {
this.containerView = containerView;
if (proxyAdapterView == null) {
return;
}
Log.w(TAG, "The containerView has changed while the proxyAdapterView exists.");
if (containerView != null) {
setInputConnectionTarget(proxyAdapterView);
}
}
/**
* Set our proxy adapter view to use its cached input connection instead of creating new ones.
*
* <p>This is used to avoid losing our input connection when the virtual display is resized.
*/
void lockInputConnection() {
if (proxyAdapterView == null) {
return;
}
proxyAdapterView.setLocked(true);
}
/** Sets the proxy adapter view back to its default behavior. */
void unlockInputConnection() {
if (proxyAdapterView == null) {
return;
}
proxyAdapterView.setLocked(false);
}
/** Restore the original InputConnection, if needed. */
void dispose() {
resetInputConnection();
}
/**
* Creates an InputConnection from the IME thread when needed.
*
* <p>We only need to create a {@link ThreadedInputConnectionProxyAdapterView} and create an
* InputConnectionProxy on the IME thread when WebView is doing the same thing. So we rely on the
* system calling this method for WebView's proxy view in order to know when we need to create our
* own.
*
* <p>This method would normally be called for any View that used the InputMethodManager. We rely
* on flutter/engine filtering the calls we receive down to the ones in our hierarchy and the
* system WebView in order to know whether or not the system WebView expects an InputConnection on
* the IME thread.
*/
@Override
public boolean checkInputConnectionProxy(final View view) {
// Check to see if the view param is WebView's ThreadedInputConnectionProxyView.
View previousProxy = threadedInputConnectionProxyView;
threadedInputConnectionProxyView = view;
if (previousProxy == view) {
// This isn't a new ThreadedInputConnectionProxyView. Ignore it.
return super.checkInputConnectionProxy(view);
}
if (containerView == null) {
Log.e(
TAG,
"Can't create a proxy view because there's no container view. Text input may not work.");
return super.checkInputConnectionProxy(view);
}
// We've never seen this before, so we make the assumption that this is WebView's
// ThreadedInputConnectionProxyView. We are making the assumption that the only view that could
// possibly be interacting with the IMM here is WebView's ThreadedInputConnectionProxyView.
proxyAdapterView =
new ThreadedInputConnectionProxyAdapterView(
/*containerView=*/ containerView,
/*targetView=*/ view,
/*imeHandler=*/ view.getHandler());
setInputConnectionTarget(/*targetView=*/ proxyAdapterView);
return super.checkInputConnectionProxy(view);
}
/**
* Ensure that input creation happens back on {@link #containerView}'s thread once this view no
* longer has focus.
*
* <p>The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's
* thread for all connections. We undo it here so users will be able to go back to typing in
* Flutter UIs as expected.
*/
@Override
public void clearFocus() {
super.clearFocus();
resetInputConnection();
}
/**
* Ensure that input creation happens back on {@link #containerView}.
*
* <p>The logic in {@link #checkInputConnectionProxy} forces input creation to happen on Webview's
* thread for all connections. We undo it here so users will be able to go back to typing in
* Flutter UIs as expected.
*/
private void resetInputConnection() {
if (proxyAdapterView == null) {
// No need to reset the InputConnection to the default thread if we've never changed it.
return;
}
if (containerView == null) {
Log.e(TAG, "Can't reset the input connection to the container view because there is none.");
return;
}
setInputConnectionTarget(/*targetView=*/ containerView);
}
/**
* This is the crucial trick that gets the InputConnection creation to happen on the correct
* thread pre Android N.
* https://cs.chromium.org/chromium/src/content/public/android/java/src/org/chromium/content/browser/input/ThreadedInputConnectionFactory.java?l=169&rcl=f0698ee3e4483fad5b0c34159276f71cfaf81f3a
*
* <p>{@code targetView} should have a {@link View#getHandler} method with the thread that future
* InputConnections should be created on.
*/
private void setInputConnectionTarget(final View targetView) {
if (containerView == null) {
Log.e(
TAG,
"Can't set the input connection target because there is no containerView to use as a handler.");
return;
}
targetView.requestFocus();
containerView.post(
new Runnable() {
@Override
public void run() {
InputMethodManager imm =
(InputMethodManager) getContext().getSystemService(INPUT_METHOD_SERVICE);
// This is a hack to make InputMethodManager believe that the target view now has focus.
// As a result, InputMethodManager will think that targetView is focused, and will call
// getHandler() of the view when creating input connection.
// Step 1: Set targetView as InputMethodManager#mNextServedView. This does not affect
// the real window focus.
targetView.onWindowFocusChanged(true);
// Step 2: Have InputMethodManager focus in on targetView. As a result, IMM will call
// onCreateInputConnection() on targetView on the same thread as
// targetView.getHandler(). It will also call subsequent InputConnection methods on this
// thread. This is the IME thread in cases where targetView is our proxyAdapterView.
imm.isActive(containerView);
}
});
}
}