summaryrefslogtreecommitdiffstats
path: root/core/java/android/webkit/ZoomManager.java
blob: 9d472e02ae46b848bdd90eac84ab11d6fa286a29 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.webkit;

import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.Log;
import android.view.ScaleGestureDetector;
import android.view.View;

/**
 * The ZoomManager is responsible for maintaining the WebView's current zoom
 * level state.  It is also responsible for managing the on-screen zoom controls
 * as well as any animation of the WebView due to zooming.
 *
 * Currently, there are two methods for animating the zoom of a WebView.
 *
 * (1) The first method is triggered by startZoomAnimation(...) and is a fixed
 * length animation where the final zoom scale is known at startup.  This type of
 * animation notifies webkit of the final scale BEFORE it animates. The animation
 * is then done by scaling the CANVAS incrementally based on a stepping function.
 *
 * (2) The second method is triggered by a multi-touch pinch and the new scale
 * is determined dynamically based on the user's gesture. This type of animation
 * only notifies webkit of new scale AFTER the gesture is complete. The animation
 * effect is achieved by scaling the VIEWS (both WebView and ViewManager.ChildView)
 * to the new scale in response to events related to the user's gesture.
 */
class ZoomManager {

    static final String LOGTAG = "webviewZoom";

    private final WebView mWebView;
    private final CallbackProxy mCallbackProxy;

    // Widgets responsible for the on-screen zoom functions of the WebView.
    private ZoomControlEmbedded mEmbeddedZoomControl;
    private ZoomControlExternal mExternalZoomControl;

    /*
     * For large screen devices, the defaultScale usually set to 1.0 and
     * equal to the overview scale, to differentiate the zoom level for double tapping,
     * a default reading level scale is used.
     */
    private static final float DEFAULT_READING_LEVEL_SCALE = 1.5f;

    /*
     * The scale factors that determine the upper and lower bounds for the
     * default zoom scale.
     */
    protected static final float DEFAULT_MAX_ZOOM_SCALE_FACTOR = 4.00f;
    protected static final float DEFAULT_MIN_ZOOM_SCALE_FACTOR = 0.25f;

    // The default scale limits, which are dependent on the display density.
    private float mDefaultMaxZoomScale;
    private float mDefaultMinZoomScale;

    // The actual scale limits, which can be set through a webpage's viewport
    // meta-tag.
    private float mMaxZoomScale;
    private float mMinZoomScale;

    // Locks the minimum ZoomScale to the value currently set in mMinZoomScale.
    private boolean mMinZoomScaleFixed = true;

    /*
     * When loading a new page the WebView does not initially know the final
     * width of the page. Therefore, when a new page is loaded in overview mode
     * the overview scale is initialized to a default value. This flag is then
     * set and used to notify the ZoomManager to take the width of the next
     * picture from webkit and use that width to enter into zoom overview mode.
     */
    private boolean mInitialZoomOverview = false;

    /*
     * When in the zoom overview mode, the page's width is fully fit to the
     * current window. Additionally while the page is in this state it is
     * active, in other words, you can click to follow the links. We cache a
     * boolean to enable us to quickly check whether or not we are in overview
     * mode, but this value should only be modified by changes to the zoom
     * scale.
     */
    private boolean mInZoomOverview = false;
    private int mZoomOverviewWidth;
    private float mInvZoomOverviewWidth;

    /*
     * These variables track the center point of the zoom and they are used to
     * determine the point around which we should zoom. They are stored in view
     * coordinates.
     */
    private float mZoomCenterX;
    private float mZoomCenterY;

    /*
     * These values represent the point around which the screen should be
     * centered after zooming. In other words it is used to determine the center
     * point of the visible document after the page has finished zooming. This
     * is important because the zoom may have potentially reflowed the text and
     * we need to ensure the proper portion of the document remains on the
     * screen.
     */
    private int mAnchorX;
    private int mAnchorY;

    // The scale factor that is used to determine the column width for text
    private float mTextWrapScale;

    /*
     * The default zoom scale is the scale factor used when the user triggers a
     * zoom in by double tapping on the WebView. The value is initially set
     * based on the display density, but can be changed at any time via the
     * WebSettings.
     */
    private float mDefaultScale;
    private float mInvDefaultScale;

    // the current computed zoom scale and its inverse.
    private float mActualScale;
    private float mInvActualScale;
    
    /*
     * The initial scale for the WebView. 0 means default. If initial scale is
     * greater than 0 the WebView starts with this value as its initial scale. The
     * value is converted from an integer percentage so it is guarenteed to have
     * no more than 2 significant digits after the decimal.  This restriction
     * allows us to convert the scale back to the original percentage by simply
     * multiplying the value by 100.
     */
    private float mInitialScale;

    private static float MINIMUM_SCALE_INCREMENT = 0.007f;

    /*
     *  The touch points could be changed even the fingers stop moving.
     *  We use the following to filter out the zooming jitters.
     */
    private static float MINIMUM_SCALE_WITHOUT_JITTER = 0.007f;

