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