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