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