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