    /*
     * The following member variables are only to be used for animating zoom. If
     * mZoomScale is non-zero then we are in the middle of a zoom animation. The
     * other variables are used as a cache (e.g. inverse) or as a way to store
     * the state of the view prior to animating (e.g. initial scroll coords).
     */
    private float mZoomScale;
    private float mInvInitialZoomScale;
    private float mInvFinalZoomScale;
    private int mInitialScrollX;
    private int mInitialScrollY;
    private long mZoomStart;

    private static final int ZOOM_ANIMATION_LENGTH = 500;

    // whether support multi-touch
    private boolean mSupportMultiTouch;
    
    /**
     * True if we have a touch panel capable of detecting smooth pan/scale at the same time
     */
    private boolean mAllowPanAndScale;

    // use the framework's ScaleGestureDetector to handle multi-touch
    private ScaleGestureDetector mScaleDetector;
    private boolean mPinchToZoomAnimating = false;

    private boolean mHardwareAccelerated = false;
    private boolean mInHWAcceleratedZoom = false;

    public ZoomManager(WebView webView, CallbackProxy callbackProxy) {
        mWebView = webView;
        mCallbackProxy = callbackProxy;

        /*
         * Ideally mZoomOverviewWidth should be mContentWidth. But sites like
         * ESPN and Engadget always have wider mContentWidth no matter what the
         * viewport size is.
         */
        setZoomOverviewWidth(WebView.DEFAULT_VIEWPORT_WIDTH);
    }

    /**
     * Initialize both the default and actual zoom scale to the given density.
     *
     * @param density The logical density of the display. This is a scaling factor
     * for the Density Independent Pixel unit, where one DIP is one pixel on an
     * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
     */
    public void init(float density) {
        assert density > 0;

        setDefaultZoomScale(density);
        mActualScale = density;
        mInvActualScale = 1 / density;
        mTextWrapScale = density;
    }

    /**
     * Update the default zoom scale using the given density. It will also reset
     * the current min and max zoom scales to the default boundaries as well as
     * ensure that the actual scale falls within those boundaries.
     *
     * @param density The logical density of the display. This is a scaling factor
     * for the Density Independent Pixel unit, where one DIP is one pixel on an
     * approximately 160 dpi screen (see android.util.DisplayMetrics.density).
     */
    public void updateDefaultZoomDensity(float density) {
        assert density > 0;

        if (Math.abs(density - mDefaultScale) > MINIMUM_SCALE_INCREMENT) {
            // set the new default density
            setDefaultZoomScale(density);
            // adjust the scale if it falls outside the new zoom bounds
            setZoomScale(mActualScale, true);
        }
    }

    private void setDefaultZoomScale(float defaultScale) {
        final float originalDefault = mDefaultScale;
        mDefaultScale = defaultScale;
        mInvDefaultScale = 1 / defaultScale;
        mDefaultMaxZoomScale = defaultScale * DEFAULT_MAX_ZOOM_SCALE_FACTOR;
        mDefaultMinZoomScale = defaultScale * DEFAULT_MIN_ZOOM_SCALE_FACTOR;
        if (originalDefault > 0.0 && mMaxZoomScale > 0.0) {
            // Keeps max zoom scale when zoom density changes.
            mMaxZoomScale = defaultScale / originalDefault * mMaxZoomScale;
        } else {
            mMaxZoomScale = mDefaultMaxZoomScale;
        }
        if (originalDefault > 0.0 && mMinZoomScale > 0.0) {
            // Keeps min zoom scale when zoom density changes.
            mMinZoomScale = defaultScale / originalDefault * mMinZoomScale;
        } else {
            mMinZoomScale = mDefaultMinZoomScale;
        }
        if (!exceedsMinScaleIncrement(mMinZoomScale, mMaxZoomScale)) {
            mMaxZoomScale = mMinZoomScale;
        }
    }

    public final float getScale() {
        return mActualScale;
    }

    public final float getInvScale() {
        return mInvActualScale;
    }

    public final float getTextWrapScale() {
        return mTextWrapScale;
    }

    public final float getMaxZoomScale() {
        return mMaxZoomScale;
    }

    public final float getMinZoomScale() {
        return mMinZoomScale;
    }

    public final float getDefaultScale() {
        return mDefaultScale;
    }

    public final float getReadingLevelScale() {
        // The reading scale is at least 0.5f apart from the overview scale.
        final float MIN_SCALE_DIFF = 0.5f;
        return computeScaleWithLimits(
                   Math.max(getZoomOverviewScale() + MIN_SCALE_DIFF,
                            DEFAULT_READING_LEVEL_SCALE));
    }

    public final float getInvDefaultScale() {
        return mInvDefaultScale;
    }

    public final float getDefaultMaxZoomScale() {
        return mDefaultMaxZoomScale;
    }

    public final float getDefaultMinZoomScale() {
        return mDefaultMinZoomScale;
    }

