167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown/*
267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * Copyright (C) 2010 The Android Open Source Project
367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown *
467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * Licensed under the Apache License, Version 2.0 (the "License");
567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * you may not use this file except in compliance with the License.
667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * You may obtain a copy of the License at
767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown *
867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown *      http://www.apache.org/licenses/LICENSE-2.0
967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown *
1067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * Unless required by applicable law or agreed to in writing, software
1167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * distributed under the License is distributed on an "AS IS" BASIS,
1267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * See the License for the specific language governing permissions and
1467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * limitations under the License.
1567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown */
1667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
1767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brownpackage android.text.method;
1867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
1967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brownimport android.text.Layout;
2067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brownimport android.text.Spannable;
218f34567c71003505456a9b1a0d461a4e62883d70Jeff Brownimport android.view.InputDevice;
2267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brownimport android.view.KeyEvent;
2367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brownimport android.view.MotionEvent;
2467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brownimport android.widget.TextView;
2567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
2667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown/**
2767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown * Base classes for movement methods.
2867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown */
2967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brownpublic class BaseMovementMethod implements MovementMethod {
3067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
3167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public boolean canSelectArbitrarily() {
3267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
3367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
3467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
3567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
3667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public void initialize(TextView widget, Spannable text) {
3767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
3867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
3967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
4067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) {
4167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        final int movementMetaState = getMovementMetaState(text, event);
4267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event);
4367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        if (handled) {
4467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            MetaKeyKeyListener.adjustMetaAfterKeypress(text);
4567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            MetaKeyKeyListener.resetLockedMeta(text);
4667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        }
4767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return handled;
4867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
4967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
5067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
5167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) {
5267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        final int movementMetaState = getMovementMetaState(text, event);
5367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        final int keyCode = event.getKeyCode();
5467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        if (keyCode != KeyEvent.KEYCODE_UNKNOWN
5567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
5667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            final int repeat = event.getRepeatCount();
5767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            boolean handled = false;
5867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            for (int i = 0; i < repeat; i++) {
5967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) {
6067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    break;
6167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
6267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                handled = true;
6367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            }
6467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            if (handled) {
6567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                MetaKeyKeyListener.adjustMetaAfterKeypress(text);
6667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                MetaKeyKeyListener.resetLockedMeta(text);
6767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            }
6867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            return handled;
6967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        }
7067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
7167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
7267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
7367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
7467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
7567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
7667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
7767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
7867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
7967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public void onTakeFocus(TextView widget, Spannable text, int direction) {
8067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
8167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
8267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
8367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) {
8467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
8567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
8667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
8767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    @Override
8867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
8967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
9067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
9167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
928f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    @Override
938f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) {
948f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
958f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            switch (event.getAction()) {
968f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                case MotionEvent.ACTION_SCROLL: {
978f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    final float vscroll;
988f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    final float hscroll;
998f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
1008f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        vscroll = 0;
1018f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1028f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    } else {
1038f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
1048f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
1058f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    }
1068f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
1078f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    boolean handled = false;
1088f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    if (hscroll < 0) {
1098f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll));
1108f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    } else if (hscroll > 0) {
1118f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        handled |= scrollRight(widget, text, (int)Math.ceil(hscroll));
1128f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    }
1138f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    if (vscroll < 0) {
1148f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll));
1158f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    } else if (vscroll > 0) {
1168f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                        handled |= scrollDown(widget, text, (int)Math.ceil(vscroll));
1178f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    }
1188f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    return handled;
1198f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                }
1208f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            }
1218f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
1228f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
1238f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
1248f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
12567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
12667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Gets the meta state used for movement using the modifiers tracked by the text
12767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * buffer as well as those present in the key event.
12867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
12967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * The movement meta state excludes the state of locked modifiers or the SHIFT key
13067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * since they are not used by movement actions (but they may be used for selection).
13167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
13267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
13367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param event The key event.
13467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return The keyboard meta states used for movement.
13567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
13667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected int getMovementMetaState(Spannable buffer, KeyEvent event) {
13767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        // We ignore locked modifiers and SHIFT.
13814f10e5d5f51bc54ca2a45ee62d3cfb6debd3af0Raph Levien        int metaState = MetaKeyKeyListener.getMetaState(buffer, event)
13967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED);
14067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK;
14167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
14267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
14367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
14467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a movement key action.
14567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * The default implementation decodes the key down and invokes movement actions
14667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * such as {@link #down} and {@link #up}.
14767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once
14867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * to handle an {@link KeyEvent#ACTION_DOWN}.
14967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly
15067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}.
15167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
15267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
15367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
15467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param event The key event.
15567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param keyCode The key code.
15667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param movementMetaState The keyboard meta states used for movement.
15767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param event The key event.
15867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
15967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
16067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean handleMovementKey(TextView widget, Spannable buffer,
16167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            int keyCode, int movementMetaState, KeyEvent event) {
16267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        switch (keyCode) {
16367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_DPAD_LEFT:
16467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
16567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return left(widget, buffer);
16667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
167e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                        KeyEvent.META_CTRL_ON)) {
168e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                    return leftWord(widget, buffer);
169e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
17067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                        KeyEvent.META_ALT_ON)) {
17167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return lineStart(widget, buffer);
17267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
17367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
17467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
17567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_DPAD_RIGHT:
17667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
17767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return right(widget, buffer);
17867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
179e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                        KeyEvent.META_CTRL_ON)) {
180e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                    return rightWord(widget, buffer);
181e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
18267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                        KeyEvent.META_ALT_ON)) {
18367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return lineEnd(widget, buffer);
18467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
18567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
18667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
18767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_DPAD_UP:
18867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
18967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return up(widget, buffer);
19067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
19167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                        KeyEvent.META_ALT_ON)) {
19267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return top(widget, buffer);
19367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
19467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
19567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
19667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_DPAD_DOWN:
19767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
19867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return down(widget, buffer);
19967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
20067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                        KeyEvent.META_ALT_ON)) {
20167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return bottom(widget, buffer);
20267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
20367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
20467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
20567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_PAGE_UP:
20667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
20767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return pageUp(widget, buffer);
20867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
20967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                        KeyEvent.META_ALT_ON)) {
21067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return top(widget, buffer);
21167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
21267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
21367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
21467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_PAGE_DOWN:
21567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
21667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return pageDown(widget, buffer);
21767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
21867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                        KeyEvent.META_ALT_ON)) {
21967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return bottom(widget, buffer);
22067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
22167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
22267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
22367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_MOVE_HOME:
22467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
22567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return home(widget, buffer);
226e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
227e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                        KeyEvent.META_CTRL_ON)) {
228e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                    return top(widget, buffer);
22967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
23067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
23167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
23267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown            case KeyEvent.KEYCODE_MOVE_END:
23367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
23467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                    return end(widget, buffer);
235e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
236e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                        KeyEvent.META_CTRL_ON)) {
237e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey                    return bottom(widget, buffer);
23867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                }
23967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown                break;
24067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        }
24167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
24267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
24367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
24467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
24567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a left movement action.
24667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls left by one character.
24767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
24867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
24967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
25067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
25167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
25267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean left(TextView widget, Spannable buffer) {
25367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
25467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
25567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
25667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
25767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a right movement action.
25867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls right by one character.
25967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
26067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
26167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
26267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
26367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
26467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean right(TextView widget, Spannable buffer) {
26567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
26667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
26767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
26867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
26967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs an up movement action.
27067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls up by one line.
27167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
27267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
27367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
27467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
27567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
27667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean up(TextView widget, Spannable buffer) {
27767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
27867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
27967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
28067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
28167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a down movement action.
28267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls down by one line.
28367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
28467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
28567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
28667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
28767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
28867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean down(TextView widget, Spannable buffer) {
28967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
29067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
29167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
29267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
29367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a page-up movement action.
29467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls up by one page.
29567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
29667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
29767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
29867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
29967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
30067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean pageUp(TextView widget, Spannable buffer) {
30167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
30267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
30367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
30467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
30567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a page-down movement action.
30667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls down by one page.
30767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
30867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
30967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
31067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
31167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
31267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean pageDown(TextView widget, Spannable buffer) {
31367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
31467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
31567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
31667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
31767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a top movement action.
31867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls to the top of the buffer.
31967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
32067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
32167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
32267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
32367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
32467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean top(TextView widget, Spannable buffer) {
32567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
32667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
32767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
32867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
32967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a bottom movement action.
33067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls to the bottom of the buffer.
33167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
33267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
33367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
33467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
33567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
33667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean bottom(TextView widget, Spannable buffer) {
33767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
33867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
33967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
34067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
34167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a line-start movement action.
34267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls to the start of the line.
34367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
34467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
34567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
34667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
34767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
34867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean lineStart(TextView widget, Spannable buffer) {
34967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
35067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
35167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
35267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
353f76a50ce8fdc6aea22cabc77b2977a1a15a79630Ken Wakasa     * Performs a line-end movement action.
35467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls to the end of the line.
35567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
35667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
35767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
35867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
35967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
36067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean lineEnd(TextView widget, Spannable buffer) {
36167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
36267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
36367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
364e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey    /** {@hide} */
365e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey    protected boolean leftWord(TextView widget, Spannable buffer) {
366e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey        return false;
367e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey    }
368e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey
369e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey    /** {@hide} */
370e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey    protected boolean rightWord(TextView widget, Spannable buffer) {
371e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey        return false;
372e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey    }
373e982dfc1bae36620f67371efc7b0a0f8adc9450dJeff Sharkey
37467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
37567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs a home movement action.
37667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls to the start of the line or to the top of the
37767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * document depending on whether the insertion point is being moved or
37867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * the document is being scrolled.
37967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
38067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
38167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
38267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
38367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
38467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean home(TextView widget, Spannable buffer) {
38567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
38667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
38767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown
38867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    /**
38967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Performs an end movement action.
39067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * Moves the cursor or scrolls to the start of the line or to the top of the
39167b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * document depending on whether the insertion point is being moved or
39267b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * the document is being scrolled.
39367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     *
39467b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param widget The text view.
39567b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @param buffer The text buffer.
39667b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     * @return True if the event was handled.
39767b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown     */
39867b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    protected boolean end(TextView widget, Spannable buffer) {
39967b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown        return false;
40067b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown    }
4018f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4028f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    private int getTopLine(TextView widget) {
4038f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return widget.getLayout().getLineForVertical(widget.getScrollY());
4048f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4058f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4068f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    private int getBottomLine(TextView widget) {
4078f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget));
4088f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4098f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4108f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    private int getInnerWidth(TextView widget) {
4118f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight();
4128f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4138f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4148f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    private int getInnerHeight(TextView widget) {
4158f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom();
4168f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4178f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4188f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    private int getCharacterWidth(TextView widget) {
4198f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return (int) Math.ceil(widget.getPaint().getFontSpacing());
4208f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4218f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4228f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    private int getScrollBoundsLeft(TextView widget) {
4238f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
4248f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int topLine = getTopLine(widget);
4258f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int bottomLine = getBottomLine(widget);
4268f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (topLine > bottomLine) {
4278f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return 0;
4288f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
4298f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int left = Integer.MAX_VALUE;
4308f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        for (int line = topLine; line <= bottomLine; line++) {
4318f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            final int lineLeft = (int) Math.floor(layout.getLineLeft(line));
4328f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            if (lineLeft < left) {
4338f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                left = lineLeft;
4348f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            }
4358f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
4368f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return left;
4378f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4388f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4398f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    private int getScrollBoundsRight(TextView widget) {
4408f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
4418f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int topLine = getTopLine(widget);
4428f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int bottomLine = getBottomLine(widget);
4438f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (topLine > bottomLine) {
4448f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return 0;
4458f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
4468f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int right = Integer.MIN_VALUE;
4478f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        for (int line = topLine; line <= bottomLine; line++) {
4488f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            final int lineRight = (int) Math.ceil(layout.getLineRight(line));
4498f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            if (lineRight > right) {
4508f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                right = lineRight;
4518f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            }
4528f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
4538f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return right;
4548f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4558f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4568f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
4578f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll left action.
4588f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls left by the specified number of characters.
4598f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
4608f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
4618f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
4628f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param amount The number of characters to scroll by.  Must be at least 1.
4638f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
4648f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
4658f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
4668f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) {
4678f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int minScrollX = getScrollBoundsLeft(widget);
4688f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int scrollX = widget.getScrollX();
4698f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (scrollX > minScrollX) {
4708f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX);
4718f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            widget.scrollTo(scrollX, widget.getScrollY());
4728f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
4738f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
4748f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
4758f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4768f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4778f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
4788f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll right action.
4798f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls right by the specified number of characters.
4808f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
4818f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
4828f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
4838f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param amount The number of characters to scroll by.  Must be at least 1.
4848f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
4858f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
4868f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
4878f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollRight(TextView widget, Spannable buffer, int amount) {
4888f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
4898f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int scrollX = widget.getScrollX();
4908f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (scrollX < maxScrollX) {
4918f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX);
4928f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            widget.scrollTo(scrollX, widget.getScrollY());
4938f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
4948f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
4958f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
4968f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
4978f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
4988f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
4998f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll up action.
5008f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls up by the specified number of lines.
5018f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
5028f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
5038f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
5048f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param amount The number of lines to scroll by.  Must be at least 1.
5058f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
5068f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
5078f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
5088f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollUp(TextView widget, Spannable buffer, int amount) {
5098f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
5108f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int top = widget.getScrollY();
5118f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int topLine = layout.getLineForVertical(top);
5128f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (layout.getLineTop(topLine) == top) {
5138f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            // If the top line is partially visible, bring it all the way
5148f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            // into view; otherwise, bring the previous line into view.
5158f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            topLine -= 1;
5168f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
5178f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (topLine >= 0) {
5188f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            topLine = Math.max(topLine - amount + 1, 0);
5198f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
5208f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
5218f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
5228f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
5238f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
5248f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
5258f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
5268f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll down action.
5278f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls down by the specified number of lines.
5288f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
5298f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
5308f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
5318f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param amount The number of lines to scroll by.  Must be at least 1.
5328f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
5338f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
5348f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
5358f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollDown(TextView widget, Spannable buffer, int amount) {
5368f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
5378f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int innerHeight = getInnerHeight(widget);
5388f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int bottom = widget.getScrollY() + innerHeight;
5398f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int bottomLine = layout.getLineForVertical(bottom);
5408f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (layout.getLineTop(bottomLine + 1) < bottom + 1) {
5418f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            // Less than a pixel of this line is out of view,
5428f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            // so we must have tried to make it entirely in view
5438f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            // and now want the next line to be in view instead.
5448f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            bottomLine += 1;
5458f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
5468f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int limit = layout.getLineCount() - 1;
5478f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (bottomLine <= limit) {
5488f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            bottomLine = Math.min(bottomLine + amount - 1, limit);
5498f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            Touch.scrollTo(widget, layout, widget.getScrollX(),
5508f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    layout.getLineTop(bottomLine + 1) - innerHeight);
5518f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
5528f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
5538f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
5548f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
5558f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
5568f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
5578f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll page up action.
5588f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls up by one page.
5598f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
5608f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
5618f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
5628f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
5638f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
5648f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
5658f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollPageUp(TextView widget, Spannable buffer) {
5668f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
5678f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int top = widget.getScrollY() - getInnerHeight(widget);
5688f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int topLine = layout.getLineForVertical(top);
5698f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (topLine >= 0) {
5708f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
5718f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
5728f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
5738f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
5748f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
5758f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
5768f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
5778f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll page up action.
5788f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls down by one page.
5798f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
5808f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
5818f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
5828f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
5838f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
5848f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
5858f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollPageDown(TextView widget, Spannable buffer) {
5868f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
5878f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int innerHeight = getInnerHeight(widget);
5888f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int bottom = widget.getScrollY() + innerHeight + innerHeight;
5898f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int bottomLine = layout.getLineForVertical(bottom);
5908f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (bottomLine <= layout.getLineCount() - 1) {
5918f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            Touch.scrollTo(widget, layout, widget.getScrollX(),
5928f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    layout.getLineTop(bottomLine + 1) - innerHeight);
5938f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
5948f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
5958f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
5968f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
5978f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
5988f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
5998f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll to top action.
6008f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls to the top of the document.
6018f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
6028f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
6038f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
6048f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
6058f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
6068f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
6078f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollTop(TextView widget, Spannable buffer) {
6088f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
6098f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (getTopLine(widget) >= 0) {
6108f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0));
6118f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
6128f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
6138f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
6148f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
6158f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
6168f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
6178f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll to bottom action.
6188f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls to the bottom of the document.
6198f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
6208f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
6218f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
6228f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
6238f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
6248f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
6258f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollBottom(TextView widget, Spannable buffer) {
6268f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final Layout layout = widget.getLayout();
6278f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int lineCount = layout.getLineCount();
6288f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (getBottomLine(widget) <= lineCount - 1) {
6298f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            Touch.scrollTo(widget, layout, widget.getScrollX(),
6308f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown                    layout.getLineTop(lineCount) - getInnerHeight(widget));
6318f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
6328f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
6338f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
6348f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
6358f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
6368f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
6378f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll to line start action.
6388f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls to the start of the line.
6398f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
6408f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
6418f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
6428f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
6438f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
6448f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
6458f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollLineStart(TextView widget, Spannable buffer) {
6468f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int minScrollX = getScrollBoundsLeft(widget);
6478f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int scrollX = widget.getScrollX();
6488f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (scrollX > minScrollX) {
6498f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            widget.scrollTo(minScrollX, widget.getScrollY());
6508f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
6518f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
6528f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
6538f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
6548f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown
6558f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    /**
6568f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Performs a scroll to line end action.
6578f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * Scrolls to the end of the line.
6588f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     *
6598f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param widget The text view.
6608f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @param buffer The text buffer.
6618f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @return True if the event was handled.
6628f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     * @hide
6638f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown     */
6648f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    protected boolean scrollLineEnd(TextView widget, Spannable buffer) {
6658f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
6668f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        int scrollX = widget.getScrollX();
6678f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        if (scrollX < maxScrollX) {
6688f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            widget.scrollTo(maxScrollX, widget.getScrollY());
6698f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown            return true;
6708f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        }
6718f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown        return false;
6728f34567c71003505456a9b1a0d461a4e62883d70Jeff Brown    }
67367b6ab72ae96a9f2be929de2c32c110df5390fddJeff Brown}
674