Skip to content

Commit dec9d6e

Browse files
committed
Fix keyboard detection across Android versions and vendor keyboards
Verified against AOSP source (Android 10, 11, 13) that isOnScreen=, mViewVisibility=, touchable region=, and mGivenContentInsets= are present on all versions. The parsing now uses a 3-strategy approach: 1. touchable region (most accurate, used by stock keyboards) 2. mFrame + mGivenContentInsets (for vendor keyboards like Samsung/Xiaomi that don't set touchable insets — content insets reveal keyboard top) 3. mFrame alone with 60% height sanity check (rejects full-screen InputMethod windows that would cause false positives) Negative signals (isOnScreen=false, mViewVisibility=0x8) bail early.
1 parent ee2b59d commit dec9d6e

3 files changed

Lines changed: 113 additions & 20 deletions

File tree

pkg/driver/devicelab/keyboard.go

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,72 @@ import (
1212

1313
// Patterns for extracting keyboard bounds from "dumpsys window InputMethod".
1414
var (
15-
// Android <=12: "mFrame=[left,top][right,bottom]"
15+
// Android <=12: "mFrame=[left,top][right,bottom]" (not present on Android 13+)
1616
mFrameRegex = regexp.MustCompile(`mFrame=\[(\d+),(\d+)\]\[(\d+),(\d+)\]`)
1717

18-
// Android 13+: "touchable region=SkRegion((left,top,right,bottom))"
18+
// "touchable region=SkRegion((left,top,right,bottom))" — present on all versions
19+
// when the keyboard sets mTouchableInsets (stock keyboards do; some vendor keyboards don't).
1920
touchableRegionRegex = regexp.MustCompile(`touchable region=SkRegion\(\((\d+),(\d+),(\d+),(\d+)\)\)`)
21+
22+
// "mGivenContentInsets=[left,top][right,bottom]" — tells us where keyboard content
23+
// starts within the InputMethod window. The top inset is the transparent gap above
24+
// the keyboard. Present on all versions.
25+
contentInsetsRegex = regexp.MustCompile(`mGivenContentInsets=\[(\d+),(\d+)\]\[(\d+),(\d+)\]`)
2026
)
2127

2228
// parseKeyboardFrame extracts keyboard bounds from "dumpsys window InputMethod" output.
2329
// Returns nil if keyboard is not visible.
30+
//
31+
// Strategy order (verified against AOSP source for Android 10, 11, 13):
32+
// 1. touchable region — most accurate, gives actual keyboard area.
33+
// 2. mFrame + mGivenContentInsets — for vendor keyboards (Samsung, Xiaomi, etc.)
34+
// that don't set touchable insets. Content insets reveal where keyboard starts.
35+
// 3. mFrame alone — only if the frame looks like a keyboard (not a full-screen window).
2436
func parseKeyboardFrame(dumpsysOutput string) *core.Bounds {
25-
// Bail early if the window is explicitly not on screen.
37+
// isOnScreen= is present on all Android versions (10+). mViewVisibility=0x8 means GONE.
2638
if strings.Contains(dumpsysOutput, "isOnScreen=false") ||
2739
strings.Contains(dumpsysOutput, "mViewVisibility=0x8") {
2840
return nil
2941
}
3042

31-
// Prefer touchable region — gives the actual keyboard area, not the full
32-
// InputMethod window. Available on Android 11+ (SDK 30+). Safe to use here
33-
// because the caller already checks mInputShown before calling this function.
43+
// Strategy 1: touchable region — the actual keyboard touchable area.
44+
// Printed when mTouchableInsets != 0, which stock keyboards set but some vendor keyboards don't.
3445
if matches := touchableRegionRegex.FindStringSubmatch(dumpsysOutput); matches != nil {
3546
return boundsFromMatches(matches)
3647
}
3748

38-
// Fallback for older Android (<=10) that lacks touchable region.
39-
if matches := mFrameRegex.FindStringSubmatch(dumpsysOutput); matches != nil {
40-
return boundsFromMatches(matches)
49+
// Strategy 2+3: mFrame-based fallback (Android <=12 only; Android 13+ uses Frames: format).
50+
frameMatches := mFrameRegex.FindStringSubmatch(dumpsysOutput)
51+
if frameMatches == nil {
52+
return nil
53+
}
54+
bounds := boundsFromMatches(frameMatches)
55+
if bounds == nil {
56+
return nil
4157
}
4258

43-
return nil
59+
// Strategy 2: adjust mFrame by content insets. mGivenContentInsets.top tells us how many
60+
// pixels from the window top are transparent (not keyboard). This handles vendor keyboards
61+
// that use a full-screen InputMethod window but report content insets correctly.
62+
if insetsMatches := contentInsetsRegex.FindStringSubmatch(dumpsysOutput); insetsMatches != nil {
63+
topInset, _ := strconv.Atoi(insetsMatches[2])
64+
if topInset > 0 {
65+
bounds.Y += topInset
66+
bounds.Height -= topInset
67+
if bounds.Height <= 0 {
68+
return nil
69+
}
70+
return bounds
71+
}
72+
}
73+
74+
// Strategy 3: bare mFrame. Sanity check — a real keyboard is at most ~60% of screen height.
75+
// If the frame is taller, it's the full InputMethod window, not the keyboard.
76+
screenBottom := bounds.Y + bounds.Height
77+
if screenBottom > 0 && bounds.Height > screenBottom*6/10 {
78+
return nil
79+
}
80+
return bounds
4481
}
4582

4683
// boundsFromMatches converts regex matches [_, left, top, right, bottom] to Bounds.

pkg/driver/uiautomator2/keyboard.go

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,35 +12,72 @@ import (
1212

1313
// Patterns for extracting keyboard bounds from "dumpsys window InputMethod".
1414
var (
15-
// Android <=12: "mFrame=[left,top][right,bottom]"
15+
// Android <=12: "mFrame=[left,top][right,bottom]" (not present on Android 13+)
1616
mFrameRegex = regexp.MustCompile(`mFrame=\[(\d+),(\d+)\]\[(\d+),(\d+)\]`)
1717

18-
// Android 13+: "touchable region=SkRegion((left,top,right,bottom))"
18+
// "touchable region=SkRegion((left,top,right,bottom))" — present on all versions
19+
// when the keyboard sets mTouchableInsets (stock keyboards do; some vendor keyboards don't).
1920
touchableRegionRegex = regexp.MustCompile(`touchable region=SkRegion\(\((\d+),(\d+),(\d+),(\d+)\)\)`)
21+
22+
// "mGivenContentInsets=[left,top][right,bottom]" — tells us where keyboard content
23+
// starts within the InputMethod window. The top inset is the transparent gap above
24+
// the keyboard. Present on all versions.
25+
contentInsetsRegex = regexp.MustCompile(`mGivenContentInsets=\[(\d+),(\d+)\]\[(\d+),(\d+)\]`)
2026
)
2127

2228
// parseKeyboardFrame extracts keyboard bounds from "dumpsys window InputMethod" output.
2329
// Returns nil if keyboard is not visible.
30+
//
31+
// Strategy order (verified against AOSP source for Android 10, 11, 13):
32+
// 1. touchable region — most accurate, gives actual keyboard area.
33+
// 2. mFrame + mGivenContentInsets — for vendor keyboards (Samsung, Xiaomi, etc.)
34+
// that don't set touchable insets. Content insets reveal where keyboard starts.
35+
// 3. mFrame alone — only if the frame looks like a keyboard (not a full-screen window).
2436
func parseKeyboardFrame(dumpsysOutput string) *core.Bounds {
25-
// Bail early if the window is explicitly not on screen.
37+
// isOnScreen= is present on all Android versions (10+). mViewVisibility=0x8 means GONE.
2638
if strings.Contains(dumpsysOutput, "isOnScreen=false") ||
2739
strings.Contains(dumpsysOutput, "mViewVisibility=0x8") {
2840
return nil
2941
}
3042

31-
// Prefer touchable region — gives the actual keyboard area, not the full
32-
// InputMethod window. Available on Android 11+ (SDK 30+). Safe to use here
33-
// because the caller already checks mInputShown before calling this function.
43+
// Strategy 1: touchable region — the actual keyboard touchable area.
44+
// Printed when mTouchableInsets != 0, which stock keyboards set but some vendor keyboards don't.
3445
if matches := touchableRegionRegex.FindStringSubmatch(dumpsysOutput); matches != nil {
3546
return boundsFromMatches(matches)
3647
}
3748

38-
// Fallback for older Android (<=10) that lacks touchable region.
39-
if matches := mFrameRegex.FindStringSubmatch(dumpsysOutput); matches != nil {
40-
return boundsFromMatches(matches)
49+
// Strategy 2+3: mFrame-based fallback (Android <=12 only; Android 13+ uses Frames: format).
50+
frameMatches := mFrameRegex.FindStringSubmatch(dumpsysOutput)
51+
if frameMatches == nil {
52+
return nil
53+
}
54+
bounds := boundsFromMatches(frameMatches)
55+
if bounds == nil {
56+
return nil
4157
}
4258

43-
return nil
59+
// Strategy 2: adjust mFrame by content insets. mGivenContentInsets.top tells us how many
60+
// pixels from the window top are transparent (not keyboard). This handles vendor keyboards
61+
// that use a full-screen InputMethod window but report content insets correctly.
62+
if insetsMatches := contentInsetsRegex.FindStringSubmatch(dumpsysOutput); insetsMatches != nil {
63+
topInset, _ := strconv.Atoi(insetsMatches[2])
64+
if topInset > 0 {
65+
bounds.Y += topInset
66+
bounds.Height -= topInset
67+
if bounds.Height <= 0 {
68+
return nil
69+
}
70+
return bounds
71+
}
72+
}
73+
74+
// Strategy 3: bare mFrame. Sanity check — a real keyboard is at most ~60% of screen height.
75+
// If the frame is taller, it's the full InputMethod window, not the keyboard.
76+
screenBottom := bounds.Y + bounds.Height
77+
if screenBottom > 0 && bounds.Height > screenBottom*6/10 {
78+
return nil
79+
}
80+
return bounds
4481
}
4582

4683
// boundsFromMatches converts regex matches [_, left, top, right, bottom] to Bounds.

pkg/driver/uiautomator2/keyboard_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,25 @@ func TestParseKeyboardFrame(t *testing.T) {
100100
mHasSurface=true`,
101101
want: &core.Bounds{X: 0, Y: 1428, Width: 1080, Height: 912},
102102
},
103+
{
104+
name: "vendor keyboard — no touchable region, uses mFrame + content insets",
105+
input: ` Window #1 Window{abcdef InputMethod}:
106+
mFrame=[0,84][1080,2400]
107+
mViewVisibility=0x0 mHaveFrame=true mObscured=false
108+
mGivenContentInsets=[0,1292][0,0] mGivenVisibleInsets=[0,1292][0,0]
109+
mHasSurface=true isReadyForDisplay()=true
110+
isOnScreen=true`,
111+
want: &core.Bounds{X: 0, Y: 1376, Width: 1080, Height: 1024},
112+
},
113+
{
114+
name: "full-screen mFrame without insets or touchable region — rejected",
115+
input: ` Window #1 Window{abcdef InputMethod}:
116+
mFrame=[0,84][1080,2400]
117+
mViewVisibility=0x0 mHaveFrame=true
118+
mGivenContentInsets=[0,0][0,0]
119+
isOnScreen=true`,
120+
want: nil,
121+
},
103122
}
104123

105124
for _, tt := range tests {

0 commit comments

Comments
 (0)