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