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