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