/*
 * Copyright (C) 2008 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.text.method.cts;

import dalvik.annotation.KnownFailure;
import dalvik.annotation.TestLevel;
import dalvik.annotation.TestTargetClass;
import dalvik.annotation.TestTargetNew;
import dalvik.annotation.TestTargets;
import dalvik.annotation.ToBeFixed;

import android.os.SystemClock;
import android.test.ActivityInstrumentationTestCase2;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.method.MovementMethod;
import android.text.method.ScrollingMovementMethod;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.TextView;
import android.widget.TextView.BufferType;
import android.widget.cts.WidgetTestUtils;

/**
 * Test {@link ScrollingMovementMethod}. The class is an implementation of interface
 * {@link MovementMethod}. The typical usage of {@link MovementMethod} is tested in
 * {@link android.widget.cts.TextViewTest} and this test case is only focused on the
 * implementation of the methods.
 *
 * @see android.widget.cts.TextViewTest
 */
@TestTargetClass(ScrollingMovementMethod.class)
public class ScrollingMovementMethodTest extends ActivityInstrumentationTestCase2<StubActivity> {
    private static final int LITTLE_SPACE = 20;

    private static final String THREE_LINES_TEXT = "first line\nsecond line\nlast line";

    private TextView mTextView;

    private Spannable mSpannable;

    private int mScaledTouchSlop;