    public final int getDocumentAnchorX() {
        return mAnchorX;
    }

    public final int getDocumentAnchorY() {
        return mAnchorY;
    }

    public final void clearDocumentAnchor() {
        mAnchorX = mAnchorY = 0;
    }

    public final void setZoomCenter(float x, float y) {
        mZoomCenterX = x;
        mZoomCenterY = y;
    }

    public final void setInitialScaleInPercent(int scaleInPercent) {
        mInitialScale = scaleInPercent * 0.01f;
    }

    public final float computeScaleWithLimits(float scale) {
        if (scale < mMinZoomScale) {
            scale = mMinZoomScale;
        } else if (scale > mMaxZoomScale) {
            scale = mMaxZoomScale;
        }
        return scale;
    }

    public final boolean isZoomScaleFixed() {
        return mMinZoomScale >= mMaxZoomScale;
    }

    public static final boolean exceedsMinScaleIncrement(float scaleA, float scaleB) {
        return Math.abs(scaleA - scaleB) >= MINIMUM_SCALE_INCREMENT;
    }

    public boolean willScaleTriggerZoom(float scale) {
        return exceedsMinScaleIncrement(scale, mActualScale);
    }

    public final boolean canZoomIn() {
        return mMaxZoomScale - mActualScale > MINIMUM_SCALE_INCREMENT;
    }

    public final boolean canZoomOut() {
        return mActualScale - mMinZoomScale > MINIMUM_SCALE_INCREMENT;
    }

    public boolean zoomIn() {
        return zoom(1.25f);
    }

    public boolean zoomOut() {
        return zoom(0.8f);
    }

    // returns TRUE if zoom out succeeds and FALSE if no zoom changes.
    private boolean zoom(float zoomMultiplier) {
        // TODO: alternatively we can disallow this during draw history mode
        mWebView.switchOutDrawHistory();
        // Center zooming to the center of the screen.
        mZoomCenterX = mWebView.getViewWidth() * .5f;
        mZoomCenterY = mWebView.getViewHeight() * .5f;
        mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
        mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
        return startZoomAnimation(mActualScale * zoomMultiplier, 
            !mWebView.getSettings().getUseFixedViewport());
    }

    /**
     * Initiates an animated zoom of the WebView.
     *
     * @return true if the new scale triggered an animation and false otherwise.
     */
    public boolean startZoomAnimation(float scale, boolean reflowText) {
        float oldScale = mActualScale;
        mInitialScrollX = mWebView.getScrollX();
        mInitialScrollY = mWebView.getScrollY();

        // snap to reading level scale if it is close
        if (!exceedsMinScaleIncrement(scale, getReadingLevelScale())) {
            scale = getReadingLevelScale();
        }

        if (mHardwareAccelerated) {
            mInHWAcceleratedZoom = true;
        }

        setZoomScale(scale, reflowText);

        if (oldScale != mActualScale) {
            // use mZoomPickerScale to see zoom preview first
            mZoomStart = SystemClock.uptimeMillis();
            mInvInitialZoomScale = 1.0f / oldScale;
            mInvFinalZoomScale = 1.0f / mActualScale;
            mZoomScale = mActualScale;
            mWebView.onFixedLengthZoomAnimationStart();
            mWebView.invalidate();
            return true;
        } else {
            return false;
        }
    }

    /**
     * This method is called by the WebView's drawing code when a fixed length zoom
     * animation is occurring. Its purpose is to animate the zooming of the canvas
     * to the desired scale which was specified in startZoomAnimation(...).
     *
     * A fixed length animation begins when startZoomAnimation(...) is called and
     * continues until the ZOOM_ANIMATION_LENGTH time has elapsed. During that
     * interval each time the WebView draws it calls this function which is 
     * responsible for generating the animation.
     *
     * Additionally, the WebView can check to see if such an animation is currently
     * in progress by calling isFixedLengthAnimationInProgress().
     */
    public void animateZoom(Canvas canvas) {
        if (mZoomScale == 0) {
            Log.w(LOGTAG, "A WebView is attempting to perform a fixed length "
                    + "zoom animation when no zoom is in progress");
            return;
        }

        float zoomScale;
        int interval = (int) (SystemClock.uptimeMillis() - mZoomStart);
        if (interval < ZOOM_ANIMATION_LENGTH) {
            float ratio = (float) interval / ZOOM_ANIMATION_LENGTH;
            zoomScale = 1.0f / (mInvInitialZoomScale
                    + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio);
            mWebView.invalidate();
        } else {
            zoomScale = mZoomScale;
            // set mZoomScale to be 0 as we have finished animating
            mZoomScale = 0;
            mWebView.onFixedLengthZoomAnimationEnd();
        }
        // calculate the intermediate scroll position. Since we need to use
        // zoomScale, we can't use the WebView's pinLocX/Y functions directly.
        float scale = zoomScale * mInvInitialZoomScale;
        int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX);
        tx = -WebView.pinLoc(tx, mWebView.getViewWidth(), Math.round(mWebView.getContentWidth()
                * zoomScale)) + mWebView.getScrollX();
        int titleHeight = mWebView.getTitleHeight();
        int ty = Math.round(scale
                * (mInitialScrollY + mZoomCenterY - titleHeight)
                - (mZoomCenterY - titleHeight));
        ty = -(ty <= titleHeight ? Math.max(ty, 0) : WebView.pinLoc(ty
                - titleHeight, mWebView.getViewHeight(), Math.round(mWebView.getContentHeight()
                * zoomScale)) + titleHeight) + mWebView.getScrollY();

