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