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