    public ScrollingMovementMethodTest() {
        super("com.android.cts.stub", StubActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mTextView = new TextView(getActivity());
        mTextView.setText(THREE_LINES_TEXT, BufferType.EDITABLE);
        mSpannable = (Spannable) mTextView.getText();
        mScaledTouchSlop = ViewConfiguration.get(getActivity()).getScaledTouchSlop();
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        method = "ScrollingMovementMethod",
        args = {}
    )
    public void testConstructor() {
        new ScrollingMovementMethod();
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        method = "getInstance",
        args = {}
    )
    public void testGetInstance() {
        MovementMethod method0 = ScrollingMovementMethod.getInstance();
        assertTrue(method0 instanceof ScrollingMovementMethod);

        MovementMethod method1 = ScrollingMovementMethod.getInstance();
        assertSame(method0, method1);
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "Test the method wtih horizontal motion event.",
        method = "onTouchEvent",
        args = {TextView.class, Spannable.class, MotionEvent.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. There is no "
            + "document about the behaviour of this method.")
    public void testOnTouchEventHorizontalMotion() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        final ScrollingMovementMethod method = new ScrollingMovementMethod();
        runActionOnUiThread(new Runnable() {
            public void run() {
                mTextView.setText("hello world", BufferType.SPANNABLE);
                mTextView.setSingleLine();
                mSpannable = (Spannable) mTextView.getText();
                int width = WidgetTestUtils.convertDipToPixels(getActivity(), LITTLE_SPACE);
                getActivity().setContentView(mTextView,
                        new ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT));
            }
        });
        assertNotNull(mTextView.getLayout());

        float rightMost = mTextView.getLayout().getLineRight(0) - mTextView.getWidth()
                + mTextView.getTotalPaddingLeft() + mTextView.getTotalPaddingRight();
        int leftMost = mTextView.getScrollX();

        final long now = SystemClock.uptimeMillis();
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // press
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 0, 0, 0));
            }
        }));

        final int tinyDist = -(mScaledTouchSlop - 1);
        int previousScrollX = mTextView.getScrollX();
        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move for short distance
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, tinyDist, 0, 0));
            }
        }));
        assertEquals(previousScrollX, mTextView.getScrollX());

        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // release
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, tinyDist, 0, 0));
            }
        }));

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // press
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 0, 0, 0));
            }
        }));

        final int distFar = -mScaledTouchSlop;
        previousScrollX = mTextView.getScrollX();
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move for enough distance
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, distFar, 0, 0));
            }
        }));
        assertTrue(mTextView.getScrollX() > previousScrollX);
        assertTrue(mTextView.getScrollX() < rightMost);

        previousScrollX = mTextView.getScrollX();
        final int distTooFar = (int) (-rightMost * 10);
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move for long distance
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, distTooFar, 0, 0));
            }
        }));
        assertTrue(mTextView.getScrollX() > previousScrollX);
        assertEquals(rightMost, mTextView.getScrollX(), 0f);

        previousScrollX = mTextView.getScrollX();
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move back
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, 0, 0, 0));
            }
        }));
        assertTrue(mTextView.getScrollX() < previousScrollX);
        assertEquals(leftMost, mTextView.getScrollX());

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // release
                mResult = method.onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0, 0, 0));
            }
        }));
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "Test the method wtih vertical motion event.",
        method = "onTouchEvent",
        args = {TextView.class, Spannable.class, MotionEvent.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. There is no "
            + "document about the behaviour of this method.")
    public void testOnTouchEventVerticalMotion() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        final ScrollingMovementMethod method = new ScrollingMovementMethod();
        runActionOnUiThread(new Runnable() {
            public void run() {
                mTextView.setLines(1);
                getActivity().setContentView(mTextView,
                        new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
            }
        });
        assertNotNull(mTextView.getLayout());

        float bottom = mTextView.getLayout().getHeight() - mTextView.getHeight()
                + mTextView.getTotalPaddingTop() + mTextView.getTotalPaddingBottom();
        int top = mTextView.getScrollY();

        final long now = SystemClock.uptimeMillis();
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // press
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_DOWN, 0, 0, 0));
            }
        }));

        final int tinyDist = -(mScaledTouchSlop - 1);
        int previousScrollY = mTextView.getScrollY();
        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move for short distance
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_MOVE, 0, tinyDist, 0));
            }
        }));
        assertEquals(previousScrollY, mTextView.getScrollY());

        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // release
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_UP, 0, tinyDist, 0));
            }
        }));

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // press
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_DOWN, 0, 0, 0));
            }
        }));

        final int distFar = -mScaledTouchSlop;
        previousScrollY = mTextView.getScrollY();
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move for enough distance
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_MOVE, 0, distFar, 0));
            }
        }));
        assertTrue(mTextView.getScrollY() > previousScrollY);
        assertTrue(mTextView.getScrollX() < bottom);

        previousScrollY = mTextView.getScrollY();
        final int distTooFar = (int) (-bottom * 10);
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move for long distance
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_MOVE, 0, distTooFar, 0));
            }
        }));
        assertTrue(mTextView.getScrollY() > previousScrollY);
        assertEquals(bottom, mTextView.getScrollY(), 0f);

        previousScrollY = mTextView.getScrollY();
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // move back
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_MOVE, 0, 0, 0));
            }
        }));
        assertTrue(mTextView.getScrollY() < previousScrollY);
        assertEquals(top, mTextView.getScrollX());

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                // release
                mResult = method.onTouchEvent(mTextView, mSpannable, MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_UP, 0, 0, 0));
            }
        }));
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "Test the method with null parameters.",
        method = "onTouchEvent",
        args = {TextView.class, Spannable.class, MotionEvent.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. @throws clause "
            + "should be added into javadoc of "
            + "ScrollingMovementMethod#onTouchEvent(TextView, Spannable, MotionEvent) "
            + "when the param widget, buffer or text is null")
    public void testOnTouchEventExceptional() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        runActionOnUiThread(new Runnable() {
            public void run() {
                int width = WidgetTestUtils.convertDipToPixels(getActivity(), LITTLE_SPACE);
                getActivity().setContentView(mTextView,
                        new ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT));
            }
        });
        assertNotNull(mTextView.getLayout());

        runActionOnUiThread(new Runnable() {
            public void run() {
                try {
                    new ScrollingMovementMethod().onTouchEvent(mTextView, mSpannable, null);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                long now = SystemClock.uptimeMillis();
                try {
                    new ScrollingMovementMethod().onTouchEvent(mTextView, null,
                            MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 0, 0, 0));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    new ScrollingMovementMethod().onTouchEvent(null, mSpannable,
                            MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 0, 0, 0));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                new ScrollingMovementMethod().onTouchEvent(mTextView, mSpannable,
                        MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 0, 0, 0));
                try {
                    new ScrollingMovementMethod().onTouchEvent(null, mSpannable,
                            MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, - 10000, 0, 0));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    new ScrollingMovementMethod().onTouchEvent(mTextView, null,
                            MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, - 10000, 0, 0));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    new ScrollingMovementMethod().onTouchEvent(null, mSpannable,
                            MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, - 10000, 0, 0));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    new ScrollingMovementMethod().onTouchEvent(mTextView, null,
                            MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, - 10000, 0, 0));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }
            }
        });
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "It always returns false.",
        method = "canSelectArbitrarily",
        args = {}
    )
    public void testCanSelectArbitrarily() {
        assertFalse(new ScrollingMovementMethod().canSelectArbitrarily());
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "Test {@link ScrollingMovementMethod#onKeyDown(TextView, Spannable, int, "
                + "KeyEvent)}. The params event and buffer are never read.",
        method = "onKeyDown",
        args = {TextView.class, Spannable.class, int.class, KeyEvent.class}
    )
    public void testOnKeyDownVerticalMovement() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        runActionOnUiThread(new Runnable() {
            public void run() {
                getActivity().setContentView(mTextView);
            }
        });
        assertNotNull(mTextView.getLayout());

        assertVisibleLineInTextView(0);
        final MyScrollingMovementMethod method = new MyScrollingMovementMethod();
        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onKeyDown(mTextView, null, KeyEvent.KEYCODE_DPAD_DOWN,
                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN));
            }
        });
        assertVisibleLineInTextView(1);

        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onKeyDown(mTextView, null, KeyEvent.KEYCODE_DPAD_UP,
                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP));
            }
        });
        assertVisibleLineInTextView(0);
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "The params event and buffer are never read.",
        method = "onKeyDown",
        args = {TextView.class, Spannable.class, int.class, KeyEvent.class}
    )
    public void testOnKeyDownHorizontalMovement() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        runActionOnUiThread(new Runnable() {
            public void run() {
                mTextView.setText("short");
                mTextView.setSingleLine();
                int width = WidgetTestUtils.convertDipToPixels(getActivity(), LITTLE_SPACE);
                getActivity().setContentView(mTextView,
                        new ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT));
            }
        });
        assertNotNull(mTextView.getLayout());

        final MyScrollingMovementMethod method = new MyScrollingMovementMethod();
        int previousScrollX = mTextView.getScrollX();
        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onKeyDown(mTextView, null, KeyEvent.KEYCODE_DPAD_RIGHT,
                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT));
            }
        });
        assertTrue(mTextView.getScrollX() > previousScrollX);

        previousScrollX = mTextView.getScrollX();
        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onKeyDown(mTextView, null, KeyEvent.KEYCODE_DPAD_LEFT,
                        new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT));
            }
        });
        assertTrue(mTextView.getScrollX() < previousScrollX);

        previousScrollX = mTextView.getScrollX();
        assertVisibleLineInTextView(0);
        runActionOnUiThread(new Runnable() {
            public void run() {
                assertFalse(method.onKeyDown(mTextView, mSpannable, 0, null));
            }
        });
        assertEquals(previousScrollX, mTextView.getScrollX());
        assertVisibleLineInTextView(0);
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "The params event and buffer are never read.",
        method = "onKeyDown",
        args = {TextView.class, Spannable.class, int.class, KeyEvent.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. @throws clause "
            + "should be added into javadoc of "
            + "ScrollingMovementMethod#onKeyDown(TextView, Spannable, int, KeyEvent) "
            + "when the param widget, buffer or text is null")
    public void testOnKeyDownExceptions() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        runActionOnUiThread(new Runnable() {
            public void run() {
                getActivity().setContentView(mTextView);
            }
        });
        assertNotNull(mTextView.getLayout());

        final MyScrollingMovementMethod method = new MyScrollingMovementMethod();
        runActionOnUiThread(new Runnable() {
            public void run() {
                try {
                    method.onKeyDown(null, mSpannable, KeyEvent.KEYCODE_DPAD_RIGHT,
                            new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    method.onKeyDown(mTextView, null, KeyEvent.KEYCODE_DPAD_RIGHT,
                            new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP));
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    method.onKeyDown(mTextView, mSpannable, KeyEvent.KEYCODE_DPAD_RIGHT, null);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }
            }
        });
    }

    @TestTargets({
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            notes = "The parameter buffer is never read.",
            method = "down",
            args = {android.widget.TextView.class, android.text.Spannable.class}
        ),
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            notes = "The parameter buffer is never read.",
            method = "up",
            args = {android.widget.TextView.class, android.text.Spannable.class}
        )
    })
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. @throws clause "
            + "should be added into javadoc of "
            + "ScrollingMovementMethod#down(TextView, Spannable) and "
            + "ScrollingMovementMethod#up(TextView, Spannable)"
            + "when the param widget or buffer is null")
    public void testVerticalMovement() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        final MyScrollingMovementMethod method = new MyScrollingMovementMethod();
        runActionOnUiThread(new Runnable() {
            public void run() {
                mTextView.setLines(1);
                getActivity().setContentView(mTextView,
                        new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
            }
        });
        assertNotNull(mTextView.getLayout());

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.down(mTextView, mSpannable);
            }
        }));
        assertVisibleLineInTextView(1);

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.down(mTextView, mSpannable);
            }
        }));
        assertVisibleLineInTextView(2);

        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.down(mTextView, mSpannable);
            }
        }));
        assertVisibleLineInTextView(2);

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.up(mTextView, mSpannable);
            }
        }));
        assertVisibleLineInTextView(1);

        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.up(mTextView, mSpannable);
            }
        }));
        assertVisibleLineInTextView(0);

        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.up(mTextView, mSpannable);
            }
        }));
        assertVisibleLineInTextView(0);

        runActionOnUiThread(new Runnable() {
            public void run() {
                try {
                    method.up(null, mSpannable);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    method.up(mTextView, null);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    method.down(null, mSpannable);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    method.down(mTextView, null);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }
            }
        });
    }

    @TestTargets({
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            method = "onKeyDown",
            args = {TextView.class, Spannable.class, int.class, KeyEvent.class}
        ),
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            method = "onTouchEvent",
            args = {TextView.class, Spannable.class, MotionEvent.class}
        ),
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            method = "up",
            args = {TextView.class, Spannable.class}
        ),
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            method = "down",
            args = {TextView.class, Spannable.class}
        ),
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            method = "left",
            args = {TextView.class, Spannable.class}
        ),
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            method = "right",
            args = {TextView.class, Spannable.class}
        )
    })
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. @throws clause "
            + "should be added into javadoc of all these methods when the widget does not get "
            + "layout")
    public void testMovementWithNullLayout() {
        assertNull(mTextView.getLayout());
        try {
            new MyScrollingMovementMethod().down(mTextView, mSpannable);
        } catch (NullPointerException e) {
            // NPE is acceptable
        }

        try {
            new MyScrollingMovementMethod().up(mTextView, mSpannable);
        } catch (NullPointerException e) {
            // NPE is acceptable
        }

        try {
            new MyScrollingMovementMethod().left(mTextView, mSpannable);
        } catch (NullPointerException e) {
            // NPE is acceptable
        }

        try {
            new MyScrollingMovementMethod().right(mTextView, mSpannable);
        } catch (NullPointerException e) {
            // NPE is acceptable
        }

        long now = SystemClock.uptimeMillis();
        try {
            KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
            new ScrollingMovementMethod().onKeyDown(mTextView, mSpannable,
                    KeyEvent.KEYCODE_DPAD_RIGHT, event);
        } catch (NullPointerException e) {
            // NPE is acceptable
        }

        new ScrollingMovementMethod().onTouchEvent(mTextView, mSpannable,
                MotionEvent.obtain(now, now, MotionEvent.ACTION_DOWN, 0, 0, 0));
        try {
            new ScrollingMovementMethod().onTouchEvent(mTextView, mSpannable,
                    MotionEvent.obtain(now, now, MotionEvent.ACTION_MOVE, - 10000, 0, 0));
        } catch (NullPointerException e) {
            // NPE is acceptable
        }
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "This is a blank method.",
        method = "initialize",
        args = {TextView.class, Spannable.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete.")
    public void testInitialize() {
        new ScrollingMovementMethod().initialize(null, null);
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "This method always returns false.",
        method = "onTrackballEvent",
        args = {TextView.class, Spannable.class, MotionEvent.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete.")
    public void testOnTrackballEvent() {
        long now = SystemClock.uptimeMillis();
        MotionEvent event = MotionEvent.obtain(now, now, 0, 2, -2, 0);
        MyScrollingMovementMethod mockMethod = new MyScrollingMovementMethod();

        assertFalse(mockMethod.onTrackballEvent(mTextView, mSpannable, event));
        assertFalse(mockMethod.onTrackballEvent(null, mSpannable, event));
        assertFalse(mockMethod.onTrackballEvent(mTextView, mSpannable, null));
        assertFalse(mockMethod.onTrackballEvent(mTextView, null, event));
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "It always returns false.",
        method = "onKeyUp",
        args = {TextView.class, Spannable.class, int.class, KeyEvent.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete.")
    public void testOnKeyUp() {
        ScrollingMovementMethod method = new ScrollingMovementMethod();
        SpannableString spannable = new SpannableString("Test Content");
        KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_0);
        TextView view = new TextView(getActivity());

        assertFalse(method.onKeyUp(view, spannable, KeyEvent.KEYCODE_0, event));
        assertFalse(method.onKeyUp(null, null, 0, null));
        assertFalse(method.onKeyUp(null, spannable, KeyEvent.KEYCODE_0, event));
        assertFalse(method.onKeyUp(view, null, KeyEvent.KEYCODE_0, event));
        assertFalse(method.onKeyUp(view, spannable, 0, event));
        assertFalse(method.onKeyUp(view, spannable, KeyEvent.KEYCODE_0, null));
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        notes = "The parameter text is never read.",
        method = "onTakeFocus",
        args = {TextView.class, Spannable.class, int.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. @throws clause "
            + "should be added into javadoc of "
            + "ScrollingMovementMethod#onTakeFocus(TextView, Spannable, int) "
            + "when the param text is null")
    public void testOnTakeFocus() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        final ScrollingMovementMethod method = new ScrollingMovementMethod();
        // wait until the text view gets layout
        assertNull(mTextView.getLayout());
        try {
            method.onTakeFocus(mTextView, mSpannable, View.FOCUS_BACKWARD);
        } catch (NullPointerException e) {
            // NPE is acceptable
        }

        runActionOnUiThread(new Runnable() {
            public void run() {
                int height = WidgetTestUtils.convertDipToPixels(getActivity(), LITTLE_SPACE);
                getActivity().setContentView(mTextView,
                        new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                                height));
            }
        });
        Layout layout = mTextView.getLayout();
        assertNotNull(layout);

        int previousScrollY = mTextView.getScrollY();
        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onTakeFocus(mTextView, mSpannable, View.FOCUS_BACKWARD);
            }
        });
        assertTrue(mTextView.getScrollY() >= previousScrollY);
        assertVisibleLineInTextView(2);

        previousScrollY = mTextView.getScrollY();
        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onTakeFocus(mTextView, mSpannable, View.FOCUS_FORWARD);
            }
        });
        assertTrue(mTextView.getScrollY() <= previousScrollY);
        assertVisibleLineInTextView(0);

        runActionOnUiThread(new Runnable() {
            public void run() {
                try {
                    method.onTakeFocus(null, mSpannable, View.FOCUS_FORWARD);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }

                try {
                    method.onTakeFocus(mTextView, null, View.FOCUS_FORWARD);
                } catch (NullPointerException e) {
                    // NPE is acceptable
                }
            }
        });
    }

    @TestTargets({
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            notes = "The parameter buffer is never read.",
            method = "left",
            args = {TextView.class, Spannable.class}
        ),
        @TestTargetNew(
            level = TestLevel.COMPLETE,
            notes = "The parameter buffer is never read.",
            method = "right",
            args = {TextView.class, Spannable.class}
        )
    })
    public void testHorizontalMovement() throws Throwable {
        /*
         * All these assertions depends on whether the TextView has a layout.The text view will not
         * get layout in setContent method but in otherhandler's function. Assertion which is
         * following the setContent will not get the expecting result. It have to wait all the
         * handlers' operations on the UiTread to finish. So all these cases are divided into
         * several steps,setting the content at first, waiting the layout, and checking the
         * assertion at last.
         */
        final MyScrollingMovementMethod method = new MyScrollingMovementMethod();
        runActionOnUiThread(new Runnable() {
            public void run() {
                mTextView.setText("short");
                mTextView.setSingleLine();
                int width = WidgetTestUtils.convertDipToPixels(getActivity(), LITTLE_SPACE);
                getActivity().setContentView(mTextView,
                        new ViewGroup.LayoutParams(width, ViewGroup.LayoutParams.WRAP_CONTENT));
            }
        });
        assertNotNull(mTextView.getLayout());

        int previousScrollX = mTextView.getScrollX();
        assertTrue(getActionResult(new ActionRunnerWithResult() {

            public void run() {
                mResult = method.right(mTextView, mSpannable);
            }
        }));
        assertTrue(mTextView.getScrollX() > previousScrollX);

        previousScrollX = mTextView.getScrollX();
        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.right(mTextView, mSpannable);
            }
        }));
        assertEquals(previousScrollX, mTextView.getScrollX());

        previousScrollX = mTextView.getScrollX();
        assertTrue(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.left(mTextView, mSpannable);
            }
        }));
        assertTrue(mTextView.getScrollX() < previousScrollX);

        previousScrollX = mTextView.getScrollX();
        assertFalse(getActionResult(new ActionRunnerWithResult() {
            public void run() {
                mResult = method.left(mTextView, mSpannable);
            }
        }));
        assertEquals(previousScrollX, mTextView.getScrollX());
    }

    @TestTargetNew(
        level = TestLevel.COMPLETE,
        method = "onKeyOther",
        args = {TextView.class, Spannable.class, KeyEvent.class}
    )
    @ToBeFixed(bug = "1695243", explanation = "Android API javadocs are incomplete. There is no "
            + "document about the behaviour of this method.")
    public void testOnKeyOther() throws Throwable {
        runActionOnUiThread(new Runnable() {
            public void run() {
                getActivity().setContentView(mTextView);
            }
        });
        assertNotNull(mTextView.getLayout());

        assertVisibleLineInTextView(0);
        final MyScrollingMovementMethod method = new MyScrollingMovementMethod();
        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onKeyOther(mTextView, null,
                        new KeyEvent(0, 0, KeyEvent.ACTION_MULTIPLE,
                                KeyEvent.KEYCODE_DPAD_DOWN, 2));
            }
        });
        assertVisibleLineInTextView(1);

        runActionOnUiThread(new Runnable() {
            public void run() {
                method.onKeyOther(mTextView, null,
                        new KeyEvent(0, 0, KeyEvent.ACTION_MULTIPLE,
                                KeyEvent.KEYCODE_DPAD_UP, 2));
            }
        });
        assertVisibleLineInTextView(0);
    }

    private void assertVisibleLineInTextView(int line) {
        Layout layout = mTextView.getLayout();
        int scrollY = mTextView.getScrollY();
        int padding = mTextView.getTotalPaddingTop() + mTextView.getTotalPaddingBottom();
        assertTrue(layout.getLineForVertical(scrollY) <= line);
        assertTrue(layout.getLineForVertical(scrollY + mTextView.getHeight() - padding) >= line);
    }

    private boolean getActionResult(ActionRunnerWithResult actionRunner) throws Throwable {
        runActionOnUiThread(actionRunner);
        return actionRunner.getResult();
    }

    private void runActionOnUiThread(Runnable actionRunner) throws Throwable {
        runTestOnUiThread(actionRunner);
        getInstrumentation().waitForIdleSync();
    }

    private static abstract class ActionRunnerWithResult implements Runnable {
        protected boolean mResult = false;

        public boolean getResult() {
            return mResult;
        }
    }

    private static class MyScrollingMovementMethod extends ScrollingMovementMethod {
        @Override
        protected boolean down(TextView widget, Spannable buffer) {
            return super.down(widget, buffer);
        }

        @Override
        protected boolean left(TextView widget, Spannable buffer) {
            return super.left(widget, buffer);
        }

        @Override
        protected boolean right(TextView widget, Spannable buffer) {
            return super.right(widget, buffer);
        }

        @Override
        protected boolean up(TextView widget, Spannable buffer) {
            return super.up(widget, buffer);
        }
    }
}
