blob: f39bddd7f0324e5deb29bec1d2d32592abf22639 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* 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 android.view.stylus;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_HOVER_MOVE;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_UP;
import static android.view.stylus.HandwritingTestUtil.createView;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Instrumentation;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.view.HandwritingInitiator;
import android.view.InputDevice;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
/**
* Tests for {@link HandwritingInitiator}
*
* Build/Install/Run:
* atest FrameworksCoreTests:android.view.stylus.HandwritingInitiatorTest
*/
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class HandwritingInitiatorTest {
private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout();
private static final int HW_BOUNDS_OFFSETS_LEFT_PX = 10;
private static final int HW_BOUNDS_OFFSETS_TOP_PX = 20;
private static final int HW_BOUNDS_OFFSETS_RIGHT_PX = 30;
private static final int HW_BOUNDS_OFFSETS_BOTTOM_PX = 40;
private int mHandwritingSlop = 2;
private static final Rect sHwArea1;
private static final Rect sHwArea2;
static {
sHwArea1 = new Rect(100, 200, 500, 500);
// The extended handwriting area bounds of the two views are overlapping.
int hwArea2Top = sHwArea1.bottom + HW_BOUNDS_OFFSETS_TOP_PX / 2;
sHwArea2 = new Rect(sHwArea1.left, hwArea2Top, sHwArea1.right, hwArea2Top + 300);
}
private HandwritingInitiator mHandwritingInitiator;
private EditText mTestView1;
private EditText mTestView2;
private Context mContext;
@Before
public void setup() throws Exception {
final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
mContext = instrumentation.getTargetContext();
final ViewConfiguration viewConfiguration = ViewConfiguration.get(mContext);
mHandwritingSlop = viewConfiguration.getScaledHandwritingSlop();
InputMethodManager inputMethodManager = mContext.getSystemService(InputMethodManager.class);
mHandwritingInitiator =
spy(new HandwritingInitiator(viewConfiguration, inputMethodManager));
mTestView1 = createView(sHwArea1, /* autoHandwritingEnabled= */ true,
/* isStylusHandwritingAvailable= */ true,
HW_BOUNDS_OFFSETS_LEFT_PX,
HW_BOUNDS_OFFSETS_TOP_PX,
HW_BOUNDS_OFFSETS_RIGHT_PX,
HW_BOUNDS_OFFSETS_BOTTOM_PX);
mTestView2 = createView(sHwArea2, /* autoHandwritingEnabled= */ true,
/* isStylusHandwritingAvailable= */ true,
HW_BOUNDS_OFFSETS_LEFT_PX,
HW_BOUNDS_OFFSETS_TOP_PX,
HW_BOUNDS_OFFSETS_RIGHT_PX,
HW_BOUNDS_OFFSETS_BOTTOM_PX);
mHandwritingInitiator.updateHandwritingAreasForView(mTestView1);
mHandwritingInitiator.updateHandwritingAreasForView(mTestView2);
doReturn(true).when(mHandwritingInitiator).tryAcceptStylusHandwritingDelegation(any());
}
@Test
public void onTouchEvent_startHandwriting_when_stylusMoveOnce_withinHWArea() {
mTestView1.setText("hello");
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
boolean onTouchEventResult1 = mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
boolean onTouchEventResult2 = mHandwritingInitiator.onTouchEvent(stylusEvent2);
// Stylus movement within HandwritingArea should trigger IMM.startHandwriting once.
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
assertThat(onTouchEventResult1).isFalse();
// After IMM.startHandwriting is triggered, onTouchEvent should return true for ACTION_MOVE
// events so that the events are not dispatched to the view tree.
assertThat(onTouchEventResult2).isTrue();
// Since the stylus down point was inside the TextView's bounds, the handwriting initiator
// does not need to set the cursor position.
verify(mTestView1, never()).setSelection(anyInt());
}
@Test
public void onTouchEvent_startHandwritingOnce_when_stylusMoveMultiTimes_withinHWArea() {
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
boolean onTouchEventResult1 = mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop / 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
boolean onTouchEventResult2 = mHandwritingInitiator.onTouchEvent(stylusEvent2);
final int x3 = x2 + mHandwritingSlop * 2;
final int y3 = y1;
MotionEvent stylusEvent3 = createStylusEvent(ACTION_MOVE, x3, y3, 0);
boolean onTouchEventResult3 = mHandwritingInitiator.onTouchEvent(stylusEvent3);
final int x4 = x3 + mHandwritingSlop * 2;
final int y4 = y1;
MotionEvent stylusEvent4 = createStylusEvent(ACTION_MOVE, x4, y4, 0);
boolean onTouchEventResult4 = mHandwritingInitiator.onTouchEvent(stylusEvent4);
MotionEvent stylusEvent5 = createStylusEvent(ACTION_UP, x4, y4, 0);
boolean onTouchEventResult5 = mHandwritingInitiator.onTouchEvent(stylusEvent5);
// It only calls startHandwriting once for each ACTION_DOWN.
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
assertThat(onTouchEventResult1).isFalse();
// stylusEvent2 does not trigger IMM.startHandwriting since the touch slop distance has not
// been exceeded. onTouchEvent should return false so that the event is dispatched to the
// view tree.
assertThat(onTouchEventResult2).isFalse();
// After IMM.startHandwriting is triggered by stylusEvent3, onTouchEvent should return true
// for ACTION_MOVE events so that the events are not dispatched to the view tree.
assertThat(onTouchEventResult3).isTrue();
assertThat(onTouchEventResult4).isTrue();
assertThat(onTouchEventResult5).isFalse();
}
@Test
public void onTouchEvent_startHandwriting_when_stylusMove_withinExtendedHWArea() {
mTestView1.setText("hello");
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// Stylus movement within extended HandwritingArea should trigger IMM.startHandwriting once.
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
// Since the stylus down point was outside the TextView's bounds, the handwriting initiator
// sets the cursor position.
verify(mTestView1).setSelection(4);
}
@Test
public void onTouchEvent_startHandwriting_inputConnectionBuiltAfterStylusMove() {
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// InputConnection is created after stylus movement.
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
}
@Test
public void onTouchEvent_startHandwriting_inputConnectionBuilt_stylusMoveInExtendedHWArea() {
mTestView1.setText("hello");
when(mTestView1.getOffsetForPosition(anyFloat(), anyFloat())).thenReturn(4);
// The stylus down point is between mTestView1 and mTestView2, but it is within the
// extended handwriting area of both views. It is closer to mTestView1.
final int x1 = sHwArea1.right + HW_BOUNDS_OFFSETS_RIGHT_PX / 2;
final int y1 = sHwArea1.bottom + (sHwArea2.top - sHwArea1.bottom) / 3;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// First create InputConnection for mTestView2 and verify that handwriting is not started.
mHandwritingInitiator.onInputConnectionCreated(mTestView2);
verify(mHandwritingInitiator, never()).startHandwriting(mTestView2);
// Next create InputConnection for mTextView1. Handwriting is started for this view since
// the stylus down point is closest to this view.
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
verify(mHandwritingInitiator).startHandwriting(mTestView1);
// Since the stylus down point was outside the TextView's bounds, the handwriting initiator
// sets the cursor position.
verify(mTestView1).setSelection(4);
}
@Test
public void onTouchEvent_tryAcceptDelegation_delegatorCallbackCreatesInputConnection() {
View delegateView = new EditText(mContext);
delegateView.setIsHandwritingDelegate(true);
mTestView1.setHandwritingDelegatorCallback(
() -> mHandwritingInitiator.onInputConnectionCreated(delegateView));
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView);
}
@Test
public void onTouchEvent_tryAcceptDelegation_delegatorCallbackFocusesDelegate() {
View delegateView = new EditText(mContext);
delegateView.setIsHandwritingDelegate(true);
mHandwritingInitiator.onInputConnectionCreated(delegateView);
reset(mHandwritingInitiator);
mTestView1.setHandwritingDelegatorCallback(
() -> mHandwritingInitiator.onDelegateViewFocused(delegateView));
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(delegateView);
}
@Test
public void onTouchEvent_startHandwriting_delegate_touchEventsHandled() {
// There is no delegator view and the delegate callback does nothing so handwriting will not
// be started. This is so we can test how touch events are handled before handwriting is
// started.
mTestView1.setHandwritingDelegatorCallback(() -> {});
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y, 0);
boolean onTouchEventResult1 = mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop / 2;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y, 0);
boolean onTouchEventResult2 = mHandwritingInitiator.onTouchEvent(stylusEvent2);
final int x3 = x2 + mHandwritingSlop * 2;
MotionEvent stylusEvent3 = createStylusEvent(ACTION_MOVE, x3, y, 0);
boolean onTouchEventResult3 = mHandwritingInitiator.onTouchEvent(stylusEvent3);
final int x4 = x3 + mHandwritingSlop * 2;
MotionEvent stylusEvent4 = createStylusEvent(ACTION_MOVE, x4, y, 0);
boolean onTouchEventResult4 = mHandwritingInitiator.onTouchEvent(stylusEvent4);
assertThat(onTouchEventResult1).isFalse();
// stylusEvent2 does not trigger delegation since the touch slop distance has not been
// exceeded. onTouchEvent should return false so that the event is dispatched to the view
// tree.
assertThat(onTouchEventResult2).isFalse();
// After delegation is triggered by stylusEvent3, onTouchEvent should return true for
// ACTION_MOVE events so that the events are not dispatched to the view tree.
assertThat(onTouchEventResult3).isTrue();
assertThat(onTouchEventResult4).isTrue();
}
@Test
public void onTouchEvent_notStartHandwriting_whenHandwritingNotAvailable() {
final Rect rect = new Rect(600, 600, 900, 900);
final View testView = createView(rect, true /* autoHandwritingEnabled */,
false /* isStylusHandwritingAvailable */);
mHandwritingInitiator.updateHandwritingAreasForView(testView);
mHandwritingInitiator.onInputConnectionCreated(testView);
final int x1 = (rect.left + rect.right) / 2;
final int y1 = (rect.top + rect.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// Stylus movement within HandwritingArea should not trigger IMM.startHandwriting since
// the current IME doesn't support handwriting.
verify(mHandwritingInitiator, never()).startHandwriting(mTestView1);
}
@Test
public void onTouchEvent_notStartHandwriting_when_stylusTap_withinHWArea() {
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
final int x1 = 200;
final int y1 = 200;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop / 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_UP, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
verify(mHandwritingInitiator, never()).startHandwriting(mTestView1);
}
@Test
public void onTouchEvent_notStartHandwriting_when_stylusMove_outOfHWArea() {
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
final int x1 = 10;
final int y1 = 10;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
verify(mHandwritingInitiator, never()).startHandwriting(mTestView1);
}
@Test
public void onTouchEvent_notStartHandwriting_when_stylusMove_afterTimeOut() {
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
final int x1 = 10;
final int y1 = 10;
final long time1 = 10L;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
final long time2 = time1 + TIMEOUT + 10L;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, time2);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// stylus movement is after TAP_TIMEOUT it shouldn't call startHandwriting.
verify(mHandwritingInitiator, never()).startHandwriting(mTestView1);
}
@Test
public void onTouchEvent_focusView_stylusMoveOnce_withinHWArea() {
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// HandwritingInitiator will request focus for the registered view.
verify(mTestView1, times(1)).requestFocus();
}
@Test
public void onTouchEvent_focusView_inputConnectionAlreadyBuilt_stylusMoveOnce_withinHWArea() {
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// View has input connection but not focus, so HandwritingInitiator will request focus
// before starting handwriting.
verify(mTestView1).requestFocus();
verify(mHandwritingInitiator).startHandwriting(mTestView1);
}
@Test
public void onTouchEvent_focusView_stylusMoveOnce_withinExtendedHWArea() {
final int x1 = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
final int y1 = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
// HandwritingInitiator will request focus for the registered view.
verify(mTestView1, times(1)).requestFocus();
}
@Test
public void onTouchEvent_handwritingAreaOverlapped_initiateForCloserView() {
// The ACTION_DOWN location is within the handwriting bounds of both mTestView1 and
// mTestView2. Because it's closer to mTestView2's handwriting bounds, handwriting is
// initiated for mTestView2.
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = sHwArea1.bottom + HW_BOUNDS_OFFSETS_BOTTOM_PX - 1;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
verify(mTestView2, times(1)).requestFocus();
mHandwritingInitiator.onInputConnectionCreated(mTestView2);
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView2);
}
@Test
public void onTouchEvent_handwritingAreaOverlapped_focusedViewHasPriority() {
// Simulate the case where mTestView1 is focused.
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
// The ACTION_DOWN location is within the handwriting bounds of both mTestView1 and
// mTestView2. Although it's closer to mTestView2's handwriting bounds, handwriting is
// initiated for mTestView1 because it's focused.
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = sHwArea1.bottom + HW_BOUNDS_OFFSETS_BOTTOM_PX - 1;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
}
@Test
public void onResolvePointerIcon_withinHWArea_showPointerIcon() {
MotionEvent hoverEvent = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY());
PointerIcon icon = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent);
assertThat(icon.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING);
}
@Test
public void onResolvePointerIcon_withinExtendedHWArea_showPointerIcon() {
int x = sHwArea1.left - HW_BOUNDS_OFFSETS_LEFT_PX / 2;
int y = sHwArea1.top - HW_BOUNDS_OFFSETS_TOP_PX / 2;
MotionEvent hoverEvent = createStylusHoverEvent(x, y);
PointerIcon icon = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent);
assertThat(icon.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING);
}
@Test
public void onResolvePointerIcon_afterHandwriting_hidePointerIconForConnectedView() {
// simulate the case where sTestView1 is focused.
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Verify that handwriting started for sTestView1.
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
MotionEvent hoverEvent1 = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY());
PointerIcon icon1 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
// After handwriting is initiated for the connected view, hide the hover icon.
assertThat(icon1).isNull();
MotionEvent hoverEvent2 = createStylusHoverEvent(sHwArea2.centerX(), sHwArea2.centerY());
PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent2);
// Now stylus is hovering on another editor, show the hover icon.
assertThat(icon2.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING);
// After the hover icon is displayed again, it will show hover icon for the connected view
// again.
PointerIcon icon3 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
assertThat(icon3.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING);
}
@Test
public void onResolvePointerIcon_afterHandwriting_hidePointerIconForDelegatorView() {
// Set mTextView2 to be the delegate of mTestView1.
mTestView2.setIsHandwritingDelegate(true);
mTestView1.setHandwritingDelegatorCallback(
() -> mHandwritingInitiator.onInputConnectionCreated(mTestView2));
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Prerequisite check, verify that handwriting started for delegateView.
verify(mHandwritingInitiator, times(1)).tryAcceptStylusHandwritingDelegation(mTestView2);
MotionEvent hoverEvent = createStylusHoverEvent(sHwArea2.centerX(), sHwArea2.centerY());
PointerIcon icon = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent);
// After handwriting is initiated for the connected view, hide the hover icon.
assertThat(icon).isNull();
}
@Test
public void onResolvePointerIcon_showHoverIconAfterTap() {
// Simulate the case where sTestView1 is focused.
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Verify that handwriting started for sTestView1.
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
MotionEvent hoverEvent1 = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY());
PointerIcon icon1 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
// After handwriting is initiated for the connected view, hide the hover icon.
assertThat(icon1).isNull();
// When exceedsHwSlop is false, it simulates a tap.
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ false);
PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
assertThat(icon2.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING);
}
@Test
public void onResolvePointerIcon_showHoverIconAfterFocusChange() {
// Simulate the case where sTestView1 is focused.
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
injectStylusEvent(mHandwritingInitiator, sHwArea1.centerX(), sHwArea1.centerY(),
/* exceedsHWSlop */ true);
// Verify that handwriting started for sTestView1.
verify(mHandwritingInitiator, times(1)).startHandwriting(mTestView1);
MotionEvent hoverEvent1 = createStylusHoverEvent(sHwArea1.centerX(), sHwArea1.centerY());
PointerIcon icon1 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
// After handwriting is initiated for the connected view, hide the hover icon.
assertThat(icon1).isNull();
// Simulate that focus is switched to mTestView2 first and then switched back.
mHandwritingInitiator.onInputConnectionCreated(mTestView2);
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
PointerIcon icon2 = mHandwritingInitiator.onResolvePointerIcon(mContext, hoverEvent1);
// After the change of focus, hover icon shows again.
assertThat(icon2.getType()).isEqualTo(PointerIcon.TYPE_HANDWRITING);
}
@Test
public void autoHandwriting_whenDisabled_wontStartHW() {
View mockView = createView(sHwArea1, false /* autoHandwritingEnabled */,
true /* isStylusHandwritingAvailable */);
mHandwritingInitiator.onInputConnectionCreated(mockView);
final int x1 = (sHwArea1.left + sHwArea1.right) / 2;
final int y1 = (sHwArea1.top + sHwArea1.bottom) / 2;
MotionEvent stylusEvent1 = createStylusEvent(ACTION_DOWN, x1, y1, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent1);
final int x2 = x1 + mHandwritingSlop * 2;
final int y2 = y1;
MotionEvent stylusEvent2 = createStylusEvent(ACTION_MOVE, x2, y2, 0);
mHandwritingInitiator.onTouchEvent(stylusEvent2);
verify(mHandwritingInitiator, never()).startHandwriting(mTestView1);
}
@Test
public void onInputConnectionCreated() {
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
assertThat(mHandwritingInitiator.mConnectedView).isNotNull();
assertThat(mHandwritingInitiator.mConnectedView.get()).isEqualTo(mTestView1);
}
@Test
public void onInputConnectionCreated_whenAutoHandwritingIsDisabled() {
View view = new View(mContext);
view.setAutoHandwritingEnabled(false);
assertThat(view.isAutoHandwritingEnabled()).isFalse();
mHandwritingInitiator.onInputConnectionCreated(view);
assertThat(mHandwritingInitiator.mConnectedView).isNull();
}
@Test
public void onInputConnectionClosed() {
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
mHandwritingInitiator.onInputConnectionClosed(mTestView1);
assertThat(mHandwritingInitiator.mConnectedView).isNull();
}
@Test
public void onInputConnectionClosed_whenAutoHandwritingIsDisabled() {
View view = new View(mContext);
view.setAutoHandwritingEnabled(false);
mHandwritingInitiator.onInputConnectionCreated(view);
mHandwritingInitiator.onInputConnectionClosed(view);
assertThat(mHandwritingInitiator.mConnectedView).isNull();
}
@Test
public void onInputConnectionCreated_inputConnectionRestarted() {
// When IMM restarts input connection, View#onInputConnectionCreatedInternal might be
// called before View#onInputConnectionClosedInternal. As a result, we need to handle the
// case where "one view "2 InputConnections".
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
mHandwritingInitiator.onInputConnectionCreated(mTestView1);
mHandwritingInitiator.onInputConnectionClosed(mTestView1);
assertThat(mHandwritingInitiator.mConnectedView).isNotNull();
assertThat(mHandwritingInitiator.mConnectedView.get()).isEqualTo(mTestView1);
}
@Test
public void startHandwriting_hidesHint() {
EditText editText =
new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
editText.setHint("hint");
editText.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
verifyEditTextDrawsText(editText, "hint");
mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
mHandwritingInitiator.startHandwriting(editText);
verifyEditTextDrawsText(editText, null);
}
@Test
public void startHandwriting_clearFocus_restoresHint() {
EditText editText =
new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
editText.setHint("hint");
editText.setLayoutParams(new ViewGroup.LayoutParams(1024, 1024));
editText.requestFocus();
verifyEditTextDrawsText(editText, "hint");
mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
mHandwritingInitiator.startHandwriting(editText);
verifyEditTextDrawsText(editText, null);
editText.clearFocus();
verifyEditTextDrawsText(editText, "hint");
}
@Test
public void startHandwriting_setHint_restoresHint() {
EditText editText =
new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
editText.setHint("hint");
editText.setLayoutParams(new ViewGroup.LayoutParams(1024, 1024));
verifyEditTextDrawsText(editText, "hint");
mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
mHandwritingInitiator.startHandwriting(editText);
verifyEditTextDrawsText(editText, null);
editText.setHint("new hint");
verifyEditTextDrawsText(editText, "new hint");
}
@Test
public void startHandwriting_setText_restoresHint() {
EditText editText =
new EditText(InstrumentationRegistry.getInstrumentation().getTargetContext());
editText.setHint("hint");
editText.setLayoutParams(new ViewGroup.LayoutParams(1024, 1024));
verifyEditTextDrawsText(editText, "hint");
mHandwritingInitiator.onTouchEvent(createStylusEvent(ACTION_DOWN, 0, 0, 0));
mHandwritingInitiator.startHandwriting(editText);
verifyEditTextDrawsText(editText, null);
editText.setText("a");
editText.setText("");
verifyEditTextDrawsText(editText, "hint");
}
private void verifyEditTextDrawsText(EditText editText, String text) {
editText.measure(
View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(1024, View.MeasureSpec.AT_MOST));
Canvas canvas = prepareMockCanvas(editText);
editText.draw(canvas);
if (text != null) {
ArgumentCaptor<CharSequence> textCaptor = ArgumentCaptor.forClass(CharSequence.class);
verify(canvas).drawText(
textCaptor.capture(), anyInt(), anyInt(), anyFloat(), anyFloat(), any());
assertThat(textCaptor.getValue().toString()).isEqualTo(text);
} else {
verify(canvas, never()).drawText(
any(CharSequence.class), anyInt(), anyInt(), anyFloat(), anyFloat(), any());
}
}
private Canvas prepareMockCanvas(View view) {
Canvas canvas = mock(Canvas.class);
when(canvas.getClipBounds(any())).thenAnswer(invocation -> {
Rect outRect = invocation.getArgument(0);
outRect.top = 0;
outRect.left = 0;
outRect.right = view.getMeasuredWidth();
outRect.bottom = view.getMeasuredHeight();
return true;
});
return canvas;
}
/**
* Inject {@link MotionEvent}s to the {@link HandwritingInitiator}.
* @param x the x coordinate of the first {@link MotionEvent}.
* @param y the y coordinate of the first {@link MotionEvent}.
* @param exceedsHWSlop whether the injected {@link MotionEvent} movements exceed the
* handwriting slop. If true, it simulates handwriting. Otherwise, it
* simulates a tap/click,
*/
private void injectStylusEvent(HandwritingInitiator handwritingInitiator, int x, int y,
boolean exceedsHWSlop) {
MotionEvent event1 = createStylusEvent(ACTION_DOWN, x, y, 0);
if (exceedsHWSlop) {
x += mHandwritingSlop * 2;
} else {
x += mHandwritingSlop / 2;
}
MotionEvent event2 = createStylusEvent(ACTION_MOVE, x, y, 0);
MotionEvent event3 = createStylusEvent(ACTION_UP, x, y, 0);
handwritingInitiator.onTouchEvent(event1);
handwritingInitiator.onTouchEvent(event2);
handwritingInitiator.onTouchEvent(event3);
}
private MotionEvent createStylusHoverEvent(int x, int y) {
return createStylusEvent(ACTION_HOVER_MOVE, x, y, /* eventTime */ 0);
}
private MotionEvent createStylusEvent(int action, int x, int y, long eventTime) {
MotionEvent.PointerProperties[] properties = MotionEvent.PointerProperties.createArray(1);
properties[0].toolType = MotionEvent.TOOL_TYPE_STYLUS;
MotionEvent.PointerCoords[] coords = MotionEvent.PointerCoords.createArray(1);
coords[0].x = x;
coords[0].y = y;
return MotionEvent.obtain(0 /* downTime */, eventTime /* eventTime */, action, 1,
properties, coords, 0 /* metaState */, 0 /* buttonState */, 1 /* xPrecision */,
1 /* yPrecision */, 0 /* deviceId */, 0 /* edgeFlags */,
InputDevice.SOURCE_STYLUS, 0 /* flags */);
}
}