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