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