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