michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.tests.components; michael@0: michael@0: import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull; michael@0: import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotSame; michael@0: import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue; michael@0: michael@0: import org.mozilla.gecko.R; michael@0: import org.mozilla.gecko.tests.UITestContext; michael@0: import org.mozilla.gecko.tests.helpers.FrameworkHelper; michael@0: import org.mozilla.gecko.tests.helpers.WaitHelper; michael@0: michael@0: import android.content.Context; michael@0: import android.content.ContextWrapper; michael@0: import android.os.Handler; michael@0: import android.os.Looper; michael@0: import android.view.View; michael@0: import android.view.inputmethod.EditorInfo; michael@0: import android.view.inputmethod.InputConnection; michael@0: import android.view.inputmethod.InputMethodManager; michael@0: michael@0: import com.jayway.android.robotium.solo.Condition; michael@0: michael@0: /** michael@0: * A class representing any interactions that take place on GeckoView. michael@0: */ michael@0: public class GeckoViewComponent extends BaseComponent { michael@0: michael@0: public interface InputConnectionTest { michael@0: public void test(InputConnection ic, EditorInfo info); michael@0: } michael@0: michael@0: public final TextInput mTextInput; michael@0: michael@0: public GeckoViewComponent(final UITestContext testContext) { michael@0: super(testContext); michael@0: mTextInput = new TextInput(); michael@0: } michael@0: michael@0: /** michael@0: * Returns the GeckoView. michael@0: */ michael@0: private View getView() { michael@0: // Solo.getView asserts returning a valid View michael@0: return mSolo.getView(R.id.layer_view); michael@0: } michael@0: michael@0: private void setContext(final Context newContext) { michael@0: final View geckoView = getView(); michael@0: // Switch to a no-InputMethodManager context to avoid interference michael@0: mTestContext.getInstrumentation().runOnMainSync(new Runnable() { michael@0: @Override michael@0: public void run() { michael@0: FrameworkHelper.setViewContext(geckoView, newContext); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: public class TextInput { michael@0: private TextInput() { michael@0: } michael@0: michael@0: private InputMethodManager getInputMethodManager() { michael@0: final InputMethodManager imm = (InputMethodManager) michael@0: mActivity.getSystemService(Context.INPUT_METHOD_SERVICE); michael@0: fAssertNotNull("Must have an InputMethodManager", imm); michael@0: return imm; michael@0: } michael@0: michael@0: /** michael@0: * Returns whether text input is being directed to the GeckoView. michael@0: */ michael@0: private boolean isActive() { michael@0: return getInputMethodManager().isActive(getView()); michael@0: } michael@0: michael@0: public TextInput assertActive() { michael@0: fAssertTrue("Current view should be the active input view", isActive()); michael@0: return this; michael@0: } michael@0: michael@0: public TextInput waitForActive() { michael@0: WaitHelper.waitFor("current view to become the active input view", new Condition() { michael@0: @Override michael@0: public boolean isSatisfied() { michael@0: return isActive(); michael@0: } michael@0: }); michael@0: return this; michael@0: } michael@0: michael@0: /** michael@0: * Returns whether an InputConnection is avaiable. michael@0: * An InputConnection is available when text input is being directed to the michael@0: * GeckoView, and a text field (input, textarea, contentEditable, etc.) is michael@0: * currently focused inside the GeckoView. michael@0: */ michael@0: private boolean hasInputConnection() { michael@0: final InputMethodManager imm = getInputMethodManager(); michael@0: return imm.isActive(getView()) && imm.isAcceptingText(); michael@0: } michael@0: michael@0: public TextInput assertInputConnection() { michael@0: fAssertTrue("Current view should have an active InputConnection", hasInputConnection()); michael@0: return this; michael@0: } michael@0: michael@0: public TextInput waitForInputConnection() { michael@0: WaitHelper.waitFor("current view to have an active InputConnection", new Condition() { michael@0: @Override michael@0: public boolean isSatisfied() { michael@0: return hasInputConnection(); michael@0: } michael@0: }); michael@0: return this; michael@0: } michael@0: michael@0: /** michael@0: * Starts an InputConnectionTest. An InputConnectionTest must run on the michael@0: * InputConnection thread which may or may not be the main UI thread. Also, michael@0: * during an InputConnectionTest, the system InputMethodManager service must michael@0: * be temporarily disabled to prevent the system IME from interfering with our michael@0: * tests. We disable the service by override the GeckoView's context with one michael@0: * that returns a null InputMethodManager service. michael@0: * michael@0: * @param test Test to run michael@0: */ michael@0: public TextInput testInputConnection(final InputConnectionTest test) { michael@0: michael@0: fAssertNotNull("Test must not be null", test); michael@0: assertInputConnection(); michael@0: michael@0: // GeckoInputConnection can run on another thread than the main thread, michael@0: // so we need to be testing it on that same thread it's running on michael@0: final View geckoView = getView(); michael@0: final Handler inputConnectionHandler = geckoView.getHandler(); michael@0: final Context oldGeckoViewContext = FrameworkHelper.getViewContext(geckoView); michael@0: michael@0: setContext(new ContextWrapper(oldGeckoViewContext) { michael@0: @Override michael@0: public Object getSystemService(String name) { michael@0: if (Context.INPUT_METHOD_SERVICE.equals(name)) { michael@0: return null; michael@0: } michael@0: return super.getSystemService(name); michael@0: } michael@0: }); michael@0: michael@0: (new InputConnectionTestRunner(test)).runOnHandler(inputConnectionHandler); michael@0: michael@0: setContext(oldGeckoViewContext); michael@0: return this; michael@0: } michael@0: michael@0: private class InputConnectionTestRunner implements Runnable { michael@0: private final InputConnectionTest mTest; michael@0: private boolean mDone; michael@0: michael@0: public InputConnectionTestRunner(final InputConnectionTest test) { michael@0: mTest = test; michael@0: } michael@0: michael@0: public synchronized void runOnHandler(final Handler inputConnectionHandler) { michael@0: // Below, we are blocking the instrumentation thread to wait on the michael@0: // InputConnection thread. Therefore, the InputConnection thread must not be michael@0: // the same as the instrumentation thread to avoid a deadlock. This should michael@0: // always be the case and we perform a sanity check to make sure. michael@0: fAssertNotSame("InputConnection should not be running on instrumentation thread", michael@0: Looper.myLooper(), inputConnectionHandler.getLooper()); michael@0: michael@0: mDone = false; michael@0: inputConnectionHandler.post(this); michael@0: do { michael@0: try { michael@0: wait(); michael@0: } catch (InterruptedException e) { michael@0: // Ignore interrupts michael@0: } michael@0: } while (!mDone); michael@0: } michael@0: michael@0: @Override michael@0: public void run() { michael@0: final EditorInfo info = new EditorInfo(); michael@0: final InputConnection ic = getView().onCreateInputConnection(info); michael@0: fAssertNotNull("Must have an InputConnection", ic); michael@0: // Restore the IC to a clean state michael@0: ic.clearMetaKeyStates(-1); michael@0: ic.finishComposingText(); michael@0: mTest.test(ic, info); michael@0: synchronized (this) { michael@0: // Test finished; return from runOnHandler michael@0: mDone = true; michael@0: notify(); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }