|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.tests.components; |
|
6 |
|
7 import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; |
|
8 import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotSame; |
|
9 import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; |
|
10 |
|
11 import org.mozilla.gecko.R; |
|
12 import org.mozilla.gecko.tests.UITestContext; |
|
13 import org.mozilla.gecko.tests.helpers.FrameworkHelper; |
|
14 import org.mozilla.gecko.tests.helpers.WaitHelper; |
|
15 |
|
16 import android.content.Context; |
|
17 import android.content.ContextWrapper; |
|
18 import android.os.Handler; |
|
19 import android.os.Looper; |
|
20 import android.view.View; |
|
21 import android.view.inputmethod.EditorInfo; |
|
22 import android.view.inputmethod.InputConnection; |
|
23 import android.view.inputmethod.InputMethodManager; |
|
24 |
|
25 import com.jayway.android.robotium.solo.Condition; |
|
26 |
|
27 /** |
|
28 * A class representing any interactions that take place on GeckoView. |
|
29 */ |
|
30 public class GeckoViewComponent extends BaseComponent { |
|
31 |
|
32 public interface InputConnectionTest { |
|
33 public void test(InputConnection ic, EditorInfo info); |
|
34 } |
|
35 |
|
36 public final TextInput mTextInput; |
|
37 |
|
38 public GeckoViewComponent(final UITestContext testContext) { |
|
39 super(testContext); |
|
40 mTextInput = new TextInput(); |
|
41 } |
|
42 |
|
43 /** |
|
44 * Returns the GeckoView. |
|
45 */ |
|
46 private View getView() { |
|
47 // Solo.getView asserts returning a valid View |
|
48 return mSolo.getView(R.id.layer_view); |
|
49 } |
|
50 |
|
51 private void setContext(final Context newContext) { |
|
52 final View geckoView = getView(); |
|
53 // Switch to a no-InputMethodManager context to avoid interference |
|
54 mTestContext.getInstrumentation().runOnMainSync(new Runnable() { |
|
55 @Override |
|
56 public void run() { |
|
57 FrameworkHelper.setViewContext(geckoView, newContext); |
|
58 } |
|
59 }); |
|
60 } |
|
61 |
|
62 public class TextInput { |
|
63 private TextInput() { |
|
64 } |
|
65 |
|
66 private InputMethodManager getInputMethodManager() { |
|
67 final InputMethodManager imm = (InputMethodManager) |
|
68 mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); |
|
69 fAssertNotNull("Must have an InputMethodManager", imm); |
|
70 return imm; |
|
71 } |
|
72 |
|
73 /** |
|
74 * Returns whether text input is being directed to the GeckoView. |
|
75 */ |
|
76 private boolean isActive() { |
|
77 return getInputMethodManager().isActive(getView()); |
|
78 } |
|
79 |
|
80 public TextInput assertActive() { |
|
81 fAssertTrue("Current view should be the active input view", isActive()); |
|
82 return this; |
|
83 } |
|
84 |
|
85 public TextInput waitForActive() { |
|
86 WaitHelper.waitFor("current view to become the active input view", new Condition() { |
|
87 @Override |
|
88 public boolean isSatisfied() { |
|
89 return isActive(); |
|
90 } |
|
91 }); |
|
92 return this; |
|
93 } |
|
94 |
|
95 /** |
|
96 * Returns whether an InputConnection is avaiable. |
|
97 * An InputConnection is available when text input is being directed to the |
|
98 * GeckoView, and a text field (input, textarea, contentEditable, etc.) is |
|
99 * currently focused inside the GeckoView. |
|
100 */ |
|
101 private boolean hasInputConnection() { |
|
102 final InputMethodManager imm = getInputMethodManager(); |
|
103 return imm.isActive(getView()) && imm.isAcceptingText(); |
|
104 } |
|
105 |
|
106 public TextInput assertInputConnection() { |
|
107 fAssertTrue("Current view should have an active InputConnection", hasInputConnection()); |
|
108 return this; |
|
109 } |
|
110 |
|
111 public TextInput waitForInputConnection() { |
|
112 WaitHelper.waitFor("current view to have an active InputConnection", new Condition() { |
|
113 @Override |
|
114 public boolean isSatisfied() { |
|
115 return hasInputConnection(); |
|
116 } |
|
117 }); |
|
118 return this; |
|
119 } |
|
120 |
|
121 /** |
|
122 * Starts an InputConnectionTest. An InputConnectionTest must run on the |
|
123 * InputConnection thread which may or may not be the main UI thread. Also, |
|
124 * during an InputConnectionTest, the system InputMethodManager service must |
|
125 * be temporarily disabled to prevent the system IME from interfering with our |
|
126 * tests. We disable the service by override the GeckoView's context with one |
|
127 * that returns a null InputMethodManager service. |
|
128 * |
|
129 * @param test Test to run |
|
130 */ |
|
131 public TextInput testInputConnection(final InputConnectionTest test) { |
|
132 |
|
133 fAssertNotNull("Test must not be null", test); |
|
134 assertInputConnection(); |
|
135 |
|
136 // GeckoInputConnection can run on another thread than the main thread, |
|
137 // so we need to be testing it on that same thread it's running on |
|
138 final View geckoView = getView(); |
|
139 final Handler inputConnectionHandler = geckoView.getHandler(); |
|
140 final Context oldGeckoViewContext = FrameworkHelper.getViewContext(geckoView); |
|
141 |
|
142 setContext(new ContextWrapper(oldGeckoViewContext) { |
|
143 @Override |
|
144 public Object getSystemService(String name) { |
|
145 if (Context.INPUT_METHOD_SERVICE.equals(name)) { |
|
146 return null; |
|
147 } |
|
148 return super.getSystemService(name); |
|
149 } |
|
150 }); |
|
151 |
|
152 (new InputConnectionTestRunner(test)).runOnHandler(inputConnectionHandler); |
|
153 |
|
154 setContext(oldGeckoViewContext); |
|
155 return this; |
|
156 } |
|
157 |
|
158 private class InputConnectionTestRunner implements Runnable { |
|
159 private final InputConnectionTest mTest; |
|
160 private boolean mDone; |
|
161 |
|
162 public InputConnectionTestRunner(final InputConnectionTest test) { |
|
163 mTest = test; |
|
164 } |
|
165 |
|
166 public synchronized void runOnHandler(final Handler inputConnectionHandler) { |
|
167 // Below, we are blocking the instrumentation thread to wait on the |
|
168 // InputConnection thread. Therefore, the InputConnection thread must not be |
|
169 // the same as the instrumentation thread to avoid a deadlock. This should |
|
170 // always be the case and we perform a sanity check to make sure. |
|
171 fAssertNotSame("InputConnection should not be running on instrumentation thread", |
|
172 Looper.myLooper(), inputConnectionHandler.getLooper()); |
|
173 |
|
174 mDone = false; |
|
175 inputConnectionHandler.post(this); |
|
176 do { |
|
177 try { |
|
178 wait(); |
|
179 } catch (InterruptedException e) { |
|
180 // Ignore interrupts |
|
181 } |
|
182 } while (!mDone); |
|
183 } |
|
184 |
|
185 @Override |
|
186 public void run() { |
|
187 final EditorInfo info = new EditorInfo(); |
|
188 final InputConnection ic = getView().onCreateInputConnection(info); |
|
189 fAssertNotNull("Must have an InputConnection", ic); |
|
190 // Restore the IC to a clean state |
|
191 ic.clearMetaKeyStates(-1); |
|
192 ic.finishComposingText(); |
|
193 mTest.test(ic, info); |
|
194 synchronized (this) { |
|
195 // Test finished; return from runOnHandler |
|
196 mDone = true; |
|
197 notify(); |
|
198 } |
|
199 } |
|
200 } |
|
201 } |
|
202 } |