Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Swiftkey Android keyboard #25

Merged
merged 11 commits into from
May 21, 2024
4 changes: 2 additions & 2 deletions .github/badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/swiftkey_setup_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/swiftkey_setup_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/swiftkey_setup_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/swiftkey_setup_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions docs/emu_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,40 @@ Make sure to disable the clipboard :
!!! failure "Layout"
For now, the only layout supported is `english US`. Make sure this is the layout GBoard is using.

### Preparing Swiftkey

Swiftkey keyboard isn't installed on the emulator by default : you need to install it first.

!!! note
If you want to run the tests in parallel on several emulators, you need to repeat these steps for each emulator.

Start the emulator, then go to Google, and paste [this link](https://play.google.com/store/apps/details?id=com.touchtype.swiftkey&hl=en_US&gl=US) to install Swiftkey.

!!! tip
If the clipboard isn't shared with the emulator, open a terminal and run :

```bash
adb shell input text "https://play.google.com/store/apps/details?id=com.touchtype.swiftkey&hl=en_US&gl=US"
```

Install the keyboard on your emulator :

![](assets/swiftkey_setup_1.png){ width="300" }

Open the app, follow the instructions to activate the keyboard.

---

By default, Swiftkey has the clipboard enabled, and it may interfere with the layout detection. You can disable the clipboard. First, access the clipboard settings :

![](assets/swiftkey_setup_2.png){ width="300" }

![](assets/swiftkey_setup_3.png){ width="300" }

And disable the clipboard suggestions :

![](assets/swiftkey_setup_4.png){ width="300" }

## Setting up iOS emulator

### Creating the emulator
Expand Down
4 changes: 2 additions & 2 deletions kebbie/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def instantiate_correctors(
Returns:
The list of created Correctors.
"""
if keyboard in ["gboard", "tappa"]:
if keyboard in ["gboard", "tappa", "swiftkey"]:
# Android keyboards
return [
EmulatorCorrector(
Expand Down Expand Up @@ -68,7 +68,7 @@ def common_args(parser: argparse.ArgumentParser):
dest="keyboard",
type=str,
required=True,
choices=["gboard", "ios", "kbkitpro", "kbkitoss", "tappa", "fleksy"],
choices=["gboard", "ios", "kbkitpro", "kbkitoss", "tappa", "fleksy", "swiftkey"],
help="Which keyboard, to be tested, is currently installed on the emulator.",
)

Expand Down
124 changes: 116 additions & 8 deletions kebbie/emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
FLEKSY = "fleksy"
KBKITPRO = "kbkitpro"
KBKITOSS = "kbkitoss"
SWIFTKEY = "swiftkey"
KEYBOARD_PACKAGE = {
GBOARD: "com.google.android.inputmethod.latin",
SWIFTKEY: "com.touchtype.swiftkey",
TAPPA: "com.tappa.keyboard",
}
ANDROID_CAPABILITIES = {
"platformName": "android",
"automationName": "UiAutomator2",
Expand Down Expand Up @@ -98,10 +104,13 @@
"Keyboard Type - emojis": "smiley",
"Search": "enter",
"return": "enter",
"Enter": "enter",
"Symbol keyboard": "numbers",
"Symbols": "numbers",
"Symbols and numbers": "numbers",
"Keyboard Type - numeric": "numbers",
"Voice input": "mic",
",, alternatives available, Voice typing, long press to activate": "mic",
"Close features menu": "magic",
"Open features menu": "magic",
"underline": "_",
Expand All @@ -123,7 +132,35 @@
"Digit keyboard": "numbers",
"More symbols": "shift",
"Keyboard Type - symbolic": "shift",
"Double tap for uppercase": "shift",
"Double tap for caps lock": "shift",
"capital Q": "Q",
"capital W": "W",
"capital E": "E",
"capital R": "R",
"capital T": "T",
"capital Y": "Y",
"capital U": "U",
"capital I": "I",
"Capital I": "I",
"capital O": "O",
"capital P": "P",
"capital A": "A",
"capital S": "S",
"capital D": "D",
"capital F": "F",
"capital G": "G",
"capital H": "H",
"capital J": "J",
"capital K": "K",
"capital L": "L",
"capital Z": "Z",
"capital X": "X",
"capital C": "C",
"capital V": "V",
"capital B": "B",
"capital N": "N",
"capital M": "M",
}
FLEKSY_LAYOUT = {
"keyboard_frame": [0, 517, 393, 266], # Only the keyboard frame is defined as absolute position
Expand Down Expand Up @@ -252,7 +289,7 @@ class Emulator:
ValueError: Error raised if the given platform doesn't exist.
"""

def __init__(
def __init__( # noqa: C901
self,
platform: str,
keyboard: str,
Expand Down Expand Up @@ -293,6 +330,10 @@ def __init__(
self.last_char_is_space = False
self.last_char_is_eos = False

# Set the keyboard as default
if self.platform == ANDROID:
self.select_keyboard(keyboard)

# Get the right layout
if self.keyboard == GBOARD:
self.detected = GboardLayoutDetector(self.driver, self._tap)
Expand All @@ -312,10 +353,13 @@ def __init__(
elif self.keyboard == KBKITOSS:
self.detected = KbkitossLayoutDetector(self.driver, self._tap)
self.layout = self.detected.layout
elif self.keyboard == SWIFTKEY:
self.detected = SwiftkeyLayoutDetector(self.driver, self._tap)
self.layout = self.detected.layout
else:
raise ValueError(
f"Unknown keyboard : {self.keyboard}. Please specify `{GBOARD}`, `{TAPPA}`, `{FLEKSY}`, `{KBKITPRO}`, "
f"`{KBKITOSS}` or `{IOS}`."
f"Unknown keyboard : {self.keyboard}. Please specify `{GBOARD}`, `{TAPPA}`, `{FLEKSY}`, "
f"`{SWIFTKEY}`, `{KBKITPRO}`, `{KBKITOSS}` or `{IOS}`."
)

self.typing_field.clear()
Expand Down Expand Up @@ -352,6 +396,33 @@ def get_android_devices() -> List[str]:
devices = [d.split()[0] for d in devices if not (d.startswith("List of devices attached") or len(d) == 0)]
return devices

def select_keyboard(self, keyboard):
"""Searches the IME of the desired keyboard and selects it, only for Android.

Args:
keyboard (str): Keyboard to search.
"""
if keyboard not in KEYBOARD_PACKAGE:
print(
f"Warning ! {keyboard}'s IME isn't provided (in `KEYBOARD_PACKAGE`), can't automatically select the "
"keyboard."
)
return

ime_list = subprocess.check_output(["adb", "shell", "ime", "list", "-s"], universal_newlines=True)
ime_name = None
for ime in ime_list.strip().split("\n"):
if KEYBOARD_PACKAGE[keyboard] in ime:
ime_name = ime
break
if ime_name:
subprocess.run(
["adb", "shell", "settings", "put", "secure", "show_ime_with_hard_keyboard", "1"],
stdout=subprocess.PIPE,
)
subprocess.run(["adb", "shell", "ime", "enable", ime_name], stdout=subprocess.PIPE)
subprocess.run(["adb", "shell", "ime", "set", ime_name], stdout=subprocess.PIPE)

def get_ios_devices() -> List[Tuple[str, str]]:
"""Static method that uses the `xcrun simctl` command to retrieve the
list of booted devices.
Expand Down Expand Up @@ -453,6 +524,9 @@ def type_characters(self, characters: str): # noqa: C901
if self.kb_is_upper:
# If the keyboard is in uppercase mode, change it to lowercase
self._tap(self.layout["uppercase"]["shift"])
if self.keyboard == SWIFTKEY:
# Swiftkey needs double tap, otherwise we are capslocking
self._tap(self.layout["uppercase"]["shift"])
self._tap(self.layout["lowercase"][c])
elif c in self.layout["uppercase"]:
# The character is an uppercase character
Expand All @@ -470,9 +544,9 @@ def type_characters(self, characters: str): # noqa: C901
self._tap(self.layout["lowercase"]["numbers"])
self._tap(self.layout["numbers"][c])

if c != "'" or self.keyboard == GBOARD:
if c != "'" or self.keyboard in [GBOARD, SWIFTKEY]:
# For some reason, when `'` is typed, the keyboard automatically goes back
# to lowercase, so no need to re-tap the button (unless the keyboard is GBoard).
# to lowercase, so no need to re-tap the button (unless the keyboard is GBoard / Swiftkey).
# In all other cases, switch back to letters keyboard
self._tap(self.layout["numbers"]["letters"])
else:
Expand Down Expand Up @@ -866,7 +940,7 @@ class GboardLayoutDetector(LayoutDetector):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
xpath_root="./*/*[@package='com.google.android.inputmethod.latin']",
xpath_root=f"./*/*[@package='{KEYBOARD_PACKAGE[GBOARD]}']",
xpath_keys=".//*[@resource-id][@content-desc]",
**kwargs,
)
Expand Down Expand Up @@ -1009,6 +1083,40 @@ def get_suggestions(self) -> List[str]:
return suggestions


class SwiftkeyLayoutDetector(LayoutDetector):
"""Layout detector for the Swiftkey keyboard. See `LayoutDetector` for more
information.
"""

def __init__(self, *args, **kwargs):
super().__init__(
*args,
xpath_root=f"./*/*[@package='{KEYBOARD_PACKAGE[SWIFTKEY]}']",
xpath_keys=".//*[@class='android.view.View'][@content-desc]",
**kwargs,
)

def get_suggestions(self) -> List[str]:
"""Method to retrieve the keyboard suggestions from the XML tree.

Returns:
List of suggestions from the keyboard.
"""
suggestions = []

# Get the raw content as text, weed out useless elements
for data in self.driver.page_source.split("<android.widget.FrameLayout"):
if "com.touchtype.swiftkey" in data and "<android.view.View " in data:
sections = data.split("<android.view.View ")
for section in sections[1:]:
m = re.search(r"content-desc=\"([^\"]*)\"", section)
if m:
suggestions.append(html.unescape(m.group(1)))
break

return suggestions


class TappaLayoutDetector(LayoutDetector):
"""Layout detector for the Tappa keyboard. See `LayoutDetector` for more
information.
Expand All @@ -1017,7 +1125,7 @@ class TappaLayoutDetector(LayoutDetector):
def __init__(self, *args, **kwargs):
super().__init__(
*args,
xpath_root="./*/*[@package='com.tappa.keyboard']",
xpath_root=f"./*/*[@package='{KEYBOARD_PACKAGE[TAPPA]}']",
xpath_keys=".//com.mocha.keyboard.inputmethod.keyboard.Key",
**kwargs,
)
Expand All @@ -1031,7 +1139,7 @@ def get_suggestions(self) -> List[str]:
suggestions = []

# Get the raw content as text, weed out useless elements
section = self.driver.page_source.split("com.tappa.keyboard:id/toolbar")[1].split(
section = self.driver.page_source.split(f"{KEYBOARD_PACKAGE[TAPPA]}:id/toolbar")[1].split(
"</android.widget.FrameLayout>"
)[0]

Expand Down
12 changes: 8 additions & 4 deletions tests/test_emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ class SubprocessResult:
def test_get_android_devices(monkeypatch):
def android_subprocess(*args, **kwargs):
return SubprocessResult(
DummyStdout("""List of devices attached
DummyStdout(
"""List of devices attached
emulator-5554 device
emulator-5558 device

""")
"""
)
)

monkeypatch.setattr(subprocess, "run", android_subprocess)
Expand All @@ -42,7 +44,8 @@ def android_subprocess(*args, **kwargs):
def test_get_ios_devices(monkeypatch):
def ios_subprocess(*args, **kwargs):
return SubprocessResult(
DummyStdout("""== Devices ==
DummyStdout(
"""== Devices ==
-- iOS 14.4 --
iPhone 12 mini (8A192CB8-A72C-4BBA-9A98-2476E66ABEF8) (Shutdown) (unavailable)
iPhone 12 (C0E1F6AB-FDA5-4953-BB22-7CDB09D3B303) (Shutdown) (unavailable)
Expand Down Expand Up @@ -71,7 +74,8 @@ def ios_subprocess(*args, **kwargs):
iPad mini (6th generation) (EC7F8EF2-D5A6-437B-9531-E2DBE924FB5A) (Shutdown) (unavailable)
iPad Pro (11-inch) (4th generation) (49F56D2F-5722-41CD-9C11-D4979084DA3E) (Shutdown) (unavailable)
iPad Pro (12.9-inch) (6th generation) (08E5485D-D293-4B7B-9831-F29D55EDA053) (Shutdown) (unavailable)
""")
"""
)
)

monkeypatch.setattr(subprocess, "run", ios_subprocess)
Expand Down