From 4c349e0101d6b7a1057722aeb21d7955b0eb0aeb Mon Sep 17 00:00:00 2001 From: Kevin Jin Date: Fri, 13 Mar 2015 10:27:53 -0700 Subject: Add CloseKeyboardAction for InstrumentationDriver Change-Id: I10c0d9cd8c88ccf88f1798a9b0daf7cc8ed796ad --- src/io/appium/droiddriver/UiElement.java | 22 ++--- src/io/appium/droiddriver/actions/EventAction.java | 2 +- .../actions/accessibility/AccessibilityAction.java | 2 +- .../actions/view/CloseKeyboardAction.java | 102 +++++++++++++++++++++ .../droiddriver/actions/view/ViewAction.java | 48 ++++++++++ .../droiddriver/util/InstrumentationUtils.java | 5 + 6 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 src/io/appium/droiddriver/actions/view/CloseKeyboardAction.java create mode 100644 src/io/appium/droiddriver/actions/view/ViewAction.java diff --git a/src/io/appium/droiddriver/UiElement.java b/src/io/appium/droiddriver/UiElement.java index e160c35..a003367 100644 --- a/src/io/appium/droiddriver/UiElement.java +++ b/src/io/appium/droiddriver/UiElement.java @@ -145,19 +145,15 @@ public interface UiElement { boolean perform(Action action); /** - * Sets the text of this element. The implementation may not work on all - * UiElements if the underlying view is not EditText. - *

- * If this element already has text, it is cleared first if the device has API 11 or higher. - *

- * TODO: Support this behavior on older devices. - *

- * If the {@code text} ends with {@code '\n'}, the IME may be closed automatically after this - * call. If the IME is open after this call, you can call - *

-   * perform(SingleKeyAction.BACK);
-   * 
- * to close the IME. + * Sets the text of this element. The implementation may not work on all UiElements if the + * underlying view is not EditText.

If this element already has text, it is cleared first if + * the device has API 11 or higher.

TODO: Support this behavior on older devices.

The IME + * (soft keyboard) may be shown after this call. If the {@code text} ends with {@code '\n'}, the + * IME may be closed automatically. If the IME is open, you can call {@link UiDevice#pressBack()} + * to close it.

If you are using {@link io.appium.droiddriver.instrumentation.InstrumentationDriver}, + * you may use {@link io.appium.droiddriver.actions.view.CloseKeyboardAction} to close it. The + * advantage of {@code CloseKeyboardAction} is that it is a no-op if the IME is hidden. This is + * useful when the state of the IME cannot be determined. * * @param text the text to enter */ diff --git a/src/io/appium/droiddriver/actions/EventAction.java b/src/io/appium/droiddriver/actions/EventAction.java index b271646..79e5060 100644 --- a/src/io/appium/droiddriver/actions/EventAction.java +++ b/src/io/appium/droiddriver/actions/EventAction.java @@ -29,7 +29,7 @@ public abstract class EventAction extends BaseAction { } @Override - public boolean perform(UiElement element) { + public final boolean perform(UiElement element) { return perform(element.getInjector(), element); } diff --git a/src/io/appium/droiddriver/actions/accessibility/AccessibilityAction.java b/src/io/appium/droiddriver/actions/accessibility/AccessibilityAction.java index 34ba85a..8d5f7ad 100644 --- a/src/io/appium/droiddriver/actions/accessibility/AccessibilityAction.java +++ b/src/io/appium/droiddriver/actions/accessibility/AccessibilityAction.java @@ -32,7 +32,7 @@ public abstract class AccessibilityAction extends BaseAction { } @Override - public boolean perform(UiElement element) { + public final boolean perform(UiElement element) { return perform(((UiAutomationElement) element).getRawElement(), element); } diff --git a/src/io/appium/droiddriver/actions/view/CloseKeyboardAction.java b/src/io/appium/droiddriver/actions/view/CloseKeyboardAction.java new file mode 100644 index 0000000..d653397 --- /dev/null +++ b/src/io/appium/droiddriver/actions/view/CloseKeyboardAction.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2015 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.droiddriver.actions.view; + +import android.content.Context; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.os.SystemClock; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import io.appium.droiddriver.UiElement; +import io.appium.droiddriver.exceptions.ActionException; +import io.appium.droiddriver.exceptions.DroidDriverException; +import io.appium.droiddriver.util.InstrumentationUtils; +import io.appium.droiddriver.util.Logs; + +/** + * Closes soft keyboard. Based on the Espresso + * code under the same name. + */ +public class CloseKeyboardAction extends ViewAction { + /** Defaults timeoutMillis to 2000 */ + public static final CloseKeyboardAction DEFAULT_INSTANCE = new CloseKeyboardAction(2000L, 1000L); + + private final long keyboardDismissalDelayMillis; + + /** + * @param timeoutMillis the value returned by {@link #getTimeoutMillis} + * @param keyboardDismissalDelayMillis a + * delay for the soft keyboard to finish closing + */ + public CloseKeyboardAction(long timeoutMillis, long keyboardDismissalDelayMillis) { + super(timeoutMillis); + this.keyboardDismissalDelayMillis = keyboardDismissalDelayMillis; + } + + protected boolean perform(View view, UiElement element) { + InputMethodManager imm = (InputMethodManager) InstrumentationUtils.getTargetContext() + .getSystemService(Context.INPUT_METHOD_SERVICE); + final AtomicInteger resultCodeHolder = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + + ResultReceiver resultReceiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + resultCodeHolder.set(resultCode); + latch.countDown(); + } + }; + + if (!imm.hideSoftInputFromWindow(view.getWindowToken(), 0, resultReceiver)) { + Logs.log(Log.INFO, "InputMethodManager.hideSoftInputFromWindow returned false"); + // Soft keyboard is not shown if hideSoftInputFromWindow returned false + return true; + } + + try { + if (!latch.await(getTimeoutMillis(), TimeUnit.MILLISECONDS)) { + throw new ActionException("Timed out after " + getTimeoutMillis() + " milliseconds" + + " waiting for resultCode from InputMethodManager.hideSoftInputFromWindow"); + } + } catch (InterruptedException e) { + throw DroidDriverException.propagate(e); + } + + int resultCode = resultCodeHolder.get(); + if (resultCode != InputMethodManager.RESULT_UNCHANGED_HIDDEN + && resultCode != InputMethodManager.RESULT_HIDDEN) { + throw new ActionException("resultCode from InputMethodManager.hideSoftInputFromWindow=" + + resultCode); + } + + // Wait for the soft keyboard to finish closing + SystemClock.sleep(keyboardDismissalDelayMillis); + return true; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/io/appium/droiddriver/actions/view/ViewAction.java b/src/io/appium/droiddriver/actions/view/ViewAction.java new file mode 100644 index 0000000..07fd7b9 --- /dev/null +++ b/src/io/appium/droiddriver/actions/view/ViewAction.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 DroidDriver committers + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.appium.droiddriver.actions.view; + +import android.view.View; + +import io.appium.droiddriver.UiElement; +import io.appium.droiddriver.actions.BaseAction; +import io.appium.droiddriver.instrumentation.ViewElement; + +/** + * Implements {@link io.appium.droiddriver.actions.Action} using the associated {@link View}. This + * can only be used with {@link ViewElement}. + */ +public abstract class ViewAction extends BaseAction { + protected ViewAction(long timeoutMillis) { + super(timeoutMillis); + } + + @Override + public final boolean perform(UiElement element) { + return perform(((ViewElement) element).getRawElement(), element); + } + + /** + * Performs the action on the associated {@link View}. + * + * @param view the View associated with the UiElement + * @param element the UiElement to perform the action on + * @return Whether the action is successful. Some actions throw exceptions in case of failure, + * when that behavior is more appropriate. + */ + protected abstract boolean perform(View view, UiElement element); +} diff --git a/src/io/appium/droiddriver/util/InstrumentationUtils.java b/src/io/appium/droiddriver/util/InstrumentationUtils.java index 95eb48c..c4f280d 100644 --- a/src/io/appium/droiddriver/util/InstrumentationUtils.java +++ b/src/io/appium/droiddriver/util/InstrumentationUtils.java @@ -17,6 +17,7 @@ package io.appium.droiddriver.util; import android.app.Instrumentation; +import android.content.Context; import android.os.Bundle; import android.os.Looper; import android.util.Log; @@ -73,6 +74,10 @@ public class InstrumentationUtils { return instrumentation; } + public static Context getTargetContext() { + return getInstrumentation().getTargetContext(); + } + /** * Gets the am instrument options. -- cgit v1.2.3