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