WebView.java revision 076357b8567458d4b6dfdcf839ef751634cd2bfb
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
19import android.app.AlertDialog;
20import android.content.Context;
21import android.content.DialogInterface;
22import android.content.Intent;
23import android.content.DialogInterface.OnCancelListener;
24import android.content.res.TypedArray;
25import android.graphics.Bitmap;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Paint;
29import android.graphics.Path;
30import android.graphics.Picture;
31import android.graphics.Point;
32import android.graphics.Rect;
33import android.graphics.Region;
34import android.net.http.SslCertificate;
35import android.net.Uri;
36import android.os.Bundle;
37import android.os.Handler;
38import android.os.Message;
39import android.os.ServiceManager;
40import android.os.SystemClock;
41import android.provider.Checkin;
42import android.text.IClipboard;
43import android.text.Selection;
44import android.text.Spannable;
45import android.util.AttributeSet;
46import android.util.Config;
47import android.util.EventLog;
48import android.util.Log;
49import android.view.animation.AlphaAnimation;
50import android.view.animation.Animation;
51import android.view.animation.AnimationUtils;
52import android.view.Gravity;
53import android.view.KeyEvent;
54import android.view.LayoutInflater;
55import android.view.MotionEvent;
56import android.view.SoundEffectConstants;
57import android.view.VelocityTracker;
58import android.view.View;
59import android.view.ViewConfiguration;
60import android.view.ViewGroup;
61import android.view.ViewParent;
62import android.view.ViewTreeObserver;
63import android.view.inputmethod.InputMethodManager;
64import android.webkit.TextDialog.AutoCompleteAdapter;
65import android.webkit.WebViewCore.EventHub;
66import android.widget.AbsoluteLayout;
67import android.widget.AdapterView;
68import android.widget.ArrayAdapter;
69import android.widget.ImageView;
70import android.widget.ListView;
71import android.widget.RelativeLayout;
72import android.widget.Scroller;
73import android.widget.Toast;
74import android.widget.ZoomControls;
75import android.widget.ZoomRingController;
76import android.widget.FrameLayout;
77import android.widget.AdapterView.OnItemClickListener;
78
79import java.io.File;
80import java.io.FileInputStream;
81import java.io.FileNotFoundException;
82import java.io.FileOutputStream;
83import java.io.IOException;
84import java.net.URLDecoder;
85import java.util.ArrayList;
86import java.util.HashMap;
87import java.util.List;
88
89/**
90 * <p>A View that displays web pages. This class is the basis upon which you
91 * can roll your own web browser or simply display some online content within your Activity.
92 * It uses the WebKit rendering engine to display
93 * web pages and includes methods to navigate forward and backward
94 * through a history, zoom in and out, perform text searches and more.</p>
95 * <p>Note that, in order for your Activity to access the Internet and load web pages
96 * in a WebView, you must add the <var>INTERNET</var> permissions to your
97 * Android Manifest file:</p>
98 * <pre>&lt;uses-permission android:name="android.permission.INTERNET" /></pre>
99 * <p>This must be a child of the <code>&lt;manifest></code> element.</p>
100 */
101public class WebView extends AbsoluteLayout
102        implements ViewTreeObserver.OnGlobalFocusChangeListener,
103        ViewGroup.OnHierarchyChangeListener {
104
105    // if AUTO_REDRAW_HACK is true, then the CALL key will toggle redrawing
106    // the screen all-the-time. Good for profiling our drawing code
107    static private final boolean AUTO_REDRAW_HACK = false;
108    // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK
109    private boolean mAutoRedraw;
110
111    // keep debugging parameters near the top of the file
112    static final String LOGTAG = "webview";
113    static final boolean DEBUG = false;
114    static final boolean LOGV_ENABLED = DEBUG ? Config.LOGD : Config.LOGV;
115
116    private class ExtendedZoomControls extends FrameLayout {
117        public ExtendedZoomControls(Context context, AttributeSet attrs) {
118            super(context, attrs);
119            LayoutInflater inflater = (LayoutInflater)
120                    context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
121            inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true);
122            mZoomControls = (ZoomControls) findViewById(com.android.internal.R.id.zoomControls);
123            mZoomMagnify = (ImageView) findViewById(com.android.internal.R.id.zoomMagnify);
124        }
125
126        public void show(boolean showZoom, boolean canZoomOut) {
127            mZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE);
128            mZoomMagnify.setVisibility(canZoomOut ? View.VISIBLE : View.GONE);
129            fade(View.VISIBLE, 0.0f, 1.0f);
130        }
131
132        public void hide() {
133            fade(View.GONE, 1.0f, 0.0f);
134        }
135
136        private void fade(int visibility, float startAlpha, float endAlpha) {
137            AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha);
138            anim.setDuration(500);
139            startAnimation(anim);
140            setVisibility(visibility);
141        }
142
143        public void setIsZoomMagnifyEnabled(boolean isEnabled) {
144            mZoomMagnify.setEnabled(isEnabled);
145        }
146
147        public boolean hasFocus() {
148            return mZoomControls.hasFocus() || mZoomMagnify.hasFocus();
149        }
150
151        public void setOnZoomInClickListener(OnClickListener listener) {
152            mZoomControls.setOnZoomInClickListener(listener);
153        }
154
155        public void setOnZoomOutClickListener(OnClickListener listener) {
156            mZoomControls.setOnZoomOutClickListener(listener);
157        }
158
159        public void setOnZoomMagnifyClickListener(OnClickListener listener) {
160            mZoomMagnify.setOnClickListener(listener);
161        }
162
163        ZoomControls mZoomControls;
164        ImageView mZoomMagnify;
165    }
166
167    /**
168     *  Transportation object for returning WebView across thread boundaries.
169     */
170    public class WebViewTransport {
171        private WebView mWebview;
172
173        /**
174         * Set the WebView to the transportation object.
175         * @param webview The WebView to transport.
176         */
177        public synchronized void setWebView(WebView webview) {
178            mWebview = webview;
179        }
180
181        /**
182         * Return the WebView object.
183         * @return WebView The transported WebView object.
184         */
185        public synchronized WebView getWebView() {
186            return mWebview;
187        }
188    }
189
190    // A final CallbackProxy shared by WebViewCore and BrowserFrame.
191    private final CallbackProxy mCallbackProxy;
192
193    private final WebViewDatabase mDatabase;
194
195    // SSL certificate for the main top-level page (if secure)
196    private SslCertificate mCertificate;
197
198    // Native WebView pointer that is 0 until the native object has been
199    // created.
200    private int mNativeClass;
201    // This would be final but it needs to be set to null when the WebView is
202    // destroyed.
203    private WebViewCore mWebViewCore;
204    // Handler for dispatching UI messages.
205    /* package */ final Handler mPrivateHandler = new PrivateHandler();
206    private TextDialog mTextEntry;
207    // Used to ignore changes to webkit text that arrives to the UI side after
208    // more key events.
209    private int mTextGeneration;
210
211    // The list of loaded plugins.
212    private static PluginList sPluginList;
213
214    /**
215     * Position of the last touch event.
216     */
217    private float mLastTouchX;
218    private float mLastTouchY;
219
220    /**
221     * Time of the last touch event.
222     */
223    private long mLastTouchTime;
224
225    /**
226     * Time of the last time sending touch event to WebViewCore
227     */
228    private long mLastSentTouchTime;
229
230    /**
231     * The minimum elapsed time before sending another ACTION_MOVE event to
232     * WebViewCore
233     */
234    private static final int TOUCH_SENT_INTERVAL = 100;
235
236    /**
237     * Helper class to get velocity for fling
238     */
239    VelocityTracker mVelocityTracker;
240
241    private static boolean mShowZoomRingTutorial = true;
242    private static final int ZOOM_RING_TUTORIAL_DURATION = 3000;
243
244    /**
245     * Touch mode
246     */
247    private int mTouchMode = TOUCH_DONE_MODE;
248    private static final int TOUCH_INIT_MODE = 1;
249    private static final int TOUCH_DRAG_START_MODE = 2;
250    private static final int TOUCH_DRAG_MODE = 3;
251    private static final int TOUCH_SHORTPRESS_START_MODE = 4;
252    private static final int TOUCH_SHORTPRESS_MODE = 5;
253    private static final int TOUCH_DOUBLECLICK_MODE = 6;
254    private static final int TOUCH_DONE_MODE = 7;
255    private static final int TOUCH_SELECT_MODE = 8;
256    // touch mode values specific to scale+scroll
257    private static final int FIRST_SCROLL_ZOOM = 9;
258    private static final int SCROLL_ZOOM_ANIMATION_IN = 9;
259    private static final int SCROLL_ZOOM_ANIMATION_OUT = 10;
260    private static final int SCROLL_ZOOM_OUT = 11;
261    private static final int LAST_SCROLL_ZOOM = 11;
262    // end of touch mode values specific to scale+scroll
263
264    // Whether to forward the touch events to WebCore
265    private boolean mForwardTouchEvents = false;
266
267    // Whether we are in the drag tap mode, which exists starting at the second
268    // tap's down, through its move, and includes its up. These events should be
269    // given to the method on the zoom controller.
270    private boolean mInZoomTapDragMode = false;
271
272    // Whether to prevent drag during touch. The initial value depends on
273    // mForwardTouchEvents. If WebCore wants touch events, we assume it will
274    // take control of touch events unless it says no for touch down event.
275    private boolean mPreventDrag;
276
277    // If updateTextEntry gets called while we are out of focus, use this
278    // variable to remember to do it next time we gain focus.
279    private boolean mNeedsUpdateTextEntry = false;
280
281    // Whether or not to draw the focus ring.
282    private boolean mDrawFocusRing = true;
283
284    /**
285     * Customizable constant
286     */
287    // pre-computed square of ViewConfiguration.getTouchSlop()
288    private int mTouchSlopSquare;
289    // This should be ViewConfiguration.getTapTimeout()
290    // But system time out is 100ms, which is too short for the browser.
291    // In the browser, if it switches out of tap too soon, jump tap won't work.
292    private static final int TAP_TIMEOUT = 200;
293    // The duration in milliseconds we will wait to see if it is a double tap.
294    private static final int DOUBLE_TAP_TIMEOUT = 250;
295    // This should be ViewConfiguration.getLongPressTimeout()
296    // But system time out is 500ms, which is too short for the browser.
297    // With a short timeout, it's difficult to treat trigger a short press.
298    private static final int LONG_PRESS_TIMEOUT = 1000;
299    // needed to avoid flinging after a pause of no movement
300    private static final int MIN_FLING_TIME = 250;
301    // The time that the Zoom Controls are visible before fading away
302    private static final long ZOOM_CONTROLS_TIMEOUT =
303            ViewConfiguration.getZoomControlsTimeout();
304    // The amount of content to overlap between two screens when going through
305    // pages with the space bar, in pixels.
306    private static final int PAGE_SCROLL_OVERLAP = 24;
307
308    /**
309     * These prevent calling requestLayout if either dimension is fixed. This
310     * depends on the layout parameters and the measure specs.
311     */
312    boolean mWidthCanMeasure;
313    boolean mHeightCanMeasure;
314
315    // Remember the last dimensions we sent to the native side so we can avoid
316    // sending the same dimensions more than once.
317    int mLastWidthSent;
318    int mLastHeightSent;
319
320    private int mContentWidth;   // cache of value from WebViewCore
321    private int mContentHeight;  // cache of value from WebViewCore
322
323    static int MAX_FLOAT_CONTENT_WIDTH = 480;
324    private int mMinContentWidth;
325
326    // Need to have the separate control for horizontal and vertical scrollbar
327    // style than the View's single scrollbar style
328    private boolean mOverlayHorizontalScrollbar = true;
329    private boolean mOverlayVerticalScrollbar = false;
330
331    // our standard speed. this way small distances will be traversed in less
332    // time than large distances, but we cap the duration, so that very large
333    // distances won't take too long to get there.
334    private static final int STD_SPEED = 480;  // pixels per second
335    // time for the longest scroll animation
336    private static final int MAX_DURATION = 750;   // milliseconds
337    private Scroller mScroller;
338
339    private boolean mWrapContent;
340
341    // The View containing the zoom controls
342    private ExtendedZoomControls mZoomControls;
343    private Runnable mZoomControlRunnable;
344
345    // true if we should call webcore to draw the content, false means we have
346    // requested something but it isn't ready to draw yet.
347    private WebViewCore.FocusData mFocusData;
348    /**
349     * Private message ids
350     */
351    private static final int REMEMBER_PASSWORD = 1;
352    private static final int NEVER_REMEMBER_PASSWORD = 2;
353    private static final int SWITCH_TO_SHORTPRESS = 3;
354    private static final int SWITCH_TO_LONGPRESS = 4;
355    private static final int RELEASE_SINGLE_TAP = 5;
356    private static final int UPDATE_TEXT_ENTRY_ADAPTER = 6;
357    private static final int SWITCH_TO_ENTER = 7;
358    private static final int RESUME_WEBCORE_UPDATE = 8;
359    private static final int DISMISS_ZOOM_RING_TUTORIAL = 9;
360
361    //! arg1=x, arg2=y
362    static final int SCROLL_TO_MSG_ID               = 10;
363    static final int SCROLL_BY_MSG_ID               = 11;
364    //! arg1=x, arg2=y
365    static final int SPAWN_SCROLL_TO_MSG_ID         = 12;
366    //! arg1=x, arg2=y
367    static final int SYNC_SCROLL_TO_MSG_ID          = 13;
368    static final int NEW_PICTURE_MSG_ID             = 14;
369    static final int UPDATE_TEXT_ENTRY_MSG_ID       = 15;
370    static final int WEBCORE_INITIALIZED_MSG_ID     = 16;
371    static final int UPDATE_TEXTFIELD_TEXT_MSG_ID   = 17;
372    static final int DID_FIRST_LAYOUT_MSG_ID        = 18;
373    static final int RECOMPUTE_FOCUS_MSG_ID         = 19;
374    static final int NOTIFY_FOCUS_SET_MSG_ID        = 20;
375    static final int MARK_NODE_INVALID_ID           = 21;
376    static final int UPDATE_CLIPBOARD               = 22;
377    static final int LONG_PRESS_ENTER               = 23;
378    static final int PREVENT_TOUCH_ID               = 24;
379    static final int WEBCORE_NEED_TOUCH_EVENTS      = 25;
380    // obj=Rect in doc coordinates
381    static final int INVAL_RECT_MSG_ID              = 26;
382
383    static final String[] HandlerDebugString = {
384        "REMEMBER_PASSWORD", // = 1;
385        "NEVER_REMEMBER_PASSWORD", // = 2;
386        "SWITCH_TO_SHORTPRESS", // = 3;
387        "SWITCH_TO_LONGPRESS", // = 4;
388        "RELEASE_SINGLE_TAP", // = 5;
389        "UPDATE_TEXT_ENTRY_ADAPTER", // = 6;
390        "SWITCH_TO_ENTER", // = 7;
391        "RESUME_WEBCORE_UPDATE", // = 8;
392        "9",
393        "SCROLL_TO_MSG_ID", //               = 10;
394        "SCROLL_BY_MSG_ID", //               = 11;
395        "SPAWN_SCROLL_TO_MSG_ID", //         = 12;
396        "SYNC_SCROLL_TO_MSG_ID", //          = 13;
397        "NEW_PICTURE_MSG_ID", //             = 14;
398        "UPDATE_TEXT_ENTRY_MSG_ID", //       = 15;
399        "WEBCORE_INITIALIZED_MSG_ID", //     = 16;
400        "UPDATE_TEXTFIELD_TEXT_MSG_ID", //   = 17;
401        "DID_FIRST_LAYOUT_MSG_ID", //        = 18;
402        "RECOMPUTE_FOCUS_MSG_ID", //         = 19;
403        "NOTIFY_FOCUS_SET_MSG_ID", //        = 20;
404        "MARK_NODE_INVALID_ID", //           = 21;
405        "UPDATE_CLIPBOARD", //               = 22;
406        "LONG_PRESS_ENTER", //               = 23;
407        "PREVENT_TOUCH_ID", //               = 24;
408        "WEBCORE_NEED_TOUCH_EVENTS", //      = 25;
409        "INVAL_RECT_MSG_ID" //               = 26;
410    };
411
412    // width which view is considered to be fully zoomed out
413    static final int ZOOM_OUT_WIDTH = 1024;
414
415    private static final float MAX_ZOOM_RING_ANGLE = (float) (Math.PI * 2 / 3);
416    private static final int ZOOM_RING_STEPS = 4;
417    private static final float ZOOM_RING_ANGLE_UNIT = MAX_ZOOM_RING_ANGLE
418            / ZOOM_RING_STEPS;
419
420    private static final float DEFAULT_MAX_ZOOM_SCALE = 2;
421    private static final float DEFAULT_MIN_ZOOM_SCALE = (float) 1/3;
422    // scale limit, which can be set through viewport meta tag in the web page
423    private float mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE;
424    private float mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE;
425
426    // initial scale in percent. 0 means using default.
427    private int mInitialScale = 0;
428
429    // set to true temporarily while the zoom control is being dragged
430    private boolean mPreviewZoomOnly = false;
431
432    // computed scale and inverse, from mZoomWidth.
433    private float mActualScale = 1;
434    private float mInvActualScale = 1;
435    // if this is non-zero, it is used on drawing rather than mActualScale
436    private float mZoomScale;
437    private float mInvInitialZoomScale;
438    private float mInvFinalZoomScale;
439    private long mZoomStart;
440    private static final int ZOOM_ANIMATION_LENGTH = 500;
441
442    private boolean mUserScroll = false;
443
444    private int mSnapScrollMode = SNAP_NONE;
445    private static final int SNAP_NONE = 1;
446    private static final int SNAP_X = 2;
447    private static final int SNAP_Y = 3;
448    private static final int SNAP_X_LOCK = 4;
449    private static final int SNAP_Y_LOCK = 5;
450    private boolean mSnapPositive;
451
452    // Used to match key downs and key ups
453    private boolean mGotKeyDown;
454
455    /* package */ static boolean mLogEvent = true;
456    private static final int EVENT_LOG_ZOOM_LEVEL_CHANGE = 70101;
457    private static final int EVENT_LOG_DOUBLE_TAP_DURATION = 70102;
458
459    // for event log
460    private long mLastTouchUpTime = 0;
461
462    /**
463     * URI scheme for telephone number
464     */
465    public static final String SCHEME_TEL = "tel:";
466    /**
467     * URI scheme for email address
468     */
469    public static final String SCHEME_MAILTO = "mailto:";
470    /**
471     * URI scheme for map address
472     */
473    public static final String SCHEME_GEO = "geo:0,0?q=";
474
475    private int mBackgroundColor = Color.WHITE;
476
477    // Used to notify listeners of a new picture.
478    private PictureListener mPictureListener;
479    /**
480     * Interface to listen for new pictures as they change.
481     */
482    public interface PictureListener {
483        /**
484         * Notify the listener that the picture has changed.
485         * @param view The WebView that owns the picture.
486         * @param picture The new picture.
487         */
488        public void onNewPicture(WebView view, Picture picture);
489    }
490
491    public class HitTestResult {
492        /**
493         * Default HitTestResult, where the target is unknown
494         */
495        public static final int UNKNOWN_TYPE = 0;
496        /**
497         * HitTestResult for hitting a HTML::a tag
498         */
499        public static final int ANCHOR_TYPE = 1;
500        /**
501         * HitTestResult for hitting a phone number
502         */
503        public static final int PHONE_TYPE = 2;
504        /**
505         * HitTestResult for hitting a map address
506         */
507        public static final int GEO_TYPE = 3;
508        /**
509         * HitTestResult for hitting an email address
510         */
511        public static final int EMAIL_TYPE = 4;
512        /**
513         * HitTestResult for hitting an HTML::img tag
514         */
515        public static final int IMAGE_TYPE = 5;
516        /**
517         * HitTestResult for hitting a HTML::a tag which contains HTML::img
518         */
519        public static final int IMAGE_ANCHOR_TYPE = 6;
520        /**
521         * HitTestResult for hitting a HTML::a tag with src=http
522         */
523        public static final int SRC_ANCHOR_TYPE = 7;
524        /**
525         * HitTestResult for hitting a HTML::a tag with src=http + HTML::img
526         */
527        public static final int SRC_IMAGE_ANCHOR_TYPE = 8;
528        /**
529         * HitTestResult for hitting an edit text area
530         */
531        public static final int EDIT_TEXT_TYPE = 9;
532
533        private int mType;
534        private String mExtra;
535
536        HitTestResult() {
537            mType = UNKNOWN_TYPE;
538        }
539
540        private void setType(int type) {
541            mType = type;
542        }
543
544        private void setExtra(String extra) {
545            mExtra = extra;
546        }
547
548        public int getType() {
549            return mType;
550        }
551
552        public String getExtra() {
553            return mExtra;
554        }
555    }
556
557    private ZoomRingController mZoomRingController;
558    private ImageView mZoomRingOverview;
559    private Animation mZoomRingOverviewExitAnimation;
560
561    // These keep track of the center point of the zoom ring.  They are used to
562    // determine the point around which we should zoom.
563    private float mZoomCenterX;
564    private float mZoomCenterY;
565
566    private ZoomRingController.OnZoomListener mZoomListener =
567            new ZoomRingController.OnZoomListener() {
568
569        private float mClockwiseBound;
570        private float mCounterClockwiseBound;
571        private float mStartScale;
572
573        public void onCenter(int x, int y) {
574            // Don't translate when the control is invoked, hence we do nothing
575            // in this callback
576        }
577
578        public void onBeginPan() {
579            setZoomOverviewVisible(false);
580            if (mLogEvent) {
581                Checkin.updateStats(mContext.getContentResolver(),
582                        Checkin.Stats.Tag.BROWSER_ZOOM_RING_DRAG, 1, 0.0);
583            }
584        }
585
586        public boolean onPan(int deltaX, int deltaY) {
587            return pinScrollBy(deltaX, deltaY, false, 0);
588        }
589
590        public void onEndPan() {
591        }
592
593        public void onVisibilityChanged(boolean visible) {
594            if (visible) {
595                switchOutDrawHistory();
596                if (mMaxZoomScale - 1 > ZOOM_RING_STEPS * 0.01f) {
597                    mClockwiseBound = (float) (2 * Math.PI - MAX_ZOOM_RING_ANGLE);
598                } else {
599                    mClockwiseBound = (float) (2 * Math.PI);
600                }
601                mZoomRingController.setThumbClockwiseBound(mClockwiseBound);
602                if (1 - mMinZoomScale > ZOOM_RING_STEPS * 0.01f) {
603                    mCounterClockwiseBound = MAX_ZOOM_RING_ANGLE;
604                } else {
605                    mCounterClockwiseBound = 0;
606                }
607                mZoomRingController
608                        .setThumbCounterclockwiseBound(mCounterClockwiseBound);
609                float angle = 0f;
610                if (mActualScale > 1 && mClockwiseBound < (float) (2 * Math.PI)) {
611                    angle = -(float) Math.round(ZOOM_RING_STEPS
612                            * (mActualScale - 1) / (mMaxZoomScale - 1))
613                            / ZOOM_RING_STEPS;
614                } else if (mActualScale < 1 && mCounterClockwiseBound > 0) {
615                    angle = (float) Math.round(ZOOM_RING_STEPS
616                            * (1 - mActualScale) / (1 - mMinZoomScale))
617                            / ZOOM_RING_STEPS;
618                }
619                mZoomRingController.setThumbAngle(angle * MAX_ZOOM_RING_ANGLE);
620
621                // Show the zoom overview tab on the ring
622                setZoomOverviewVisible(true);
623                if (mLogEvent) {
624                    Checkin.updateStats(mContext.getContentResolver(),
625                            Checkin.Stats.Tag.BROWSER_ZOOM_RING, 1, 0.0);
626                }
627            }
628        }
629
630        public void onBeginDrag() {
631            mPreviewZoomOnly = true;
632            mStartScale = mActualScale;
633            setZoomOverviewVisible(false);
634        }
635
636        public void onEndDrag() {
637            mPreviewZoomOnly = false;
638            if (mLogEvent) {
639                EventLog.writeEvent(EVENT_LOG_ZOOM_LEVEL_CHANGE,
640                        (int) mStartScale * 100, (int) mActualScale * 100,
641                        System.currentTimeMillis());
642            }
643            setNewZoomScale(mActualScale, true);
644        }
645
646        public boolean onDragZoom(int deltaZoomLevel, int centerX,
647                int centerY, float startAngle, float curAngle) {
648            if (deltaZoomLevel < 0
649                    && Math.abs(mActualScale - mMinZoomScale) < 0.01f
650                    || deltaZoomLevel > 0
651                    &&  Math.abs(mActualScale - mMaxZoomScale) < 0.01f
652                    || deltaZoomLevel == 0) {
653                return false;
654            }
655            mZoomCenterX = (float) centerX;
656            mZoomCenterY = (float) centerY;
657
658            float scale = 1.0f;
659            // curAngle is [0, 2 * Math.PI)
660            if (curAngle < (float) Math.PI) {
661                if (curAngle >= mCounterClockwiseBound) {
662                    scale = mMinZoomScale;
663                } else {
664                    scale = 1 - (float) Math.round(curAngle
665                            / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS
666                            * (1 - mMinZoomScale);
667                }
668            } else {
669                if (curAngle <= mClockwiseBound) {
670                    scale = mMaxZoomScale;
671                } else {
672                    scale = 1 + (float) Math.round(
673                            ((float) 2 * Math.PI - curAngle)
674                            / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS
675                            * (mMaxZoomScale - 1);
676                }
677            }
678            zoomWithPreview(scale);
679            return true;
680        }
681
682        public void onSimpleZoom(boolean zoomIn) {
683            if (zoomIn) {
684                zoomIn();
685            } else {
686                zoomOut();
687            }
688        }
689
690    };
691
692    /**
693     * Construct a new WebView with a Context object.
694     * @param context A Context object used to access application assets.
695     */
696    public WebView(Context context) {
697        this(context, null);
698    }
699
700    /**
701     * Construct a new WebView with layout parameters.
702     * @param context A Context object used to access application assets.
703     * @param attrs An AttributeSet passed to our parent.
704     */
705    public WebView(Context context, AttributeSet attrs) {
706        this(context, attrs, com.android.internal.R.attr.webViewStyle);
707    }
708
709    /**
710     * Construct a new WebView with layout parameters and a default style.
711     * @param context A Context object used to access application assets.
712     * @param attrs An AttributeSet passed to our parent.
713     * @param defStyle The default style resource ID.
714     */
715    public WebView(Context context, AttributeSet attrs, int defStyle) {
716        super(context, attrs, defStyle);
717        init();
718
719        mCallbackProxy = new CallbackProxy(context, this);
720        mWebViewCore = new WebViewCore(context, this, mCallbackProxy);
721        mDatabase = WebViewDatabase.getInstance(context);
722        mFocusData = new WebViewCore.FocusData();
723        mFocusData.mFrame = 0;
724        mFocusData.mNode = 0;
725        mFocusData.mX = 0;
726        mFocusData.mY = 0;
727        mScroller = new Scroller(context);
728        mZoomRingController = new ZoomRingController(context, this);
729        mZoomRingController.setResetThumbAutomatically(false);
730        mZoomRingController.setCallback(mZoomListener);
731        mZoomRingController.setZoomRingTrack(
732                com.android.internal.R.drawable.zoom_ring_track_absolute);
733        mZoomRingController.setPannerAcceleration(160);
734        mZoomRingController.setPannerStartAcceleratingDuration(700);
735        createZoomRingOverviewTab();
736    }
737
738    private void init() {
739        setWillNotDraw(false);
740        setFocusable(true);
741        setFocusableInTouchMode(true);
742        setClickable(true);
743        setLongClickable(true);
744
745        final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
746        mTouchSlopSquare = slop * slop;
747        mMinLockSnapReverseDistance = slop;
748    }
749
750    private void createZoomRingOverviewTab() {
751        Context context = getContext();
752
753        mZoomRingOverviewExitAnimation = AnimationUtils.loadAnimation(context,
754                com.android.internal.R.anim.fade_out);
755
756        mZoomRingOverview = new ImageView(context);
757        mZoomRingOverview.setBackgroundResource(
758                com.android.internal.R.drawable.zoom_ring_overview_tab);
759        mZoomRingOverview.setImageResource(com.android.internal.R.drawable.btn_zoom_page);
760
761        FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
762                FrameLayout.LayoutParams.WRAP_CONTENT,
763                FrameLayout.LayoutParams.WRAP_CONTENT,
764                Gravity.CENTER);
765        // TODO: magic constant that's based on the zoom ring radius + some offset
766        lp.topMargin = 208;
767        mZoomRingOverview.setLayoutParams(lp);
768        mZoomRingOverview.setOnClickListener(new View.OnClickListener() {
769            public void onClick(View v) {
770                // Hide the zoom ring
771                mZoomRingController.setVisible(false);
772                if (mLogEvent) {
773                    Checkin.updateStats(mContext.getContentResolver(),
774                            Checkin.Stats.Tag.BROWSER_ZOOM_OVERVIEW, 1, 0.0);
775                }
776                zoomScrollOut();
777            }});
778
779        // Measure the overview View to figure out its height
780        mZoomRingOverview.forceLayout();
781        mZoomRingOverview.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
782                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
783
784        ViewGroup container = mZoomRingController.getContainer();
785        // Find the index of the zoom ring in the container
786        View zoomRing = container.findViewById(mZoomRingController.getZoomRingId());
787        int zoomRingIndex;
788        for (zoomRingIndex = container.getChildCount() - 1; zoomRingIndex >= 0; zoomRingIndex--) {
789            if (container.getChildAt(zoomRingIndex) == zoomRing) break;
790        }
791        // Add the overview tab below the zoom ring (so we don't steal its events)
792        container.addView(mZoomRingOverview, zoomRingIndex);
793        // Since we use margins to adjust the vertical placement of the tab, the widget
794        // ends up getting clipped off. Ensure the container is big enough for
795        // us.
796        int myHeight = mZoomRingOverview.getMeasuredHeight() + lp.topMargin / 2;
797        // Multiplied by 2 b/c the zoom ring needs to be centered on the screen
798        container.setMinimumHeight(myHeight * 2);
799    }
800
801    private void setZoomOverviewVisible(boolean visible) {
802        int newVisibility = visible ? View.VISIBLE : View.INVISIBLE;
803        if (mZoomRingOverview.getVisibility() == newVisibility) return;
804
805        if (!visible) {
806            mZoomRingOverview.startAnimation(mZoomRingOverviewExitAnimation);
807        }
808        mZoomRingOverview.setVisibility(newVisibility);
809    }
810
811    /* package */ boolean onSavePassword(String schemePlusHost, String username,
812            String password, final Message resumeMsg) {
813       boolean rVal = false;
814       if (resumeMsg == null) {
815           // null resumeMsg implies saving password silently
816           mDatabase.setUsernamePassword(schemePlusHost, username, password);
817       } else {
818            final Message remember = mPrivateHandler.obtainMessage(
819                    REMEMBER_PASSWORD);
820            remember.getData().putString("host", schemePlusHost);
821            remember.getData().putString("username", username);
822            remember.getData().putString("password", password);
823            remember.obj = resumeMsg;
824
825            final Message neverRemember = mPrivateHandler.obtainMessage(
826                    NEVER_REMEMBER_PASSWORD);
827            neverRemember.getData().putString("host", schemePlusHost);
828            neverRemember.getData().putString("username", username);
829            neverRemember.getData().putString("password", password);
830            neverRemember.obj = resumeMsg;
831
832            new AlertDialog.Builder(getContext())
833                    .setTitle(com.android.internal.R.string.save_password_label)
834                    .setMessage(com.android.internal.R.string.save_password_message)
835                    .setPositiveButton(com.android.internal.R.string.save_password_notnow,
836                    new DialogInterface.OnClickListener() {
837                        public void onClick(DialogInterface dialog, int which) {
838                            resumeMsg.sendToTarget();
839                        }
840                    })
841                    .setNeutralButton(com.android.internal.R.string.save_password_remember,
842                    new DialogInterface.OnClickListener() {
843                        public void onClick(DialogInterface dialog, int which) {
844                            remember.sendToTarget();
845                        }
846                    })
847                    .setNegativeButton(com.android.internal.R.string.save_password_never,
848                    new DialogInterface.OnClickListener() {
849                        public void onClick(DialogInterface dialog, int which) {
850                            neverRemember.sendToTarget();
851                        }
852                    })
853                    .setOnCancelListener(new OnCancelListener() {
854                        public void onCancel(DialogInterface dialog) {
855                            resumeMsg.sendToTarget();
856                        }
857                    }).show();
858            // Return true so that WebViewCore will pause while the dialog is
859            // up.
860            rVal = true;
861        }
862       return rVal;
863    }
864
865    @Override
866    public void setScrollBarStyle(int style) {
867        if (style == View.SCROLLBARS_INSIDE_INSET
868                || style == View.SCROLLBARS_OUTSIDE_INSET) {
869            mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = false;
870        } else {
871            mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = true;
872        }
873        super.setScrollBarStyle(style);
874    }
875
876    /**
877     * Specify whether the horizontal scrollbar has overlay style.
878     * @param overlay TRUE if horizontal scrollbar should have overlay style.
879     */
880    public void setHorizontalScrollbarOverlay(boolean overlay) {
881        mOverlayHorizontalScrollbar = overlay;
882    }
883
884    /**
885     * Specify whether the vertical scrollbar has overlay style.
886     * @param overlay TRUE if vertical scrollbar should have overlay style.
887     */
888    public void setVerticalScrollbarOverlay(boolean overlay) {
889        mOverlayVerticalScrollbar = overlay;
890    }
891
892    /**
893     * Return whether horizontal scrollbar has overlay style
894     * @return TRUE if horizontal scrollbar has overlay style.
895     */
896    public boolean overlayHorizontalScrollbar() {
897        return mOverlayHorizontalScrollbar;
898    }
899
900    /**
901     * Return whether vertical scrollbar has overlay style
902     * @return TRUE if vertical scrollbar has overlay style.
903     */
904    public boolean overlayVerticalScrollbar() {
905        return mOverlayVerticalScrollbar;
906    }
907
908    /*
909     * Return the width of the view where the content of WebView should render
910     * to.
911     */
912    private int getViewWidth() {
913        if (!isVerticalScrollBarEnabled() || mOverlayVerticalScrollbar) {
914            return getWidth();
915        } else {
916            return getWidth() - getVerticalScrollbarWidth();
917        }
918    }
919
920    /*
921     * Return the height of the view where the content of WebView should render
922     * to.
923     */
924    private int getViewHeight() {
925        if (!isHorizontalScrollBarEnabled() || mOverlayHorizontalScrollbar) {
926            return getHeight();
927        } else {
928            return getHeight() - getHorizontalScrollbarHeight();
929        }
930    }
931
932    /**
933     * @return The SSL certificate for the main top-level page or null if
934     * there is no certificate (the site is not secure).
935     */
936    public SslCertificate getCertificate() {
937        return mCertificate;
938    }
939
940    /**
941     * Sets the SSL certificate for the main top-level page.
942     */
943    public void setCertificate(SslCertificate certificate) {
944        // here, the certificate can be null (if the site is not secure)
945        mCertificate = certificate;
946    }
947
948    //-------------------------------------------------------------------------
949    // Methods called by activity
950    //-------------------------------------------------------------------------
951
952    /**
953     * Save the username and password for a particular host in the WebView's
954     * internal database.
955     * @param host The host that required the credentials.
956     * @param username The username for the given host.
957     * @param password The password for the given host.
958     */
959    public void savePassword(String host, String username, String password) {
960        mDatabase.setUsernamePassword(host, username, password);
961    }
962
963    /**
964     * Set the HTTP authentication credentials for a given host and realm.
965     *
966     * @param host The host for the credentials.
967     * @param realm The realm for the credentials.
968     * @param username The username for the password. If it is null, it means
969     *                 password can't be saved.
970     * @param password The password
971     */
972    public void setHttpAuthUsernamePassword(String host, String realm,
973            String username, String password) {
974        mDatabase.setHttpAuthUsernamePassword(host, realm, username, password);
975    }
976
977    /**
978     * Retrieve the HTTP authentication username and password for a given
979     * host & realm pair
980     *
981     * @param host The host for which the credentials apply.
982     * @param realm The realm for which the credentials apply.
983     * @return String[] if found, String[0] is username, which can be null and
984     *         String[1] is password. Return null if it can't find anything.
985     */
986    public String[] getHttpAuthUsernamePassword(String host, String realm) {
987        return mDatabase.getHttpAuthUsernamePassword(host, realm);
988    }
989
990    /**
991     * Destroy the internal state of the WebView. This method should be called
992     * after the WebView has been removed from the view system. No other
993     * methods may be called on a WebView after destroy.
994     */
995    public void destroy() {
996        clearTextEntry();
997        if (mWebViewCore != null) {
998            // Set the handlers to null before destroying WebViewCore so no
999            // more messages will be posted.
1000            mCallbackProxy.setWebViewClient(null);
1001            mCallbackProxy.setWebChromeClient(null);
1002            // Tell WebViewCore to destroy itself
1003            WebViewCore webViewCore = mWebViewCore;
1004            mWebViewCore = null; // prevent using partial webViewCore
1005            webViewCore.destroy();
1006            // Remove any pending messages that might not be serviced yet.
1007            mPrivateHandler.removeCallbacksAndMessages(null);
1008            mCallbackProxy.removeCallbacksAndMessages(null);
1009            // Wake up the WebCore thread just in case it is waiting for a
1010            // javascript dialog.
1011            synchronized (mCallbackProxy) {
1012                mCallbackProxy.notify();
1013            }
1014        }
1015        if (mNativeClass != 0) {
1016            nativeDestroy();
1017            mNativeClass = 0;
1018        }
1019    }
1020
1021    /**
1022     * Enables platform notifications of data state and proxy changes.
1023     */
1024    public static void enablePlatformNotifications() {
1025        Network.enablePlatformNotifications();
1026    }
1027
1028    /**
1029     * If platform notifications are enabled, this should be called
1030     * from onPause() or onStop().
1031     */
1032    public static void disablePlatformNotifications() {
1033        Network.disablePlatformNotifications();
1034    }
1035
1036    /**
1037     * Inform WebView of the network state. This is used to set
1038     * the javascript property window.navigator.isOnline and
1039     * generates the online/offline event as specified in HTML5, sec. 5.7.7
1040     * @param networkUp boolean indicating if network is available
1041     *
1042     * @hide pending API Council approval
1043     */
1044    public void setNetworkAvailable(boolean networkUp) {
1045        BrowserFrame.sJavaBridge.setNetworkOnLine(networkUp);
1046    }
1047
1048    /**
1049     * Save the state of this WebView used in
1050     * {@link android.app.Activity#onSaveInstanceState}. Please note that this
1051     * method no longer stores the display data for this WebView. The previous
1052     * behavior could potentially leak files if {@link #restoreState} was never
1053     * called. See {@link #savePicture} and {@link #restorePicture} for saving
1054     * and restoring the display data.
1055     * @param outState The Bundle to store the WebView state.
1056     * @return The same copy of the back/forward list used to save the state. If
1057     *         saveState fails, the returned list will be null.
1058     * @see #savePicture
1059     * @see #restorePicture
1060     */
1061    public WebBackForwardList saveState(Bundle outState) {
1062        if (outState == null) {
1063            return null;
1064        }
1065        // We grab a copy of the back/forward list because a client of WebView
1066        // may have invalidated the history list by calling clearHistory.
1067        WebBackForwardList list = copyBackForwardList();
1068        final int currentIndex = list.getCurrentIndex();
1069        final int size = list.getSize();
1070        // We should fail saving the state if the list is empty or the index is
1071        // not in a valid range.
1072        if (currentIndex < 0 || currentIndex >= size || size == 0) {
1073            return null;
1074        }
1075        outState.putInt("index", currentIndex);
1076        // FIXME: This should just be a byte[][] instead of ArrayList but
1077        // Parcel.java does not have the code to handle multi-dimensional
1078        // arrays.
1079        ArrayList<byte[]> history = new ArrayList<byte[]>(size);
1080        for (int i = 0; i < size; i++) {
1081            WebHistoryItem item = list.getItemAtIndex(i);
1082            byte[] data = item.getFlattenedData();
1083            if (data == null) {
1084                // It would be very odd to not have any data for a given history
1085                // item. And we will fail to rebuild the history list without
1086                // flattened data.
1087                return null;
1088            }
1089            history.add(data);
1090        }
1091        outState.putSerializable("history", history);
1092        if (mCertificate != null) {
1093            outState.putBundle("certificate",
1094                               SslCertificate.saveState(mCertificate));
1095        }
1096        return list;
1097    }
1098
1099    /**
1100     * Save the current display data to the Bundle given. Used in conjunction
1101     * with {@link #saveState}.
1102     * @param b A Bundle to store the display data.
1103     * @param dest The file to store the serialized picture data. Will be
1104     *             overwritten with this WebView's picture data.
1105     * @return True if the picture was successfully saved.
1106     */
1107    public boolean savePicture(Bundle b, File dest) {
1108        if (dest == null || b == null) {
1109            return false;
1110        }
1111        final Picture p = capturePicture();
1112        try {
1113            final FileOutputStream out = new FileOutputStream(dest);
1114            p.writeToStream(out);
1115            out.close();
1116        } catch (FileNotFoundException e){
1117            e.printStackTrace();
1118        } catch (IOException e) {
1119            e.printStackTrace();
1120        } catch (RuntimeException e) {
1121            e.printStackTrace();
1122        }
1123        if (dest.length() > 0) {
1124            b.putInt("scrollX", mScrollX);
1125            b.putInt("scrollY", mScrollY);
1126            b.putFloat("scale", mActualScale);
1127            return true;
1128        }
1129        return false;
1130    }
1131
1132    /**
1133     * Restore the display data that was save in {@link #savePicture}. Used in
1134     * conjunction with {@link #restoreState}.
1135     * @param b A Bundle containing the saved display data.
1136     * @param src The file where the picture data was stored.
1137     * @return True if the picture was successfully restored.
1138     */
1139    public boolean restorePicture(Bundle b, File src) {
1140        if (src == null || b == null) {
1141            return false;
1142        }
1143        if (src.exists()) {
1144            Picture p = null;
1145            try {
1146                final FileInputStream in = new FileInputStream(src);
1147                p = Picture.createFromStream(in);
1148                in.close();
1149            } catch (FileNotFoundException e){
1150                e.printStackTrace();
1151            } catch (RuntimeException e) {
1152                e.printStackTrace();
1153            } catch (IOException e) {
1154                e.printStackTrace();
1155            }
1156            if (p != null) {
1157                int sx = b.getInt("scrollX", 0);
1158                int sy = b.getInt("scrollY", 0);
1159                float scale = b.getFloat("scale", 1.0f);
1160                mDrawHistory = true;
1161                mHistoryPicture = p;
1162                mScrollX = sx;
1163                mScrollY = sy;
1164                mHistoryWidth = Math.round(p.getWidth() * scale);
1165                mHistoryHeight = Math.round(p.getHeight() * scale);
1166                // as getWidth() / getHeight() of the view are not
1167                // available yet, set up mActualScale, so that when
1168                // onSizeChanged() is called, the rest will be set
1169                // correctly
1170                mActualScale = scale;
1171                invalidate();
1172                return true;
1173            }
1174        }
1175        return false;
1176    }
1177
1178    /**
1179     * Restore the state of this WebView from the given map used in
1180     * {@link android.app.Activity#onRestoreInstanceState}. This method should
1181     * be called to restore the state of the WebView before using the object. If
1182     * it is called after the WebView has had a chance to build state (load
1183     * pages, create a back/forward list, etc.) there may be undesirable
1184     * side-effects. Please note that this method no longer restores the
1185     * display data for this WebView. See {@link #savePicture} and {@link
1186     * #restorePicture} for saving and restoring the display data.
1187     * @param inState The incoming Bundle of state.
1188     * @return The restored back/forward list or null if restoreState failed.
1189     * @see #savePicture
1190     * @see #restorePicture
1191     */
1192    public WebBackForwardList restoreState(Bundle inState) {
1193        WebBackForwardList returnList = null;
1194        if (inState == null) {
1195            return returnList;
1196        }
1197        if (inState.containsKey("index") && inState.containsKey("history")) {
1198            mCertificate = SslCertificate.restoreState(
1199                inState.getBundle("certificate"));
1200
1201            final WebBackForwardList list = mCallbackProxy.getBackForwardList();
1202            final int index = inState.getInt("index");
1203            // We can't use a clone of the list because we need to modify the
1204            // shared copy, so synchronize instead to prevent concurrent
1205            // modifications.
1206            synchronized (list) {
1207                final List<byte[]> history =
1208                        (List<byte[]>) inState.getSerializable("history");
1209                final int size = history.size();
1210                // Check the index bounds so we don't crash in native code while
1211                // restoring the history index.
1212                if (index < 0 || index >= size) {
1213                    return null;
1214                }
1215                for (int i = 0; i < size; i++) {
1216                    byte[] data = history.remove(0);
1217                    if (data == null) {
1218                        // If we somehow have null data, we cannot reconstruct
1219                        // the item and thus our history list cannot be rebuilt.
1220                        return null;
1221                    }
1222                    WebHistoryItem item = new WebHistoryItem(data);
1223                    list.addHistoryItem(item);
1224                }
1225                // Grab the most recent copy to return to the caller.
1226                returnList = copyBackForwardList();
1227                // Update the copy to have the correct index.
1228                returnList.setCurrentIndex(index);
1229            }
1230            // Remove all pending messages because we are restoring previous
1231            // state.
1232            mWebViewCore.removeMessages();
1233            // Send a restore state message.
1234            mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index);
1235        }
1236        return returnList;
1237    }
1238
1239    /**
1240     * Load the given url.
1241     * @param url The url of the resource to load.
1242     */
1243    public void loadUrl(String url) {
1244        switchOutDrawHistory();
1245        mWebViewCore.sendMessage(EventHub.LOAD_URL, url);
1246        clearTextEntry();
1247    }
1248
1249    /**
1250     * Load the given data into the WebView. This will load the data into
1251     * WebView using the data: scheme. Content loaded through this mechanism
1252     * does not have the ability to load content from the network.
1253     * @param data A String of data in the given encoding.
1254     * @param mimeType The MIMEType of the data. i.e. text/html, image/jpeg
1255     * @param encoding The encoding of the data. i.e. utf-8, base64
1256     */
1257    public void loadData(String data, String mimeType, String encoding) {
1258        loadUrl("data:" + mimeType + ";" + encoding + "," + data);
1259    }
1260
1261    /**
1262     * Load the given data into the WebView, use the provided URL as the base
1263     * URL for the content. The base URL is the URL that represents the page
1264     * that is loaded through this interface. As such, it is used for the
1265     * history entry and to resolve any relative URLs. The failUrl is used if
1266     * browser fails to load the data provided. If it is empty or null, and the
1267     * load fails, then no history entry is created.
1268     * <p>
1269     * Note for post 1.0. Due to the change in the WebKit, the access to asset
1270     * files through "file:///android_asset/" for the sub resources is more
1271     * restricted. If you provide null or empty string as baseUrl, you won't be
1272     * able to access asset files. If the baseUrl is anything other than
1273     * http(s)/ftp(s)/about/javascript as scheme, you can access asset files for
1274     * sub resources.
1275     *
1276     * @param baseUrl Url to resolve relative paths with, if null defaults to
1277     *            "about:blank"
1278     * @param data A String of data in the given encoding.
1279     * @param mimeType The MIMEType of the data. i.e. text/html. If null,
1280     *            defaults to "text/html"
1281     * @param encoding The encoding of the data. i.e. utf-8, us-ascii
1282     * @param failUrl URL to use if the content fails to load or null.
1283     */
1284    public void loadDataWithBaseURL(String baseUrl, String data,
1285            String mimeType, String encoding, String failUrl) {
1286
1287        if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) {
1288            loadData(data, mimeType, encoding);
1289            return;
1290        }
1291        switchOutDrawHistory();
1292        HashMap arg = new HashMap();
1293        arg.put("baseUrl", baseUrl);
1294        arg.put("data", data);
1295        arg.put("mimeType", mimeType);
1296        arg.put("encoding", encoding);
1297        arg.put("failUrl", failUrl);
1298        mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg);
1299        clearTextEntry();
1300    }
1301
1302    /**
1303     * Stop the current load.
1304     */
1305    public void stopLoading() {
1306        // TODO: should we clear all the messages in the queue before sending
1307        // STOP_LOADING?
1308        switchOutDrawHistory();
1309        mWebViewCore.sendMessage(EventHub.STOP_LOADING);
1310    }
1311
1312    /**
1313     * Reload the current url.
1314     */
1315    public void reload() {
1316        switchOutDrawHistory();
1317        mWebViewCore.sendMessage(EventHub.RELOAD);
1318    }
1319
1320    /**
1321     * Return true if this WebView has a back history item.
1322     * @return True iff this WebView has a back history item.
1323     */
1324    public boolean canGoBack() {
1325        WebBackForwardList l = mCallbackProxy.getBackForwardList();
1326        synchronized (l) {
1327            if (l.getClearPending()) {
1328                return false;
1329            } else {
1330                return l.getCurrentIndex() > 0;
1331            }
1332        }
1333    }
1334
1335    /**
1336     * Go back in the history of this WebView.
1337     */
1338    public void goBack() {
1339        goBackOrForward(-1);
1340    }
1341
1342    /**
1343     * Return true if this WebView has a forward history item.
1344     * @return True iff this Webview has a forward history item.
1345     */
1346    public boolean canGoForward() {
1347        WebBackForwardList l = mCallbackProxy.getBackForwardList();
1348        synchronized (l) {
1349            if (l.getClearPending()) {
1350                return false;
1351            } else {
1352                return l.getCurrentIndex() < l.getSize() - 1;
1353            }
1354        }
1355    }
1356
1357    /**
1358     * Go forward in the history of this WebView.
1359     */
1360    public void goForward() {
1361        goBackOrForward(1);
1362    }
1363
1364    /**
1365     * Return true if the page can go back or forward the given
1366     * number of steps.
1367     * @param steps The negative or positive number of steps to move the
1368     *              history.
1369     */
1370    public boolean canGoBackOrForward(int steps) {
1371        WebBackForwardList l = mCallbackProxy.getBackForwardList();
1372        synchronized (l) {
1373            if (l.getClearPending()) {
1374                return false;
1375            } else {
1376                int newIndex = l.getCurrentIndex() + steps;
1377                return newIndex >= 0 && newIndex < l.getSize();
1378            }
1379        }
1380    }
1381
1382    /**
1383     * Go to the history item that is the number of steps away from
1384     * the current item. Steps is negative if backward and positive
1385     * if forward.
1386     * @param steps The number of steps to take back or forward in the back
1387     *              forward list.
1388     */
1389    public void goBackOrForward(int steps) {
1390        goBackOrForward(steps, false);
1391    }
1392
1393    private void goBackOrForward(int steps, boolean ignoreSnapshot) {
1394        // every time we go back or forward, we want to reset the
1395        // WebView certificate:
1396        // if the new site is secure, we will reload it and get a
1397        // new certificate set;
1398        // if the new site is not secure, the certificate must be
1399        // null, and that will be the case
1400        mCertificate = null;
1401        if (steps != 0) {
1402            clearTextEntry();
1403            mWebViewCore.sendMessage(EventHub.GO_BACK_FORWARD, steps,
1404                    ignoreSnapshot ? 1 : 0);
1405        }
1406    }
1407
1408    private boolean extendScroll(int y) {
1409        int finalY = mScroller.getFinalY();
1410        int newY = pinLocY(finalY + y);
1411        if (newY == finalY) return false;
1412        mScroller.setFinalY(newY);
1413        mScroller.extendDuration(computeDuration(0, y));
1414        return true;
1415    }
1416
1417    /**
1418     * Scroll the contents of the view up by half the view size
1419     * @param top true to jump to the top of the page
1420     * @return true if the page was scrolled
1421     */
1422    public boolean pageUp(boolean top) {
1423        if (mNativeClass == 0) {
1424            return false;
1425        }
1426        nativeClearFocus(-1, -1);
1427        if (top) {
1428            // go to the top of the document
1429            return pinScrollTo(mScrollX, 0, true, 0);
1430        }
1431        // Page up
1432        int h = getHeight();
1433        int y;
1434        if (h > 2 * PAGE_SCROLL_OVERLAP) {
1435            y = -h + PAGE_SCROLL_OVERLAP;
1436        } else {
1437            y = -h / 2;
1438        }
1439        mUserScroll = true;
1440        return mScroller.isFinished() ? pinScrollBy(0, y, true, 0)
1441                : extendScroll(y);
1442    }
1443
1444    /**
1445     * Scroll the contents of the view down by half the page size
1446     * @param bottom true to jump to bottom of page
1447     * @return true if the page was scrolled
1448     */
1449    public boolean pageDown(boolean bottom) {
1450        if (mNativeClass == 0) {
1451            return false;
1452        }
1453        nativeClearFocus(-1, -1);
1454        if (bottom) {
1455            return pinScrollTo(mScrollX, mContentHeight, true, 0);
1456        }
1457        // Page down.
1458        int h = getHeight();
1459        int y;
1460        if (h > 2 * PAGE_SCROLL_OVERLAP) {
1461            y = h - PAGE_SCROLL_OVERLAP;
1462        } else {
1463            y = h / 2;
1464        }
1465        mUserScroll = true;
1466        return mScroller.isFinished() ? pinScrollBy(0, y, true, 0)
1467                : extendScroll(y);
1468    }
1469
1470    /**
1471     * Clear the view so that onDraw() will draw nothing but white background,
1472     * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY
1473     */
1474    public void clearView() {
1475        mContentWidth = 0;
1476        mContentHeight = 0;
1477        mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT);
1478    }
1479
1480    /**
1481     * Return a new picture that captures the current display of the webview.
1482     * This is a copy of the display, and will be unaffected if the webview
1483     * later loads a different URL.
1484     *
1485     * @return a picture containing the current contents of the view. Note this
1486     *         picture is of the entire document, and is not restricted to the
1487     *         bounds of the view.
1488     */
1489    public Picture capturePicture() {
1490        if (null == mWebViewCore) return null; // check for out of memory tab
1491        return mWebViewCore.copyContentPicture();
1492    }
1493
1494    /**
1495     *  Return true if the browser is displaying a TextView for text input.
1496     */
1497    private boolean inEditingMode() {
1498        return mTextEntry != null && mTextEntry.getParent() != null
1499                && mTextEntry.hasFocus();
1500    }
1501
1502    private void clearTextEntry() {
1503        if (inEditingMode()) {
1504            mTextEntry.remove();
1505        }
1506    }
1507
1508    /**
1509     * Return the current scale of the WebView
1510     * @return The current scale.
1511     */
1512    public float getScale() {
1513        return mActualScale;
1514    }
1515
1516    /**
1517     * Set the initial scale for the WebView. 0 means default. If
1518     * {@link WebSettings#getUseWideViewPort()} is true, it zooms out all the
1519     * way. Otherwise it starts with 100%. If initial scale is greater than 0,
1520     * WebView starts will this value as initial scale.
1521     *
1522     * @param scaleInPercent The initial scale in percent.
1523     */
1524    public void setInitialScale(int scaleInPercent) {
1525        mInitialScale = scaleInPercent;
1526    }
1527
1528    /**
1529     * Invoke the graphical zoom picker widget for this WebView. This will
1530     * result in the zoom widget appearing on the screen to control the zoom
1531     * level of this WebView.
1532     */
1533    public void invokeZoomPicker() {
1534        if (!getSettings().supportZoom()) {
1535            Log.w(LOGTAG, "This WebView doesn't support zoom.");
1536            return;
1537        }
1538        clearTextEntry();
1539        ExtendedZoomControls zoomControls = (ExtendedZoomControls)
1540                getZoomControls();
1541        zoomControls.show(true, canZoomScrollOut());
1542        zoomControls.requestFocus();
1543        mPrivateHandler.removeCallbacks(mZoomControlRunnable);
1544        mPrivateHandler.postDelayed(mZoomControlRunnable,
1545                ZOOM_CONTROLS_TIMEOUT);
1546    }
1547
1548    /**
1549     * Return a HitTestResult based on the current focus node. If a HTML::a tag
1550     * is found and the anchor has a non-javascript url, the HitTestResult type
1551     * is set to SRC_ANCHOR_TYPE and the url is set in the "extra" field. If the
1552     * anchor does not have a url or if it is a javascript url, the type will
1553     * be UNKNOWN_TYPE and the url has to be retrieved through
1554     * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is
1555     * found, the HitTestResult type is set to IMAGE_TYPE and the url is set in
1556     * the "extra" field. A type of
1557     * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a url that has an image as
1558     * a child node. If a phone number is found, the HitTestResult type is set
1559     * to PHONE_TYPE and the phone number is set in the "extra" field of
1560     * HitTestResult. If a map address is found, the HitTestResult type is set
1561     * to GEO_TYPE and the address is set in the "extra" field of HitTestResult.
1562     * If an email address is found, the HitTestResult type is set to EMAIL_TYPE
1563     * and the email is set in the "extra" field of HitTestResult. Otherwise,
1564     * HitTestResult type is set to UNKNOWN_TYPE.
1565     */
1566    public HitTestResult getHitTestResult() {
1567        if (mNativeClass == 0) {
1568            return null;
1569        }
1570
1571        HitTestResult result = new HitTestResult();
1572
1573        if (nativeUpdateFocusNode()) {
1574            FocusNode node = mFocusNode;
1575            if (node.mIsTextField || node.mIsTextArea) {
1576                result.setType(HitTestResult.EDIT_TEXT_TYPE);
1577            } else if (node.mText != null) {
1578                String text = node.mText;
1579                if (text.startsWith(SCHEME_TEL)) {
1580                    result.setType(HitTestResult.PHONE_TYPE);
1581                    result.setExtra(text.substring(SCHEME_TEL.length()));
1582                } else if (text.startsWith(SCHEME_MAILTO)) {
1583                    result.setType(HitTestResult.EMAIL_TYPE);
1584                    result.setExtra(text.substring(SCHEME_MAILTO.length()));
1585                } else if (text.startsWith(SCHEME_GEO)) {
1586                    result.setType(HitTestResult.GEO_TYPE);
1587                    result.setExtra(URLDecoder.decode(text
1588                            .substring(SCHEME_GEO.length())));
1589                } else if (node.mIsAnchor) {
1590                    result.setType(HitTestResult.SRC_ANCHOR_TYPE);
1591                    result.setExtra(text);
1592                }
1593            }
1594        }
1595        int type = result.getType();
1596        if (type == HitTestResult.UNKNOWN_TYPE
1597                || type == HitTestResult.SRC_ANCHOR_TYPE) {
1598            // Now check to see if it is an image.
1599            int contentX = viewToContent((int) mLastTouchX + mScrollX);
1600            int contentY = viewToContent((int) mLastTouchY + mScrollY);
1601            String text = nativeImageURI(contentX, contentY);
1602            if (text != null) {
1603                result.setType(type == HitTestResult.UNKNOWN_TYPE ?
1604                        HitTestResult.IMAGE_TYPE :
1605                        HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
1606                result.setExtra(text);
1607            }
1608        }
1609        return result;
1610    }
1611
1612    /**
1613     * Request the href of an anchor element due to getFocusNodePath returning
1614     * "href." If hrefMsg is null, this method returns immediately and does not
1615     * dispatch hrefMsg to its target.
1616     *
1617     * @param hrefMsg This message will be dispatched with the result of the
1618     *            request as the data member with "url" as key. The result can
1619     *            be null.
1620     */
1621    public void requestFocusNodeHref(Message hrefMsg) {
1622        if (hrefMsg == null || mNativeClass == 0) {
1623            return;
1624        }
1625        if (nativeUpdateFocusNode()) {
1626            FocusNode node = mFocusNode;
1627            if (node.mIsAnchor) {
1628                // NOTE: We may already have the url of the anchor stored in
1629                // node.mText but it may be out of date or the caller may want
1630                // to know about javascript urls.
1631                mWebViewCore.sendMessage(EventHub.REQUEST_FOCUS_HREF,
1632                        node.mFramePointer, node.mNodePointer, hrefMsg);
1633            }
1634        }
1635    }
1636
1637    /**
1638     * Request the url of the image last touched by the user. msg will be sent
1639     * to its target with a String representing the url as its object.
1640     *
1641     * @param msg This message will be dispatched with the result of the request
1642     *            as the data member with "url" as key. The result can be null.
1643     */
1644    public void requestImageRef(Message msg) {
1645        int contentX = viewToContent((int) mLastTouchX + mScrollX);
1646        int contentY = viewToContent((int) mLastTouchY + mScrollY);
1647        String ref = nativeImageURI(contentX, contentY);
1648        Bundle data = msg.getData();
1649        data.putString("url", ref);
1650        msg.setData(data);
1651        msg.sendToTarget();
1652    }
1653
1654    private static int pinLoc(int x, int viewMax, int docMax) {
1655//        Log.d(LOGTAG, "-- pinLoc " + x + " " + viewMax + " " + docMax);
1656        if (docMax < viewMax) {   // the doc has room on the sides for "blank"
1657            x = -(viewMax - docMax) >> 1;
1658//            Log.d(LOGTAG, "--- center " + x);
1659        } else if (x < 0) {
1660            x = 0;
1661//            Log.d(LOGTAG, "--- zero");
1662        } else if (x + viewMax > docMax) {
1663            x = docMax - viewMax;
1664//            Log.d(LOGTAG, "--- pin " + x);
1665        }
1666        return x;
1667    }
1668
1669    // Expects x in view coordinates
1670    private int pinLocX(int x) {
1671        return pinLoc(x, getViewWidth(), computeHorizontalScrollRange());
1672    }
1673
1674    // Expects y in view coordinates
1675    private int pinLocY(int y) {
1676        return pinLoc(y, getViewHeight(), computeVerticalScrollRange());
1677    }
1678
1679    /*package*/ int viewToContent(int x) {
1680        return Math.round(x * mInvActualScale);
1681    }
1682
1683    private int contentToView(int x) {
1684        return Math.round(x * mActualScale);
1685    }
1686
1687    // Called by JNI to invalidate the View, given rectangle coordinates in
1688    // content space
1689    private void viewInvalidate(int l, int t, int r, int b) {
1690        invalidate(contentToView(l), contentToView(t), contentToView(r),
1691                contentToView(b));
1692    }
1693
1694    // Called by JNI to invalidate the View after a delay, given rectangle
1695    // coordinates in content space
1696    private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) {
1697        postInvalidateDelayed(delay, contentToView(l), contentToView(t),
1698                contentToView(r), contentToView(b));
1699    }
1700
1701    private Rect contentToView(Rect x) {
1702        return new Rect(contentToView(x.left), contentToView(x.top)
1703                , contentToView(x.right), contentToView(x.bottom));
1704    }
1705
1706    /* call from webcoreview.draw(), so we're still executing in the UI thread
1707    */
1708    private void recordNewContentSize(int w, int h, boolean updateLayout) {
1709
1710        // premature data from webkit, ignore
1711        if ((w | h) == 0) {
1712            return;
1713        }
1714
1715        // don't abort a scroll animation if we didn't change anything
1716        if (mContentWidth != w || mContentHeight != h) {
1717            // record new dimensions
1718            mContentWidth = w;
1719            mContentHeight = h;
1720            // If history Picture is drawn, don't update scroll. They will be
1721            // updated when we get out of that mode.
1722            if (!mDrawHistory) {
1723                // repin our scroll, taking into account the new content size
1724                int oldX = mScrollX;
1725                int oldY = mScrollY;
1726                mScrollX = pinLocX(mScrollX);
1727                mScrollY = pinLocY(mScrollY);
1728                // android.util.Log.d("skia", "recordNewContentSize -
1729                // abortAnimation");
1730                mScroller.abortAnimation(); // just in case
1731                if (oldX != mScrollX || oldY != mScrollY) {
1732                    sendOurVisibleRect();
1733                }
1734            }
1735        }
1736        contentSizeChanged(updateLayout);
1737    }
1738
1739    private void setNewZoomScale(float scale, boolean force) {
1740        if (scale < mMinZoomScale) {
1741            scale = mMinZoomScale;
1742        } else if (scale > mMaxZoomScale) {
1743            scale = mMaxZoomScale;
1744        }
1745        if (scale != mActualScale || force) {
1746            if (mDrawHistory) {
1747                // If history Picture is drawn, don't update scroll. They will
1748                // be updated when we get out of that mode.
1749                if (scale != mActualScale && !mPreviewZoomOnly) {
1750                    mCallbackProxy.onScaleChanged(mActualScale, scale);
1751                }
1752                mActualScale = scale;
1753                mInvActualScale = 1 / scale;
1754                if (!mPreviewZoomOnly) {
1755                    sendViewSizeZoom();
1756                }
1757            } else {
1758                // update our scroll so we don't appear to jump
1759                // i.e. keep the center of the doc in the center of the view
1760
1761                int oldX = mScrollX;
1762                int oldY = mScrollY;
1763                float ratio = scale * mInvActualScale;   // old inverse
1764                float sx = ratio * oldX + (ratio - 1) * mZoomCenterX;
1765                float sy = ratio * oldY + (ratio - 1) * mZoomCenterY;
1766
1767                // now update our new scale and inverse
1768                if (scale != mActualScale && !mPreviewZoomOnly) {
1769                    mCallbackProxy.onScaleChanged(mActualScale, scale);
1770                }
1771                mActualScale = scale;
1772                mInvActualScale = 1 / scale;
1773
1774                // as we don't have animation for scaling, don't do animation
1775                // for scrolling, as it causes weird intermediate state
1776                //        pinScrollTo(Math.round(sx), Math.round(sy));
1777                mScrollX = pinLocX(Math.round(sx));
1778                mScrollY = pinLocY(Math.round(sy));
1779
1780                if (!mPreviewZoomOnly) {
1781                    sendViewSizeZoom();
1782                    sendOurVisibleRect();
1783                }
1784            }
1785        }
1786    }
1787
1788    // Used to avoid sending many visible rect messages.
1789    private Rect mLastVisibleRectSent;
1790    private Rect mLastGlobalRect;
1791
1792    private Rect sendOurVisibleRect() {
1793        Rect rect = new Rect();
1794        calcOurContentVisibleRect(rect);
1795        if (mFindIsUp) {
1796            rect.bottom -= viewToContent(FIND_HEIGHT);
1797        }
1798        // Rect.equals() checks for null input.
1799        if (!rect.equals(mLastVisibleRectSent)) {
1800            mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET,
1801                                     rect.left, rect.top);
1802            mLastVisibleRectSent = rect;
1803        }
1804        Rect globalRect = new Rect();
1805        if (getGlobalVisibleRect(globalRect)
1806                && !globalRect.equals(mLastGlobalRect)) {
1807            // TODO: the global offset is only used by windowRect()
1808            // in ChromeClientAndroid ; other clients such as touch
1809            // and mouse events could return view + screen relative points.
1810            mWebViewCore.sendMessage(EventHub.SET_GLOBAL_BOUNDS, globalRect);
1811            mLastGlobalRect = globalRect;
1812        }
1813        return rect;
1814    }
1815
1816    // Sets r to be the visible rectangle of our webview in view coordinates
1817    private void calcOurVisibleRect(Rect r) {
1818        Point p = new Point();
1819        getGlobalVisibleRect(r, p);
1820        r.offset(-p.x, -p.y);
1821    }
1822
1823    // Sets r to be our visible rectangle in content coordinates
1824    private void calcOurContentVisibleRect(Rect r) {
1825        calcOurVisibleRect(r);
1826        r.left = viewToContent(r.left);
1827        r.top = viewToContent(r.top);
1828        r.right = viewToContent(r.right);
1829        r.bottom = viewToContent(r.bottom);
1830    }
1831
1832    /**
1833     * Compute unzoomed width and height, and if they differ from the last
1834     * values we sent, send them to webkit (to be used has new viewport)
1835     *
1836     * @return true if new values were sent
1837     */
1838    private boolean sendViewSizeZoom() {
1839        int viewWidth = getViewWidth();
1840        int newWidth = Math.round(viewWidth * mInvActualScale);
1841        int newHeight = Math.round(getViewHeight() * mInvActualScale);
1842        /*
1843         * Because the native side may have already done a layout before the
1844         * View system was able to measure us, we have to send a height of 0 to
1845         * remove excess whitespace when we grow our width. This will trigger a
1846         * layout and a change in content size. This content size change will
1847         * mean that contentSizeChanged will either call this method directly or
1848         * indirectly from onSizeChanged.
1849         */
1850        if (newWidth > mLastWidthSent && mWrapContent) {
1851            newHeight = 0;
1852        }
1853        // Avoid sending another message if the dimensions have not changed.
1854        if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) {
1855            mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED,
1856                    newWidth, newHeight, new Integer(viewWidth));
1857            mLastWidthSent = newWidth;
1858            mLastHeightSent = newHeight;
1859            return true;
1860        }
1861        return false;
1862    }
1863
1864    @Override
1865    protected int computeHorizontalScrollRange() {
1866        if (mDrawHistory) {
1867            return mHistoryWidth;
1868        } else {
1869            return contentToView(mContentWidth);
1870        }
1871    }
1872
1873    // Make sure this stays in sync with the actual height of the FindDialog.
1874    private static final int FIND_HEIGHT = 79;
1875
1876    @Override
1877    protected int computeVerticalScrollRange() {
1878        if (mDrawHistory) {
1879            return mHistoryHeight;
1880        } else {
1881            int height = contentToView(mContentHeight);
1882            if (mFindIsUp) {
1883                height += FIND_HEIGHT;
1884            }
1885            return height;
1886        }
1887    }
1888
1889    /**
1890     * Get the url for the current page. This is not always the same as the url
1891     * passed to WebViewClient.onPageStarted because although the load for
1892     * that url has begun, the current page may not have changed.
1893     * @return The url for the current page.
1894     */
1895    public String getUrl() {
1896        WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem();
1897        return h != null ? h.getUrl() : null;
1898    }
1899
1900    /**
1901     * Get the original url for the current page. This is not always the same
1902     * as the url passed to WebViewClient.onPageStarted because although the
1903     * load for that url has begun, the current page may not have changed.
1904     * Also, there may have been redirects resulting in a different url to that
1905     * originally requested.
1906     * @return The url that was originally requested for the current page.
1907     *
1908     * @hide pending API Council approval
1909     */
1910    public String getOriginalUrl() {
1911        WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem();
1912        return h != null ? h.getOriginalUrl() : null;
1913    }
1914
1915    /**
1916     * Get the title for the current page. This is the title of the current page
1917     * until WebViewClient.onReceivedTitle is called.
1918     * @return The title for the current page.
1919     */
1920    public String getTitle() {
1921        WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem();
1922        return h != null ? h.getTitle() : null;
1923    }
1924
1925    /**
1926     * Get the favicon for the current page. This is the favicon of the current
1927     * page until WebViewClient.onReceivedIcon is called.
1928     * @return The favicon for the current page.
1929     */
1930    public Bitmap getFavicon() {
1931        WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem();
1932        return h != null ? h.getFavicon() : null;
1933    }
1934
1935    /**
1936     * Get the progress for the current page.
1937     * @return The progress for the current page between 0 and 100.
1938     */
1939    public int getProgress() {
1940        return mCallbackProxy.getProgress();
1941    }
1942
1943    /**
1944     * @return the height of the HTML content.
1945     */
1946    public int getContentHeight() {
1947        return mContentHeight;
1948    }
1949
1950    /**
1951     * Pause all layout, parsing, and javascript timers. This can be useful if
1952     * the WebView is not visible or the application has been paused.
1953     */
1954    public void pauseTimers() {
1955        mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS);
1956    }
1957
1958    /**
1959     * Resume all layout, parsing, and javascript timers. This will resume
1960     * dispatching all timers.
1961     */
1962    public void resumeTimers() {
1963        mWebViewCore.sendMessage(EventHub.RESUME_TIMERS);
1964    }
1965
1966    /**
1967     * Clear the resource cache. This will cause resources to be re-downloaded
1968     * if accessed again.
1969     * <p>
1970     * Note: this really needs to be a static method as it clears cache for all
1971     * WebView. But we need mWebViewCore to send message to WebCore thread, so
1972     * we can't make this static.
1973     */
1974    public void clearCache(boolean includeDiskFiles) {
1975        mWebViewCore.sendMessage(EventHub.CLEAR_CACHE,
1976                includeDiskFiles ? 1 : 0, 0);
1977    }
1978
1979    /**
1980     * Make sure that clearing the form data removes the adapter from the
1981     * currently focused textfield if there is one.
1982     */
1983    public void clearFormData() {
1984        if (inEditingMode()) {
1985            AutoCompleteAdapter adapter = null;
1986            mTextEntry.setAdapterCustom(adapter);
1987        }
1988    }
1989
1990    /**
1991     * Tell the WebView to clear its internal back/forward list.
1992     */
1993    public void clearHistory() {
1994        mCallbackProxy.getBackForwardList().setClearPending();
1995        mWebViewCore.sendMessage(EventHub.CLEAR_HISTORY);
1996    }
1997
1998    /**
1999     * Clear the SSL preferences table stored in response to proceeding with SSL
2000     * certificate errors.
2001     */
2002    public void clearSslPreferences() {
2003        mWebViewCore.sendMessage(EventHub.CLEAR_SSL_PREF_TABLE);
2004    }
2005
2006    /**
2007     * Return the WebBackForwardList for this WebView. This contains the
2008     * back/forward list for use in querying each item in the history stack.
2009     * This is a copy of the private WebBackForwardList so it contains only a
2010     * snapshot of the current state. Multiple calls to this method may return
2011     * different objects. The object returned from this method will not be
2012     * updated to reflect any new state.
2013     */
2014    public WebBackForwardList copyBackForwardList() {
2015        return mCallbackProxy.getBackForwardList().clone();
2016    }
2017
2018    /*
2019     * Highlight and scroll to the next occurance of String in findAll.
2020     * Wraps the page infinitely, and scrolls.  Must be called after
2021     * calling findAll.
2022     *
2023     * @param forward Direction to search.
2024     */
2025    public void findNext(boolean forward) {
2026        nativeFindNext(forward);
2027    }
2028
2029    /*
2030     * Find all instances of find on the page and highlight them.
2031     * @param find  String to find.
2032     * @return int  The number of occurances of the String "find"
2033     *              that were found.
2034     */
2035    public int findAll(String find) {
2036        mFindIsUp = true;
2037        int result = nativeFindAll(find.toLowerCase(), find.toUpperCase());
2038        invalidate();
2039        return result;
2040    }
2041
2042    // Used to know whether the find dialog is open.  Affects whether
2043    // or not we draw the highlights for matches.
2044    private boolean mFindIsUp;
2045
2046    private native int nativeFindAll(String findLower, String findUpper);
2047    private native void nativeFindNext(boolean forward);
2048
2049    /**
2050     * Return the first substring consisting of the address of a physical
2051     * location. Currently, only addresses in the United States are detected,
2052     * and consist of:
2053     * - a house number
2054     * - a street name
2055     * - a street type (Road, Circle, etc), either spelled out or abbreviated
2056     * - a city name
2057     * - a state or territory, either spelled out or two-letter abbr.
2058     * - an optional 5 digit or 9 digit zip code.
2059     *
2060     * All names must be correctly capitalized, and the zip code, if present,
2061     * must be valid for the state. The street type must be a standard USPS
2062     * spelling or abbreviation. The state or territory must also be spelled
2063     * or abbreviated using USPS standards. The house number may not exceed
2064     * five digits.
2065     * @param addr The string to search for addresses.
2066     *
2067     * @return the address, or if no address is found, return null.
2068     */
2069    public static String findAddress(String addr) {
2070        return WebViewCore.nativeFindAddress(addr);
2071    }
2072
2073    /*
2074     * Clear the highlighting surrounding text matches created by findAll.
2075     */
2076    public void clearMatches() {
2077        mFindIsUp = false;
2078        nativeSetFindIsDown();
2079        // Now that the dialog has been removed, ensure that we scroll to a
2080        // location that is not beyond the end of the page.
2081        pinScrollTo(mScrollX, mScrollY, false, 0);
2082        invalidate();
2083    }
2084
2085    /**
2086     * Query the document to see if it contains any image references. The
2087     * message object will be dispatched with arg1 being set to 1 if images
2088     * were found and 0 if the document does not reference any images.
2089     * @param response The message that will be dispatched with the result.
2090     */
2091    public void documentHasImages(Message response) {
2092        if (response == null) {
2093            return;
2094        }
2095        mWebViewCore.sendMessage(EventHub.DOC_HAS_IMAGES, response);
2096    }
2097
2098    @Override
2099    public void computeScroll() {
2100        if (mScroller.computeScrollOffset()) {
2101            int oldX = mScrollX;
2102            int oldY = mScrollY;
2103            mScrollX = mScroller.getCurrX();
2104            mScrollY = mScroller.getCurrY();
2105            postInvalidate();  // So we draw again
2106            if (oldX != mScrollX || oldY != mScrollY) {
2107                // as onScrollChanged() is not called, sendOurVisibleRect()
2108                // needs to be call explicitly
2109                sendOurVisibleRect();
2110            }
2111        } else {
2112            super.computeScroll();
2113        }
2114    }
2115
2116    private static int computeDuration(int dx, int dy) {
2117        int distance = Math.max(Math.abs(dx), Math.abs(dy));
2118        int duration = distance * 1000 / STD_SPEED;
2119        return Math.min(duration, MAX_DURATION);
2120    }
2121
2122    // helper to pin the scrollBy parameters (already in view coordinates)
2123    // returns true if the scroll was changed
2124    private boolean pinScrollBy(int dx, int dy, boolean animate, int animationDuration) {
2125        return pinScrollTo(mScrollX + dx, mScrollY + dy, animate, animationDuration);
2126    }
2127
2128    // helper to pin the scrollTo parameters (already in view coordinates)
2129    // returns true if the scroll was changed
2130    private boolean pinScrollTo(int x, int y, boolean animate, int animationDuration) {
2131        x = pinLocX(x);
2132        y = pinLocY(y);
2133        int dx = x - mScrollX;
2134        int dy = y - mScrollY;
2135
2136        if ((dx | dy) == 0) {
2137            return false;
2138        }
2139
2140        if (true && animate) {
2141            //        Log.d(LOGTAG, "startScroll: " + dx + " " + dy);
2142
2143            mScroller.startScroll(mScrollX, mScrollY, dx, dy,
2144                    animationDuration > 0 ? animationDuration : computeDuration(dx, dy));
2145            invalidate();
2146        } else {
2147            mScroller.abortAnimation(); // just in case
2148            scrollTo(x, y);
2149        }
2150        return true;
2151    }
2152
2153    // Scale from content to view coordinates, and pin.
2154    // Also called by jni webview.cpp
2155    private void setContentScrollBy(int cx, int cy, boolean animate) {
2156        if (mDrawHistory) {
2157            // disallow WebView to change the scroll position as History Picture
2158            // is used in the view system.
2159            // TODO: as we switchOutDrawHistory when trackball or navigation
2160            // keys are hit, this should be safe. Right?
2161            return;
2162        }
2163        cx = contentToView(cx);
2164        cy = contentToView(cy);
2165        if (mHeightCanMeasure) {
2166            // move our visible rect according to scroll request
2167            if (cy != 0) {
2168                Rect tempRect = new Rect();
2169                calcOurVisibleRect(tempRect);
2170                tempRect.offset(cx, cy);
2171                requestRectangleOnScreen(tempRect);
2172            }
2173            // FIXME: We scroll horizontally no matter what because currently
2174            // ScrollView and ListView will not scroll horizontally.
2175            // FIXME: Why do we only scroll horizontally if there is no
2176            // vertical scroll?
2177//                Log.d(LOGTAG, "setContentScrollBy cy=" + cy);
2178            if (cy == 0 && cx != 0) {
2179                pinScrollBy(cx, 0, animate, 0);
2180            }
2181        } else {
2182            pinScrollBy(cx, cy, animate, 0);
2183        }
2184    }
2185
2186    // scale from content to view coordinates, and pin
2187    // return true if pin caused the final x/y different than the request cx/cy;
2188    // return false if the view scroll to the exact position as it is requested.
2189    private boolean setContentScrollTo(int cx, int cy) {
2190        if (mDrawHistory) {
2191            // disallow WebView to change the scroll position as History Picture
2192            // is used in the view system.
2193            // One known case where this is called is that WebCore tries to
2194            // restore the scroll position. As history Picture already uses the
2195            // saved scroll position, it is ok to skip this.
2196            return false;
2197        }
2198        int vx = contentToView(cx);
2199        int vy = contentToView(cy);
2200//        Log.d(LOGTAG, "content scrollTo [" + cx + " " + cy + "] view=[" +
2201//                      vx + " " + vy + "]");
2202        pinScrollTo(vx, vy, false, 0);
2203        if (mScrollX != vx || mScrollY != vy) {
2204            return true;
2205        } else {
2206            return false;
2207        }
2208    }
2209
2210    // scale from content to view coordinates, and pin
2211    private void spawnContentScrollTo(int cx, int cy) {
2212        if (mDrawHistory) {
2213            // disallow WebView to change the scroll position as History Picture
2214            // is used in the view system.
2215            return;
2216        }
2217        int vx = contentToView(cx);
2218        int vy = contentToView(cy);
2219        pinScrollTo(vx, vy, true, 0);
2220    }
2221
2222    /**
2223     * These are from webkit, and are in content coordinate system (unzoomed)
2224     */
2225    private void contentSizeChanged(boolean updateLayout) {
2226        // suppress 0,0 since we usually see real dimensions soon after
2227        // this avoids drawing the prev content in a funny place. If we find a
2228        // way to consolidate these notifications, this check may become
2229        // obsolete
2230        if ((mContentWidth | mContentHeight) == 0) {
2231            return;
2232        }
2233
2234        if (mHeightCanMeasure) {
2235            if (getMeasuredHeight() != contentToView(mContentHeight)
2236                    && updateLayout) {
2237                requestLayout();
2238            }
2239        } else if (mWidthCanMeasure) {
2240            if (getMeasuredWidth() != contentToView(mContentWidth)
2241                    && updateLayout) {
2242                requestLayout();
2243            }
2244        } else {
2245            // If we don't request a layout, try to send our view size to the
2246            // native side to ensure that WebCore has the correct dimensions.
2247            sendViewSizeZoom();
2248        }
2249    }
2250
2251    /**
2252     * Set the WebViewClient that will receive various notifications and
2253     * requests. This will replace the current handler.
2254     * @param client An implementation of WebViewClient.
2255     */
2256    public void setWebViewClient(WebViewClient client) {
2257        mCallbackProxy.setWebViewClient(client);
2258    }
2259
2260    /**
2261     * Register the interface to be used when content can not be handled by
2262     * the rendering engine, and should be downloaded instead. This will replace
2263     * the current handler.
2264     * @param listener An implementation of DownloadListener.
2265     */
2266    public void setDownloadListener(DownloadListener listener) {
2267        mCallbackProxy.setDownloadListener(listener);
2268    }
2269
2270    /**
2271     * Set the chrome handler. This is an implementation of WebChromeClient for
2272     * use in handling Javascript dialogs, favicons, titles, and the progress.
2273     * This will replace the current handler.
2274     * @param client An implementation of WebChromeClient.
2275     */
2276    public void setWebChromeClient(WebChromeClient client) {
2277        mCallbackProxy.setWebChromeClient(client);
2278    }
2279
2280    /**
2281     * Set the Picture listener. This is an interface used to receive
2282     * notifications of a new Picture.
2283     * @param listener An implementation of WebView.PictureListener.
2284     */
2285    public void setPictureListener(PictureListener listener) {
2286        mPictureListener = listener;
2287    }
2288
2289    /**
2290     * {@hide}
2291     */
2292    /* FIXME: Debug only! Remove for SDK! */
2293    public void externalRepresentation(Message callback) {
2294        mWebViewCore.sendMessage(EventHub.REQUEST_EXT_REPRESENTATION, callback);
2295    }
2296
2297    /**
2298     * {@hide}
2299     */
2300    /* FIXME: Debug only! Remove for SDK! */
2301    public void documentAsText(Message callback) {
2302        mWebViewCore.sendMessage(EventHub.REQUEST_DOC_AS_TEXT, callback);
2303    }
2304
2305    /**
2306     * Use this function to bind an object to Javascript so that the
2307     * methods can be accessed from Javascript.
2308     * IMPORTANT, the object that is bound runs in another thread and
2309     * not in the thread that it was constructed in.
2310     * @param obj The class instance to bind to Javascript
2311     * @param interfaceName The name to used to expose the class in Javascript
2312     */
2313    public void addJavascriptInterface(Object obj, String interfaceName) {
2314        // Use Hashmap rather than Bundle as Bundles can't cope with Objects
2315        HashMap arg = new HashMap();
2316        arg.put("object", obj);
2317        arg.put("interfaceName", interfaceName);
2318        mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg);
2319    }
2320
2321    /**
2322     * Return the WebSettings object used to control the settings for this
2323     * WebView.
2324     * @return A WebSettings object that can be used to control this WebView's
2325     *         settings.
2326     */
2327    public WebSettings getSettings() {
2328        return mWebViewCore.getSettings();
2329    }
2330
2331   /**
2332    * Return the list of currently loaded plugins.
2333    * @return The list of currently loaded plugins.
2334    */
2335    public static synchronized PluginList getPluginList() {
2336        if (sPluginList == null) {
2337            sPluginList = new PluginList();
2338        }
2339        return sPluginList;
2340    }
2341
2342   /**
2343    * Signal the WebCore thread to refresh its list of plugins. Use
2344    * this if the directory contents of one of the plugin directories
2345    * has been modified and needs its changes reflecting. May cause
2346    * plugin load and/or unload.
2347    * @param reloadOpenPages Set to true to reload all open pages.
2348    */
2349    public void refreshPlugins(boolean reloadOpenPages) {
2350        if (mWebViewCore != null) {
2351            mWebViewCore.sendMessage(EventHub.REFRESH_PLUGINS, reloadOpenPages);
2352        }
2353    }
2354
2355    //-------------------------------------------------------------------------
2356    // Override View methods
2357    //-------------------------------------------------------------------------
2358
2359    @Override
2360    protected void finalize() throws Throwable {
2361        destroy();
2362    }
2363
2364    @Override
2365    protected void onDraw(Canvas canvas) {
2366        // if mNativeClass is 0, the WebView has been destroyed. Do nothing.
2367        if (mNativeClass == 0) {
2368            return;
2369        }
2370        if (mWebViewCore.mEndScaleZoom) {
2371            mWebViewCore.mEndScaleZoom = false;
2372            if (mTouchMode >= FIRST_SCROLL_ZOOM
2373                    && mTouchMode <= LAST_SCROLL_ZOOM) {
2374                setHorizontalScrollBarEnabled(true);
2375                setVerticalScrollBarEnabled(true);
2376                mTouchMode = TOUCH_DONE_MODE;
2377            }
2378        }
2379        int sc = canvas.save();
2380        if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) {
2381            scrollZoomDraw(canvas);
2382        } else {
2383            nativeRecomputeFocus();
2384            // Update the buttons in the picture, so when we draw the picture
2385            // to the screen, they are in the correct state.
2386            // Tell the native side if user is a) touching the screen,
2387            // b) pressing the trackball down, or c) pressing the enter key
2388            // If the focus is a button, we need to draw it in the pressed
2389            // state.
2390            // If mNativeClass is 0, we should not reach here, so we do not
2391            // need to check it again.
2392            nativeRecordButtons(hasFocus() && hasWindowFocus(),
2393                    mTouchMode == TOUCH_SHORTPRESS_START_MODE
2394                    || mTrackballDown || mGotEnterDown, false);
2395            drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing);
2396        }
2397        canvas.restoreToCount(sc);
2398
2399        if (AUTO_REDRAW_HACK && mAutoRedraw) {
2400            invalidate();
2401        }
2402    }
2403
2404    @Override
2405    public void setLayoutParams(ViewGroup.LayoutParams params) {
2406        if (params.height == LayoutParams.WRAP_CONTENT) {
2407            mWrapContent = true;
2408        }
2409        super.setLayoutParams(params);
2410    }
2411
2412    @Override
2413    public boolean performLongClick() {
2414        if (inEditingMode()) {
2415            return mTextEntry.performLongClick();
2416        } else {
2417            return super.performLongClick();
2418        }
2419    }
2420
2421    private void drawCoreAndFocusRing(Canvas canvas, int color,
2422        boolean drawFocus) {
2423        if (mDrawHistory) {
2424            canvas.scale(mActualScale, mActualScale);
2425            canvas.drawPicture(mHistoryPicture);
2426            return;
2427        }
2428
2429        boolean animateZoom = mZoomScale != 0;
2430        boolean animateScroll = !mScroller.isFinished()
2431                || mVelocityTracker != null;
2432        if (animateZoom) {
2433            float zoomScale;
2434            int interval = (int) (SystemClock.uptimeMillis() - mZoomStart);
2435            if (interval < ZOOM_ANIMATION_LENGTH) {
2436                float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
2437                zoomScale = 1.0f / (mInvInitialZoomScale
2438                        + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
2439                invalidate();
2440            } else {
2441                zoomScale = mZoomScale;
2442                // set mZoomScale to be 0 as we have done animation
2443                mZoomScale = 0;
2444            }
2445            float scale = (mActualScale - zoomScale) * mInvActualScale;
2446            float tx = scale * (mZoomCenterX + mScrollX);
2447            float ty = scale * (mZoomCenterY + mScrollY);
2448
2449            // this block pins the translate to "legal" bounds. This makes the
2450            // animation a bit non-obvious, but it means we won't pop when the
2451            // "real" zoom takes effect
2452            if (true) {
2453               // canvas.translate(mScrollX, mScrollY);
2454                tx -= mScrollX;
2455                ty -= mScrollY;
2456                tx = -pinLoc(-Math.round(tx), getViewWidth(), Math
2457                        .round(mContentWidth * zoomScale));
2458                ty = -pinLoc(-Math.round(ty), getViewHeight(), Math
2459                        .round(mContentHeight * zoomScale));
2460                tx += mScrollX;
2461                ty += mScrollY;
2462            }
2463            canvas.translate(tx, ty);
2464            canvas.scale(zoomScale, zoomScale);
2465        } else {
2466            canvas.scale(mActualScale, mActualScale);
2467        }
2468
2469        mWebViewCore.drawContentPicture(canvas, color, animateZoom,
2470                animateScroll);
2471
2472        if (mNativeClass == 0) return;
2473        if (mShiftIsPressed) {
2474            if (mTouchSelection) {
2475                nativeDrawSelectionRegion(canvas);
2476            } else {
2477                nativeDrawSelection(canvas, mSelectX, mSelectY,
2478                        mExtendSelection);
2479            }
2480        } else if (drawFocus) {
2481            if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) {
2482                mTouchMode = TOUCH_SHORTPRESS_MODE;
2483                HitTestResult hitTest = getHitTestResult();
2484                if (hitTest != null &&
2485                        hitTest.mType != HitTestResult.UNKNOWN_TYPE) {
2486                    mPrivateHandler.sendMessageDelayed(mPrivateHandler
2487                            .obtainMessage(SWITCH_TO_LONGPRESS),
2488                            LONG_PRESS_TIMEOUT);
2489                }
2490            }
2491            nativeDrawFocusRing(canvas);
2492        }
2493        // When the FindDialog is up, only draw the matches if we are not in
2494        // the process of scrolling them into view.
2495        if (mFindIsUp && !animateScroll) {
2496            nativeDrawMatches(canvas);
2497        }
2498    }
2499
2500    private native void nativeDrawMatches(Canvas canvas);
2501
2502    private float scrollZoomGridScale(float invScale) {
2503        float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID)
2504            / (float) SCROLL_ZOOM_GRID;
2505        return 1.0f / griddedInvScale;
2506    }
2507
2508    private float scrollZoomX(float scale) {
2509        int width = getViewWidth();
2510        float maxScrollZoomX = mContentWidth * scale - width;
2511        int maxX = mContentWidth - width;
2512        return -(maxScrollZoomX > 0 ? mZoomScrollX * maxScrollZoomX / maxX
2513                : maxScrollZoomX / 2);
2514    }
2515
2516    private float scrollZoomY(float scale) {
2517        int height = getViewHeight();
2518        float maxScrollZoomY = mContentHeight * scale - height;
2519        int maxY = mContentHeight - height;
2520        return -(maxScrollZoomY > 0 ? mZoomScrollY * maxScrollZoomY / maxY
2521                : maxScrollZoomY / 2);
2522    }
2523
2524    private void drawMagnifyFrame(Canvas canvas, Rect frame, Paint paint) {
2525        final float ADORNMENT_LEN = 16.0f;
2526        float width = frame.width();
2527        float height = frame.height();
2528        Path path = new Path();
2529        path.moveTo(-ADORNMENT_LEN, -ADORNMENT_LEN);
2530        path.lineTo(0, 0);
2531        path.lineTo(width, 0);
2532        path.lineTo(width + ADORNMENT_LEN, -ADORNMENT_LEN);
2533        path.moveTo(-ADORNMENT_LEN, height + ADORNMENT_LEN);
2534        path.lineTo(0, height);
2535        path.lineTo(width, height);
2536        path.lineTo(width + ADORNMENT_LEN, height + ADORNMENT_LEN);
2537        path.moveTo(0, 0);
2538        path.lineTo(0, height);
2539        path.moveTo(width, 0);
2540        path.lineTo(width, height);
2541        path.offset(frame.left, frame.top);
2542        canvas.drawPath(path, paint);
2543    }
2544
2545    // Returns frame surrounding magified portion of screen while
2546    // scroll-zoom is enabled. The frame is also used to center the
2547    // zoom-in zoom-out points at the start and end of the animation.
2548    private Rect scrollZoomFrame(int width, int height, float halfScale) {
2549        Rect scrollFrame = new Rect();
2550        scrollFrame.set(mZoomScrollX, mZoomScrollY,
2551                mZoomScrollX + width, mZoomScrollY + height);
2552        if (mContentWidth * mZoomScrollLimit < width) {
2553            float scale = zoomFrameScaleX(width, halfScale, 1.0f);
2554            float offsetX = (width * scale - width) * 0.5f;
2555            scrollFrame.left -= offsetX;
2556            scrollFrame.right += offsetX;
2557        }
2558        if (mContentHeight * mZoomScrollLimit < height) {
2559            float scale = zoomFrameScaleY(height, halfScale, 1.0f);
2560            float offsetY = (height * scale - height) * 0.5f;
2561            scrollFrame.top -= offsetY;
2562            scrollFrame.bottom += offsetY;
2563        }
2564        return scrollFrame;
2565    }
2566
2567    private float zoomFrameScaleX(int width, float halfScale, float noScale) {
2568        // mContentWidth > width > mContentWidth * mZoomScrollLimit
2569        if (mContentWidth <= width) {
2570            return halfScale;
2571        }
2572        float part = (width - mContentWidth * mZoomScrollLimit)
2573                / (width * (1 - mZoomScrollLimit));
2574        return halfScale * part + noScale * (1.0f - part);
2575    }
2576
2577    private float zoomFrameScaleY(int height, float halfScale, float noScale) {
2578        if (mContentHeight <= height) {
2579            return halfScale;
2580        }
2581        float part = (height - mContentHeight * mZoomScrollLimit)
2582                / (height * (1 - mZoomScrollLimit));
2583        return halfScale * part + noScale * (1.0f - part);
2584    }
2585
2586    private float scrollZoomMagScale(float invScale) {
2587        return (invScale * 2 + mInvActualScale) / 3;
2588    }
2589
2590    private void scrollZoomDraw(Canvas canvas) {
2591        float invScale = mZoomScrollInvLimit;
2592        int elapsed = 0;
2593        if (mTouchMode != SCROLL_ZOOM_OUT) {
2594            elapsed = (int) Math.min(System.currentTimeMillis()
2595                - mZoomScrollStart, SCROLL_ZOOM_DURATION);
2596            float transitionScale = (mZoomScrollInvLimit - mInvActualScale)
2597                    * elapsed / SCROLL_ZOOM_DURATION;
2598            if (mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) {
2599                invScale = mInvActualScale + transitionScale;
2600            } else { /* if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) */
2601                invScale = mZoomScrollInvLimit - transitionScale;
2602            }
2603        }
2604        float scale = scrollZoomGridScale(invScale);
2605        invScale = 1.0f / scale;
2606        int width = getViewWidth();
2607        int height = getViewHeight();
2608        float halfScale = scrollZoomMagScale(invScale);
2609        Rect scrollFrame = scrollZoomFrame(width, height, halfScale);
2610        if (elapsed == SCROLL_ZOOM_DURATION) {
2611            if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) {
2612                setHorizontalScrollBarEnabled(true);
2613                setVerticalScrollBarEnabled(true);
2614                updateTextEntry();
2615                scrollTo((int) (scrollFrame.centerX() * mActualScale)
2616                        - (width >> 1), (int) (scrollFrame.centerY()
2617                        * mActualScale) - (height >> 1));
2618                mTouchMode = TOUCH_DONE_MODE;
2619            } else {
2620                mTouchMode = SCROLL_ZOOM_OUT;
2621            }
2622        }
2623        float newX = scrollZoomX(scale);
2624        float newY = scrollZoomY(scale);
2625        if (LOGV_ENABLED) {
2626            Log.v(LOGTAG, "scrollZoomDraw scale=" + scale + " + (" + newX
2627                    + ", " + newY + ") mZoomScroll=(" + mZoomScrollX + ", "
2628                    + mZoomScrollY + ")" + " invScale=" + invScale + " scale="
2629                    + scale);
2630        }
2631        canvas.translate(newX, newY);
2632        canvas.scale(scale, scale);
2633        boolean animating = mTouchMode != SCROLL_ZOOM_OUT;
2634        if (mDrawHistory) {
2635            int sc = canvas.save(Canvas.CLIP_SAVE_FLAG);
2636            Rect clip = new Rect(0, 0, mHistoryPicture.getWidth(),
2637                    mHistoryPicture.getHeight());
2638            canvas.clipRect(clip, Region.Op.DIFFERENCE);
2639            canvas.drawColor(mBackgroundColor);
2640            canvas.restoreToCount(sc);
2641            canvas.drawPicture(mHistoryPicture);
2642        } else {
2643            mWebViewCore.drawContentPicture(canvas, mBackgroundColor,
2644                    animating, true);
2645        }
2646        if (mTouchMode == TOUCH_DONE_MODE) {
2647            return;
2648        }
2649        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
2650        paint.setStyle(Paint.Style.STROKE);
2651        paint.setStrokeWidth(30.0f);
2652        paint.setARGB(0x50, 0, 0, 0);
2653        int maxX = mContentWidth - width;
2654        int maxY = mContentHeight - height;
2655        if (true) { // experiment: draw hint to place finger off magnify area
2656            drawMagnifyFrame(canvas, scrollFrame, paint);
2657        } else {
2658            canvas.drawRect(scrollFrame, paint);
2659        }
2660        int sc = canvas.save();
2661        canvas.clipRect(scrollFrame);
2662        float halfX = (float) mZoomScrollX / maxX;
2663        if (mContentWidth * mZoomScrollLimit < width) {
2664            halfX = zoomFrameScaleX(width, 0.5f, halfX);
2665        }
2666        float halfY = (float) mZoomScrollY / maxY;
2667        if (mContentHeight * mZoomScrollLimit < height) {
2668            halfY = zoomFrameScaleY(height, 0.5f, halfY);
2669        }
2670        canvas.scale(halfScale, halfScale, mZoomScrollX + width * halfX
2671                , mZoomScrollY + height * halfY);
2672        if (LOGV_ENABLED) {
2673            Log.v(LOGTAG, "scrollZoomDraw halfScale=" + halfScale + " w/h=("
2674                    + width + ", " + height + ") half=(" + halfX + ", "
2675                    + halfY + ")");
2676        }
2677        if (mDrawHistory) {
2678            canvas.drawPicture(mHistoryPicture);
2679        } else {
2680            mWebViewCore.drawContentPicture(canvas, mBackgroundColor,
2681                    animating, false);
2682        }
2683        canvas.restoreToCount(sc);
2684        if (mTouchMode != SCROLL_ZOOM_OUT) {
2685            invalidate();
2686        }
2687    }
2688
2689    private void zoomScrollTap(float x, float y) {
2690        float scale = scrollZoomGridScale(mZoomScrollInvLimit);
2691        float left = scrollZoomX(scale);
2692        float top = scrollZoomY(scale);
2693        int width = getViewWidth();
2694        int height = getViewHeight();
2695        x -= width * scale / 2;
2696        y -= height * scale / 2;
2697        mZoomScrollX = Math.min(mContentWidth - width
2698                , Math.max(0, (int) ((x - left) / scale)));
2699        mZoomScrollY = Math.min(mContentHeight - height
2700                , Math.max(0, (int) ((y - top) / scale)));
2701        if (LOGV_ENABLED) {
2702            Log.v(LOGTAG, "zoomScrollTap scale=" + scale + " + (" + left
2703                    + ", " + top + ") mZoomScroll=(" + mZoomScrollX + ", "
2704                    + mZoomScrollY + ")" + " x=" + x + " y=" + y);
2705        }
2706    }
2707
2708    private boolean canZoomScrollOut() {
2709        if (mContentWidth == 0 || mContentHeight == 0) {
2710            return false;
2711        }
2712        int width = getViewWidth();
2713        int height = getViewHeight();
2714        float x = (float) width / (float) mContentWidth;
2715        float y = (float) height / (float) mContentHeight;
2716        mZoomScrollLimit = Math.max(DEFAULT_MIN_ZOOM_SCALE, Math.min(x, y));
2717        mZoomScrollInvLimit = 1.0f / mZoomScrollLimit;
2718        if (LOGV_ENABLED) {
2719            Log.v(LOGTAG, "canZoomScrollOut"
2720                    + " mInvActualScale=" + mInvActualScale
2721                    + " mZoomScrollLimit=" + mZoomScrollLimit
2722                    + " mZoomScrollInvLimit=" + mZoomScrollInvLimit
2723                    + " mContentWidth=" + mContentWidth
2724                    + " mContentHeight=" + mContentHeight
2725                    );
2726        }
2727        // don't zoom out unless magnify area is at least half as wide
2728        // or tall as content
2729        float limit = mZoomScrollLimit * 2;
2730        return mContentWidth >= width * limit
2731                || mContentHeight >= height * limit;
2732    }
2733
2734    private void startZoomScrollOut() {
2735        setHorizontalScrollBarEnabled(false);
2736        setVerticalScrollBarEnabled(false);
2737        if (mZoomControlRunnable != null) {
2738            mPrivateHandler.removeCallbacks(mZoomControlRunnable);
2739        }
2740        if (mZoomControls != null) {
2741            mZoomControls.hide();
2742        }
2743        int width = getViewWidth();
2744        int height = getViewHeight();
2745        int halfW = width >> 1;
2746        mLastTouchX = halfW;
2747        int halfH = height >> 1;
2748        mLastTouchY = halfH;
2749        mScroller.abortAnimation();
2750        mZoomScrollStart = System.currentTimeMillis();
2751        Rect zoomFrame = scrollZoomFrame(width, height
2752                , scrollZoomMagScale(mZoomScrollInvLimit));
2753        mZoomScrollX = Math.max(0, (int) ((mScrollX + halfW) * mInvActualScale)
2754                - (zoomFrame.width() >> 1));
2755        mZoomScrollY = Math.max(0, (int) ((mScrollY + halfH) * mInvActualScale)
2756                - (zoomFrame.height() >> 1));
2757        scrollTo(0, 0); // triggers inval, starts animation
2758        clearTextEntry();
2759        if (LOGV_ENABLED) {
2760            Log.v(LOGTAG, "startZoomScrollOut mZoomScroll=("
2761                    + mZoomScrollX + ", " + mZoomScrollY +")");
2762        }
2763    }
2764
2765    private void zoomScrollOut() {
2766        if (canZoomScrollOut() == false) {
2767            mTouchMode = TOUCH_DONE_MODE;
2768            return;
2769        }
2770        startZoomScrollOut();
2771        mTouchMode = SCROLL_ZOOM_ANIMATION_OUT;
2772        invalidate();
2773    }
2774
2775    private void moveZoomScrollWindow(float x, float y) {
2776        if (Math.abs(x - mLastZoomScrollRawX) < 1.5f
2777                && Math.abs(y - mLastZoomScrollRawY) < 1.5f) {
2778            return;
2779        }
2780        mLastZoomScrollRawX = x;
2781        mLastZoomScrollRawY = y;
2782        int oldX = mZoomScrollX;
2783        int oldY = mZoomScrollY;
2784        int width = getViewWidth();
2785        int height = getViewHeight();
2786        int maxZoomX = mContentWidth - width;
2787        if (maxZoomX > 0) {
2788            int maxScreenX = width - (int) Math.ceil(width
2789                    * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER;
2790            if (LOGV_ENABLED) {
2791                Log.v(LOGTAG, "moveZoomScrollWindow-X"
2792                        + " maxScreenX=" + maxScreenX + " width=" + width
2793                        + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x);
2794            }
2795            x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX;
2796            x *= Math.max(maxZoomX / maxScreenX, mZoomScrollInvLimit);
2797            mZoomScrollX = Math.max(0, Math.min(maxZoomX, (int) x));
2798        }
2799        int maxZoomY = mContentHeight - height;
2800        if (maxZoomY > 0) {
2801            int maxScreenY = height - (int) Math.ceil(height
2802                    * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER;
2803            if (LOGV_ENABLED) {
2804                Log.v(LOGTAG, "moveZoomScrollWindow-Y"
2805                        + " maxScreenY=" + maxScreenY + " height=" + height
2806                        + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y);
2807            }
2808            y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY;
2809            y *= Math.max(maxZoomY / maxScreenY, mZoomScrollInvLimit);
2810            mZoomScrollY = Math.max(0, Math.min(maxZoomY, (int) y));
2811        }
2812        if (oldX != mZoomScrollX || oldY != mZoomScrollY) {
2813            invalidate();
2814        }
2815        if (LOGV_ENABLED) {
2816            Log.v(LOGTAG, "moveZoomScrollWindow"
2817                    + " scrollTo=(" + mZoomScrollX + ", " + mZoomScrollY + ")"
2818                    + " mLastTouch=(" + mLastTouchX + ", " + mLastTouchY + ")"
2819                    + " maxZoom=(" + maxZoomX + ", " + maxZoomY + ")"
2820                    + " last=("+mLastScrollX+", "+mLastScrollY+")"
2821                    + " x=" + x + " y=" + y);
2822        }
2823    }
2824
2825    private void setZoomScrollIn() {
2826        mZoomScrollStart = System.currentTimeMillis();
2827    }
2828
2829    private float mZoomScrollLimit;
2830    private float mZoomScrollInvLimit;
2831    private int mLastScrollX;
2832    private int mLastScrollY;
2833    private long mZoomScrollStart;
2834    private int mZoomScrollX;
2835    private int mZoomScrollY;
2836    private float mLastZoomScrollRawX = -1000.0f;
2837    private float mLastZoomScrollRawY = -1000.0f;
2838    // The zoomed scale varies from 1.0 to DEFAULT_MIN_ZOOM_SCALE == 0.25.
2839    // The zoom animation duration SCROLL_ZOOM_DURATION == 0.5.
2840    // Two pressures compete for gridding; a high frame rate (e.g. 20 fps)
2841    // and minimizing font cache allocations (fewer frames is better).
2842    // A SCROLL_ZOOM_GRID of 6 permits about 20 zoom levels over 0.5 seconds:
2843    // the inverse of: 1.0, 1.16, 1.33, 1.5, 1.67, 1.84, 2.0, etc. to 4.0
2844    private static final int SCROLL_ZOOM_GRID = 6;
2845    private static final int SCROLL_ZOOM_DURATION = 500;
2846    // Make it easier to get to the bottom of a document by reserving a 32
2847    // pixel buffer, for when the starting drag is a bit below the bottom of
2848    // the magnify frame.
2849    private static final int SCROLL_ZOOM_FINGER_BUFFER = 32;
2850
2851    // draw history
2852    private boolean mDrawHistory = false;
2853    private Picture mHistoryPicture = null;
2854    private int mHistoryWidth = 0;
2855    private int mHistoryHeight = 0;
2856
2857    // Only check the flag, can be called from WebCore thread
2858    boolean drawHistory() {
2859        return mDrawHistory;
2860    }
2861
2862    // Should only be called in UI thread
2863    void switchOutDrawHistory() {
2864        if (null == mWebViewCore) return; // CallbackProxy may trigger this
2865        if (mDrawHistory) {
2866            mDrawHistory = false;
2867            invalidate();
2868            int oldScrollX = mScrollX;
2869            int oldScrollY = mScrollY;
2870            mScrollX = pinLocX(mScrollX);
2871            mScrollY = pinLocY(mScrollY);
2872            if (oldScrollX != mScrollX || oldScrollY != mScrollY) {
2873                mUserScroll = false;
2874                mWebViewCore.sendMessage(EventHub.SYNC_SCROLL, oldScrollX,
2875                        oldScrollY);
2876            }
2877            sendOurVisibleRect();
2878        }
2879    }
2880
2881    /**
2882     *  Class representing the node which is focused.
2883     */
2884    private class FocusNode {
2885        public FocusNode() {
2886            mBounds = new Rect();
2887        }
2888        // Only to be called by JNI
2889        private void setAll(boolean isTextField, boolean isTextArea, boolean
2890                isPassword, boolean isAnchor, boolean isRtlText, int maxLength,
2891                int textSize, int boundsX, int boundsY, int boundsRight, int
2892                boundsBottom, int nodePointer, int framePointer, String text,
2893                String name, int rootTextGeneration) {
2894            mIsTextField        = isTextField;
2895            mIsTextArea         = isTextArea;
2896            mIsPassword         = isPassword;
2897            mIsAnchor           = isAnchor;
2898            mIsRtlText          = isRtlText;
2899
2900            mMaxLength          = maxLength;
2901            mTextSize           = textSize;
2902
2903            mBounds.set(boundsX, boundsY, boundsRight, boundsBottom);
2904
2905
2906            mNodePointer        = nodePointer;
2907            mFramePointer       = framePointer;
2908            mText               = text;
2909            mName               = name;
2910            mRootTextGeneration = rootTextGeneration;
2911        }
2912        public boolean  mIsTextField;
2913        public boolean  mIsTextArea;
2914        public boolean  mIsPassword;
2915        public boolean  mIsAnchor;
2916        public boolean  mIsRtlText;
2917
2918        public int      mSelectionStart;
2919        public int      mSelectionEnd;
2920        public int      mMaxLength;
2921        public int      mTextSize;
2922
2923        public Rect     mBounds;
2924
2925        public int      mNodePointer;
2926        public int      mFramePointer;
2927        public String   mText;
2928        public String   mName;
2929        public int      mRootTextGeneration;
2930    }
2931
2932    // Warning: ONLY use mFocusNode AFTER calling nativeUpdateFocusNode(),
2933    // and ONLY if it returns true;
2934    private FocusNode mFocusNode = new FocusNode();
2935
2936    /**
2937     *  Delete text from start to end in the focused textfield. If there is no
2938     *  focus, or if start == end, silently fail.  If start and end are out of
2939     *  order, swap them.
2940     *  @param  start   Beginning of selection to delete.
2941     *  @param  end     End of selection to delete.
2942     */
2943    /* package */ void deleteSelection(int start, int end) {
2944        mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end,
2945                new WebViewCore.FocusData(mFocusData));
2946    }
2947
2948    /**
2949     *  Set the selection to (start, end) in the focused textfield. If start and
2950     *  end are out of order, swap them.
2951     *  @param  start   Beginning of selection.
2952     *  @param  end     End of selection.
2953     */
2954    /* package */ void setSelection(int start, int end) {
2955        mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end,
2956                new WebViewCore.FocusData(mFocusData));
2957    }
2958
2959    // Called by JNI when a touch event puts a textfield into focus.
2960    private void displaySoftKeyboard() {
2961        InputMethodManager imm = (InputMethodManager)
2962                getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
2963        imm.showSoftInput(mTextEntry, 0);
2964        mTextEntry.enableScrollOnScreen(true);
2965        // Now we need to fake a touch event to place the cursor where the
2966        // user touched.
2967        AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams)
2968                mTextEntry.getLayoutParams();
2969        if (lp != null) {
2970            // Take the last touch and adjust for the location of the
2971            // TextDialog.
2972            float x = mLastTouchX - lp.x;
2973            float y = mLastTouchY - lp.y;
2974            mTextEntry.fakeTouchEvent(x, y);
2975        }
2976    }
2977
2978    private void updateTextEntry() {
2979        if (mTextEntry == null) {
2980            mTextEntry = new TextDialog(mContext, WebView.this);
2981            // Initialize our generation number.
2982            mTextGeneration = 0;
2983        }
2984        // If we do not have focus, do nothing until we gain focus.
2985        if (!hasFocus() && !mTextEntry.hasFocus()
2986                || (mTouchMode >= FIRST_SCROLL_ZOOM
2987                && mTouchMode <= LAST_SCROLL_ZOOM)) {
2988            mNeedsUpdateTextEntry = true;
2989            return;
2990        }
2991        boolean alreadyThere = inEditingMode();
2992        if (0 == mNativeClass || !nativeUpdateFocusNode()) {
2993            if (alreadyThere) {
2994                mTextEntry.remove();
2995            }
2996            return;
2997        }
2998        FocusNode node = mFocusNode;
2999        if (!node.mIsTextField && !node.mIsTextArea) {
3000            if (alreadyThere) {
3001                mTextEntry.remove();
3002            }
3003            return;
3004        }
3005        mTextEntry.setTextSize(contentToView(node.mTextSize));
3006        Rect visibleRect = sendOurVisibleRect();
3007        // Note that sendOurVisibleRect calls viewToContent, so the coordinates
3008        // should be in content coordinates.
3009        if (!Rect.intersects(node.mBounds, visibleRect)) {
3010            // Node is not on screen, so do not bother.
3011            return;
3012        }
3013        int x = node.mBounds.left;
3014        int y = node.mBounds.top;
3015        int width = node.mBounds.width();
3016        int height = node.mBounds.height();
3017        if (alreadyThere && mTextEntry.isSameTextField(node.mNodePointer)) {
3018            // It is possible that we have the same textfield, but it has moved,
3019            // i.e. In the case of opening/closing the screen.
3020            // In that case, we need to set the dimensions, but not the other
3021            // aspects.
3022            // We also need to restore the selection, which gets wrecked by
3023            // calling setTextEntryRect.
3024            Spannable spannable = (Spannable) mTextEntry.getText();
3025            int start = Selection.getSelectionStart(spannable);
3026            int end = Selection.getSelectionEnd(spannable);
3027            setTextEntryRect(x, y, width, height);
3028            // If the text has been changed by webkit, update it.  However, if
3029            // there has been more UI text input, ignore it.  We will receive
3030            // another update when that text is recognized.
3031            if (node.mText != null && !node.mText.equals(spannable.toString())
3032                    && node.mRootTextGeneration == mTextGeneration) {
3033                mTextEntry.setTextAndKeepSelection(node.mText);
3034            } else {
3035                Selection.setSelection(spannable, start, end);
3036            }
3037        } else {
3038            String text = node.mText;
3039            setTextEntryRect(x, y, width, height);
3040            mTextEntry.setGravity(node.mIsRtlText ? Gravity.RIGHT :
3041                    Gravity.NO_GRAVITY);
3042            // this needs to be called before update adapter thread starts to
3043            // ensure the mTextEntry has the same node pointer
3044            mTextEntry.setNodePointer(node.mNodePointer);
3045            int maxLength = -1;
3046            if (node.mIsTextField) {
3047                maxLength = node.mMaxLength;
3048                if (mWebViewCore.getSettings().getSaveFormData()
3049                        && node.mName != null) {
3050                    HashMap data = new HashMap();
3051                    data.put("text", node.mText);
3052                    Message update = mPrivateHandler.obtainMessage(
3053                            UPDATE_TEXT_ENTRY_ADAPTER, node.mNodePointer, 0,
3054                            data);
3055                    UpdateTextEntryAdapter updater = new UpdateTextEntryAdapter(
3056                            node.mName, getUrl(), update);
3057                    Thread t = new Thread(updater);
3058                    t.start();
3059                }
3060            }
3061            mTextEntry.setMaxLength(maxLength);
3062            AutoCompleteAdapter adapter = null;
3063            mTextEntry.setAdapterCustom(adapter);
3064            mTextEntry.setSingleLine(node.mIsTextField);
3065            mTextEntry.setInPassword(node.mIsPassword);
3066            if (null == text) {
3067                mTextEntry.setText("", 0, 0);
3068            } else {
3069                // Change to true to enable the old style behavior, where
3070                // entering a textfield/textarea always set the selection to the
3071                // whole field.  This was desirable for the case where the user
3072                // intends to scroll past the field using the trackball.
3073                // However, it causes a problem when replying to emails - the
3074                // user expects the cursor to be at the beginning of the
3075                // textarea.  Testing out a new behavior, where textfields set
3076                // selection at the end, and textareas at the beginning.
3077                if (false) {
3078                    mTextEntry.setText(text, 0, text.length());
3079                } else if (node.mIsTextField) {
3080                    int length = text.length();
3081                    mTextEntry.setText(text, length, length);
3082                } else {
3083                    mTextEntry.setText(text, 0, 0);
3084                }
3085            }
3086            mTextEntry.requestFocus();
3087        }
3088    }
3089
3090    private class UpdateTextEntryAdapter implements Runnable {
3091        private String mName;
3092        private String mUrl;
3093        private Message mUpdateMessage;
3094
3095        public UpdateTextEntryAdapter(String name, String url, Message msg) {
3096            mName = name;
3097            mUrl = url;
3098            mUpdateMessage = msg;
3099        }
3100
3101        public void run() {
3102            ArrayList<String> pastEntries = mDatabase.getFormData(mUrl, mName);
3103            if (pastEntries.size() > 0) {
3104                AutoCompleteAdapter adapter = new
3105                        AutoCompleteAdapter(mContext, pastEntries);
3106                ((HashMap) mUpdateMessage.obj).put("adapter", adapter);
3107                mUpdateMessage.sendToTarget();
3108            }
3109        }
3110    }
3111
3112    private void setTextEntryRect(int x, int y, int width, int height) {
3113        x = contentToView(x);
3114        y = contentToView(y);
3115        width = contentToView(width);
3116        height = contentToView(height);
3117        mTextEntry.setRect(x, y, width, height);
3118    }
3119
3120    // This is used to determine long press with the enter key, or
3121    // a center key.  Does not affect long press with the trackball/touch.
3122    private boolean mGotEnterDown = false;
3123
3124    @Override
3125    public boolean onKeyDown(int keyCode, KeyEvent event) {
3126        if (LOGV_ENABLED) {
3127            Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis()
3128                    + ", " + event);
3129        }
3130
3131        if (mNativeClass == 0) {
3132            return false;
3133        }
3134
3135        // do this hack up front, so it always works, regardless of touch-mode
3136        if (AUTO_REDRAW_HACK && (keyCode == KeyEvent.KEYCODE_CALL)) {
3137            mAutoRedraw = !mAutoRedraw;
3138            if (mAutoRedraw) {
3139                invalidate();
3140            }
3141            return true;
3142        }
3143
3144        // Bubble up the key event if
3145        // 1. it is a system key; or
3146        // 2. the host application wants to handle it; or
3147        // 3. webview is in scroll-zoom state;
3148        if (event.isSystem()
3149                || mCallbackProxy.uiOverrideKeyEvent(event)
3150                || (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM)) {
3151            return false;
3152        }
3153
3154        if (mShiftIsPressed == false && nativeFocusNodeWantsKeyEvents() == false
3155                && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
3156                || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) {
3157            mExtendSelection = false;
3158            mShiftIsPressed = true;
3159            if (nativeUpdateFocusNode()) {
3160                FocusNode node = mFocusNode;
3161                mSelectX = contentToView(node.mBounds.left);
3162                mSelectY = contentToView(node.mBounds.top);
3163            } else {
3164                mSelectX = mScrollX + (int) mLastTouchX;
3165                mSelectY = mScrollY + (int) mLastTouchY;
3166            }
3167       }
3168
3169        if (keyCode >= KeyEvent.KEYCODE_DPAD_UP
3170                && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) {
3171            // always handle the navigation keys in the UI thread
3172            switchOutDrawHistory();
3173            if (navHandledKey(keyCode, 1, false, event.getEventTime())) {
3174                playSoundEffect(keyCodeToSoundsEffect(keyCode));
3175                return true;
3176            }
3177            // Bubble up the key event as WebView doesn't handle it
3178            return false;
3179        }
3180
3181        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
3182                || keyCode == KeyEvent.KEYCODE_ENTER) {
3183            switchOutDrawHistory();
3184            if (event.getRepeatCount() == 0) {
3185                mGotEnterDown = true;
3186                mPrivateHandler.sendMessageDelayed(mPrivateHandler
3187                        .obtainMessage(LONG_PRESS_ENTER), LONG_PRESS_TIMEOUT);
3188                // Already checked mNativeClass, so we do not need to check it
3189                // again.
3190                nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true);
3191                return true;
3192            }
3193            // Bubble up the key event as WebView doesn't handle it
3194            return false;
3195        }
3196
3197        if (getSettings().getNavDump()) {
3198            switch (keyCode) {
3199                case KeyEvent.KEYCODE_4:
3200                    // "/data/data/com.android.browser/displayTree.txt"
3201                    nativeDumpDisplayTree(getUrl());
3202                    break;
3203                case KeyEvent.KEYCODE_5:
3204                case KeyEvent.KEYCODE_6:
3205                    // 5: dump the dom tree to the file
3206                    // "/data/data/com.android.browser/domTree.txt"
3207                    // 6: dump the dom tree to the adb log
3208                    mWebViewCore.sendMessage(EventHub.DUMP_DOMTREE,
3209                            (keyCode == KeyEvent.KEYCODE_5) ? 1 : 0, 0);
3210                    break;
3211                case KeyEvent.KEYCODE_7:
3212                case KeyEvent.KEYCODE_8:
3213                    // 7: dump the render tree to the file
3214                    // "/data/data/com.android.browser/renderTree.txt"
3215                    // 8: dump the render tree to the adb log
3216                    mWebViewCore.sendMessage(EventHub.DUMP_RENDERTREE,
3217                            (keyCode == KeyEvent.KEYCODE_7) ? 1 : 0, 0);
3218                    break;
3219                case KeyEvent.KEYCODE_9:
3220                    nativeInstrumentReport();
3221                    return true;
3222            }
3223        }
3224
3225        // TODO: should we pass all the keys to DOM or check the meta tag
3226        if (nativeFocusNodeWantsKeyEvents() || true) {
3227            // pass the key to DOM
3228            mWebViewCore.sendMessage(EventHub.KEY_DOWN, event);
3229            // return true as DOM handles the key
3230            return true;
3231        }
3232
3233        // Bubble up the key event as WebView doesn't handle it
3234        return false;
3235    }
3236
3237    @Override
3238    public boolean onKeyUp(int keyCode, KeyEvent event) {
3239        if (LOGV_ENABLED) {
3240            Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis()
3241                    + ", " + event);
3242        }
3243
3244        if (mNativeClass == 0) {
3245            return false;
3246        }
3247
3248        // special CALL handling when focus node's href is "tel:XXX"
3249        if (keyCode == KeyEvent.KEYCODE_CALL && nativeUpdateFocusNode()) {
3250            FocusNode node = mFocusNode;
3251            String text = node.mText;
3252            if (!node.mIsTextField && !node.mIsTextArea && text != null
3253                    && text.startsWith(SCHEME_TEL)) {
3254                Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(text));
3255                getContext().startActivity(intent);
3256                return true;
3257            }
3258        }
3259
3260        // Bubble up the key event if
3261        // 1. it is a system key; or
3262        // 2. the host application wants to handle it;
3263        if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) {
3264            return false;
3265        }
3266
3267        // special handling in scroll_zoom state
3268        if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) {
3269            if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode
3270                    && mTouchMode != SCROLL_ZOOM_ANIMATION_IN) {
3271                setZoomScrollIn();
3272                mTouchMode = SCROLL_ZOOM_ANIMATION_IN;
3273                invalidate();
3274                return true;
3275            }
3276            return false;
3277        }
3278
3279        if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
3280                || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) {
3281            if (commitCopy()) {
3282                return true;
3283            }
3284        }
3285
3286        if (keyCode >= KeyEvent.KEYCODE_DPAD_UP
3287                && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) {
3288            // always handle the navigation keys in the UI thread
3289            // Bubble up the key event as WebView doesn't handle it
3290            return false;
3291        }
3292
3293        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER
3294                || keyCode == KeyEvent.KEYCODE_ENTER) {
3295            // remove the long press message first
3296            mPrivateHandler.removeMessages(LONG_PRESS_ENTER);
3297            mGotEnterDown = false;
3298
3299            if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) {
3300                if (mShiftIsPressed) {
3301                    return false;
3302                }
3303                if (getSettings().supportZoom()) {
3304                    if (mTouchMode == TOUCH_DOUBLECLICK_MODE) {
3305                        zoomScrollOut();
3306                    } else {
3307                        if (LOGV_ENABLED) {
3308                            Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE");
3309                        }
3310                        mPrivateHandler.sendMessageDelayed(mPrivateHandler
3311                                .obtainMessage(SWITCH_TO_ENTER), TAP_TIMEOUT);
3312                        mTouchMode = TOUCH_DOUBLECLICK_MODE;
3313                    }
3314                    return true;
3315                }
3316            }
3317
3318            Rect visibleRect = sendOurVisibleRect();
3319            // Note that sendOurVisibleRect calls viewToContent, so the
3320            // coordinates should be in content coordinates.
3321            if (nativeUpdateFocusNode()) {
3322                if (Rect.intersects(mFocusNode.mBounds, visibleRect)) {
3323                    nativeSetFollowedLink(true);
3324                    mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS,
3325                            EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0,
3326                            new WebViewCore.FocusData(mFocusData));
3327                    playSoundEffect(SoundEffectConstants.CLICK);
3328                    if (!mCallbackProxy.uiOverrideUrlLoading(mFocusNode.mText)) {
3329                        // use CLICK instead of KEY_DOWN/KEY_UP so that we can
3330                        // trigger mouse click events
3331                        mWebViewCore.sendMessage(EventHub.CLICK);
3332                    }
3333                }
3334                return true;
3335            }
3336            // Bubble up the key event as WebView doesn't handle it
3337            return false;
3338        }
3339
3340        // TODO: should we pass all the keys to DOM or check the meta tag
3341        if (nativeFocusNodeWantsKeyEvents() || true) {
3342            // pass the key to DOM
3343            mWebViewCore.sendMessage(EventHub.KEY_UP, event);
3344            // return true as DOM handles the key
3345            return true;
3346        }
3347
3348        // Bubble up the key event as WebView doesn't handle it
3349        return false;
3350    }
3351
3352    /**
3353     * @hide
3354     */
3355    public void emulateShiftHeld() {
3356        mExtendSelection = false;
3357        mShiftIsPressed = true;
3358    }
3359
3360    private boolean commitCopy() {
3361        boolean copiedSomething = false;
3362        if (mExtendSelection) {
3363            // copy region so core operates on copy without touching orig.
3364            Region selection = new Region(nativeGetSelection());
3365            if (selection.isEmpty() == false) {
3366                Toast.makeText(mContext
3367                        , com.android.internal.R.string.text_copied
3368                        , Toast.LENGTH_SHORT).show();
3369                mWebViewCore.sendMessage(EventHub.GET_SELECTION, selection);
3370                copiedSomething = true;
3371            }
3372            mExtendSelection = false;
3373        }
3374        mShiftIsPressed = false;
3375        if (mTouchMode == TOUCH_SELECT_MODE) {
3376            mTouchMode = TOUCH_INIT_MODE;
3377        }
3378        return copiedSomething;
3379    }
3380
3381    // Set this as a hierarchy change listener so we can know when this view
3382    // is removed and still have access to our parent.
3383    @Override
3384    protected void onAttachedToWindow() {
3385        super.onAttachedToWindow();
3386        ViewParent parent = getParent();
3387        if (parent instanceof ViewGroup) {
3388            ViewGroup p = (ViewGroup) parent;
3389            p.setOnHierarchyChangeListener(this);
3390        }
3391    }
3392
3393    @Override
3394    protected void onDetachedFromWindow() {
3395        super.onDetachedFromWindow();
3396        ViewParent parent = getParent();
3397        if (parent instanceof ViewGroup) {
3398            ViewGroup p = (ViewGroup) parent;
3399            p.setOnHierarchyChangeListener(null);
3400        }
3401
3402        // Clean up the zoom ring
3403        mZoomRingController.setVisible(false);
3404    }
3405
3406    // Implementation for OnHierarchyChangeListener
3407    public void onChildViewAdded(View parent, View child) {}
3408
3409    public void onChildViewRemoved(View p, View child) {
3410        if (child == this) {
3411            if (inEditingMode()) {
3412                clearTextEntry();
3413                mNeedsUpdateTextEntry = true;
3414            }
3415        }
3416    }
3417
3418    /**
3419     * @deprecated WebView should not have implemented
3420     * ViewTreeObserver.OnGlobalFocusChangeListener.  This method
3421     * does nothing now.
3422     */
3423    @Deprecated
3424    public void onGlobalFocusChanged(View oldFocus, View newFocus) {
3425    }
3426
3427    // To avoid drawing the focus ring, and remove the TextView when our window
3428    // loses focus.
3429    @Override
3430    public void onWindowFocusChanged(boolean hasWindowFocus) {
3431        if (hasWindowFocus) {
3432            if (hasFocus()) {
3433                // If our window regained focus, and we have focus, then begin
3434                // drawing the focus ring, and restore the TextView if
3435                // necessary.
3436                mDrawFocusRing = true;
3437                if (mNeedsUpdateTextEntry) {
3438                    updateTextEntry();
3439                }
3440                if (mNativeClass != 0) {
3441                    nativeRecordButtons(true, false, true);
3442                }
3443            } else {
3444                // If our window gained focus, but we do not have it, do not
3445                // draw the focus ring.
3446                mDrawFocusRing = false;
3447                // We do not call nativeRecordButtons here because we assume
3448                // that when we lost focus, or window focus, it got called with
3449                // false for the first parameter
3450            }
3451        } else {
3452            // If our window has lost focus, stop drawing the focus ring
3453            mDrawFocusRing = false;
3454            mGotKeyDown = false;
3455            mShiftIsPressed = false;
3456            if (mNativeClass != 0) {
3457                nativeRecordButtons(false, false, true);
3458            }
3459        }
3460        invalidate();
3461        super.onWindowFocusChanged(hasWindowFocus);
3462    }
3463
3464    @Override
3465    protected void onFocusChanged(boolean focused, int direction,
3466            Rect previouslyFocusedRect) {
3467        if (LOGV_ENABLED) {
3468            Log.v(LOGTAG, "MT focusChanged " + focused + ", " + direction);
3469        }
3470        if (focused) {
3471            // When we regain focus, if we have window focus, resume drawing
3472            // the focus ring, and add the TextView if necessary.
3473            if (hasWindowFocus()) {
3474                mDrawFocusRing = true;
3475                if (mNeedsUpdateTextEntry) {
3476                    updateTextEntry();
3477                    mNeedsUpdateTextEntry = false;
3478                }
3479                if (mNativeClass != 0) {
3480                    nativeRecordButtons(true, false, true);
3481                }
3482            //} else {
3483                // The WebView has gained focus while we do not have
3484                // windowfocus.  When our window lost focus, we should have
3485                // called nativeRecordButtons(false...)
3486            }
3487        } else {
3488            // When we lost focus, unless focus went to the TextView (which is
3489            // true if we are in editing mode), stop drawing the focus ring.
3490            if (!inEditingMode()) {
3491                mDrawFocusRing = false;
3492                if (mNativeClass != 0) {
3493                    nativeRecordButtons(false, false, true);
3494                }
3495            }
3496            mGotKeyDown = false;
3497        }
3498
3499        super.onFocusChanged(focused, direction, previouslyFocusedRect);
3500    }
3501
3502    @Override
3503    protected void onSizeChanged(int w, int h, int ow, int oh) {
3504        super.onSizeChanged(w, h, ow, oh);
3505        // Center zooming to the center of the screen.  This is appropriate for
3506        // this case of zooming, and it also sets us up properly if we remove
3507        // the new zoom ring controller
3508        mZoomCenterX = getViewWidth() * .5f;
3509        mZoomCenterY = getViewHeight() * .5f;
3510
3511        // update mMinZoomScale
3512        if (mMinContentWidth > MAX_FLOAT_CONTENT_WIDTH) {
3513            boolean atMin = Math.abs(mActualScale - mMinZoomScale) < 0.01f;
3514            mMinZoomScale = (float) getViewWidth() / mContentWidth;
3515            if (atMin) {
3516                // if the WebView was at the minimum zoom scale, keep it. e,g.,
3517                // the WebView was at the minimum zoom scale at the portrait
3518                // mode, rotate it to the landscape modifying the scale to the
3519                // new minimum zoom scale, when rotating back, we would like to
3520                // keep the minimum zoom scale instead of keeping the same scale
3521                // as normally we do.
3522                mActualScale = mMinZoomScale;
3523            }
3524        }
3525
3526        // we always force, in case our height changed, in which case we still
3527        // want to send the notification over to webkit
3528        setNewZoomScale(mActualScale, true);
3529    }
3530
3531    @Override
3532    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
3533        super.onScrollChanged(l, t, oldl, oldt);
3534        sendOurVisibleRect();
3535    }
3536
3537
3538    @Override
3539    public boolean dispatchKeyEvent(KeyEvent event) {
3540        boolean dispatch = true;
3541
3542        if (!inEditingMode()) {
3543            if (event.getAction() == KeyEvent.ACTION_DOWN) {
3544                mGotKeyDown = true;
3545            } else {
3546                if (!mGotKeyDown) {
3547                    /*
3548                     * We got a key up for which we were not the recipient of
3549                     * the original key down. Don't give it to the view.
3550                     */
3551                    dispatch = false;
3552                }
3553                mGotKeyDown = false;
3554            }
3555        }
3556
3557        if (dispatch) {
3558            return super.dispatchKeyEvent(event);
3559        } else {
3560            // We didn't dispatch, so let something else handle the key
3561            return false;
3562        }
3563    }
3564
3565    // Here are the snap align logic:
3566    // 1. If it starts nearly horizontally or vertically, snap align;
3567    // 2. If there is a dramitic direction change, let it go;
3568    // 3. If there is a same direction back and forth, lock it.
3569
3570    // adjustable parameters
3571    private int mMinLockSnapReverseDistance;
3572    private static final float MAX_SLOPE_FOR_DIAG = 1.5f;
3573    private static final int MIN_BREAK_SNAP_CROSS_DISTANCE = 80;
3574
3575    @Override
3576    public boolean onTouchEvent(MotionEvent ev) {
3577        if (mNativeClass == 0 || !isClickable() || !isLongClickable()) {
3578            return false;
3579        }
3580
3581        if (mShowZoomRingTutorial && getSettings().supportZoom()
3582                && (mMaxZoomScale - mMinZoomScale) > ZOOM_RING_STEPS * 0.01f) {
3583            ZoomRingController.showZoomTutorialOnce(mContext);
3584            mShowZoomRingTutorial = false;
3585            mPrivateHandler.sendMessageDelayed(mPrivateHandler
3586                    .obtainMessage(DISMISS_ZOOM_RING_TUTORIAL),
3587                    ZOOM_RING_TUTORIAL_DURATION);
3588        }
3589
3590        if (LOGV_ENABLED) {
3591            Log.v(LOGTAG, ev + " at " + ev.getEventTime() + " mTouchMode="
3592                    + mTouchMode);
3593        }
3594
3595        if (mZoomRingController.isVisible() && mInZoomTapDragMode) {
3596            if (ev.getAction() == MotionEvent.ACTION_UP) {
3597                // Just released the second tap, no longer in tap-drag mode
3598                mInZoomTapDragMode = false;
3599            }
3600            return mZoomRingController.handleDoubleTapEvent(ev);
3601        }
3602
3603        int action = ev.getAction();
3604        float x = ev.getX();
3605        float y = ev.getY();
3606        long eventTime = ev.getEventTime();
3607
3608        // Due to the touch screen edge effect, a touch closer to the edge
3609        // always snapped to the edge. As getViewWidth() can be different from
3610        // getWidth() due to the scrollbar, adjusting the point to match
3611        // getViewWidth(). Same applied to the height.
3612        if (x > getViewWidth() - 1) {
3613            x = getViewWidth() - 1;
3614        }
3615        if (y > getViewHeight() - 1) {
3616            y = getViewHeight() - 1;
3617        }
3618
3619        // pass the touch events from UI thread to WebCore thread
3620        if (mForwardTouchEvents && mTouchMode != SCROLL_ZOOM_OUT
3621                && mTouchMode != SCROLL_ZOOM_ANIMATION_IN
3622                && mTouchMode != SCROLL_ZOOM_ANIMATION_OUT
3623                && (action != MotionEvent.ACTION_MOVE ||
3624                        eventTime - mLastSentTouchTime > TOUCH_SENT_INTERVAL)) {
3625            WebViewCore.TouchEventData ted = new WebViewCore.TouchEventData();
3626            ted.mAction = action;
3627            ted.mX = viewToContent((int) x + mScrollX);
3628            ted.mY = viewToContent((int) y + mScrollY);;
3629            mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted);
3630            mLastSentTouchTime = eventTime;
3631        }
3632
3633        switch (action) {
3634            case MotionEvent.ACTION_DOWN: {
3635                if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN
3636                        || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) {
3637                    // no interaction while animation is in progress
3638                    break;
3639                } else if (mTouchMode == SCROLL_ZOOM_OUT) {
3640                    mLastScrollX = mZoomScrollX;
3641                    mLastScrollY = mZoomScrollY;
3642                    // If two taps are close, ignore the first tap
3643                } else if (!mScroller.isFinished()) {
3644                    mScroller.abortAnimation();
3645                    mTouchMode = TOUCH_DRAG_START_MODE;
3646                    mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE);
3647                } else if (mShiftIsPressed) {
3648                    mSelectX = mScrollX + (int) x;
3649                    mSelectY = mScrollY + (int) y;
3650                    mTouchMode = TOUCH_SELECT_MODE;
3651                    if (LOGV_ENABLED) {
3652                        Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY);
3653                    }
3654                    nativeMoveSelection(viewToContent(mSelectX)
3655                            , viewToContent(mSelectY), false);
3656                    mTouchSelection = mExtendSelection = true;
3657                } else if (!ZoomRingController.useOldZoom(mContext) &&
3658                        mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) {
3659                    // Found doubletap, invoke the zoom controller
3660                    mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP);
3661                    mZoomRingController.setVisible(true);
3662                    mInZoomTapDragMode = true;
3663                    if (mLogEvent) {
3664                        EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION,
3665                                (eventTime - mLastTouchUpTime), eventTime);
3666                    }
3667                    return mZoomRingController.handleDoubleTapEvent(ev);
3668                } else {
3669                    mTouchMode = TOUCH_INIT_MODE;
3670                    mPreventDrag = mForwardTouchEvents;
3671                    if (mLogEvent && eventTime - mLastTouchUpTime < 1000) {
3672                        EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION,
3673                                (eventTime - mLastTouchUpTime), eventTime);
3674                    }
3675                }
3676                // don't trigger the link if zoom ring is visible
3677                if (mTouchMode == TOUCH_INIT_MODE
3678                        && !mZoomRingController.isVisible()) {
3679                    mPrivateHandler.sendMessageDelayed(mPrivateHandler
3680                            .obtainMessage(SWITCH_TO_SHORTPRESS), TAP_TIMEOUT);
3681                }
3682                // Remember where the motion event started
3683                mLastTouchX = x;
3684                mLastTouchY = y;
3685                mLastTouchTime = eventTime;
3686                mVelocityTracker = VelocityTracker.obtain();
3687                mSnapScrollMode = SNAP_NONE;
3688                break;
3689            }
3690            case MotionEvent.ACTION_MOVE: {
3691                if (mTouchMode == TOUCH_DONE_MODE
3692                        || mTouchMode == SCROLL_ZOOM_ANIMATION_IN
3693                        || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) {
3694                    // no dragging during scroll zoom animation
3695                    break;
3696                }
3697                if (mTouchMode == SCROLL_ZOOM_OUT) {
3698                    // while fully zoomed out, move the virtual window
3699                    moveZoomScrollWindow(x, y);
3700                    break;
3701                }
3702                mVelocityTracker.addMovement(ev);
3703
3704                int deltaX = (int) (mLastTouchX - x);
3705                int deltaY = (int) (mLastTouchY - y);
3706
3707                if (mTouchMode != TOUCH_DRAG_MODE) {
3708                    if (mTouchMode == TOUCH_SELECT_MODE) {
3709                        mSelectX = mScrollX + (int) x;
3710                        mSelectY = mScrollY + (int) y;
3711                        if (LOGV_ENABLED) {
3712                            Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY);
3713                        }
3714                        nativeMoveSelection(viewToContent(mSelectX)
3715                                , viewToContent(mSelectY), true);
3716                        invalidate();
3717                        break;
3718                    }
3719                    if (mPreventDrag || (deltaX * deltaX + deltaY * deltaY)
3720                            < mTouchSlopSquare) {
3721                        break;
3722                    }
3723
3724                    if (mTouchMode == TOUCH_SHORTPRESS_MODE
3725                            || mTouchMode == TOUCH_SHORTPRESS_START_MODE) {
3726                        mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
3727                    } else if (mTouchMode == TOUCH_INIT_MODE) {
3728                        mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS);
3729                    }
3730
3731                    // if it starts nearly horizontal or vertical, enforce it
3732                    int ax = Math.abs(deltaX);
3733                    int ay = Math.abs(deltaY);
3734                    if (ax > MAX_SLOPE_FOR_DIAG * ay) {
3735                        mSnapScrollMode = SNAP_X;
3736                        mSnapPositive = deltaX > 0;
3737                    } else if (ay > MAX_SLOPE_FOR_DIAG * ax) {
3738                        mSnapScrollMode = SNAP_Y;
3739                        mSnapPositive = deltaY > 0;
3740                    }
3741
3742                    mTouchMode = TOUCH_DRAG_MODE;
3743                    WebViewCore.pauseUpdate(mWebViewCore);
3744                    int contentX = viewToContent((int) x + mScrollX);
3745                    int contentY = viewToContent((int) y + mScrollY);
3746                    if (inEditingMode()) {
3747                        mTextEntry.updateCachedTextfield();
3748                    }
3749                    nativeClearFocus(contentX, contentY);
3750                    // remove the zoom anchor if there is any
3751                    if (mZoomScale != 0) {
3752                        mWebViewCore
3753                                .sendMessage(EventHub.SET_SNAP_ANCHOR, 0, 0);
3754                    }
3755                }
3756
3757                // do pan
3758                int newScrollX = pinLocX(mScrollX + deltaX);
3759                deltaX = newScrollX - mScrollX;
3760                int newScrollY = pinLocY(mScrollY + deltaY);
3761                deltaY = newScrollY - mScrollY;
3762                boolean done = false;
3763                if (deltaX == 0 && deltaY == 0) {
3764                    done = true;
3765                } else {
3766                    if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_Y) {
3767                        int ax = Math.abs(deltaX);
3768                        int ay = Math.abs(deltaY);
3769                        if (mSnapScrollMode == SNAP_X) {
3770                            // radical change means getting out of snap mode
3771                            if (ay > MAX_SLOPE_FOR_DIAG * ax
3772                                    && ay > MIN_BREAK_SNAP_CROSS_DISTANCE) {
3773                                mSnapScrollMode = SNAP_NONE;
3774                            }
3775                            // reverse direction means lock in the snap mode
3776                            if ((ax > MAX_SLOPE_FOR_DIAG * ay) &&
3777                                    ((mSnapPositive &&
3778                                    deltaX < -mMinLockSnapReverseDistance)
3779                                    || (!mSnapPositive &&
3780                                    deltaX > mMinLockSnapReverseDistance))) {
3781                                mSnapScrollMode = SNAP_X_LOCK;
3782                            }
3783                        } else {
3784                            // radical change means getting out of snap mode
3785                            if ((ax > MAX_SLOPE_FOR_DIAG * ay)
3786                                    && ax > MIN_BREAK_SNAP_CROSS_DISTANCE) {
3787                                mSnapScrollMode = SNAP_NONE;
3788                            }
3789                            // reverse direction means lock in the snap mode
3790                            if ((ay > MAX_SLOPE_FOR_DIAG * ax) &&
3791                                    ((mSnapPositive &&
3792                                    deltaY < -mMinLockSnapReverseDistance)
3793                                    || (!mSnapPositive &&
3794                                    deltaY > mMinLockSnapReverseDistance))) {
3795                                mSnapScrollMode = SNAP_Y_LOCK;
3796                            }
3797                        }
3798                    }
3799
3800                    if (mSnapScrollMode == SNAP_X
3801                            || mSnapScrollMode == SNAP_X_LOCK) {
3802                        scrollBy(deltaX, 0);
3803                        mLastTouchX = x;
3804                    } else if (mSnapScrollMode == SNAP_Y
3805                            || mSnapScrollMode == SNAP_Y_LOCK) {
3806                        scrollBy(0, deltaY);
3807                        mLastTouchY = y;
3808                    } else {
3809                        scrollBy(deltaX, deltaY);
3810                        mLastTouchX = x;
3811                        mLastTouchY = y;
3812                    }
3813                    mLastTouchTime = eventTime;
3814                    mUserScroll = true;
3815                }
3816
3817                if (ZoomRingController.useOldZoom(mContext)) {
3818                    boolean showPlusMinus = mMinZoomScale < mMaxZoomScale;
3819                    boolean showMagnify = canZoomScrollOut();
3820                    if (mZoomControls != null && (showPlusMinus || showMagnify)) {
3821                        if (mZoomControls.getVisibility() == View.VISIBLE) {
3822                            mPrivateHandler.removeCallbacks(mZoomControlRunnable);
3823                        } else {
3824                            mZoomControls.show(showPlusMinus, showMagnify);
3825                        }
3826                        mPrivateHandler.postDelayed(mZoomControlRunnable,
3827                                ZOOM_CONTROLS_TIMEOUT);
3828                    }
3829                }
3830                if (done) {
3831                    // return false to indicate that we can't pan out of the
3832                    // view space
3833                    return false;
3834                }
3835                break;
3836            }
3837            case MotionEvent.ACTION_UP: {
3838                mLastTouchUpTime = eventTime;
3839                switch (mTouchMode) {
3840                    case TOUCH_INIT_MODE: // tap
3841                        if (mZoomRingController.isVisible()) {
3842                            // don't trigger the link if zoom ring is visible,
3843                            // but still allow the double tap
3844                            mPrivateHandler.sendMessageDelayed(mPrivateHandler
3845                                    .obtainMessage(RELEASE_SINGLE_TAP,
3846                                            new Boolean(false)),
3847                                    DOUBLE_TAP_TIMEOUT);
3848                            break;
3849                        }
3850                        mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS);
3851                        if (getSettings().supportZoom()) {
3852                            mPrivateHandler.sendMessageDelayed(mPrivateHandler
3853                                    .obtainMessage(RELEASE_SINGLE_TAP,
3854                                            new Boolean(true)),
3855                                    DOUBLE_TAP_TIMEOUT);
3856                        } else {
3857                            // do short press now
3858                            mTouchMode = TOUCH_DONE_MODE;
3859                            doShortPress();
3860                        }
3861                        break;
3862                    case TOUCH_SELECT_MODE:
3863                        commitCopy();
3864                        mTouchSelection = false;
3865                        break;
3866                    case SCROLL_ZOOM_ANIMATION_IN:
3867                    case SCROLL_ZOOM_ANIMATION_OUT:
3868                        // no action during scroll animation
3869                        break;
3870                    case SCROLL_ZOOM_OUT:
3871                        if (LOGV_ENABLED) {
3872                            Log.v(LOGTAG, "ACTION_UP SCROLL_ZOOM_OUT"
3873                                    + " eventTime - mLastTouchTime="
3874                                    + (eventTime - mLastTouchTime));
3875                        }
3876                        // for now, always zoom back when the drag completes
3877                        if (true || eventTime - mLastTouchTime < TAP_TIMEOUT) {
3878                            // but if we tap, zoom in where we tap
3879                            if (eventTime - mLastTouchTime < TAP_TIMEOUT) {
3880                                zoomScrollTap(x, y);
3881                            }
3882                            // start zooming in back to the original view
3883                            setZoomScrollIn();
3884                            mTouchMode = SCROLL_ZOOM_ANIMATION_IN;
3885                            invalidate();
3886                        }
3887                        break;
3888                    case TOUCH_SHORTPRESS_START_MODE:
3889                    case TOUCH_SHORTPRESS_MODE: {
3890                        mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
3891                        if (eventTime - mLastTouchTime < TAP_TIMEOUT
3892                                && getSettings().supportZoom()) {
3893                            // Note: window manager will not release ACTION_UP
3894                            // until all the previous action events are
3895                            // returned. If GC happens, it can cause
3896                            // SWITCH_TO_SHORTPRESS message fired before
3897                            // ACTION_UP sent even time stamp of ACTION_UP is
3898                            // less than the tap time out. We need to treat this
3899                            // as tap instead of short press.
3900                            mTouchMode = TOUCH_INIT_MODE;
3901                            mPrivateHandler.sendMessageDelayed(mPrivateHandler
3902                                    .obtainMessage(RELEASE_SINGLE_TAP,
3903                                            new Boolean(true)),
3904                                    DOUBLE_TAP_TIMEOUT);
3905                        } else {
3906                            mTouchMode = TOUCH_DONE_MODE;
3907                            doShortPress();
3908                        }
3909                        break;
3910                    }
3911                    case TOUCH_DRAG_MODE:
3912                        // if the user waits a while w/o moving before the
3913                        // up, we don't want to do a fling
3914                        if (eventTime - mLastTouchTime <= MIN_FLING_TIME) {
3915                            mVelocityTracker.addMovement(ev);
3916                            doFling();
3917                            break;
3918                        }
3919                        WebViewCore.resumeUpdate(mWebViewCore);
3920                        break;
3921                    case TOUCH_DRAG_START_MODE:
3922                    case TOUCH_DONE_MODE:
3923                        // do nothing
3924                        break;
3925                }
3926                // we also use mVelocityTracker == null to tell us that we are
3927                // not "moving around", so we can take the slower/prettier
3928                // mode in the drawing code
3929                if (mVelocityTracker != null) {
3930                    mVelocityTracker.recycle();
3931                    mVelocityTracker = null;
3932                }
3933                break;
3934            }
3935            case MotionEvent.ACTION_CANCEL: {
3936                // we also use mVelocityTracker == null to tell us that we are
3937                // not "moving around", so we can take the slower/prettier
3938                // mode in the drawing code
3939                if (mVelocityTracker != null) {
3940                    mVelocityTracker.recycle();
3941                    mVelocityTracker = null;
3942                }
3943                if (mTouchMode == SCROLL_ZOOM_OUT ||
3944                        mTouchMode == SCROLL_ZOOM_ANIMATION_IN) {
3945                    scrollTo(mZoomScrollX, mZoomScrollY);
3946                } else if (mTouchMode == TOUCH_DRAG_MODE) {
3947                    WebViewCore.resumeUpdate(mWebViewCore);
3948                }
3949                mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS);
3950                mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS);
3951                mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP);
3952                mTouchMode = TOUCH_DONE_MODE;
3953                int contentX = viewToContent((int) mLastTouchX + mScrollX);
3954                int contentY = viewToContent((int) mLastTouchY + mScrollY);
3955                if (inEditingMode()) {
3956                    mTextEntry.updateCachedTextfield();
3957                }
3958                nativeClearFocus(contentX, contentY);
3959                break;
3960            }
3961        }
3962        return true;
3963    }
3964
3965    private long mTrackballFirstTime = 0;
3966    private long mTrackballLastTime = 0;
3967    private float mTrackballRemainsX = 0.0f;
3968    private float mTrackballRemainsY = 0.0f;
3969    private int mTrackballXMove = 0;
3970    private int mTrackballYMove = 0;
3971    private boolean mExtendSelection = false;
3972    private boolean mTouchSelection = false;
3973    private static final int TRACKBALL_KEY_TIMEOUT = 1000;
3974    private static final int TRACKBALL_TIMEOUT = 200;
3975    private static final int TRACKBALL_WAIT = 100;
3976    private static final int TRACKBALL_SCALE = 400;
3977    private static final int TRACKBALL_SCROLL_COUNT = 5;
3978    private static final int TRACKBALL_MOVE_COUNT = 10;
3979    private static final int TRACKBALL_MULTIPLIER = 3;
3980    private static final int SELECT_CURSOR_OFFSET = 16;
3981    private int mSelectX = 0;
3982    private int mSelectY = 0;
3983    private boolean mShiftIsPressed = false;
3984    private boolean mTrackballDown = false;
3985    private long mTrackballUpTime = 0;
3986    private long mLastFocusTime = 0;
3987    private Rect mLastFocusBounds;
3988
3989    // Set by default; BrowserActivity clears to interpret trackball data
3990    // directly for movement. Currently, the framework only passes
3991    // arrow key events, not trackball events, from one child to the next
3992    private boolean mMapTrackballToArrowKeys = true;
3993
3994    public void setMapTrackballToArrowKeys(boolean setMap) {
3995        mMapTrackballToArrowKeys = setMap;
3996    }
3997
3998    void resetTrackballTime() {
3999        mTrackballLastTime = 0;
4000    }
4001
4002    @Override
4003    public boolean onTrackballEvent(MotionEvent ev) {
4004        long time = ev.getEventTime();
4005        if ((ev.getMetaState() & KeyEvent.META_ALT_ON) != 0) {
4006            if (ev.getY() > 0) pageDown(true);
4007            if (ev.getY() < 0) pageUp(true);
4008            return true;
4009        }
4010        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
4011            mPrivateHandler.removeMessages(SWITCH_TO_ENTER);
4012            mTrackballDown = true;
4013            if (mNativeClass != 0) {
4014                nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true);
4015            }
4016            if (time - mLastFocusTime <= TRACKBALL_TIMEOUT
4017                    && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) {
4018                nativeSelectBestAt(mLastFocusBounds);
4019            }
4020            if (LOGV_ENABLED) {
4021                Log.v(LOGTAG, "onTrackballEvent down ev=" + ev
4022                        + " time=" + time
4023                        + " mLastFocusTime=" + mLastFocusTime);
4024            }
4025            if (isInTouchMode()) requestFocusFromTouch();
4026            return false; // let common code in onKeyDown at it
4027        }
4028        if (ev.getAction() == MotionEvent.ACTION_UP) {
4029            // LONG_PRESS_ENTER is set in common onKeyDown
4030            mPrivateHandler.removeMessages(LONG_PRESS_ENTER);
4031            mTrackballDown = false;
4032            mTrackballUpTime = time;
4033            if (mShiftIsPressed) {
4034                if (mExtendSelection) {
4035                    commitCopy();
4036                } else {
4037                    mExtendSelection = true;
4038                }
4039            }
4040            if (LOGV_ENABLED) {
4041                Log.v(LOGTAG, "onTrackballEvent up ev=" + ev
4042                        + " time=" + time
4043                );
4044            }
4045            return false; // let common code in onKeyUp at it
4046        }
4047        if (mMapTrackballToArrowKeys && mShiftIsPressed == false) {
4048            if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent gmail quit");
4049            return false;
4050        }
4051        // no move if we're still waiting on SWITCH_TO_ENTER timeout
4052        if (mTouchMode == TOUCH_DOUBLECLICK_MODE) {
4053            if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent 2 click quit");
4054            return true;
4055        }
4056        if (mTrackballDown) {
4057            if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent down quit");
4058            return true; // discard move if trackball is down
4059        }
4060        if (time - mTrackballUpTime < TRACKBALL_TIMEOUT) {
4061            if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent up timeout quit");
4062            return true;
4063        }
4064        // TODO: alternatively we can do panning as touch does
4065        switchOutDrawHistory();
4066        if (time - mTrackballLastTime > TRACKBALL_TIMEOUT) {
4067            if (LOGV_ENABLED) {
4068                Log.v(LOGTAG, "onTrackballEvent time="
4069                        + time + " last=" + mTrackballLastTime);
4070            }
4071            mTrackballFirstTime = time;
4072            mTrackballXMove = mTrackballYMove = 0;
4073        }
4074        mTrackballLastTime = time;
4075        if (LOGV_ENABLED) {
4076            Log.v(LOGTAG, "onTrackballEvent ev=" + ev + " time=" + time);
4077        }
4078        mTrackballRemainsX += ev.getX();
4079        mTrackballRemainsY += ev.getY();
4080        doTrackball(time);
4081        return true;
4082    }
4083
4084    void moveSelection(float xRate, float yRate) {
4085        if (mNativeClass == 0)
4086            return;
4087        int width = getViewWidth();
4088        int height = getViewHeight();
4089        mSelectX += scaleTrackballX(xRate, width);
4090        mSelectY += scaleTrackballY(yRate, height);
4091        int maxX = width + mScrollX;
4092        int maxY = height + mScrollY;
4093        mSelectX = Math.min(maxX, Math.max(mScrollX - SELECT_CURSOR_OFFSET
4094                , mSelectX));
4095        mSelectY = Math.min(maxY, Math.max(mScrollY - SELECT_CURSOR_OFFSET
4096                , mSelectY));
4097        if (LOGV_ENABLED) {
4098            Log.v(LOGTAG, "moveSelection"
4099                    + " mSelectX=" + mSelectX
4100                    + " mSelectY=" + mSelectY
4101                    + " mScrollX=" + mScrollX
4102                    + " mScrollY=" + mScrollY
4103                    + " xRate=" + xRate
4104                    + " yRate=" + yRate
4105                    );
4106        }
4107        nativeMoveSelection(viewToContent(mSelectX)
4108                , viewToContent(mSelectY), mExtendSelection);
4109        int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET
4110                : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET
4111                : 0;
4112        int scrollY = mSelectY < mScrollY ? -SELECT_CURSOR_OFFSET
4113                : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET
4114                : 0;
4115        pinScrollBy(scrollX, scrollY, true, 0);
4116        Rect select = new Rect(mSelectX, mSelectY, mSelectX + 1, mSelectY + 1);
4117        requestRectangleOnScreen(select);
4118        invalidate();
4119   }
4120
4121    private int scaleTrackballX(float xRate, int width) {
4122        int xMove = (int) (xRate / TRACKBALL_SCALE * width);
4123        int nextXMove = xMove;
4124        if (xMove > 0) {
4125            if (xMove > mTrackballXMove) {
4126                xMove -= mTrackballXMove;
4127            }
4128        } else if (xMove < mTrackballXMove) {
4129            xMove -= mTrackballXMove;
4130        }
4131        mTrackballXMove = nextXMove;
4132        return xMove;
4133    }
4134
4135    private int scaleTrackballY(float yRate, int height) {
4136        int yMove = (int) (yRate / TRACKBALL_SCALE * height);
4137        int nextYMove = yMove;
4138        if (yMove > 0) {
4139            if (yMove > mTrackballYMove) {
4140                yMove -= mTrackballYMove;
4141            }
4142        } else if (yMove < mTrackballYMove) {
4143            yMove -= mTrackballYMove;
4144        }
4145        mTrackballYMove = nextYMove;
4146        return yMove;
4147    }
4148
4149    private int keyCodeToSoundsEffect(int keyCode) {
4150        switch(keyCode) {
4151            case KeyEvent.KEYCODE_DPAD_UP:
4152                return SoundEffectConstants.NAVIGATION_UP;
4153            case KeyEvent.KEYCODE_DPAD_RIGHT:
4154                return SoundEffectConstants.NAVIGATION_RIGHT;
4155            case KeyEvent.KEYCODE_DPAD_DOWN:
4156                return SoundEffectConstants.NAVIGATION_DOWN;
4157            case KeyEvent.KEYCODE_DPAD_LEFT:
4158                return SoundEffectConstants.NAVIGATION_LEFT;
4159        }
4160        throw new IllegalArgumentException("keyCode must be one of " +
4161                "{KEYCODE_DPAD_UP, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_DOWN, " +
4162                "KEYCODE_DPAD_LEFT}.");
4163    }
4164
4165    private void doTrackball(long time) {
4166        int elapsed = (int) (mTrackballLastTime - mTrackballFirstTime);
4167        if (elapsed == 0) {
4168            elapsed = TRACKBALL_TIMEOUT;
4169        }
4170        float xRate = mTrackballRemainsX * 1000 / elapsed;
4171        float yRate = mTrackballRemainsY * 1000 / elapsed;
4172        if (mShiftIsPressed) {
4173            moveSelection(xRate, yRate);
4174            mTrackballRemainsX = mTrackballRemainsY = 0;
4175            return;
4176        }
4177        float ax = Math.abs(xRate);
4178        float ay = Math.abs(yRate);
4179        float maxA = Math.max(ax, ay);
4180        if (LOGV_ENABLED) {
4181            Log.v(LOGTAG, "doTrackball elapsed=" + elapsed
4182                    + " xRate=" + xRate
4183                    + " yRate=" + yRate
4184                    + " mTrackballRemainsX=" + mTrackballRemainsX
4185                    + " mTrackballRemainsY=" + mTrackballRemainsY);
4186        }
4187        int width = mContentWidth - getViewWidth();
4188        int height = mContentHeight - getViewHeight();
4189        if (width < 0) width = 0;
4190        if (height < 0) height = 0;
4191        if (mTouchMode == SCROLL_ZOOM_OUT) {
4192            int oldX = mZoomScrollX;
4193            int oldY = mZoomScrollY;
4194            int maxWH = Math.max(width, height);
4195            mZoomScrollX += scaleTrackballX(xRate, maxWH);
4196            mZoomScrollY += scaleTrackballY(yRate, maxWH);
4197            if (LOGV_ENABLED) {
4198                Log.v(LOGTAG, "doTrackball SCROLL_ZOOM_OUT"
4199                        + " mZoomScrollX=" + mZoomScrollX
4200                        + " mZoomScrollY=" + mZoomScrollY);
4201            }
4202            mZoomScrollX = Math.min(width, Math.max(0, mZoomScrollX));
4203            mZoomScrollY = Math.min(height, Math.max(0, mZoomScrollY));
4204            if (oldX != mZoomScrollX || oldY != mZoomScrollY) {
4205                invalidate();
4206            }
4207            mTrackballRemainsX = mTrackballRemainsY = 0;
4208            return;
4209        }
4210        ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER);
4211        ay = Math.abs(mTrackballRemainsY * TRACKBALL_MULTIPLIER);
4212        maxA = Math.max(ax, ay);
4213        int count = Math.max(0, (int) maxA);
4214        int oldScrollX = mScrollX;
4215        int oldScrollY = mScrollY;
4216        if (count > 0) {
4217            int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ?
4218                    KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN :
4219                    mTrackballRemainsX < 0 ? KeyEvent.KEYCODE_DPAD_LEFT :
4220                    KeyEvent.KEYCODE_DPAD_RIGHT;
4221            count = Math.min(count, TRACKBALL_MOVE_COUNT);
4222            if (LOGV_ENABLED) {
4223                Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode
4224                        + " count=" + count
4225                        + " mTrackballRemainsX=" + mTrackballRemainsX
4226                        + " mTrackballRemainsY=" + mTrackballRemainsY);
4227            }
4228            if (navHandledKey(selectKeyCode, count, false, time)) {
4229                playSoundEffect(keyCodeToSoundsEffect(selectKeyCode));
4230            }
4231            mTrackballRemainsX = mTrackballRemainsY = 0;
4232        }
4233        if (count >= TRACKBALL_SCROLL_COUNT) {
4234            int xMove = scaleTrackballX(xRate, width);
4235            int yMove = scaleTrackballY(yRate, height);
4236            if (LOGV_ENABLED) {
4237                Log.v(LOGTAG, "doTrackball pinScrollBy"
4238                        + " count=" + count
4239                        + " xMove=" + xMove + " yMove=" + yMove
4240                        + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX)
4241                        + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY)
4242                        );
4243            }
4244            if (Math.abs(mScrollX - oldScrollX) > Math.abs(xMove)) {
4245                xMove = 0;
4246            }
4247            if (Math.abs(mScrollY - oldScrollY) > Math.abs(yMove)) {
4248                yMove = 0;
4249            }
4250            if (xMove != 0 || yMove != 0) {
4251                pinScrollBy(xMove, yMove, true, 0);
4252            }
4253            mUserScroll = true;
4254        }
4255        mWebViewCore.sendMessage(EventHub.UNBLOCK_FOCUS);
4256    }
4257
4258    public void flingScroll(int vx, int vy) {
4259        int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0);
4260        int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0);
4261
4262        mScroller.fling(mScrollX, mScrollY, vx, vy, 0, maxX, 0, maxY);
4263        invalidate();
4264    }
4265
4266    private void doFling() {
4267        if (mVelocityTracker == null) {
4268            return;
4269        }
4270        int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0);
4271        int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0);
4272
4273        mVelocityTracker.computeCurrentVelocity(1000);
4274        int vx = (int) mVelocityTracker.getXVelocity();
4275        int vy = (int) mVelocityTracker.getYVelocity();
4276
4277        if (mSnapScrollMode != SNAP_NONE) {
4278            if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_X_LOCK) {
4279                vy = 0;
4280            } else {
4281                vx = 0;
4282            }
4283        }
4284
4285        if (true /* EMG release: make our fling more like Maps' */) {
4286            // maps cuts their velocity in half
4287            vx = vx * 3 / 4;
4288            vy = vy * 3 / 4;
4289        }
4290
4291        mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY);
4292        // TODO: duration is calculated based on velocity, if the range is
4293        // small, the animation will stop before duration is up. We may
4294        // want to calculate how long the animation is going to run to precisely
4295        // resume the webcore update.
4296        final int time = mScroller.getDuration();
4297        mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_UPDATE, time);
4298        invalidate();
4299    }
4300
4301    private boolean zoomWithPreview(float scale) {
4302        float oldScale = mActualScale;
4303
4304        // snap to 100% if it is close
4305        if (scale > 0.95f && scale < 1.05f) {
4306            scale = 1.0f;
4307        }
4308
4309        setNewZoomScale(scale, false);
4310
4311        if (oldScale != mActualScale) {
4312            // use mZoomPickerScale to see zoom preview first
4313            mZoomStart = SystemClock.uptimeMillis();
4314            mInvInitialZoomScale = 1.0f / oldScale;
4315            mInvFinalZoomScale = 1.0f / mActualScale;
4316            mZoomScale = mActualScale;
4317            invalidate();
4318            return true;
4319        } else {
4320            return false;
4321        }
4322    }
4323
4324    /**
4325     * Returns a view containing zoom controls i.e. +/- buttons. The caller is
4326     * in charge of installing this view to the view hierarchy. This view will
4327     * become visible when the user starts scrolling via touch and fade away if
4328     * the user does not interact with it.
4329     */
4330    public View getZoomControls() {
4331        if (!getSettings().supportZoom()) {
4332            Log.w(LOGTAG, "This WebView doesn't support zoom.");
4333            return null;
4334        }
4335        if (mZoomControls == null) {
4336            mZoomControls = createZoomControls();
4337
4338            /*
4339             * need to be set to VISIBLE first so that getMeasuredHeight() in
4340             * {@link #onSizeChanged()} can return the measured value for proper
4341             * layout.
4342             */
4343            mZoomControls.setVisibility(View.VISIBLE);
4344            mZoomControlRunnable = new Runnable() {
4345                public void run() {
4346
4347                    /* Don't dismiss the controls if the user has
4348                     * focus on them. Wait and check again later.
4349                     */
4350                    if (!mZoomControls.hasFocus()) {
4351                        mZoomControls.hide();
4352                    } else {
4353                        mPrivateHandler.removeCallbacks(mZoomControlRunnable);
4354                        mPrivateHandler.postDelayed(mZoomControlRunnable,
4355                                ZOOM_CONTROLS_TIMEOUT);
4356                    }
4357                }
4358            };
4359        }
4360        return mZoomControls;
4361    }
4362
4363    /**
4364     * @hide pending API council? Assuming we make ZoomRingController itself
4365     *       public, which I think we will.
4366     */
4367    public ZoomRingController getZoomRingController() {
4368        return mZoomRingController;
4369    }
4370
4371    /**
4372     * Perform zoom in in the webview
4373     * @return TRUE if zoom in succeeds. FALSE if no zoom changes.
4374     */
4375    public boolean zoomIn() {
4376        // TODO: alternatively we can disallow this during draw history mode
4377        switchOutDrawHistory();
4378        return zoomWithPreview(mActualScale * 1.25f);
4379    }
4380
4381    /**
4382     * Perform zoom out in the webview
4383     * @return TRUE if zoom out succeeds. FALSE if no zoom changes.
4384     */
4385    public boolean zoomOut() {
4386        // TODO: alternatively we can disallow this during draw history mode
4387        switchOutDrawHistory();
4388        return zoomWithPreview(mActualScale * 0.8f);
4389    }
4390
4391    private ExtendedZoomControls createZoomControls() {
4392        ExtendedZoomControls zoomControls = new ExtendedZoomControls(mContext
4393            , null);
4394        zoomControls.setOnZoomInClickListener(new OnClickListener() {
4395            public void onClick(View v) {
4396                // reset time out
4397                mPrivateHandler.removeCallbacks(mZoomControlRunnable);
4398                mPrivateHandler.postDelayed(mZoomControlRunnable,
4399                        ZOOM_CONTROLS_TIMEOUT);
4400                zoomIn();
4401            }
4402        });
4403        zoomControls.setOnZoomOutClickListener(new OnClickListener() {
4404            public void onClick(View v) {
4405                // reset time out
4406                mPrivateHandler.removeCallbacks(mZoomControlRunnable);
4407                mPrivateHandler.postDelayed(mZoomControlRunnable,
4408                        ZOOM_CONTROLS_TIMEOUT);
4409                zoomOut();
4410            }
4411        });
4412        zoomControls.setOnZoomMagnifyClickListener(new OnClickListener() {
4413            public void onClick(View v) {
4414                // Hide the zoom ring
4415                mZoomRingController.setVisible(false);
4416
4417                mPrivateHandler.removeCallbacks(mZoomControlRunnable);
4418                mPrivateHandler.postDelayed(mZoomControlRunnable,
4419                        ZOOM_CONTROLS_TIMEOUT);
4420                zoomScrollOut();
4421            }
4422        });
4423        return zoomControls;
4424    }
4425
4426    private void updateSelection() {
4427        if (mNativeClass == 0) {
4428            return;
4429        }
4430        // mLastTouchX and mLastTouchY are the point in the current viewport
4431        int contentX = viewToContent((int) mLastTouchX + mScrollX);
4432        int contentY = viewToContent((int) mLastTouchY + mScrollY);
4433        int contentSize = ViewConfiguration.getTouchSlop();
4434        Rect rect = new Rect(contentX - contentSize, contentY - contentSize,
4435                contentX + contentSize, contentY + contentSize);
4436        // If we were already focused on a textfield, update its cache.
4437        if (inEditingMode()) {
4438            mTextEntry.updateCachedTextfield();
4439        }
4440        nativeSelectBestAt(rect);
4441    }
4442
4443    /*package*/ void shortPressOnTextField() {
4444        if (inEditingMode()) {
4445            View v = mTextEntry;
4446            int x = viewToContent((v.getLeft() + v.getRight()) >> 1);
4447            int y = viewToContent((v.getTop() + v.getBottom()) >> 1);
4448            int contentSize = ViewConfiguration.get(getContext()).getScaledTouchSlop();
4449            nativeMotionUp(x, y, contentSize, true);
4450        }
4451    }
4452
4453    private void doShortPress() {
4454        if (mNativeClass == 0) {
4455            return;
4456        }
4457        switchOutDrawHistory();
4458        // mLastTouchX and mLastTouchY are the point in the current viewport
4459        int contentX = viewToContent((int) mLastTouchX + mScrollX);
4460        int contentY = viewToContent((int) mLastTouchY + mScrollY);
4461        int contentSize = ViewConfiguration.get(getContext()).getScaledTouchSlop();
4462        if (nativeMotionUp(contentX, contentY, contentSize, true)) {
4463            if (mLogEvent) {
4464                Checkin.updateStats(mContext.getContentResolver(),
4465                        Checkin.Stats.Tag.BROWSER_SNAP_CENTER, 1, 0.0);
4466            }
4467        }
4468        if (nativeUpdateFocusNode() && !mFocusNode.mIsTextField
4469                && !mFocusNode.mIsTextArea) {
4470            playSoundEffect(SoundEffectConstants.CLICK);
4471        }
4472    }
4473
4474    // Called by JNI to handle a touch on a node representing an email address,
4475    // address, or phone number
4476    private void overrideLoading(String url) {
4477        mCallbackProxy.uiOverrideUrlLoading(url);
4478    }
4479
4480    @Override
4481    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
4482        boolean result = false;
4483        if (inEditingMode()) {
4484            result = mTextEntry.requestFocus(direction, previouslyFocusedRect);
4485        } else {
4486            result = super.requestFocus(direction, previouslyFocusedRect);
4487            if (mWebViewCore.getSettings().getNeedInitialFocus()) {
4488                // For cases such as GMail, where we gain focus from a direction,
4489                // we want to move to the first available link.
4490                // FIXME: If there are no visible links, we may not want to
4491                int fakeKeyDirection = 0;
4492                switch(direction) {
4493                    case View.FOCUS_UP:
4494                        fakeKeyDirection = KeyEvent.KEYCODE_DPAD_UP;
4495                        break;
4496                    case View.FOCUS_DOWN:
4497                        fakeKeyDirection = KeyEvent.KEYCODE_DPAD_DOWN;
4498                        break;
4499                    case View.FOCUS_LEFT:
4500                        fakeKeyDirection = KeyEvent.KEYCODE_DPAD_LEFT;
4501                        break;
4502                    case View.FOCUS_RIGHT:
4503                        fakeKeyDirection = KeyEvent.KEYCODE_DPAD_RIGHT;
4504                        break;
4505                    default:
4506                        return result;
4507                }
4508                if (mNativeClass != 0 && !nativeUpdateFocusNode()) {
4509                    navHandledKey(fakeKeyDirection, 1, true, 0);
4510                }
4511            }
4512        }
4513        return result;
4514    }
4515
4516    @Override
4517    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
4518        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
4519
4520        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
4521        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
4522        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
4523        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
4524
4525        int measuredHeight = heightSize;
4526        int measuredWidth = widthSize;
4527
4528        // Grab the content size from WebViewCore.
4529        int contentHeight = mContentHeight;
4530        int contentWidth = mContentWidth;
4531
4532//        Log.d(LOGTAG, "------- measure " + heightMode);
4533
4534        if (heightMode != MeasureSpec.EXACTLY) {
4535            mHeightCanMeasure = true;
4536            measuredHeight = contentHeight;
4537            if (heightMode == MeasureSpec.AT_MOST) {
4538                // If we are larger than the AT_MOST height, then our height can
4539                // no longer be measured and we should scroll internally.
4540                if (measuredHeight > heightSize) {
4541                    measuredHeight = heightSize;
4542                    mHeightCanMeasure = false;
4543                }
4544            }
4545        } else {
4546            mHeightCanMeasure = false;
4547        }
4548        if (mNativeClass != 0) {
4549            nativeSetHeightCanMeasure(mHeightCanMeasure);
4550        }
4551        // For the width, always use the given size unless unspecified.
4552        if (widthMode == MeasureSpec.UNSPECIFIED) {
4553            mWidthCanMeasure = true;
4554            measuredWidth = contentWidth;
4555        } else {
4556            mWidthCanMeasure = false;
4557        }
4558
4559        synchronized (this) {
4560            setMeasuredDimension(measuredWidth, measuredHeight);
4561        }
4562    }
4563
4564    @Override
4565    public boolean requestChildRectangleOnScreen(View child,
4566                                                 Rect rect,
4567                                                 boolean immediate) {
4568        rect.offset(child.getLeft() - child.getScrollX(),
4569                child.getTop() - child.getScrollY());
4570
4571        int height = getHeight() - getHorizontalScrollbarHeight();
4572        int screenTop = mScrollY;
4573        int screenBottom = screenTop + height;
4574
4575        int scrollYDelta = 0;
4576
4577        if (rect.bottom > screenBottom && rect.top > screenTop) {
4578            if (rect.height() > height) {
4579                scrollYDelta += (rect.top - screenTop);
4580            } else {
4581                scrollYDelta += (rect.bottom - screenBottom);
4582            }
4583        } else if (rect.top < screenTop) {
4584            scrollYDelta -= (screenTop - rect.top);
4585        }
4586
4587        int width = getWidth() - getVerticalScrollbarWidth();
4588        int screenLeft = mScrollX;
4589        int screenRight = screenLeft + width;
4590
4591        int scrollXDelta = 0;
4592
4593        if (rect.right > screenRight && rect.left > screenLeft) {
4594            if (rect.width() > width) {
4595                scrollXDelta += (rect.left - screenLeft);
4596            } else {
4597                scrollXDelta += (rect.right - screenRight);
4598            }
4599        } else if (rect.left < screenLeft) {
4600            scrollXDelta -= (screenLeft - rect.left);
4601        }
4602
4603        if ((scrollYDelta | scrollXDelta) != 0) {
4604            return pinScrollBy(scrollXDelta, scrollYDelta, !immediate, 0);
4605        }
4606
4607        return false;
4608    }
4609
4610    /* package */ void replaceTextfieldText(int oldStart, int oldEnd,
4611            String replace, int newStart, int newEnd) {
4612        HashMap arg = new HashMap();
4613        arg.put("focusData", new WebViewCore.FocusData(mFocusData));
4614        arg.put("replace", replace);
4615        arg.put("start", new Integer(newStart));
4616        arg.put("end", new Integer(newEnd));
4617        mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg);
4618    }
4619
4620    /* package */ void passToJavaScript(String currentText, KeyEvent event) {
4621        HashMap arg = new HashMap();
4622        arg.put("focusData", new WebViewCore.FocusData(mFocusData));
4623        arg.put("event", event);
4624        arg.put("currentText", currentText);
4625        // Increase our text generation number, and pass it to webcore thread
4626        mTextGeneration++;
4627        mWebViewCore.sendMessage(EventHub.PASS_TO_JS, mTextGeneration, 0, arg);
4628        // WebKit's document state is not saved until about to leave the page.
4629        // To make sure the host application, like Browser, has the up to date
4630        // document state when it goes to background, we force to save the
4631        // document state.
4632        mWebViewCore.removeMessages(EventHub.SAVE_DOCUMENT_STATE);
4633        mWebViewCore.sendMessageDelayed(EventHub.SAVE_DOCUMENT_STATE,
4634                new WebViewCore.FocusData(mFocusData), 1000);
4635    }
4636
4637    /* package */ WebViewCore getWebViewCore() {
4638        return mWebViewCore;
4639    }
4640
4641    //-------------------------------------------------------------------------
4642    // Methods can be called from a separate thread, like WebViewCore
4643    // If it needs to call the View system, it has to send message.
4644    //-------------------------------------------------------------------------
4645
4646    /**
4647     * General handler to receive message coming from webkit thread
4648     */
4649    class PrivateHandler extends Handler {
4650        @Override
4651        public void handleMessage(Message msg) {
4652            if (LOGV_ENABLED) {
4653                Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what
4654                        > INVAL_RECT_MSG_ID ? Integer.toString(msg.what)
4655                        : HandlerDebugString[msg.what - REMEMBER_PASSWORD]);
4656            }
4657            switch (msg.what) {
4658                case REMEMBER_PASSWORD: {
4659                    mDatabase.setUsernamePassword(
4660                            msg.getData().getString("host"),
4661                            msg.getData().getString("username"),
4662                            msg.getData().getString("password"));
4663                    ((Message) msg.obj).sendToTarget();
4664                    break;
4665                }
4666                case NEVER_REMEMBER_PASSWORD: {
4667                    mDatabase.setUsernamePassword(
4668                            msg.getData().getString("host"), null, null);
4669                    ((Message) msg.obj).sendToTarget();
4670                    break;
4671                }
4672                case SWITCH_TO_SHORTPRESS: {
4673                    if (mTouchMode == TOUCH_INIT_MODE) {
4674                        mTouchMode = TOUCH_SHORTPRESS_START_MODE;
4675                        updateSelection();
4676                    }
4677                    break;
4678                }
4679                case SWITCH_TO_LONGPRESS: {
4680                    mTouchMode = TOUCH_DONE_MODE;
4681                    performLongClick();
4682                    updateTextEntry();
4683                    break;
4684                }
4685                case RELEASE_SINGLE_TAP: {
4686                    mTouchMode = TOUCH_DONE_MODE;
4687                    if ((Boolean)msg.obj) {
4688                        doShortPress();
4689                    }
4690                    break;
4691                }
4692                case SWITCH_TO_ENTER:
4693                    if (LOGV_ENABLED) Log.v(LOGTAG, "SWITCH_TO_ENTER");
4694                    mTouchMode = TOUCH_DONE_MODE;
4695                    onKeyUp(KeyEvent.KEYCODE_ENTER
4696                            , new KeyEvent(KeyEvent.ACTION_UP
4697                            , KeyEvent.KEYCODE_ENTER));
4698                    break;
4699                case SCROLL_BY_MSG_ID:
4700                    setContentScrollBy(msg.arg1, msg.arg2, (Boolean) msg.obj);
4701                    break;
4702                case SYNC_SCROLL_TO_MSG_ID:
4703                    if (mUserScroll) {
4704                        // if user has scrolled explicitly, don't sync the
4705                        // scroll position any more
4706                        mUserScroll = false;
4707                        break;
4708                    }
4709                    // fall through
4710                case SCROLL_TO_MSG_ID:
4711                    if (setContentScrollTo(msg.arg1, msg.arg2)) {
4712                        // if we can't scroll to the exact position due to pin,
4713                        // send a message to WebCore to re-scroll when we get a
4714                        // new picture
4715                        mUserScroll = false;
4716                        mWebViewCore.sendMessage(EventHub.SYNC_SCROLL,
4717                                msg.arg1, msg.arg2);
4718                    }
4719                    break;
4720                case SPAWN_SCROLL_TO_MSG_ID:
4721                    spawnContentScrollTo(msg.arg1, msg.arg2);
4722                    break;
4723                case NEW_PICTURE_MSG_ID:
4724                    // called for new content
4725                    final WebViewCore.DrawData draw =
4726                            (WebViewCore.DrawData) msg.obj;
4727                    final Point viewSize = draw.mViewPoint;
4728                    if (mZoomScale > 0) {
4729                        // use the same logic in sendViewSizeZoom() to make sure
4730                        // the mZoomScale has matched the viewSize so that we
4731                        // can clear mZoomScale
4732                        if (Math.round(getViewWidth() / mZoomScale) == viewSize.x) {
4733                            mZoomScale = 0;
4734                            mWebViewCore.sendMessage(EventHub.SET_SNAP_ANCHOR,
4735                                    0, 0);
4736                        }
4737                    }
4738                    mMinContentWidth = msg.arg1;
4739                    if (mMinContentWidth > MAX_FLOAT_CONTENT_WIDTH) {
4740                        mMinZoomScale = (float) getViewWidth()
4741                                / draw.mWidthHeight.x;
4742                    }
4743                    // We update the layout (i.e. request a layout from the
4744                    // view system) if the last view size that we sent to
4745                    // WebCore matches the view size of the picture we just
4746                    // received in the fixed dimension.
4747                    final boolean updateLayout = viewSize.x == mLastWidthSent
4748                            && viewSize.y == mLastHeightSent;
4749                    recordNewContentSize(draw.mWidthHeight.x,
4750                            draw.mWidthHeight.y, updateLayout);
4751                    if (LOGV_ENABLED) {
4752                        Rect b = draw.mInvalRegion.getBounds();
4753                        Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" +
4754                                b.left+","+b.top+","+b.right+","+b.bottom+"}");
4755                    }
4756                    invalidate(contentToView(draw.mInvalRegion.getBounds()));
4757                    if (mPictureListener != null) {
4758                        mPictureListener.onNewPicture(WebView.this, capturePicture());
4759                    }
4760                    break;
4761                case WEBCORE_INITIALIZED_MSG_ID:
4762                    // nativeCreate sets mNativeClass to a non-zero value
4763                    nativeCreate(msg.arg1);
4764                    break;
4765                case UPDATE_TEXTFIELD_TEXT_MSG_ID:
4766                    // Make sure that the textfield is currently focused
4767                    // and representing the same node as the pointer.
4768                    if (inEditingMode() &&
4769                            mTextEntry.isSameTextField(msg.arg1)) {
4770                        if (msg.getData().getBoolean("password")) {
4771                            Spannable text = (Spannable) mTextEntry.getText();
4772                            int start = Selection.getSelectionStart(text);
4773                            int end = Selection.getSelectionEnd(text);
4774                            mTextEntry.setInPassword(true);
4775                            // Restore the selection, which may have been
4776                            // ruined by setInPassword.
4777                            Spannable pword = (Spannable) mTextEntry.getText();
4778                            Selection.setSelection(pword, start, end);
4779                        // If the text entry has created more events, ignore
4780                        // this one.
4781                        } else if (msg.arg2 == mTextGeneration) {
4782                            mTextEntry.setTextAndKeepSelection(
4783                                    (String) msg.obj);
4784                        }
4785                    }
4786                    break;
4787                case DID_FIRST_LAYOUT_MSG_ID:
4788                    if (mNativeClass == 0) {
4789                        break;
4790                    }
4791// Do not reset the focus or clear the text; the user may have already
4792// navigated or entered text at this point. The focus should have gotten
4793// reset, if need be, when the focus cache was built. Similarly, the text
4794// view should already be torn down and rebuilt if needed.
4795//                    nativeResetFocus();
4796//                    clearTextEntry();
4797                    HashMap scaleLimit = (HashMap) msg.obj;
4798                    int minScale = (Integer) scaleLimit.get("minScale");
4799                    if (minScale == 0) {
4800                        mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE;
4801                    } else {
4802                        mMinZoomScale = (float) (minScale / 100.0);
4803                    }
4804                    int maxScale = (Integer) scaleLimit.get("maxScale");
4805                    if (maxScale == 0) {
4806                        mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE;
4807                    } else {
4808                        mMaxZoomScale = (float) (maxScale / 100.0);
4809                    }
4810                    // If history Picture is drawn, don't update zoomWidth
4811                    if (mDrawHistory) {
4812                        break;
4813                    }
4814                    int width = getViewWidth();
4815                    if (width == 0) {
4816                        break;
4817                    }
4818                    int initialScale = msg.arg1;
4819                    int viewportWidth = msg.arg2;
4820                    // by default starting a new page with 100% zoom scale.
4821                    float scale = 1.0f;
4822                    if (mInitialScale > 0) {
4823                        scale = mInitialScale / 100.0f;
4824                    } else  {
4825                        if (mWebViewCore.getSettings().getUseWideViewPort()) {
4826                            // force viewSizeChanged by setting mLastWidthSent
4827                            // to 0
4828                            mLastWidthSent = 0;
4829                        }
4830                        if (initialScale == 0) {
4831                            // if viewportWidth is defined and it is smaller
4832                            // than the view width, zoom in to fill the view
4833                            if (viewportWidth > 0 && viewportWidth < width) {
4834                                scale = (float) width / viewportWidth;
4835                            }
4836                        } else {
4837                            scale = initialScale / 100.0f;
4838                        }
4839                    }
4840                    setNewZoomScale(scale, false);
4841                    break;
4842                case MARK_NODE_INVALID_ID:
4843                    nativeMarkNodeInvalid(msg.arg1);
4844                    break;
4845                case NOTIFY_FOCUS_SET_MSG_ID:
4846                    if (mNativeClass != 0) {
4847                        nativeNotifyFocusSet(inEditingMode());
4848                    }
4849                    break;
4850                case UPDATE_TEXT_ENTRY_MSG_ID:
4851                    // this is sent after finishing resize in WebViewCore. Make
4852                    // sure the text edit box is still on the  screen.
4853                    boolean alreadyThere = inEditingMode();
4854                    if (alreadyThere && nativeUpdateFocusNode()) {
4855                        FocusNode node = mFocusNode;
4856                        if (node.mIsTextField || node.mIsTextArea) {
4857                            mTextEntry.bringIntoView();
4858                        }
4859                    }
4860                    updateTextEntry();
4861                    break;
4862                case RECOMPUTE_FOCUS_MSG_ID:
4863                    if (mNativeClass != 0) {
4864                        nativeRecomputeFocus();
4865                    }
4866                    break;
4867                case INVAL_RECT_MSG_ID: {
4868                    Rect r = (Rect)msg.obj;
4869                    if (r == null) {
4870                        invalidate();
4871                    } else {
4872                        // we need to scale r from content into view coords,
4873                        // which viewInvalidate() does for us
4874                        viewInvalidate(r.left, r.top, r.right, r.bottom);
4875                    }
4876                    break;
4877                }
4878                case UPDATE_TEXT_ENTRY_ADAPTER:
4879                    HashMap data = (HashMap) msg.obj;
4880                    if (mTextEntry.isSameTextField(msg.arg1)) {
4881                        AutoCompleteAdapter adapter =
4882                                (AutoCompleteAdapter) data.get("adapter");
4883                        mTextEntry.setAdapterCustom(adapter);
4884                    }
4885                    break;
4886                case UPDATE_CLIPBOARD:
4887                    String str = (String) msg.obj;
4888                    if (LOGV_ENABLED) {
4889                        Log.v(LOGTAG, "UPDATE_CLIPBOARD " + str);
4890                    }
4891                    try {
4892                        IClipboard clip = IClipboard.Stub.asInterface(
4893                                ServiceManager.getService("clipboard"));
4894                                clip.setClipboardText(str);
4895                    } catch (android.os.RemoteException e) {
4896                        Log.e(LOGTAG, "Clipboard failed", e);
4897                    }
4898                    break;
4899                case RESUME_WEBCORE_UPDATE:
4900                    WebViewCore.resumeUpdate(mWebViewCore);
4901                    break;
4902
4903                case LONG_PRESS_ENTER:
4904                    // as this is shared by keydown and trackballdown, reset all
4905                    // the states
4906                    mGotEnterDown = false;
4907                    mTrackballDown = false;
4908                    // LONG_PRESS_ENTER is sent as a delayed message. If we
4909                    // switch to windows overview, the WebView will be
4910                    // temporarily removed from the view system. In that case,
4911                    // do nothing.
4912                    if (getParent() != null) {
4913                        performLongClick();
4914                    }
4915                    break;
4916
4917                case WEBCORE_NEED_TOUCH_EVENTS:
4918                    mForwardTouchEvents = (msg.arg1 != 0);
4919                    break;
4920
4921                case PREVENT_TOUCH_ID:
4922                    if (msg.arg1 == MotionEvent.ACTION_DOWN) {
4923                        mPreventDrag = msg.arg2 == 1;
4924                        if (mPreventDrag) {
4925                            mTouchMode = TOUCH_DONE_MODE;
4926                        }
4927                    }
4928                    break;
4929
4930                case DISMISS_ZOOM_RING_TUTORIAL:
4931                    mZoomRingController.finishZoomTutorial();
4932                    break;
4933
4934                default:
4935                    super.handleMessage(msg);
4936                    break;
4937            }
4938        }
4939    }
4940
4941    // Class used to use a dropdown for a <select> element
4942    private class InvokeListBox implements Runnable {
4943        // Strings for the labels in the listbox.
4944        private String[]    mArray;
4945        // Array representing whether each item is enabled.
4946        private boolean[]   mEnableArray;
4947        // Whether the listbox allows multiple selection.
4948        private boolean     mMultiple;
4949        // Passed in to a list with multiple selection to tell
4950        // which items are selected.
4951        private int[]       mSelectedArray;
4952        // Passed in to a list with single selection to tell
4953        // where the initial selection is.
4954        private int         mSelection;
4955
4956        private Container[] mContainers;
4957
4958        // Need these to provide stable ids to my ArrayAdapter,
4959        // which normally does not have stable ids. (Bug 1250098)
4960        private class Container extends Object {
4961            String  mString;
4962            boolean mEnabled;
4963            int     mId;
4964
4965            public String toString() {
4966                return mString;
4967            }
4968        }
4969
4970        /**
4971         *  Subclass ArrayAdapter so we can disable OptionGroupLabels,
4972         *  and allow filtering.
4973         */
4974        private class MyArrayListAdapter extends ArrayAdapter<Container> {
4975            public MyArrayListAdapter(Context context, Container[] objects, boolean multiple) {
4976                super(context,
4977                            multiple ? com.android.internal.R.layout.select_dialog_multichoice :
4978                            com.android.internal.R.layout.select_dialog_singlechoice,
4979                            objects);
4980            }
4981
4982            @Override
4983            public boolean hasStableIds() {
4984                return true;
4985            }
4986
4987            private Container item(int position) {
4988                if (position < 0 || position >= getCount()) {
4989                    return null;
4990                }
4991                return (Container) getItem(position);
4992            }
4993
4994            @Override
4995            public long getItemId(int position) {
4996                Container item = item(position);
4997                if (item == null) {
4998                    return -1;
4999                }
5000                return item.mId;
5001            }
5002
5003            @Override
5004            public boolean areAllItemsEnabled() {
5005                return false;
5006            }
5007
5008            @Override
5009            public boolean isEnabled(int position) {
5010                Container item = item(position);
5011                if (item == null) {
5012                    return false;
5013                }
5014                return item.mEnabled;
5015            }
5016        }
5017
5018        private InvokeListBox(String[] array,
5019                boolean[] enabled, int[] selected) {
5020            mMultiple = true;
5021            mSelectedArray = selected;
5022
5023            int length = array.length;
5024            mContainers = new Container[length];
5025            for (int i = 0; i < length; i++) {
5026                mContainers[i] = new Container();
5027                mContainers[i].mString = array[i];
5028                mContainers[i].mEnabled = enabled[i];
5029                mContainers[i].mId = i;
5030            }
5031        }
5032
5033        private InvokeListBox(String[] array, boolean[] enabled, int
5034                selection) {
5035            mSelection = selection;
5036            mMultiple = false;
5037
5038            int length = array.length;
5039            mContainers = new Container[length];
5040            for (int i = 0; i < length; i++) {
5041                mContainers[i] = new Container();
5042                mContainers[i].mString = array[i];
5043                mContainers[i].mEnabled = enabled[i];
5044                mContainers[i].mId = i;
5045            }
5046        }
5047
5048        public void run() {
5049            final ListView listView = (ListView) LayoutInflater.from(mContext)
5050                    .inflate(com.android.internal.R.layout.select_dialog, null);
5051            final MyArrayListAdapter adapter = new
5052                    MyArrayListAdapter(mContext, mContainers, mMultiple);
5053            AlertDialog.Builder b = new AlertDialog.Builder(mContext)
5054                    .setView(listView).setCancelable(true)
5055                    .setInverseBackgroundForced(true);
5056
5057            if (mMultiple) {
5058                b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
5059                    public void onClick(DialogInterface dialog, int which) {
5060                        mWebViewCore.sendMessage(
5061                                EventHub.LISTBOX_CHOICES,
5062                                adapter.getCount(), 0,
5063                                listView.getCheckedItemPositions());
5064                    }});
5065                b.setNegativeButton(android.R.string.cancel, null);
5066            }
5067            final AlertDialog dialog = b.create();
5068            listView.setAdapter(adapter);
5069            listView.setFocusableInTouchMode(true);
5070            // There is a bug (1250103) where the checks in a ListView with
5071            // multiple items selected are associated with the positions, not
5072            // the ids, so the items do not properly retain their checks when
5073            // filtered.  Do not allow filtering on multiple lists until
5074            // that bug is fixed.
5075
5076            // Disable filter altogether
5077            // listView.setTextFilterEnabled(!mMultiple);
5078            if (mMultiple) {
5079                listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
5080                int length = mSelectedArray.length;
5081                for (int i = 0; i < length; i++) {
5082                    listView.setItemChecked(mSelectedArray[i], true);
5083                }
5084            } else {
5085                listView.setOnItemClickListener(new OnItemClickListener() {
5086                    public void onItemClick(AdapterView parent, View v,
5087                            int position, long id) {
5088                        mWebViewCore.sendMessage(
5089                                EventHub.SINGLE_LISTBOX_CHOICE, (int)id, 0);
5090                        dialog.dismiss();
5091                    }
5092                });
5093                if (mSelection != -1) {
5094                    listView.setSelection(mSelection);
5095                    listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
5096                    listView.setItemChecked(mSelection, true);
5097                }
5098            }
5099            dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
5100                public void onCancel(DialogInterface dialog) {
5101                    mWebViewCore.sendMessage(
5102                                EventHub.SINGLE_LISTBOX_CHOICE, -2, 0);
5103                }
5104            });
5105            dialog.show();
5106        }
5107    }
5108
5109    /*
5110     * Request a dropdown menu for a listbox with multiple selection.
5111     *
5112     * @param array Labels for the listbox.
5113     * @param enabledArray  Which positions are enabled.
5114     * @param selectedArray Which positions are initally selected.
5115     */
5116    void requestListBox(String[] array, boolean[]enabledArray, int[]
5117            selectedArray) {
5118        mPrivateHandler.post(
5119                new InvokeListBox(array, enabledArray, selectedArray));
5120    }
5121
5122    /*
5123     * Request a dropdown menu for a listbox with single selection or a single
5124     * <select> element.
5125     *
5126     * @param array Labels for the listbox.
5127     * @param enabledArray  Which positions are enabled.
5128     * @param selection Which position is initally selected.
5129     */
5130    void requestListBox(String[] array, boolean[]enabledArray, int selection) {
5131        mPrivateHandler.post(
5132                new InvokeListBox(array, enabledArray, selection));
5133    }
5134
5135    // called by JNI
5136    private void sendFinalFocus(int frame, int node, int x, int y) {
5137        WebViewCore.FocusData focusData = new WebViewCore.FocusData();
5138        focusData.mFrame = frame;
5139        focusData.mNode = node;
5140        focusData.mX = x;
5141        focusData.mY = y;
5142        mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS,
5143                EventHub.NO_FOCUS_CHANGE_BLOCK, 0, focusData);
5144    }
5145
5146    // called by JNI
5147    private void setFocusData(int moveGeneration, int buildGeneration,
5148            int frame, int node, int x, int y, boolean ignoreNullFocus) {
5149        mFocusData.mMoveGeneration = moveGeneration;
5150        mFocusData.mBuildGeneration = buildGeneration;
5151        mFocusData.mFrame = frame;
5152        mFocusData.mNode = node;
5153        mFocusData.mX = x;
5154        mFocusData.mY = y;
5155        mFocusData.mIgnoreNullFocus = ignoreNullFocus;
5156    }
5157
5158    // called by JNI
5159    private void sendKitFocus() {
5160        WebViewCore.FocusData focusData = new WebViewCore.FocusData(mFocusData);
5161        mWebViewCore.sendMessage(EventHub.SET_KIT_FOCUS, focusData);
5162    }
5163
5164    // called by JNI
5165    private void sendMotionUp(int touchGeneration, int buildGeneration,
5166            int frame, int node, int x, int y, int size, boolean isClick,
5167            boolean retry) {
5168        WebViewCore.TouchUpData touchUpData = new WebViewCore.TouchUpData();
5169        touchUpData.mMoveGeneration = touchGeneration;
5170        touchUpData.mBuildGeneration = buildGeneration;
5171        touchUpData.mSize = size;
5172        touchUpData.mIsClick = isClick;
5173        touchUpData.mRetry = retry;
5174        mFocusData.mFrame = touchUpData.mFrame = frame;
5175        mFocusData.mNode = touchUpData.mNode = node;
5176        mFocusData.mX = touchUpData.mX = x;
5177        mFocusData.mY = touchUpData.mY = y;
5178        mWebViewCore.sendMessage(EventHub.TOUCH_UP, touchUpData);
5179    }
5180
5181
5182    private int getScaledMaxXScroll() {
5183        int width;
5184        if (mHeightCanMeasure == false) {
5185            width = getViewWidth() / 4;
5186        } else {
5187            Rect visRect = new Rect();
5188            calcOurVisibleRect(visRect);
5189            width = visRect.width() / 2;
5190        }
5191        // FIXME the divisor should be retrieved from somewhere
5192        return viewToContent(width);
5193    }
5194
5195    private int getScaledMaxYScroll() {
5196        int height;
5197        if (mHeightCanMeasure == false) {
5198            height = getViewHeight() / 4;
5199        } else {
5200            Rect visRect = new Rect();
5201            calcOurVisibleRect(visRect);
5202            height = visRect.height() / 2;
5203        }
5204        // FIXME the divisor should be retrieved from somewhere
5205        // the closest thing today is hard-coded into ScrollView.java
5206        // (from ScrollView.java, line 363)   int maxJump = height/2;
5207        return viewToContent(height);
5208    }
5209
5210    /**
5211     * Called by JNI to invalidate view
5212     */
5213    private void viewInvalidate() {
5214        invalidate();
5215    }
5216
5217    // return true if the key was handled
5218    private boolean navHandledKey(int keyCode, int count, boolean noScroll
5219            , long time) {
5220        if (mNativeClass == 0) {
5221            return false;
5222        }
5223        mLastFocusTime = time;
5224        mLastFocusBounds = nativeGetFocusRingBounds();
5225        boolean keyHandled = nativeMoveFocus(keyCode, count, noScroll) == false;
5226        if (LOGV_ENABLED) {
5227            Log.v(LOGTAG, "navHandledKey mLastFocusBounds=" + mLastFocusBounds
5228                    + " mLastFocusTime=" + mLastFocusTime
5229                    + " handled=" + keyHandled);
5230        }
5231        if (keyHandled == false || mHeightCanMeasure == false) {
5232            return keyHandled;
5233        }
5234        Rect contentFocus = nativeGetFocusRingBounds();
5235        if (contentFocus.isEmpty()) return keyHandled;
5236        Rect viewFocus = contentToView(contentFocus);
5237        Rect visRect = new Rect();
5238        calcOurVisibleRect(visRect);
5239        Rect outset = new Rect(visRect);
5240        int maxXScroll = visRect.width() / 2;
5241        int maxYScroll = visRect.height() / 2;
5242        outset.inset(-maxXScroll, -maxYScroll);
5243        if (Rect.intersects(outset, viewFocus) == false) {
5244            return keyHandled;
5245        }
5246        // FIXME: Necessary because ScrollView/ListView do not scroll left/right
5247        int maxH = Math.min(viewFocus.right - visRect.right, maxXScroll);
5248        if (maxH > 0) {
5249            pinScrollBy(maxH, 0, true, 0);
5250        } else {
5251            maxH = Math.max(viewFocus.left - visRect.left, -maxXScroll);
5252            if (maxH < 0) {
5253                pinScrollBy(maxH, 0, true, 0);
5254            }
5255        }
5256        if (mLastFocusBounds.isEmpty()) return keyHandled;
5257        if (mLastFocusBounds.equals(contentFocus)) return keyHandled;
5258        if (LOGV_ENABLED) {
5259            Log.v(LOGTAG, "navHandledKey contentFocus=" + contentFocus);
5260        }
5261        requestRectangleOnScreen(viewFocus);
5262        mUserScroll = true;
5263        return keyHandled;
5264    }
5265
5266    /**
5267     * Set the background color. It's white by default. Pass
5268     * zero to make the view transparent.
5269     * @param color   the ARGB color described by Color.java
5270     */
5271    public void setBackgroundColor(int color) {
5272        mBackgroundColor = color;
5273        mWebViewCore.sendMessage(EventHub.SET_BACKGROUND_COLOR, color);
5274    }
5275
5276    public void debugDump() {
5277        nativeDebugDump();
5278        mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE);
5279    }
5280
5281    /**
5282     *  Update our cache with updatedText.
5283     *  @param updatedText  The new text to put in our cache.
5284     */
5285    /* package */ void updateCachedTextfield(String updatedText) {
5286        // Also place our generation number so that when we look at the cache
5287        // we recognize that it is up to date.
5288        nativeUpdateCachedTextfield(updatedText, mTextGeneration);
5289    }
5290
5291    // Never call this version except by updateCachedTextfield(String) -
5292    // we always want to pass in our generation number.
5293    private native void     nativeUpdateCachedTextfield(String updatedText,
5294            int generation);
5295    private native void     nativeClearFocus(int x, int y);
5296    private native void     nativeCreate(int ptr);
5297    private native void     nativeDebugDump();
5298    private native void     nativeDestroy();
5299    private native void     nativeDrawFocusRing(Canvas content);
5300    private native void     nativeDrawSelection(Canvas content
5301            , int x, int y, boolean extendSelection);
5302    private native void     nativeDrawSelectionRegion(Canvas content);
5303    private native boolean  nativeUpdateFocusNode();
5304    private native Rect     nativeGetFocusRingBounds();
5305    private native Rect     nativeGetNavBounds();
5306    private native void     nativeInstrumentReport();
5307    private native void     nativeMarkNodeInvalid(int node);
5308    // return true if the page has been scrolled
5309    private native boolean  nativeMotionUp(int x, int y, int slop, boolean isClick);
5310    // returns false if it handled the key
5311    private native boolean  nativeMoveFocus(int keyCode, int count,
5312            boolean noScroll);
5313    private native void     nativeNotifyFocusSet(boolean inEditingMode);
5314    private native void     nativeRecomputeFocus();
5315    // Like many other of our native methods, you must make sure that
5316    // mNativeClass is not null before calling this method.
5317    private native void     nativeRecordButtons(boolean focused,
5318            boolean pressed, boolean invalidate);
5319    private native void     nativeResetFocus();
5320    private native void     nativeResetNavClipBounds();
5321    private native void     nativeSelectBestAt(Rect rect);
5322    private native void     nativeSetFindIsDown();
5323    private native void     nativeSetFollowedLink(boolean followed);
5324    private native void     nativeSetHeightCanMeasure(boolean measure);
5325    private native void     nativeSetNavBounds(Rect rect);
5326    private native void     nativeSetNavClipBounds(Rect rect);
5327    private native String   nativeImageURI(int x, int y);
5328    /**
5329     * Returns true if the native focus nodes says it wants to handle key events
5330     * (ala plugins). This can only be called if mNativeClass is non-zero!
5331     */
5332    private native boolean  nativeFocusNodeWantsKeyEvents();
5333    private native void     nativeMoveSelection(int x, int y
5334            , boolean extendSelection);
5335    private native Region   nativeGetSelection();
5336
5337    private native void nativeDumpDisplayTree(String urlOrNull);
5338}
5339