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