        if (mHardwareAccelerated) {
            mWebView.updateScrollCoordinates(mWebView.getScrollX() - tx, mWebView.getScrollY() - ty);
            setZoomScale(zoomScale, false);

            if (mZoomScale == 0) {
                // We've reached the end of the zoom animation.
                mInHWAcceleratedZoom = false;
            }
        } else {
            canvas.translate(tx, ty);
            canvas.scale(zoomScale, zoomScale);
        }
    }

    public boolean isZoomAnimating() {
        return isFixedLengthAnimationInProgress() || mPinchToZoomAnimating;
    }

    public boolean isFixedLengthAnimationInProgress() {
        return mZoomScale != 0;
    }

    public void refreshZoomScale(boolean reflowText) {
        setZoomScale(mActualScale, reflowText, true);
    }

    public void setZoomScale(float scale, boolean reflowText) {
        setZoomScale(scale, reflowText, false);
    }

    private void setZoomScale(float scale, boolean reflowText, boolean force) {
        final boolean isScaleLessThanMinZoom = scale < mMinZoomScale;
        scale = computeScaleWithLimits(scale);

        // determine whether or not we are in the zoom overview mode
        if (isScaleLessThanMinZoom && mMinZoomScale < mDefaultScale) {
            mInZoomOverview = true;
        } else {
            mInZoomOverview = !exceedsMinScaleIncrement(scale, getZoomOverviewScale());
        }

        if (reflowText && !mWebView.getSettings().getUseFixedViewport()) {
            mTextWrapScale = scale;
        }

        if (exceedsMinScaleIncrement(scale, mActualScale) || force) {
            float oldScale = mActualScale;
            float oldInvScale = mInvActualScale;

            if (scale != mActualScale && !mPinchToZoomAnimating) {
                mCallbackProxy.onScaleChanged(mActualScale, scale);
            }

            mActualScale = scale;
            mInvActualScale = 1 / scale;

            if (!mWebView.drawHistory() && !mInHWAcceleratedZoom) {

                // If history Picture is drawn, don't update scroll. They will
                // be updated when we get out of that mode.
                // update our scroll so we don't appear to jump
                // i.e. keep the center of the doc in the center of the view
                // If this is part of a zoom on a HW accelerated canvas, we
                // have already updated the scroll so don't do it again.
                int oldX = mWebView.getScrollX();
                int oldY = mWebView.getScrollY();
                float ratio = scale * oldInvScale;
                float sx = ratio * oldX + (ratio - 1) * mZoomCenterX;
                float sy = ratio * oldY + (ratio - 1)
                        * (mZoomCenterY - mWebView.getTitleHeight());

                // Scale all the child views
                mWebView.mViewManager.scaleAll();

                // as we don't have animation for scaling, don't do animation
                // for scrolling, as it causes weird intermediate state
                int scrollX = mWebView.pinLocX(Math.round(sx));
                int scrollY = mWebView.pinLocY(Math.round(sy));
                if(!mWebView.updateScrollCoordinates(scrollX, scrollY)) {
                    // the scroll position is adjusted at the beginning of the
                    // zoom animation. But we want to update the WebKit at the
                    // end of the zoom animation. See comments in onScaleEnd().
                    mWebView.sendOurVisibleRect();
                }
            }

            // if the we need to reflow the text then force the VIEW_SIZE_CHANGED
            // event to be sent to WebKit
            mWebView.sendViewSizeZoom(reflowText);
        }
    }

    public boolean isDoubleTapEnabled() {
        WebSettings settings = mWebView.getSettings();
        return settings != null && settings.getUseWideViewPort();
    }

    /**
     * The double tap gesture can result in different behaviors depending on the
     * content that is tapped.
     *
     * (1) PLUGINS: If the taps occur on a plugin then we maximize the plugin on
     * the screen. If the plugin is already maximized then zoom the user into
     * overview mode.
     *
     * (2) HTML/OTHER: If the taps occur outside a plugin then the following
     * heuristic is used.
     *   A. If the current text wrap scale differs from newly calculated and the
     *      layout algorithm specifies the use of NARROW_COLUMNS, then fit to
     *      column by reflowing the text.
     *   B. If the page is not in overview mode then change to overview mode.
     *   C. If the page is in overmode then change to the default scale.
     */
    public void handleDoubleTap(float lastTouchX, float lastTouchY) {
        WebSettings settings = mWebView.getSettings();
        if (!isDoubleTapEnabled()) {
            return;
        }

        setZoomCenter(lastTouchX, lastTouchY);
        mAnchorX = mWebView.viewToContentX((int) lastTouchX + mWebView.getScrollX());
        mAnchorY = mWebView.viewToContentY((int) lastTouchY + mWebView.getScrollY());
        settings.setDoubleTapToastCount(0);

        // remove the zoom control after double tap
        dismissZoomPicker();

        /*
         * If the double tap was on a plugin then either zoom to maximize the
         * plugin on the screen or scale to overview mode.
         */
        Rect pluginBounds = mWebView.getPluginBounds(mAnchorX, mAnchorY);
        if (pluginBounds != null) {
            if (mWebView.isRectFitOnScreen(pluginBounds)) {
                zoomToOverview();
            } else {
                mWebView.centerFitRect(pluginBounds);
            }
            return;
        }

        final float newTextWrapScale;
        if (settings.getUseFixedViewport()) {
            newTextWrapScale = Math.max(mActualScale, getReadingLevelScale());
        } else {
            newTextWrapScale = mActualScale;
        }
        if (settings.isNarrowColumnLayout()
                && exceedsMinScaleIncrement(mTextWrapScale, newTextWrapScale)) {
            mTextWrapScale = newTextWrapScale;
            refreshZoomScale(true);
        } else if (!mInZoomOverview && willScaleTriggerZoom(getZoomOverviewScale())) {
            zoomToOverview();
        } else {
            zoomToReadingLevel();
        }
    }

    private void setZoomOverviewWidth(int width) {
        if (width == 0) {
            mZoomOverviewWidth = WebView.DEFAULT_VIEWPORT_WIDTH;
        } else {
            mZoomOverviewWidth = width;
        }
        mInvZoomOverviewWidth = 1.0f / width;
    }

    /* package */ float getZoomOverviewScale() {
        return mWebView.getViewWidth() * mInvZoomOverviewWidth;
    }

    public boolean isInZoomOverview() {
        return mInZoomOverview;
    }

    private void zoomToOverview() {
        // Force the titlebar fully reveal in overview mode
        int scrollY = mWebView.getScrollY();
        if (scrollY < mWebView.getTitleHeight()) {
            mWebView.updateScrollCoordinates(mWebView.getScrollX(), 0);
        }
        startZoomAnimation(getZoomOverviewScale(), 
            !mWebView.getSettings().getUseFixedViewport());
    }

    private void zoomToReadingLevel() {
        final float readingScale = getReadingLevelScale();
        int left = mWebView.nativeGetBlockLeftEdge(mAnchorX, mAnchorY, mActualScale);
        if (left != WebView.NO_LEFTEDGE) {
            // add a 5pt padding to the left edge.
            int viewLeft = mWebView.contentToViewX(left < 5 ? 0 : (left - 5))
                    - mWebView.getScrollX();
            // Re-calculate the zoom center so that the new scroll x will be
            // on the left edge.
            if (viewLeft > 0) {
                mZoomCenterX = viewLeft * readingScale / (readingScale - mActualScale);
            } else {
                mWebView.scrollBy(viewLeft, 0);
                mZoomCenterX = 0;
            }
        }
        startZoomAnimation(readingScale,
            !mWebView.getSettings().getUseFixedViewport());
    }

    public void updateMultiTouchSupport(Context context) {
        // check the preconditions
        assert mWebView.getSettings() != null;

        final WebSettings settings = mWebView.getSettings();
        final PackageManager pm = context.getPackageManager();
        mSupportMultiTouch = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
                && settings.supportZoom() && settings.getBuiltInZoomControls();
        mAllowPanAndScale = pm.hasSystemFeature(
                PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT);
        if (mSupportMultiTouch && (mScaleDetector == null)) {
            mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener());
        } else if (!mSupportMultiTouch && (mScaleDetector != null)) {
            mScaleDetector = null;
        }
    }

    public boolean supportsMultiTouchZoom() {
        return mSupportMultiTouch;
    }

    public boolean supportsPanDuringZoom() {
        return mAllowPanAndScale;
    }

    /**
     * Notifies the caller that the ZoomManager is requesting that scale related
     * updates should not be sent to webkit. This can occur in cases where the
     * ZoomManager is performing an animation and does not want webkit to update
     * until the animation is complete.
     *
     * @return true if scale related updates should not be sent to webkit and
     *         false otherwise.
     */
    public boolean isPreventingWebkitUpdates() {
        // currently only animating a multi-touch zoom prevents updates, but
        // others can add their own conditions to this method if necessary.
        return mPinchToZoomAnimating;
    }

    public ScaleGestureDetector getMultiTouchGestureDetector() {
        return mScaleDetector;
    }

    private class ScaleDetectorListener implements ScaleGestureDetector.OnScaleGestureListener {

        public boolean onScaleBegin(ScaleGestureDetector detector) {
            dismissZoomPicker();
            mWebView.mViewManager.startZoom();
            mWebView.onPinchToZoomAnimationStart();
            return true;
        }

        public boolean onScale(ScaleGestureDetector detector) {
            // Prevent scaling beyond overview scale.
            float scale = Math.max(detector.getScaleFactor() * mActualScale,
                    getZoomOverviewScale());
            if (mPinchToZoomAnimating || willScaleTriggerZoom(scale)) {
                mPinchToZoomAnimating = true;
                // limit the scale change per step
                if (scale > mActualScale) {
                    scale = Math.min(scale, mActualScale * 1.25f);
                } else {
                    scale = Math.max(scale, mActualScale * 0.8f);
                }
                // if the scale change is too small, regard it as jitter and skip it.
                if (Math.abs(scale - mActualScale) < MINIMUM_SCALE_WITHOUT_JITTER) {
                    return false;
                }
                setZoomCenter(detector.getFocusX(), detector.getFocusY());
                setZoomScale(scale, false);
                mWebView.invalidate();
                return true;
            }
            return false;
        }

        public void onScaleEnd(ScaleGestureDetector detector) {
            if (mPinchToZoomAnimating) {
                mPinchToZoomAnimating = false;
                mAnchorX = mWebView.viewToContentX((int) mZoomCenterX + mWebView.getScrollX());
                mAnchorY = mWebView.viewToContentY((int) mZoomCenterY + mWebView.getScrollY());
                // don't reflow when zoom in; when zoom out, do reflow if the
                // new scale is almost minimum scale.
                boolean reflowNow = !canZoomOut() || (mActualScale <= 0.8 * mTextWrapScale);
                // force zoom after mPreviewZoomOnly is set to false so that the
                // new view size will be passed to the WebKit
                refreshZoomScale(reflowNow &&
                    !mWebView.getSettings().getUseFixedViewport());
                // call invalidate() to draw without zoom filter
                mWebView.invalidate();
            }

            mWebView.mViewManager.endZoom();
            mWebView.onPinchToZoomAnimationEnd(detector);
        }
    }

    public void onSizeChanged(int w, int h, int ow, int oh) {
        // reset zoom and anchor to the top left corner of the screen
        // unless we are already zooming
        if (!isFixedLengthAnimationInProgress()) {
            int visibleTitleHeight = mWebView.getVisibleTitleHeight();
            mZoomCenterX = 0;
            mZoomCenterY = visibleTitleHeight;
            mAnchorX = mWebView.viewToContentX(mWebView.getScrollX());
            mAnchorY = mWebView.viewToContentY(visibleTitleHeight + mWebView.getScrollY());
        }

        // update mMinZoomScale if the minimum zoom scale is not fixed
        if (!mMinZoomScaleFixed) {
            // when change from narrow screen to wide screen, the new viewWidth
            // can be wider than the old content width. We limit the minimum
            // scale to 1.0f. The proper minimum scale will be calculated when
            // the new picture shows up.
            mMinZoomScale = Math.min(1.0f, (float) mWebView.getViewWidth()
                    / (mWebView.drawHistory() ? mWebView.getHistoryPictureWidth()
                            : mZoomOverviewWidth));
            // limit the minZoomScale to the initialScale if it is set
            if (mInitialScale > 0 && mInitialScale < mMinZoomScale) {
                mMinZoomScale = mInitialScale;
            }
        }

        dismissZoomPicker();

        // onSizeChanged() is called during WebView layout. And any
        // requestLayout() is blocked during layout. As refreshZoomScale() will
        // cause its child View to reposition itself through ViewManager's
        // scaleAll(), we need to post a Runnable to ensure requestLayout().
        // Additionally, only update the text wrap scale if the width changed.
        mWebView.post(new PostScale(w != ow &&
            !mWebView.getSettings().getUseFixedViewport(), mInZoomOverview));
    }

    private class PostScale implements Runnable {
        final boolean mUpdateTextWrap;
        // Remember the zoom overview state right after rotation since
        // it could be changed between the time this callback is initiated and
        // the time it's actually run.
        final boolean mInZoomOverviewBeforeSizeChange;

        public PostScale(boolean updateTextWrap, boolean inZoomOverview) {
            mUpdateTextWrap = updateTextWrap;
            mInZoomOverviewBeforeSizeChange = inZoomOverview;
        }

        public void run() {
            if (mWebView.getWebViewCore() != null) {
                // we always force, in case our height changed, in which case we
                // still want to send the notification over to webkit.
                // Keep overview mode unchanged when rotating.
                final float zoomOverviewScale = getZoomOverviewScale();
                final float newScale = (mInZoomOverviewBeforeSizeChange) ?
                    zoomOverviewScale : Math.max(mActualScale, zoomOverviewScale); 
                setZoomScale(newScale, mUpdateTextWrap, true);
                // update the zoom buttons as the scale can be changed
                updateZoomPicker();
            }
        }
    }

    public void updateZoomRange(WebViewCore.ViewState viewState,
            int viewWidth, int minPrefWidth) {
        if (viewState.mMinScale == 0) {
            if (viewState.mMobileSite) {
                if (minPrefWidth > Math.max(0, viewWidth)) {
                    mMinZoomScale = (float) viewWidth / minPrefWidth;
                    mMinZoomScaleFixed = false;
                } else {
                    mMinZoomScale = viewState.mDefaultScale;
                    mMinZoomScaleFixed = true;
                }
            } else {
                mMinZoomScale = mDefaultMinZoomScale;
                mMinZoomScaleFixed = false;
            }
        } else {
            mMinZoomScale = viewState.mMinScale;
            mMinZoomScaleFixed = true;
        }
        if (viewState.mMaxScale == 0) {
            mMaxZoomScale = mDefaultMaxZoomScale;
        } else {
            mMaxZoomScale = viewState.mMaxScale;
        }
    }

    /**
     * Updates zoom values when Webkit produces a new picture. This method
     * should only be called from the UI thread's message handler.
     */
    public void onNewPicture(WebViewCore.DrawData drawData) {
        final int viewWidth = mWebView.getViewWidth();
        final boolean zoomOverviewWidthChanged = setupZoomOverviewWidth(drawData, viewWidth);
        final float newZoomOverviewScale = getZoomOverviewScale();
        WebSettings settings = mWebView.getSettings();
        if (zoomOverviewWidthChanged && settings.isNarrowColumnLayout() &&
            settings.getUseFixedViewport() &&
            (mInitialZoomOverview || mInZoomOverview)) {
            // Keep mobile site's text wrap scale unchanged.  For mobile sites,
            // the text wrap scale is the same as zoom overview scale, which is 1.0f.
            if (exceedsMinScaleIncrement(mTextWrapScale, 1.0f) ||
                    exceedsMinScaleIncrement(newZoomOverviewScale, 1.0f)) {
                mTextWrapScale = getReadingLevelScale();
            } else {
                mTextWrapScale = newZoomOverviewScale;
            }
        }

        if (!mMinZoomScaleFixed) {
            mMinZoomScale = newZoomOverviewScale;
        }
        // fit the content width to the current view for the first new picture
        // after first layout.
        boolean scaleHasDiff = exceedsMinScaleIncrement(newZoomOverviewScale, mActualScale);
        // Make sure the actual scale is no less than zoom overview scale.
        boolean scaleLessThanOverview =
                (newZoomOverviewScale - mActualScale) >= MINIMUM_SCALE_INCREMENT;
        // Make sure mobile sites are correctly handled since mobile site will
        // change content width after rotating.
        boolean mobileSiteInOverview = mInZoomOverview &&
                !exceedsMinScaleIncrement(newZoomOverviewScale, 1.0f);
        if (!mWebView.drawHistory() &&
                (mInitialZoomOverview || scaleLessThanOverview || mobileSiteInOverview) &&
                scaleHasDiff) {
            mInitialZoomOverview = false;
            setZoomScale(newZoomOverviewScale, !willScaleTriggerZoom(mTextWrapScale) &&
                !mWebView.getSettings().getUseFixedViewport());
        } else {
            mInZoomOverview = !scaleHasDiff;
        }
    }

    /**
     * Set up correct zoom overview width based on different settings.
     *
     * @param drawData webviewcore draw data
     * @param viewWidth current view width
     */
    private boolean setupZoomOverviewWidth(WebViewCore.DrawData drawData, final int viewWidth) {
        WebSettings settings = mWebView.getSettings();
        int newZoomOverviewWidth = mZoomOverviewWidth;
        if (settings.getUseWideViewPort()) {
            if (!settings.getUseFixedViewport()) {
                // limit mZoomOverviewWidth upper bound to
                // sMaxViewportWidth so that if the page doesn't behave
                // well, the WebView won't go insane. limit the lower
                // bound to match the default scale for mobile sites.
                newZoomOverviewWidth = Math.min(WebView.sMaxViewportWidth,
                    Math.max((int) (viewWidth * mInvDefaultScale),
                          Math.max(drawData.mMinPrefWidth, drawData.mViewSize.x)));
            } else if (drawData.mContentSize.x > 0) {
                // The webkitDraw for layers will not populate contentSize, and it'll be
                // ignored for zoom overview width update.
                final int contentWidth = Math.max(drawData.mContentSize.x, drawData.mMinPrefWidth);
                newZoomOverviewWidth = Math.min(WebView.sMaxViewportWidth, contentWidth);
            }
        } else {
            // If not use wide viewport, use view width as the zoom overview width.
            newZoomOverviewWidth = viewWidth;
        }
        if (newZoomOverviewWidth != mZoomOverviewWidth) {
            setZoomOverviewWidth(newZoomOverviewWidth);
            return true;
        }
        return false;
    }

    /**
     * Updates zoom values after Webkit completes the initial page layout. It
     * is called when visiting a page for the first time as well as when the
     * user navigates back to a page (in which case we may need to restore the
     * zoom levels to the state they were when you left the page). This method
     * should only be called from the UI thread's message handler.
     */
    public void onFirstLayout(WebViewCore.DrawData drawData) {
        // precondition check
        assert drawData != null;
        assert drawData.mViewState != null;
        assert mWebView.getSettings() != null;

        WebViewCore.ViewState viewState = drawData.mViewState;
        final Point viewSize = drawData.mViewSize;
        updateZoomRange(viewState, viewSize.x, drawData.mMinPrefWidth);
        setupZoomOverviewWidth(drawData, mWebView.getViewWidth());

        if (!mWebView.drawHistory()) {
            float scale;
            final float overviewScale = getZoomOverviewScale();
            WebSettings settings = mWebView.getSettings();

            if (mInitialScale > 0) {
                scale = mInitialScale;
            } else if (viewState.mViewScale > 0) {
                mTextWrapScale = viewState.mTextWrapScale;
                scale = viewState.mViewScale;
            } else {
                scale = overviewScale;
                if (!settings.getUseWideViewPort()
                    || !settings.getLoadWithOverviewMode()) {
                    scale = Math.max(viewState.mTextWrapScale, scale);
                }
                if (settings.isNarrowColumnLayout() &&
                    settings.getUseFixedViewport()) {
                    // When first layout, reflow using the reading level scale to avoid
                    // reflow when double tapped.
                    mTextWrapScale = getReadingLevelScale();
                }
            }
            boolean reflowText = false;
            if (!viewState.mIsRestored) {
                if (settings.getUseFixedViewport()) {
                    // Override the scale only in case of fixed viewport.
                    scale = Math.max(scale, overviewScale);
                    mTextWrapScale = Math.max(mTextWrapScale, overviewScale);
                }
                reflowText = exceedsMinScaleIncrement(mTextWrapScale, scale);
            }
            mInitialZoomOverview = !exceedsMinScaleIncrement(scale, overviewScale);
            setZoomScale(scale, reflowText);

            // update the zoom buttons as the scale can be changed
            updateZoomPicker();
        }
    }

    public void saveZoomState(Bundle b) {
        b.putFloat("scale", mActualScale);
        b.putFloat("textwrapScale", mTextWrapScale);
        b.putBoolean("overview", mInZoomOverview);
    }

    public void restoreZoomState(Bundle b) {
        // as getWidth() / getHeight() of the view are not available yet, set up
        // mActualScale, so that when onSizeChanged() is called, the rest will
        // be set correctly
        mActualScale = b.getFloat("scale", 1.0f);
        mInvActualScale = 1 / mActualScale;
        mTextWrapScale = b.getFloat("textwrapScale", mActualScale);
        mInZoomOverview = b.getBoolean("overview");
    }

    private ZoomControlBase getCurrentZoomControl() {
        if (mWebView.getSettings() != null && mWebView.getSettings().supportZoom()) {
            if (mWebView.getSettings().getBuiltInZoomControls()) {
                if ((mEmbeddedZoomControl == null)
                        && mWebView.getSettings().getDisplayZoomControls()) {
                    mEmbeddedZoomControl = new ZoomControlEmbedded(this, mWebView);
                }
                return mEmbeddedZoomControl;
            } else {
                if (mExternalZoomControl == null) {
                    mExternalZoomControl = new ZoomControlExternal(mWebView);
                }
                return mExternalZoomControl;
            }
        }
        return null;
    }

    public void invokeZoomPicker() {
        ZoomControlBase control = getCurrentZoomControl();
        if (control != null) {
            control.show();
        }
    }

    public void dismissZoomPicker() {
        ZoomControlBase control = getCurrentZoomControl();
        if (control != null) {
            control.hide();
        }
    }

    public boolean isZoomPickerVisible() {
        ZoomControlBase control = getCurrentZoomControl();
        return (control != null) ? control.isVisible() : false;
    }

    public void updateZoomPicker() {
        ZoomControlBase control = getCurrentZoomControl();
        if (control != null) {
            control.update();
        }
    }

    /**
     * The embedded zoom control intercepts touch events and automatically stays
     * visible. The external control needs to constantly refresh its internal
     * timer to stay visible.
     */
    public void keepZoomPickerVisible() {
        ZoomControlBase control = getCurrentZoomControl();
        if (control != null && control == mExternalZoomControl) {
            control.show();
        }
    }

    public View getExternalZoomPicker() {
        ZoomControlBase control = getCurrentZoomControl();
        if (control != null && control == mExternalZoomControl) {
            return mExternalZoomControl.getControls();
        } else {
            return null;
        }
    }

    public void setHardwareAccelerated() {
        mHardwareAccelerated = true;
    }

    /**
     * OnPageFinished called by webview when a page is fully loaded.
     */
    /* package*/ void onPageFinished(String url) {
        // Turn off initial zoom overview flag when a page is fully loaded.
        mInitialZoomOverview = false;
    }
}