1/*
2 * Copyright (C) 2013 DroidDriver committers
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package io.appium.droiddriver.actions;
18
19import android.annotation.SuppressLint;
20import android.os.Build;
21import android.os.SystemClock;
22import android.view.KeyCharacterMap;
23import android.view.KeyEvent;
24import android.view.ViewConfiguration;
25import io.appium.droiddriver.UiElement;
26import io.appium.droiddriver.exceptions.ActionException;
27import io.appium.droiddriver.util.Preconditions;
28import io.appium.droiddriver.util.Strings;
29
30/** An action to type text. */
31public class TextAction extends KeyAction {
32
33  @SuppressLint("InlinedApi")
34  @SuppressWarnings("deprecation")
35  private static final KeyCharacterMap KEY_CHAR_MAP =
36      Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
37          ? KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD)
38          : KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
39  // KeyRepeatDelay is a good heuristic for KeyInjectionDelay.
40  private static long keyInjectionDelayMillis = ViewConfiguration.getKeyRepeatDelay();
41  private final String text;
42
43  /** Defaults timeoutMillis to 100. */
44  public TextAction(String text) {
45    this(text, 100L, false);
46  }
47
48  public TextAction(String text, long timeoutMillis, boolean checkFocused) {
49    super(timeoutMillis, checkFocused);
50    this.text = Preconditions.checkNotNull(text);
51  }
52
53  public static long getKeyInjectionDelayMillis() {
54    return keyInjectionDelayMillis;
55  }
56
57  public static void setKeyInjectionDelayMillis(long keyInjectionDelayMillis) {
58    TextAction.keyInjectionDelayMillis = keyInjectionDelayMillis;
59  }
60
61  @Override
62  public boolean perform(InputInjector injector, UiElement element) {
63    maybeCheckFocused(element);
64
65    // TODO: recycle events?
66    KeyEvent[] events = KEY_CHAR_MAP.getEvents(text.toCharArray());
67
68    if (events != null) {
69      for (KeyEvent event : events) {
70        // We have to change the time of an event before injecting it because
71        // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
72        // time stamp and the system rejects too old events. Hence, it is
73        // possible for an event to become stale before it is injected if it
74        // takes too long to inject the preceding ones.
75        KeyEvent modifiedEvent = KeyEvent.changeTimeRepeat(event, SystemClock.uptimeMillis(), 0);
76        if (!injector.injectInputEvent(modifiedEvent)) {
77          throw new ActionException("Failed to inject " + event);
78        }
79        SystemClock.sleep(keyInjectionDelayMillis);
80      }
81    } else {
82      throw new ActionException("The given text is not supported: " + text);
83    }
84    return true;
85  }
86
87  @Override
88  public String toString() {
89    return Strings.toStringHelper(this).addValue(text).toString();
90  }
91}
92