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