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