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