/* * Copyright (C) 2006 The Android Open Source Project * Copyright (c) 2010-2011, The Linux Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.webkit; import com.android.internal.R; import android.annotation.Widget; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Interpolator; import android.graphics.Paint; import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Region; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.net.Uri; import android.net.http.SslCertificate; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.os.SystemClock; import android.text.IClipboard; import android.text.Selection; import android.text.Spannable; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; import android.util.TypedValue; import android.view.Display; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.SoundEffectConstants; import android.view.VelocityTracker; import android.view.View; import android.view.ViewParent; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.OvershootInterpolator; import android.view.animation.RotateAnimation; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.view.WindowManager; import android.webkit.WebTextView.AutoCompleteAdapter; import android.webkit.WebViewCore.EventHub; import android.webkit.WebViewCore.TouchEventData; import android.widget.AbsoluteLayout; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.CheckedTextView; import android.widget.EdgeGlow; import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.OverScroller; import android.widget.Toast; import android.widget.ZoomButtonsController; import android.widget.ZoomControls; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import junit.framework.Assert; /** *

A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. * It uses the WebKit rendering engine to display * web pages and includes methods to navigate forward and backward * through a history, zoom in and out, perform text searches and more.

*

To enable the built-in zoom, set * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} * (introduced in API version 3). *

Note that, in order for your Activity to access the Internet and load web pages * in a WebView, you must add the {@code INTERNET} permissions to your * Android Manifest file:

*
<uses-permission android:name="android.permission.INTERNET" />
* *

This must be a child of the {@code <manifest>} * element.

* *

See the Web View * tutorial.

* *

Basic usage

* *

By default, a WebView provides no browser-like widgets, does not * enable JavaScript and web page errors are ignored. If your goal is only * to display some HTML as a part of your UI, this is probably fine; * the user won't need to interact with the web page beyond reading * it, and the web page won't need to interact with the user. If you * actually want a full-blown web browser, then you probably want to * invoke the Browser application with a URL Intent rather than show it * with a WebView. For example: *

 * Uri uri = Uri.parse("http://www.example.com");
 * Intent intent = new Intent(Intent.ACTION_VIEW, uri);
 * startActivity(intent);
 * 
*

See {@link android.content.Intent} for more information.

* *

To provide a WebView in your own Activity, include a {@code <WebView>} in your layout, * or set the entire Activity window as a WebView during {@link * android.app.Activity#onCreate(Bundle) onCreate()}:

*
 * WebView webview = new WebView(this);
 * setContentView(webview);
 * 
* *

Then load the desired web page:

*
 * // Simplest usage: note that an exception will NOT be thrown
 * // if there is an error loading this page (see below).
 * webview.loadUrl("http://slashdot.org/");
 *
 * // OR, you can also load from an HTML string:
 * String summary = "<html><body>You scored <b>192</b> points.</body></html>";
 * webview.loadData(summary, "text/html", "utf-8");
 * // ... although note that there are restrictions on what this HTML can do.
 * // See the JavaDocs for {@link #loadData(String,String,String) loadData()} and {@link
 * #loadDataWithBaseURL(String,String,String,String,String) loadDataWithBaseURL()} for more info.
 * 
* *

A WebView has several customization points where you can add your * own behavior. These are:

* * * *

Here's a more complicated example, showing error handling, * settings, and progress notification:

* *
 * // Let's display the progress in the activity title bar, like the
 * // browser app does.
 * getWindow().requestFeature(Window.FEATURE_PROGRESS);
 *
 * webview.getSettings().setJavaScriptEnabled(true);
 *
 * final Activity activity = this;
 * webview.setWebChromeClient(new WebChromeClient() {
 *   public void onProgressChanged(WebView view, int progress) {
 *     // Activities and WebViews measure progress with different scales.
 *     // The progress meter will automatically disappear when we reach 100%
 *     activity.setProgress(progress * 1000);
 *   }
 * });
 * webview.setWebViewClient(new WebViewClient() {
 *   public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
 *     Toast.makeText(activity, "Oh no! " + description, Toast.LENGTH_SHORT).show();
 *   }
 * });
 *
 * webview.loadUrl("http://slashdot.org/");
 * 
* *

Cookie and window management

* *

For obvious security reasons, your application has its own * cache, cookie store etc.—it does not share the Browser * application's data. Cookies are managed on a separate thread, so * operations like index building don't block the UI * thread. Follow the instructions in {@link android.webkit.CookieSyncManager} * if you want to use cookies in your application. *

* *

By default, requests by the HTML to open new windows are * ignored. This is true whether they be opened by JavaScript or by * the target attribute on a link. You can customize your * {@link WebChromeClient} to provide your own behaviour for opening multiple windows, * and render them in whatever manner you want.

* *

The standard behavior for an Activity is to be destroyed and * recreated when the device orientation or any other configuration changes. This will cause * the WebView to reload the current page. If you don't want that, you * can set your Activity to handle the {@code orientation} and {@code keyboardHidden} * changes, and then just leave the WebView alone. It'll automatically * re-orient itself as appropriate. Read Handling Runtime Changes for * more information about how to handle configuration changes during runtime.

* * *

Building web pages to support different screen densities

* *

The screen density of a device is based on the screen resolution. A screen with low density * has fewer available pixels per inch, where a screen with high density * has more — sometimes significantly more — pixels per inch. The density of a * screen is important because, other things being equal, a UI element (such as a button) whose * height and width are defined in terms of screen pixels will appear larger on the lower density * screen and smaller on the higher density screen. * For simplicity, Android collapses all actual screen densities into three generalized densities: * high, medium, and low.

*

By default, WebView scales a web page so that it is drawn at a size that matches the default * appearance on a medium density screen. So, it applies 1.5x scaling on a high density screen * (because its pixels are smaller) and 0.75x scaling on a low density screen (because its pixels * are bigger). * Starting with API Level 5 (Android 2.0), WebView supports DOM, CSS, and meta tag features to help * you (as a web developer) target screens with different screen densities.

*

Here's a summary of the features you can use to handle different screen densities:

* *

If you want to modify your web page for different densities, by using the {@code * -webkit-device-pixel-ratio} CSS media query and/or the {@code * window.devicePixelRatio} DOM property, then you should set the {@code target-densitydpi} meta * property to {@code device-dpi}. This stops Android from performing scaling in your web page and * allows you to make the necessary adjustments for each density via CSS and JavaScript.

* * */ @Widget public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener { // enable debug output for drag trackers private static final boolean DEBUG_DRAG_TRACKER = false; // if AUTO_REDRAW_HACK is true, then the CALL key will toggle redrawing // the screen all-the-time. Good for profiling our drawing code static private final boolean AUTO_REDRAW_HACK = false; // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; private AddressCacheMonitor mAddressCacheMonitor; static final String LOGTAG = "webview"; private final float TO_DEGREES = 0.0f; private final float PIVOT_X_VALUE = 0.5f; private final float PIVOT_Y_VALUE = 0.5f; private final float OVERSHOOT_TENSION = 3.0f; private final long ANIMATION_DURATION = 1000; private static class ExtendedZoomControls extends FrameLayout { public ExtendedZoomControls(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true); mPlusMinusZoomControls = (ZoomControls) findViewById( com.android.internal.R.id.zoomControls); findViewById(com.android.internal.R.id.zoomMagnify).setVisibility( View.GONE); } public void show(boolean showZoom, boolean canZoomOut) { mPlusMinusZoomControls.setVisibility( showZoom ? View.VISIBLE : View.GONE); fade(View.VISIBLE, 0.0f, 1.0f); } public void hide() { fade(View.GONE, 1.0f, 0.0f); } private void fade(int visibility, float startAlpha, float endAlpha) { AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); anim.setDuration(500); startAnimation(anim); setVisibility(visibility); } public boolean hasFocus() { return mPlusMinusZoomControls.hasFocus(); } public void setOnZoomInClickListener(OnClickListener listener) { mPlusMinusZoomControls.setOnZoomInClickListener(listener); } public void setOnZoomOutClickListener(OnClickListener listener) { mPlusMinusZoomControls.setOnZoomOutClickListener(listener); } ZoomControls mPlusMinusZoomControls; } /** * Transportation object for returning WebView across thread boundaries. */ public class WebViewTransport { private WebView mWebview; /** * Set the WebView to the transportation object. * @param webview The WebView to transport. */ public synchronized void setWebView(WebView webview) { mWebview = webview; } /** * Return the WebView object. * @return WebView The transported WebView object. */ public synchronized WebView getWebView() { return mWebview; } } // A final CallbackProxy shared by WebViewCore and BrowserFrame. private final CallbackProxy mCallbackProxy; private final WebViewDatabase mDatabase; // SSL certificate for the main top-level page (if secure) private SslCertificate mCertificate; // Native WebView pointer that is 0 until the native object has been // created. private int mNativeClass; // This would be final but it needs to be set to null when the WebView is // destroyed. private WebViewCore mWebViewCore; // Handler for dispatching UI messages. /* package */ final Handler mPrivateHandler = new PrivateHandler(); private WebTextView mWebTextView; // Used to ignore changes to webkit text that arrives to the UI side after // more key events. private int mTextGeneration; // Used by WebViewCore to create child views. /* package */ final ViewManager mViewManager; // Used to display in full screen mode PluginFullScreenHolder mFullScreenHolder; /** * Position of the last touch event. */ private float mLastTouchX; private float mLastTouchY; /** * Time of the last touch event. */ private long mLastTouchTime; /** * Time of the last time sending touch event to WebViewCore */ private long mLastSentTouchTime; /** * The minimum elapsed time before sending another ACTION_MOVE event to * WebViewCore. This really should be tuned for each type of the devices. * For example in Google Map api test case, it takes Dream device at least * 150ms to do a full cycle in the WebViewCore by processing a touch event, * triggering the layout and drawing the picture. While the same process * takes 60+ms on the current high speed device. If we make * TOUCH_SENT_INTERVAL too small, there will be multiple touch events sent * to WebViewCore queue and the real layout and draw events will be pushed * to further, which slows down the refresh rate. Choose 50 to favor the * current high speed devices. For Dream like devices, 100 is a better * choice. Maybe make this in the buildspec later. */ private static final int TOUCH_SENT_INTERVAL = 50; private int mCurrentTouchInterval = TOUCH_SENT_INTERVAL; /** * Helper class to get velocity for fling */ VelocityTracker mVelocityTracker; private int mMaximumFling; private float mLastVelocity; private float mLastVelX; private float mLastVelY; /** * Touch mode */ private int mTouchMode = TOUCH_DONE_MODE; private static final int TOUCH_INIT_MODE = 1; private static final int TOUCH_DRAG_START_MODE = 2; private static final int TOUCH_DRAG_MODE = 3; private static final int TOUCH_SHORTPRESS_START_MODE = 4; private static final int TOUCH_SHORTPRESS_MODE = 5; private static final int TOUCH_DOUBLE_TAP_MODE = 6; private static final int TOUCH_DONE_MODE = 7; private static final int TOUCH_PINCH_DRAG = 8; /** * True if we have a touch panel capable of detecting smooth pan/scale at the same time */ private boolean mAllowPanAndScale; // Whether to forward the touch events to WebCore private boolean mForwardTouchEvents = false; // Whether to prevent default during touch. The initial value depends on // mForwardTouchEvents. If WebCore wants all the touch events, it says yes // for touch down. Otherwise UI will wait for the answer of the first // confirmed move before taking over the control. private static final int PREVENT_DEFAULT_NO = 0; private static final int PREVENT_DEFAULT_MAYBE_YES = 1; private static final int PREVENT_DEFAULT_NO_FROM_TOUCH_DOWN = 2; private static final int PREVENT_DEFAULT_YES = 3; private static final int PREVENT_DEFAULT_IGNORE = 4; private int mPreventDefault = PREVENT_DEFAULT_IGNORE; // true when the touch movement exceeds the slop private boolean mConfirmMove; // if true, touch events will be first processed by WebCore, if prevent // default is not set, the UI will continue handle them. private boolean mDeferTouchProcess; // to avoid interfering with the current touch events, track them // separately. Currently no snapping or fling in the deferred process mode private int mDeferTouchMode = TOUCH_DONE_MODE; private float mLastDeferTouchX; private float mLastDeferTouchY; // To keep track of whether the current drag was initiated by a WebTextView, // so that we know not to hide the cursor boolean mDragFromTextInput; // Whether or not to draw the cursor ring. private boolean mDrawCursorRing = true; // true if onPause has been called (and not onResume) private boolean mIsPaused; // true if, during a transition to a new page, we're delaying // deleting a root layer until there's something to draw of the new page. private boolean mDelayedDeleteRootLayer; /** * Customizable constant */ // pre-computed square of ViewConfiguration.getScaledTouchSlop() private int mTouchSlopSquare; // pre-computed square of ViewConfiguration.getScaledDoubleTapSlop() private int mDoubleTapSlopSquare; // pre-computed density adjusted navigation slop private int mNavSlop; // This should be ViewConfiguration.getTapTimeout() // But system time out is 100ms, which is too short for the browser. // In the browser, if it switches out of tap too soon, jump tap won't work. private static final int TAP_TIMEOUT = 200; // This should be ViewConfiguration.getLongPressTimeout() // But system time out is 500ms, which is too short for the browser. // With a short timeout, it's difficult to treat trigger a short press. private static final int LONG_PRESS_TIMEOUT = 1000; // needed to avoid flinging after a pause of no movement private static final int MIN_FLING_TIME = 250; // draw unfiltered after drag is held without movement private static final int MOTIONLESS_TIME = 100; // The time that the Zoom Controls are visible before fading away private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout(); // The amount of content to overlap between two screens when going through // pages with the space bar, in pixels. private static final int PAGE_SCROLL_OVERLAP = 24; /** * These prevent calling requestLayout if either dimension is fixed. This * depends on the layout parameters and the measure specs. */ boolean mWidthCanMeasure; boolean mHeightCanMeasure; // Remember the last dimensions we sent to the native side so we can avoid // sending the same dimensions more than once. int mLastWidthSent; int mLastHeightSent; private int mContentWidth; // cache of value from WebViewCore private int mContentHeight; // cache of value from WebViewCore // Need to have the separate control for horizontal and vertical scrollbar // style than the View's single scrollbar style private boolean mOverlayHorizontalScrollbar = true; private boolean mOverlayVerticalScrollbar = false; // our standard speed. this way small distances will be traversed in less // time than large distances, but we cap the duration, so that very large // distances won't take too long to get there. private static final int STD_SPEED = 480; // pixels per second // time for the longest scroll animation private static final int MAX_DURATION = 750; // milliseconds private static final int SLIDE_TITLE_DURATION = 500; // milliseconds private OverScroller mScroller; private boolean mInOverScrollMode = false; private static Paint mOverScrollBackground; private static Paint mOverScrollBorder; private boolean mWrapContent; private static final int MOTIONLESS_FALSE = 0; private static final int MOTIONLESS_PENDING = 1; private static final int MOTIONLESS_TRUE = 2; private static final int MOTIONLESS_IGNORE = 3; private int mHeldMotionless; // whether support multi-touch private boolean mSupportMultiTouch; // use the framework's ScaleGestureDetector to handle multi-touch private ScaleGestureDetector mScaleDetector; // the anchor point in the document space where VIEW_SIZE_CHANGED should // apply to private int mAnchorX; private int mAnchorY; /* * Private message ids */ private static final int REMEMBER_PASSWORD = 1; private static final int NEVER_REMEMBER_PASSWORD = 2; private static final int SWITCH_TO_SHORTPRESS = 3; private static final int SWITCH_TO_LONGPRESS = 4; private static final int RELEASE_SINGLE_TAP = 5; private static final int REQUEST_FORM_DATA = 6; private static final int RESUME_WEBCORE_PRIORITY = 7; private static final int DRAG_HELD_MOTIONLESS = 8; private static final int AWAKEN_SCROLL_BARS = 9; private static final int PREVENT_DEFAULT_TIMEOUT = 10; private static final int FIRST_PRIVATE_MSG_ID = REMEMBER_PASSWORD; private static final int LAST_PRIVATE_MSG_ID = PREVENT_DEFAULT_TIMEOUT; /* * Package message ids */ //! arg1=x, arg2=y static final int SCROLL_TO_MSG_ID = 101; static final int SCROLL_BY_MSG_ID = 102; //! arg1=x, arg2=y static final int SPAWN_SCROLL_TO_MSG_ID = 103; //! arg1=x, arg2=y static final int SYNC_SCROLL_TO_MSG_ID = 104; static final int NEW_PICTURE_MSG_ID = 105; static final int UPDATE_TEXT_ENTRY_MSG_ID = 106; static final int WEBCORE_INITIALIZED_MSG_ID = 107; static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 108; static final int UPDATE_ZOOM_RANGE = 109; static final int MOVE_OUT_OF_PLUGIN = 110; static final int CLEAR_TEXT_ENTRY = 111; static final int UPDATE_TEXT_SELECTION_MSG_ID = 112; static final int SHOW_RECT_MSG_ID = 113; static final int LONG_PRESS_CENTER = 114; static final int PREVENT_TOUCH_ID = 115; static final int WEBCORE_NEED_TOUCH_EVENTS = 116; // obj=Rect in doc coordinates static final int INVAL_RECT_MSG_ID = 117; static final int REQUEST_KEYBOARD = 118; static final int DO_MOTION_UP = 119; static final int SHOW_FULLSCREEN = 120; static final int HIDE_FULLSCREEN = 121; static final int DOM_FOCUS_CHANGED = 122; static final int IMMEDIATE_REPAINT_MSG_ID = 123; static final int SET_ROOT_LAYER_MSG_ID = 124; static final int RETURN_LABEL = 125; static final int FIND_AGAIN = 126; static final int CENTER_FIT_RECT = 127; static final int REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID = 128; static final int SET_SCROLLBAR_MODES = 129; private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID; private static final int LAST_PACKAGE_MSG_ID = SET_SCROLLBAR_MODES; static final String[] HandlerPrivateDebugString = { "REMEMBER_PASSWORD", // = 1; "NEVER_REMEMBER_PASSWORD", // = 2; "SWITCH_TO_SHORTPRESS", // = 3; "SWITCH_TO_LONGPRESS", // = 4; "RELEASE_SINGLE_TAP", // = 5; "REQUEST_FORM_DATA", // = 6; "RESUME_WEBCORE_PRIORITY", // = 7; "DRAG_HELD_MOTIONLESS", // = 8; "AWAKEN_SCROLL_BARS", // = 9; "PREVENT_DEFAULT_TIMEOUT" // = 10; }; static final String[] HandlerPackageDebugString = { "SCROLL_TO_MSG_ID", // = 101; "SCROLL_BY_MSG_ID", // = 102; "SPAWN_SCROLL_TO_MSG_ID", // = 103; "SYNC_SCROLL_TO_MSG_ID", // = 104; "NEW_PICTURE_MSG_ID", // = 105; "UPDATE_TEXT_ENTRY_MSG_ID", // = 106; "WEBCORE_INITIALIZED_MSG_ID", // = 107; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 108; "UPDATE_ZOOM_RANGE", // = 109; "MOVE_OUT_OF_PLUGIN", // = 110; "CLEAR_TEXT_ENTRY", // = 111; "UPDATE_TEXT_SELECTION_MSG_ID", // = 112; "SHOW_RECT_MSG_ID", // = 113; "LONG_PRESS_CENTER", // = 114; "PREVENT_TOUCH_ID", // = 115; "WEBCORE_NEED_TOUCH_EVENTS", // = 116; "INVAL_RECT_MSG_ID", // = 117; "REQUEST_KEYBOARD", // = 118; "DO_MOTION_UP", // = 119; "SHOW_FULLSCREEN", // = 120; "HIDE_FULLSCREEN", // = 121; "DOM_FOCUS_CHANGED", // = 122; "IMMEDIATE_REPAINT_MSG_ID", // = 123; "SET_ROOT_LAYER_MSG_ID", // = 124; "RETURN_LABEL", // = 125; "FIND_AGAIN", // = 126; "CENTER_FIT_RECT", // = 127; "REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID", // = 128; "SET_SCROLLBAR_MODES" // = 129; }; // If the site doesn't use the viewport meta tag to specify the viewport, // use DEFAULT_VIEWPORT_WIDTH as the default viewport width static final int DEFAULT_VIEWPORT_WIDTH = 800; // normally we try to fit the content to the minimum preferred width // calculated by the Webkit. To avoid the bad behavior when some site's // minimum preferred width keeps growing when changing the viewport width or // the minimum preferred width is huge, an upper limit is needed. static int sMaxViewportWidth = DEFAULT_VIEWPORT_WIDTH; // default scale limit. Depending on the display density private static float DEFAULT_MAX_ZOOM_SCALE; private static float DEFAULT_MIN_ZOOM_SCALE; // scale limit, which can be set through viewport meta tag in the web page private float mMaxZoomScale; private float mMinZoomScale; private boolean mMinZoomScaleFixed = true; // initial scale in percent. 0 means using default. private int mInitialScaleInPercent = 0; // while in the zoom overview mode, the page's width is fully fit to the // current window. The page is alive, in another words, you can click to // follow the links. Double tap will toggle between zoom overview mode and // the last zoom scale. boolean mInZoomOverview = false; // ideally mZoomOverviewWidth should be mContentWidth. But sites like espn, // engadget always have wider mContentWidth no matter what viewport size is. int mZoomOverviewWidth = DEFAULT_VIEWPORT_WIDTH; float mTextWrapScale; // default scale. Depending on the display density. static int DEFAULT_SCALE_PERCENT; private float mDefaultScale; private static float MINIMUM_SCALE_INCREMENT = 0.01f; // set to true temporarily during ScaleGesture triggered zoom private boolean mPreviewZoomOnly = false; // computed scale and inverse, from mZoomWidth. private float mActualScale; private float mInvActualScale; // if this is non-zero, it is used on drawing rather than mActualScale private float mZoomScale; private float mInvInitialZoomScale; private float mInvFinalZoomScale; private int mInitialScrollX; private int mInitialScrollY; private long mZoomStart; private static final int ZOOM_ANIMATION_LENGTH = 500; private boolean mUserScroll = false; private int mSnapScrollMode = SNAP_NONE; private static final int SNAP_NONE = 0; private static final int SNAP_LOCK = 1; // not a separate state private static final int SNAP_X = 2; // may be combined with SNAP_LOCK private static final int SNAP_Y = 4; // may be combined with SNAP_LOCK private boolean mSnapPositive; // keep these in sync with their counterparts in WebView.cpp private static final int DRAW_EXTRAS_NONE = 0; private static final int DRAW_EXTRAS_FIND = 1; private static final int DRAW_EXTRAS_SELECTION = 2; private static final int DRAW_EXTRAS_CURSOR_RING = 3; // keep this in sync with WebCore:ScrollbarMode in WebKit private static final int SCROLLBAR_AUTO = 0; private static final int SCROLLBAR_ALWAYSOFF = 1; // as we auto fade scrollbar, this is ignored. private static final int SCROLLBAR_ALWAYSON = 2; private int mHorizontalScrollBarMode = SCROLLBAR_AUTO; private int mVerticalScrollBarMode = SCROLLBAR_AUTO; /** * Max distance to overscroll by in pixels. * This how far content can be pulled beyond its normal bounds by the user. */ private int mOverscrollDistance; /** * Max distance to overfling by in pixels. * This is how far flinged content can move beyond the end of its normal bounds. */ private int mOverflingDistance; /* * These manage the edge glow effect when flung or pulled beyond the edges. * If one is not null, all are not null. Checking one for null is as good as checking each. */ private EdgeGlow mEdgeGlowTop; private EdgeGlow mEdgeGlowBottom; private EdgeGlow mEdgeGlowLeft; private EdgeGlow mEdgeGlowRight; /* * These manage the delta the user has pulled beyond the edges. */ private int mOverscrollDeltaX; private int mOverscrollDeltaY; // Used to match key downs and key ups private boolean mGotKeyDown; /* package */ static boolean mLogEvent = true; // for event log private long mLastTouchUpTime = 0; /** * URI scheme for telephone number */ public static final String SCHEME_TEL = "tel:"; /** * URI scheme for email address */ public static final String SCHEME_MAILTO = "mailto:"; /** * URI scheme for map address */ public static final String SCHEME_GEO = "geo:0,0?q="; private int mBackgroundColor = Color.WHITE; // Used to notify listeners of a new picture. private PictureListener mPictureListener; /** * Interface to listen for new pictures as they change. */ public interface PictureListener { /** * Notify the listener that the picture has changed. * @param view The WebView that owns the picture. * @param picture The new picture. */ public void onNewPicture(WebView view, Picture picture); } // FIXME: Want to make this public, but need to change the API file. public /*static*/ class HitTestResult { /** * Default HitTestResult, where the target is unknown */ public static final int UNKNOWN_TYPE = 0; /** * HitTestResult for hitting a HTML::a tag */ public static final int ANCHOR_TYPE = 1; /** * HitTestResult for hitting a phone number */ public static final int PHONE_TYPE = 2; /** * HitTestResult for hitting a map address */ public static final int GEO_TYPE = 3; /** * HitTestResult for hitting an email address */ public static final int EMAIL_TYPE = 4; /** * HitTestResult for hitting an HTML::img tag */ public static final int IMAGE_TYPE = 5; /** * HitTestResult for hitting a HTML::a tag which contains HTML::img */ public static final int IMAGE_ANCHOR_TYPE = 6; /** * HitTestResult for hitting a HTML::a tag with src=http */ public static final int SRC_ANCHOR_TYPE = 7; /** * HitTestResult for hitting a HTML::a tag with src=http + HTML::img */ public static final int SRC_IMAGE_ANCHOR_TYPE = 8; /** * HitTestResult for hitting an edit text area */ public static final int EDIT_TEXT_TYPE = 9; private int mType; private String mExtra; HitTestResult() { mType = UNKNOWN_TYPE; } private void setType(int type) { mType = type; } private void setExtra(String extra) { mExtra = extra; } public int getType() { return mType; } public String getExtra() { return mExtra; } } // The View containing the zoom controls private ExtendedZoomControls mZoomControls; private Runnable mZoomControlRunnable; // mZoomButtonsController will be lazy initialized in // getZoomButtonsController() to get better performance. private ZoomButtonsController mZoomButtonsController; // These keep track of the center point of the zoom. They are used to // determine the point around which we should zoom. private float mZoomCenterX; private float mZoomCenterY; // smooth rotate private int mOrientation; private ZoomButtonsController.OnZoomListener mZoomListener = new ZoomButtonsController.OnZoomListener() { public void onVisibilityChanged(boolean visible) { if (visible) { switchOutDrawHistory(); // Bring back the hidden zoom controls. mZoomButtonsController.getZoomControls().setVisibility( View.VISIBLE); updateZoomButtonsEnabled(); } } public void onZoom(boolean zoomIn) { if (zoomIn) { zoomIn(); } else { zoomOut(); } updateZoomButtonsEnabled(); } }; /** * Construct a new WebView with a Context object. * @param context A Context object used to access application assets. */ public WebView(Context context) { this(context, null); } /** * Construct a new WebView with layout parameters. * @param context A Context object used to access application assets. * @param attrs An AttributeSet passed to our parent. */ public WebView(Context context, AttributeSet attrs) { this(context, attrs, com.android.internal.R.attr.webViewStyle); } /** * Construct a new WebView with layout parameters and a default style. * @param context A Context object used to access application assets. * @param attrs An AttributeSet passed to our parent. * @param defStyle The default style resource ID. */ public WebView(Context context, AttributeSet attrs, int defStyle) { this(context, attrs, defStyle, null); } /** * Construct a new WebView with layout parameters, a default style and a set * of custom Javscript interfaces to be added to the WebView at initialization * time. This guarantees that these interfaces will be available when the JS * context is initialized. * @param context A Context object used to access application assets. * @param attrs An AttributeSet passed to our parent. * @param defStyle The default style resource ID. * @param javascriptInterfaces is a Map of intareface names, as keys, and * object implementing those interfaces, as values. * @hide pending API council approval. */ protected WebView(Context context, AttributeSet attrs, int defStyle, Map javascriptInterfaces) { super(context, attrs, defStyle); init(); mCallbackProxy = new CallbackProxy(context, this); mViewManager = new ViewManager(this); mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); mScroller = new OverScroller(context); updateMultiTouchSupport(context); //smooth rotate Display display = ((WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); int orientation = display.getRotation(); mOrientation = orientation * 90; mAddressCacheMonitor = AddressCacheMonitor.getAddressCacheMonitor(context); } void updateMultiTouchSupport(Context context) { WebSettings settings = getSettings(); final PackageManager pm = context.getPackageManager(); mSupportMultiTouch = pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH) && settings.supportZoom() && settings.getBuiltInZoomControls(); mAllowPanAndScale = pm.hasSystemFeature( PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT); if (mSupportMultiTouch && (mScaleDetector == null)) { mScaleDetector = new ScaleGestureDetector(context, new ScaleDetectorListener()); } else if (!mSupportMultiTouch && (mScaleDetector != null)) { mScaleDetector = null; } } private void updateZoomButtonsEnabled() { if (mZoomButtonsController == null) return; boolean canZoomIn = mActualScale < mMaxZoomScale; boolean canZoomOut = mActualScale > mMinZoomScale && !mInZoomOverview; if (!canZoomIn && !canZoomOut) { // Hide the zoom in and out buttons, as well as the fit to page // button, if the page cannot zoom mZoomButtonsController.getZoomControls().setVisibility(View.GONE); } else { // Set each one individually, as a page may be able to zoom in // or out. mZoomButtonsController.setZoomInEnabled(canZoomIn); mZoomButtonsController.setZoomOutEnabled(canZoomOut); } } private void init() { setWillNotDraw(false); setFocusable(true); setFocusableInTouchMode(true); setClickable(true); setLongClickable(true); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); int slop = configuration.getScaledTouchSlop(); mTouchSlopSquare = slop * slop; mMinLockSnapReverseDistance = slop; slop = configuration.getScaledDoubleTapSlop(); mDoubleTapSlopSquare = slop * slop; final float density = getContext().getResources().getDisplayMetrics().density; // use one line height, 16 based on our current default font, for how // far we allow a touch be away from the edge of a link mNavSlop = (int) (16 * density); // density adjusted scale factors DEFAULT_SCALE_PERCENT = (int) (100 * density); mDefaultScale = density; mActualScale = density; mInvActualScale = 1 / density; mTextWrapScale = density; DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; mMaximumFling = configuration.getScaledMaximumFlingVelocity(); mOverscrollDistance = configuration.getScaledOverscrollDistance(); mOverflingDistance = configuration.getScaledOverflingDistance(); } @Override public void setOverScrollMode(int mode) { super.setOverScrollMode(mode); if (mode != OVER_SCROLL_NEVER) { if (mEdgeGlowTop == null) { final Resources res = getContext().getResources(); final Drawable edge = res.getDrawable(R.drawable.overscroll_edge); final Drawable glow = res.getDrawable(R.drawable.overscroll_glow); mEdgeGlowTop = new EdgeGlow(edge, glow); mEdgeGlowBottom = new EdgeGlow(edge, glow); mEdgeGlowLeft = new EdgeGlow(edge, glow); mEdgeGlowRight = new EdgeGlow(edge, glow); } } else { mEdgeGlowTop = null; mEdgeGlowBottom = null; mEdgeGlowLeft = null; mEdgeGlowRight = null; } } /* package */void updateDefaultZoomDensity(int zoomDensity) { final float density = getContext().getResources().getDisplayMetrics().density * 100 / zoomDensity; if (Math.abs(density - mDefaultScale) > 0.01) { float scaleFactor = density / mDefaultScale; // adjust the limits mNavSlop = (int) (16 * density); DEFAULT_SCALE_PERCENT = (int) (100 * density); DEFAULT_MAX_ZOOM_SCALE = 4.0f * density; DEFAULT_MIN_ZOOM_SCALE = 0.25f * density; mDefaultScale = density; mMaxZoomScale *= scaleFactor; mMinZoomScale *= scaleFactor; setNewZoomScale(mActualScale * scaleFactor, true, false); } } /* package */ boolean onSavePassword(String schemePlusHost, String username, String password, final Message resumeMsg) { boolean rVal = false; if (resumeMsg == null) { // null resumeMsg implies saving password silently mDatabase.setUsernamePassword(schemePlusHost, username, password); } else { final Message remember = mPrivateHandler.obtainMessage( REMEMBER_PASSWORD); remember.getData().putString("host", schemePlusHost); remember.getData().putString("username", username); remember.getData().putString("password", password); remember.obj = resumeMsg; final Message neverRemember = mPrivateHandler.obtainMessage( NEVER_REMEMBER_PASSWORD); neverRemember.getData().putString("host", schemePlusHost); neverRemember.getData().putString("username", username); neverRemember.getData().putString("password", password); neverRemember.obj = resumeMsg; new AlertDialog.Builder(getContext()) .setTitle(com.android.internal.R.string.save_password_label) .setMessage(com.android.internal.R.string.save_password_message) .setPositiveButton(com.android.internal.R.string.save_password_notnow, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { resumeMsg.sendToTarget(); } }) .setNeutralButton(com.android.internal.R.string.save_password_remember, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { remember.sendToTarget(); } }) .setNegativeButton(com.android.internal.R.string.save_password_never, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { neverRemember.sendToTarget(); } }) .setOnCancelListener(new OnCancelListener() { public void onCancel(DialogInterface dialog) { resumeMsg.sendToTarget(); } }).show(); // Return true so that WebViewCore will pause while the dialog is // up. rVal = true; } return rVal; } @Override public void setScrollBarStyle(int style) { if (style == View.SCROLLBARS_INSIDE_INSET || style == View.SCROLLBARS_OUTSIDE_INSET) { mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = false; } else { mOverlayHorizontalScrollbar = mOverlayVerticalScrollbar = true; } super.setScrollBarStyle(style); } /** * Specify whether the horizontal scrollbar has overlay style. * @param overlay TRUE if horizontal scrollbar should have overlay style. */ public void setHorizontalScrollbarOverlay(boolean overlay) { mOverlayHorizontalScrollbar = overlay; } /** * Specify whether the vertical scrollbar has overlay style. * @param overlay TRUE if vertical scrollbar should have overlay style. */ public void setVerticalScrollbarOverlay(boolean overlay) { mOverlayVerticalScrollbar = overlay; } /** * Return whether horizontal scrollbar has overlay style * @return TRUE if horizontal scrollbar has overlay style. */ public boolean overlayHorizontalScrollbar() { return mOverlayHorizontalScrollbar; } /** * Return whether vertical scrollbar has overlay style * @return TRUE if vertical scrollbar has overlay style. */ public boolean overlayVerticalScrollbar() { return mOverlayVerticalScrollbar; } /* * Return the width of the view where the content of WebView should render * to. * Note: this can be called from WebCoreThread. */ /* package */ int getViewWidth() { if (!isVerticalScrollBarEnabled() || mOverlayVerticalScrollbar) { return getWidth(); } else { return getWidth() - getVerticalScrollbarWidth(); } } /* * returns the height of the titlebarview (if any). Does not care about * scrolling */ private int getTitleHeight() { return mTitleBar != null ? mTitleBar.getHeight() : 0; } /* * Return the amount of the titlebarview (if any) that is visible */ private int getVisibleTitleHeight() { // need to restrict mScrollY due to over scroll return Math.max(getTitleHeight() - Math.max(0, mScrollY), 0); } /* * Return the height of the view where the content of WebView should render * to. Note that this excludes mTitleBar, if there is one. * Note: this can be called from WebCoreThread. */ /* package */ int getViewHeight() { return getViewHeightWithTitle() - getVisibleTitleHeight(); } private int getViewHeightWithTitle() { int height = getHeight(); if (isHorizontalScrollBarEnabled() && !mOverlayHorizontalScrollbar) { height -= getHorizontalScrollbarHeight(); } return height; } /** * @return The SSL certificate for the main top-level page or null if * there is no certificate (the site is not secure). */ public SslCertificate getCertificate() { return mCertificate; } /** * Sets the SSL certificate for the main top-level page. */ public void setCertificate(SslCertificate certificate) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "setCertificate=" + certificate); } // here, the certificate can be null (if the site is not secure) mCertificate = certificate; } //------------------------------------------------------------------------- // Methods called by activity //------------------------------------------------------------------------- /** * Save the username and password for a particular host in the WebView's * internal database. * @param host The host that required the credentials. * @param username The username for the given host. * @param password The password for the given host. */ public void savePassword(String host, String username, String password) { mDatabase.setUsernamePassword(host, username, password); } /** * Set the HTTP authentication credentials for a given host and realm. * * @param host The host for the credentials. * @param realm The realm for the credentials. * @param username The username for the password. If it is null, it means * password can't be saved. * @param password The password */ public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { mDatabase.setHttpAuthUsernamePassword(host, realm, username, password); } /** * Retrieve the HTTP authentication username and password for a given * host & realm pair * * @param host The host for which the credentials apply. * @param realm The realm for which the credentials apply. * @return String[] if found, String[0] is username, which can be null and * String[1] is password. Return null if it can't find anything. */ public String[] getHttpAuthUsernamePassword(String host, String realm) { return mDatabase.getHttpAuthUsernamePassword(host, realm); } private void clearHelpers() { clearTextEntry(false); selectionDone(); } /** * Destroy the internal state of the WebView. This method should be called * after the WebView has been removed from the view system. No other * methods may be called on a WebView after destroy. */ public void destroy() { clearHelpers(); if (mWebViewCore != null) { // Set the handlers to null before destroying WebViewCore so no // more messages will be posted. mCallbackProxy.setWebViewClient(null); mCallbackProxy.setWebChromeClient(null); // Tell WebViewCore to destroy itself synchronized (this) { WebViewCore webViewCore = mWebViewCore; mWebViewCore = null; // prevent using partial webViewCore webViewCore.destroy(); } // Remove any pending messages that might not be serviced yet. mPrivateHandler.removeCallbacksAndMessages(null); mCallbackProxy.removeCallbacksAndMessages(null); // Wake up the WebCore thread just in case it is waiting for a // javascript dialog. synchronized (mCallbackProxy) { mCallbackProxy.notify(); } } if (mNativeClass != 0) { nativeDestroy(); mNativeClass = 0; } } /** * Enables platform notifications of data state and proxy changes. */ public static void enablePlatformNotifications() { Network.enablePlatformNotifications(); } /** * If platform notifications are enabled, this should be called * from the Activity's onPause() or onStop(). */ public static void disablePlatformNotifications() { Network.disablePlatformNotifications(); } /** * Sets JavaScript engine flags. * * @param flags JS engine flags in a String * * @hide pending API solidification */ public void setJsFlags(String flags) { mWebViewCore.sendMessage(EventHub.SET_JS_FLAGS, flags); } /** * @hide pending API council approval. */ public void startDnsPrefetch() { mWebViewCore.sendMessage(EventHub.START_DNS_PREFETCH); } /** * Inform WebView of the network state. This is used to set * the javascript property window.navigator.isOnline and * generates the online/offline event as specified in HTML5, sec. 5.7.7 * @param networkUp boolean indicating if network is available */ public void setNetworkAvailable(boolean networkUp) { mWebViewCore.sendMessage(EventHub.SET_NETWORK_STATE, networkUp ? 1 : 0, 0); } /** * Inform WebView about the current network type. * {@hide} */ public void setNetworkType(String type, String subtype) { Map map = new HashMap(); map.put("type", type); map.put("subtype", subtype); mWebViewCore.sendMessage(EventHub.SET_NETWORK_TYPE, map); } /** * Save the state of this WebView used in * {@link android.app.Activity#onSaveInstanceState}. Please note that this * method no longer stores the display data for this WebView. The previous * behavior could potentially leak files if {@link #restoreState} was never * called. See {@link #savePicture} and {@link #restorePicture} for saving * and restoring the display data. * @param outState The Bundle to store the WebView state. * @return The same copy of the back/forward list used to save the state. If * saveState fails, the returned list will be null. * @see #savePicture * @see #restorePicture */ public WebBackForwardList saveState(Bundle outState) { if (outState == null) { return null; } // We grab a copy of the back/forward list because a client of WebView // may have invalidated the history list by calling clearHistory. WebBackForwardList list = copyBackForwardList(); final int currentIndex = list.getCurrentIndex(); final int size = list.getSize(); // We should fail saving the state if the list is empty or the index is // not in a valid range. if (currentIndex < 0 || currentIndex >= size || size == 0) { return null; } outState.putInt("index", currentIndex); // FIXME: This should just be a byte[][] instead of ArrayList but // Parcel.java does not have the code to handle multi-dimensional // arrays. ArrayList history = new ArrayList(size); for (int i = 0; i < size; i++) { WebHistoryItem item = list.getItemAtIndex(i); if (null == item) { // FIXME: this shouldn't happen // need to determine how item got set to null Log.w(LOGTAG, "saveState: Unexpected null history item."); return null; } byte[] data = item.getFlattenedData(); if (data == null) { // It would be very odd to not have any data for a given history // item. And we will fail to rebuild the history list without // flattened data. return null; } history.add(data); } outState.putSerializable("history", history); if (mCertificate != null) { outState.putBundle("certificate", SslCertificate.saveState(mCertificate)); } return list; } /** * Save the current display data to the Bundle given. Used in conjunction * with {@link #saveState}. * @param b A Bundle to store the display data. * @param dest The file to store the serialized picture data. Will be * overwritten with this WebView's picture data. * @return True if the picture was successfully saved. */ public boolean savePicture(Bundle b, final File dest) { if (dest == null || b == null) { return false; } final Picture p = capturePicture(); // Use a temporary file while writing to ensure the destination file // contains valid data. final File temp = new File(dest.getPath() + ".writing"); new Thread(new Runnable() { public void run() { FileOutputStream out = null; try { out = new FileOutputStream(temp); p.writeToStream(out); // Writing the picture succeeded, rename the temporary file // to the destination. temp.renameTo(dest); } catch (Exception e) { // too late to do anything about it. } finally { if (out != null) { try { out.close(); } catch (Exception e) { // Can't do anything about that } } temp.delete(); } } }).start(); // now update the bundle b.putInt("scrollX", mScrollX); b.putInt("scrollY", mScrollY); b.putFloat("scale", mActualScale); b.putFloat("textwrapScale", mTextWrapScale); b.putBoolean("overview", mInZoomOverview); return true; } private void restoreHistoryPictureFields(Picture p, Bundle b) { int sx = b.getInt("scrollX", 0); int sy = b.getInt("scrollY", 0); float scale = b.getFloat("scale", 1.0f); mDrawHistory = true; mHistoryPicture = p; mScrollX = sx; mScrollY = sy; mHistoryWidth = Math.round(p.getWidth() * scale); mHistoryHeight = Math.round(p.getHeight() * scale); // as getWidth() / getHeight() of the view are not available yet, set up // mActualScale, so that when onSizeChanged() is called, the rest will // be set correctly mActualScale = scale; mInvActualScale = 1 / scale; mTextWrapScale = b.getFloat("textwrapScale", scale); mInZoomOverview = b.getBoolean("overview"); invalidate(); } /** * Restore the display data that was save in {@link #savePicture}. Used in * conjunction with {@link #restoreState}. * @param b A Bundle containing the saved display data. * @param src The file where the picture data was stored. * @return True if the picture was successfully restored. */ public boolean restorePicture(Bundle b, File src) { if (src == null || b == null) { return false; } if (!src.exists()) { return false; } try { final FileInputStream in = new FileInputStream(src); final Bundle copy = new Bundle(b); new Thread(new Runnable() { public void run() { try { final Picture p = Picture.createFromStream(in); if (p != null) { // Post a runnable on the main thread to update the // history picture fields. mPrivateHandler.post(new Runnable() { public void run() { restoreHistoryPictureFields(p, copy); } }); } } finally { try { in.close(); } catch (Exception e) { // Nothing we can do now. } } } }).start(); } catch (FileNotFoundException e){ e.printStackTrace(); } return true; } /** * Restore the state of this WebView from the given map used in * {@link android.app.Activity#onRestoreInstanceState}. This method should * be called to restore the state of the WebView before using the object. If * it is called after the WebView has had a chance to build state (load * pages, create a back/forward list, etc.) there may be undesirable * side-effects. Please note that this method no longer restores the * display data for this WebView. See {@link #savePicture} and {@link * #restorePicture} for saving and restoring the display data. * @param inState The incoming Bundle of state. * @return The restored back/forward list or null if restoreState failed. * @see #savePicture * @see #restorePicture */ public WebBackForwardList restoreState(Bundle inState) { WebBackForwardList returnList = null; if (inState == null) { return returnList; } if (inState.containsKey("index") && inState.containsKey("history")) { mCertificate = SslCertificate.restoreState( inState.getBundle("certificate")); final WebBackForwardList list = mCallbackProxy.getBackForwardList(); final int index = inState.getInt("index"); // We can't use a clone of the list because we need to modify the // shared copy, so synchronize instead to prevent concurrent // modifications. synchronized (list) { final List history = (List) inState.getSerializable("history"); final int size = history.size(); // Check the index bounds so we don't crash in native code while // restoring the history index. if (index < 0 || index >= size) { return null; } for (int i = 0; i < size; i++) { byte[] data = history.remove(0); if (data == null) { // If we somehow have null data, we cannot reconstruct // the item and thus our history list cannot be rebuilt. return null; } WebHistoryItem item = new WebHistoryItem(data); list.addHistoryItem(item); } // Grab the most recent copy to return to the caller. returnList = copyBackForwardList(); // Update the copy to have the correct index. returnList.setCurrentIndex(index); } // Remove all pending messages because we are restoring previous // state. mWebViewCore.removeMessages(); // Send a restore state message. mWebViewCore.sendMessage(EventHub.RESTORE_STATE, index); } return returnList; } /** * Load the given url with the extra headers. * @param url The url of the resource to load. * @param extraHeaders The extra headers sent with this url. This should not * include the common headers like "user-agent". If it does, it * will be replaced by the intrinsic value of the WebView. */ public void loadUrl(String url, Map extraHeaders) { switchOutDrawHistory(); WebViewCore.GetUrlData arg = new WebViewCore.GetUrlData(); arg.mUrl = url; arg.mExtraHeaders = extraHeaders; mWebViewCore.sendMessage(EventHub.LOAD_URL, arg); clearHelpers(); } /** * Load the given url. * @param url The url of the resource to load. */ public void loadUrl(String url) { if (url == null) { return; } loadUrl(url, null); } /** * Load the url with postData using "POST" method into the WebView. If url * is not a network url, it will be loaded with {link * {@link #loadUrl(String)} instead. * * @param url The url of the resource to load. * @param postData The data will be passed to "POST" request. */ public void postUrl(String url, byte[] postData) { if (URLUtil.isNetworkUrl(url)) { switchOutDrawHistory(); WebViewCore.PostUrlData arg = new WebViewCore.PostUrlData(); arg.mUrl = url; arg.mPostData = postData; mWebViewCore.sendMessage(EventHub.POST_URL, arg); clearHelpers(); } else { loadUrl(url); } } /** * Load the given data into the WebView. This will load the data into * WebView using the data: scheme. Content loaded through this mechanism * does not have the ability to load content from the network. * @param data A String of data in the given encoding. The date must * be URI-escaped -- '#', '%', '\', '?' should be replaced by %23, %25, * %27, %3f respectively. * @param mimeType The MIMEType of the data. i.e. text/html, image/jpeg * @param encoding The encoding of the data. i.e. utf-8, base64 */ public void loadData(String data, String mimeType, String encoding) { loadUrl("data:" + mimeType + ";" + encoding + "," + data); } /** * Load the given data into the WebView, use the provided URL as the base * URL for the content. The base URL is the URL that represents the page * that is loaded through this interface. As such, it is used to resolve any * relative URLs. The historyUrl is used for the history entry. *

* Note for post 1.0. Due to the change in the WebKit, the access to asset * files through "file:///android_asset/" for the sub resources is more * restricted. If you provide null or empty string as baseUrl, you won't be * able to access asset files. If the baseUrl is anything other than * http(s)/ftp(s)/about/javascript as scheme, you can access asset files for * sub resources. * * @param baseUrl Url to resolve relative paths with, if null defaults to * "about:blank" * @param data A String of data in the given encoding. * @param mimeType The MIMEType of the data. i.e. text/html. If null, * defaults to "text/html" * @param encoding The encoding of the data. i.e. utf-8, us-ascii * @param historyUrl URL to use as the history entry. Can be null. */ public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { loadData(data, mimeType, encoding); return; } switchOutDrawHistory(); WebViewCore.BaseUrlData arg = new WebViewCore.BaseUrlData(); arg.mBaseUrl = baseUrl; arg.mData = data; arg.mMimeType = mimeType; arg.mEncoding = encoding; arg.mHistoryUrl = historyUrl; mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg); clearHelpers(); } /** * Stop the current load. */ public void stopLoading() { // TODO: should we clear all the messages in the queue before sending // STOP_LOADING? switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.STOP_LOADING); } /** * Reload the current url. */ public void reload() { clearHelpers(); switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.RELOAD); } /** * Return true if this WebView has a back history item. * @return True iff this WebView has a back history item. */ public boolean canGoBack() { WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { return false; } else { return l.getCurrentIndex() > 0; } } } /** * Go back in the history of this WebView. */ public void goBack() { goBackOrForward(-1); } /** * Return true if this WebView has a forward history item. * @return True iff this Webview has a forward history item. */ public boolean canGoForward() { WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { return false; } else { return l.getCurrentIndex() < l.getSize() - 1; } } } /** * Go forward in the history of this WebView. */ public void goForward() { goBackOrForward(1); } /** * Return true if the page can go back or forward the given * number of steps. * @param steps The negative or positive number of steps to move the * history. */ public boolean canGoBackOrForward(int steps) { WebBackForwardList l = mCallbackProxy.getBackForwardList(); synchronized (l) { if (l.getClearPending()) { return false; } else { int newIndex = l.getCurrentIndex() + steps; return newIndex >= 0 && newIndex < l.getSize(); } } } /** * Go to the history item that is the number of steps away from * the current item. Steps is negative if backward and positive * if forward. * @param steps The number of steps to take back or forward in the back * forward list. */ public void goBackOrForward(int steps) { goBackOrForward(steps, false); } private void goBackOrForward(int steps, boolean ignoreSnapshot) { if (steps != 0) { clearHelpers(); mWebViewCore.sendMessage(EventHub.GO_BACK_FORWARD, steps, ignoreSnapshot ? 1 : 0); } } private boolean extendScroll(int y) { int finalY = mScroller.getFinalY(); int newY = pinLocY(finalY + y); if (newY == finalY) return false; mScroller.setFinalY(newY); mScroller.extendDuration(computeDuration(0, y)); return true; } /** * Scroll the contents of the view up by half the view size * @param top true to jump to the top of the page * @return true if the page was scrolled */ public boolean pageUp(boolean top) { if (mNativeClass == 0) { return false; } nativeClearCursor(); // start next trackball movement from page edge if (top) { // go to the top of the document return pinScrollTo(mScrollX, 0, true, 0); } // Page up int h = getHeight(); int y; if (h > 2 * PAGE_SCROLL_OVERLAP) { y = -h + PAGE_SCROLL_OVERLAP; } else { y = -h / 2; } mUserScroll = true; return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } /** * Scroll the contents of the view down by half the page size * @param bottom true to jump to bottom of page * @return true if the page was scrolled */ public boolean pageDown(boolean bottom) { if (mNativeClass == 0) { return false; } nativeClearCursor(); // start next trackball movement from page edge if (bottom) { return pinScrollTo(mScrollX, computeRealVerticalScrollRange(), true, 0); } // Page down. int h = getHeight(); int y; if (h > 2 * PAGE_SCROLL_OVERLAP) { y = h - PAGE_SCROLL_OVERLAP; } else { y = h / 2; } mUserScroll = true; return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } /** * Clear the view so that onDraw() will draw nothing but white background, * and onMeasure() will return 0 if MeasureSpec is not MeasureSpec.EXACTLY */ public void clearView() { mContentWidth = 0; mContentHeight = 0; mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } /** * Return a new picture that captures the current display of the webview. * This is a copy of the display, and will be unaffected if the webview * later loads a different URL. * * @return a picture containing the current contents of the view. Note this * picture is of the entire document, and is not restricted to the * bounds of the view. */ public Picture capturePicture() { if (null == mWebViewCore) return null; // check for out of memory tab return mWebViewCore.copyContentPicture(); } /** * Return true if the browser is displaying a TextView for text input. */ private boolean inEditingMode() { return mWebTextView != null && mWebTextView.getParent() != null; } /** * Remove the WebTextView. * @param disableFocusController If true, send a message to webkit * disabling the focus controller, so the caret stops blinking. */ private void clearTextEntry(boolean disableFocusController) { if (inEditingMode()) { mWebTextView.remove(); if (disableFocusController) { setFocusControllerInactive(); } } } /** * Return the current scale of the WebView * @return The current scale. */ public float getScale() { return mActualScale; } /** * Set the initial scale for the WebView. 0 means default. If * {@link WebSettings#getUseWideViewPort()} is true, it zooms out all the * way. Otherwise it starts with 100%. If initial scale is greater than 0, * WebView starts will this value as initial scale. * * @param scaleInPercent The initial scale in percent. */ public void setInitialScale(int scaleInPercent) { mInitialScaleInPercent = scaleInPercent; } /** * Invoke the graphical zoom picker widget for this WebView. This will * result in the zoom widget appearing on the screen to control the zoom * level of this WebView. */ public void invokeZoomPicker() { if (!getSettings().supportZoom()) { Log.w(LOGTAG, "This WebView doesn't support zoom."); return; } clearTextEntry(false); if (getSettings().getBuiltInZoomControls()) { getZoomButtonsController().setVisible(true); } else { mPrivateHandler.removeCallbacks(mZoomControlRunnable); mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); } } /** * Return a HitTestResult based on the current cursor node. If a HTML::a tag * is found and the anchor has a non-javascript url, the HitTestResult type * is set to SRC_ANCHOR_TYPE and the url is set in the "extra" field. If the * anchor does not have a url or if it is a javascript url, the type will * be UNKNOWN_TYPE and the url has to be retrieved through * {@link #requestFocusNodeHref} asynchronously. If a HTML::img tag is * found, the HitTestResult type is set to IMAGE_TYPE and the url is set in * the "extra" field. A type of * SRC_IMAGE_ANCHOR_TYPE indicates an anchor with a url that has an image as * a child node. If a phone number is found, the HitTestResult type is set * to PHONE_TYPE and the phone number is set in the "extra" field of * HitTestResult. If a map address is found, the HitTestResult type is set * to GEO_TYPE and the address is set in the "extra" field of HitTestResult. * If an email address is found, the HitTestResult type is set to EMAIL_TYPE * and the email is set in the "extra" field of HitTestResult. Otherwise, * HitTestResult type is set to UNKNOWN_TYPE. */ public HitTestResult getHitTestResult() { if (mNativeClass == 0) { return null; } HitTestResult result = new HitTestResult(); if (nativeHasCursorNode()) { if (nativeCursorIsTextInput()) { result.setType(HitTestResult.EDIT_TEXT_TYPE); } else { String text = nativeCursorText(); if (text != null) { if (text.startsWith(SCHEME_TEL)) { result.setType(HitTestResult.PHONE_TYPE); result.setExtra(text.substring(SCHEME_TEL.length())); } else if (text.startsWith(SCHEME_MAILTO)) { result.setType(HitTestResult.EMAIL_TYPE); result.setExtra(text.substring(SCHEME_MAILTO.length())); } else if (text.startsWith(SCHEME_GEO)) { result.setType(HitTestResult.GEO_TYPE); result.setExtra(URLDecoder.decode(text .substring(SCHEME_GEO.length()))); } else if (nativeCursorIsAnchor()) { result.setType(HitTestResult.SRC_ANCHOR_TYPE); result.setExtra(text); } } } } int type = result.getType(); if (type == HitTestResult.UNKNOWN_TYPE || type == HitTestResult.SRC_ANCHOR_TYPE) { // Now check to see if it is an image. int contentX = viewToContentX((int) mLastTouchX + mScrollX); int contentY = viewToContentY((int) mLastTouchY + mScrollY); String text = nativeImageURI(contentX, contentY); if (text != null) { result.setType(type == HitTestResult.UNKNOWN_TYPE ? HitTestResult.IMAGE_TYPE : HitTestResult.SRC_IMAGE_ANCHOR_TYPE); result.setExtra(text); } } return result; } // Called by JNI when the DOM has changed the focus. Clear the focus so // that new keys will go to the newly focused field private void domChangedFocus() { if (inEditingMode()) { mPrivateHandler.obtainMessage(DOM_FOCUS_CHANGED).sendToTarget(); } } /** * Request the href of an anchor element due to getFocusNodePath returning * "href." If hrefMsg is null, this method returns immediately and does not * dispatch hrefMsg to its target. * * @param hrefMsg This message will be dispatched with the result of the * request as the data member with "url" as key. The result can * be null. */ // FIXME: API change required to change the name of this function. We now // look at the cursor node, and not the focus node. Also, what is // getFocusNodePath? public void requestFocusNodeHref(Message hrefMsg) { if (hrefMsg == null || mNativeClass == 0) { return; } if (nativeCursorIsAnchor()) { mWebViewCore.sendMessage(EventHub.REQUEST_CURSOR_HREF, nativeCursorFramePointer(), nativeCursorNodePointer(), hrefMsg); } } /** * Request the url of the image last touched by the user. msg will be sent * to its target with a String representing the url as its object. * * @param msg This message will be dispatched with the result of the request * as the data member with "url" as key. The result can be null. */ public void requestImageRef(Message msg) { if (0 == mNativeClass) return; // client isn't initialized int contentX = viewToContentX((int) mLastTouchX + mScrollX); int contentY = viewToContentY((int) mLastTouchY + mScrollY); String ref = nativeImageURI(contentX, contentY); Bundle data = msg.getData(); data.putString("url", ref); msg.setData(data); msg.sendToTarget(); } private static int pinLoc(int x, int viewMax, int docMax) { // Log.d(LOGTAG, "-- pinLoc " + x + " " + viewMax + " " + docMax); if (docMax < viewMax) { // the doc has room on the sides for "blank" // pin the short document to the top/left of the screen x = 0; // Log.d(LOGTAG, "--- center " + x); } else if (x < 0) { x = 0; // Log.d(LOGTAG, "--- zero"); } else if (x + viewMax > docMax) { x = docMax - viewMax; // Log.d(LOGTAG, "--- pin " + x); } return x; } // Expects x in view coordinates private int pinLocX(int x) { if (mInOverScrollMode) return x; return pinLoc(x, getViewWidth(), computeRealHorizontalScrollRange()); } // Expects y in view coordinates private int pinLocY(int y) { if (mInOverScrollMode) return y; return pinLoc(y, getViewHeightWithTitle(), computeRealVerticalScrollRange() + getTitleHeight()); } /** * A title bar which is embedded in this WebView, and scrolls along with it * vertically, but not horizontally. */ private View mTitleBar; /** * Since we draw the title bar ourselves, we removed the shadow from the * browser's activity. We do want a shadow at the bottom of the title bar, * or at the top of the screen if the title bar is not visible. This * drawable serves that purpose. */ private Drawable mTitleShadow; /** * Add or remove a title bar to be embedded into the WebView, and scroll * along with it vertically, while remaining in view horizontally. Pass * null to remove the title bar from the WebView, and return to drawing * the WebView normally without translating to account for the title bar. * @hide */ public void setEmbeddedTitleBar(View v) { if (mTitleBar == v) return; if (mTitleBar != null) { removeView(mTitleBar); } if (null != v) { addView(v, new AbsoluteLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0)); if (mTitleShadow == null) { mTitleShadow = (Drawable) mContext.getResources().getDrawable( com.android.internal.R.drawable.title_bar_shadow); } } mTitleBar = v; } /** * Given a distance in view space, convert it to content space. Note: this * does not reflect translation, just scaling, so this should not be called * with coordinates, but should be called for dimensions like width or * height. */ private int viewToContentDimension(int d) { return Math.round(d * mInvActualScale); } /** * Given an x coordinate in view space, convert it to content space. Also * may be used for absolute heights (such as for the WebTextView's * textSize, which is unaffected by the height of the title bar). */ /*package*/ int viewToContentX(int x) { return viewToContentDimension(x); } /** * Given a y coordinate in view space, convert it to content space. * Takes into account the height of the title bar if there is one * embedded into the WebView. */ /*package*/ int viewToContentY(int y) { return viewToContentDimension(y - getTitleHeight()); } /** * Given a x coordinate in view space, convert it to content space. * Returns the result as a float. */ private float viewToContentXf(int x) { return x * mInvActualScale; } /** * Given a y coordinate in view space, convert it to content space. * Takes into account the height of the title bar if there is one * embedded into the WebView. Returns the result as a float. */ private float viewToContentYf(int y) { return (y - getTitleHeight()) * mInvActualScale; } /** * Given a distance in content space, convert it to view space. Note: this * does not reflect translation, just scaling, so this should not be called * with coordinates, but should be called for dimensions like width or * height. */ /*package*/ int contentToViewDimension(int d) { return Math.round(d * mActualScale); } /** * Given an x coordinate in content space, convert it to view * space. */ /*package*/ int contentToViewX(int x) { return contentToViewDimension(x); } /** * Given a y coordinate in content space, convert it to view * space. Takes into account the height of the title bar. */ /*package*/ int contentToViewY(int y) { return contentToViewDimension(y) + getTitleHeight(); } private Rect contentToViewRect(Rect x) { return new Rect(contentToViewX(x.left), contentToViewY(x.top), contentToViewX(x.right), contentToViewY(x.bottom)); } /* To invalidate a rectangle in content coordinates, we need to transform the rect into view coordinates, so we can then call invalidate(...). Normally, we would just call contentToView[XY](...), which eventually calls Math.round(coordinate * mActualScale). However, for invalidates, we need to account for the slop that occurs with antialiasing. To address that, we are a little more liberal in the size of the rect that we invalidate. This liberal calculation calls floor() for the top/left, and ceil() for the bottom/right coordinates. This catches the possible extra pixels of antialiasing that we might have missed with just round(). */ // Called by JNI to invalidate the View, given rectangle coordinates in // content space private void viewInvalidate(int l, int t, int r, int b) { final float scale = mActualScale; final int dy = getTitleHeight(); invalidate((int)Math.floor(l * scale), (int)Math.floor(t * scale) + dy, (int)Math.ceil(r * scale), (int)Math.ceil(b * scale) + dy); } // Called by JNI to invalidate the View after a delay, given rectangle // coordinates in content space private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) { final float scale = mActualScale; final int dy = getTitleHeight(); postInvalidateDelayed(delay, (int)Math.floor(l * scale), (int)Math.floor(t * scale) + dy, (int)Math.ceil(r * scale), (int)Math.ceil(b * scale) + dy); } private void invalidateContentRect(Rect r) { viewInvalidate(r.left, r.top, r.right, r.bottom); } // stop the scroll animation, and don't let a subsequent fling add // to the existing velocity private void abortAnimation() { mScroller.abortAnimation(); mLastVelocity = 0; } /* call from webcoreview.draw(), so we're still executing in the UI thread */ private void recordNewContentSize(int w, int h, boolean updateLayout) { // premature data from webkit, ignore if ((w | h) == 0) { return; } // don't abort a scroll animation if we didn't change anything if (mContentWidth != w || mContentHeight != h) { // record new dimensions mContentWidth = w; mContentHeight = h; // If history Picture is drawn, don't update scroll. They will be // updated when we get out of that mode. if (!mDrawHistory) { // repin our scroll, taking into account the new content size int oldX = mScrollX; int oldY = mScrollY; mScrollX = pinLocX(mScrollX); mScrollY = pinLocY(mScrollY); if (oldX != mScrollX || oldY != mScrollY) { onScrollChanged(mScrollX, mScrollY, oldX, oldY); } if (!mScroller.isFinished()) { // We are in the middle of a scroll. Repin the final scroll // position. mScroller.setFinalX(pinLocX(mScroller.getFinalX())); mScroller.setFinalY(pinLocY(mScroller.getFinalY())); } } } contentSizeChanged(updateLayout); } private void setNewZoomScale(float scale, boolean updateTextWrapScale, boolean force) { if (scale < mMinZoomScale) { scale = mMinZoomScale; // set mInZoomOverview for non mobile sites if (scale < mDefaultScale) mInZoomOverview = true; } else if (scale > mMaxZoomScale) { scale = mMaxZoomScale; } if (updateTextWrapScale) { mTextWrapScale = scale; // reset mLastHeightSent to force VIEW_SIZE_CHANGED sent to WebKit mLastHeightSent = 0; } if (scale != mActualScale || force) { if (mDrawHistory) { // If history Picture is drawn, don't update scroll. They will // be updated when we get out of that mode. if (scale != mActualScale && !mPreviewZoomOnly) { mCallbackProxy.onScaleChanged(mActualScale, scale); } mActualScale = scale; mInvActualScale = 1 / scale; sendViewSizeZoom(); } else { // update our scroll so we don't appear to jump // i.e. keep the center of the doc in the center of the view int oldX = mScrollX; int oldY = mScrollY; float ratio = scale * mInvActualScale; // old inverse float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; float sy = ratio * oldY + (ratio - 1) * (mZoomCenterY - getTitleHeight()); // now update our new scale and inverse if (scale != mActualScale && !mPreviewZoomOnly) { mCallbackProxy.onScaleChanged(mActualScale, scale); } mActualScale = scale; mInvActualScale = 1 / scale; // Scale all the child views mViewManager.scaleAll(); // as we don't have animation for scaling, don't do animation // for scrolling, as it causes weird intermediate state // pinScrollTo(Math.round(sx), Math.round(sy)); mScrollX = pinLocX(Math.round(sx)); mScrollY = pinLocY(Math.round(sy)); // update webkit if (oldX != mScrollX || oldY != mScrollY) { onScrollChanged(mScrollX, mScrollY, oldX, oldY); } else { // the scroll position is adjusted at the beginning of the // zoom animation. But we want to update the WebKit at the // end of the zoom animation. See comments in onScaleEnd(). sendOurVisibleRect(); } sendViewSizeZoom(); } } } // Used to avoid sending many visible rect messages. private Rect mLastVisibleRectSent; private Rect mLastGlobalRect; private Rect sendOurVisibleRect() { if (mPreviewZoomOnly) return mLastVisibleRectSent; Rect rect = new Rect(); calcOurContentVisibleRect(rect); // Rect.equals() checks for null input. if (!rect.equals(mLastVisibleRectSent)) { Point pos = new Point(rect.left, rect.top); mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, nativeMoveGeneration(), 0, pos); mLastVisibleRectSent = rect; } Rect globalRect = new Rect(); if (getGlobalVisibleRect(globalRect) && !globalRect.equals(mLastGlobalRect)) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "sendOurVisibleRect=(" + globalRect.left + "," + globalRect.top + ",r=" + globalRect.right + ",b=" + globalRect.bottom); } // TODO: the global offset is only used by windowRect() // in ChromeClientAndroid ; other clients such as touch // and mouse events could return view + screen relative points. mWebViewCore.sendMessage(EventHub.SET_GLOBAL_BOUNDS, globalRect); mLastGlobalRect = globalRect; } return rect; } // Sets r to be the visible rectangle of our webview in view coordinates private void calcOurVisibleRect(Rect r) { Point p = new Point(); getGlobalVisibleRect(r, p); r.offset(-p.x, -p.y); if (mFindIsUp) { r.bottom -= mFindHeight; } } // Sets r to be our visible rectangle in content coordinates private void calcOurContentVisibleRect(Rect r) { calcOurVisibleRect(r); // since we might overscroll, pin the rect to the bounds of the content r.left = Math.max(viewToContentX(r.left), 0); // viewToContentY will remove the total height of the title bar. Add // the visible height back in to account for the fact that if the title // bar is partially visible, the part of the visible rect which is // displaying our content is displaced by that amount. r.top = Math.max(viewToContentY(r.top + getVisibleTitleHeight()), 0); r.right = Math.min(viewToContentX(r.right), mContentWidth); r.bottom = Math.min(viewToContentY(r.bottom), mContentHeight); } // Sets r to be our visible rectangle in content coordinates. We use this // method on the native side to compute the position of the fixed layers. // Uses floating coordinates (necessary to correctly place elements when // the scale factor is not 1) private void calcOurContentVisibleRectF(RectF r) { Rect ri = new Rect(0,0,0,0); calcOurVisibleRect(ri); // pin the rect to the bounds of the content r.left = Math.max(viewToContentXf(ri.left), 0.0f); // viewToContentY will remove the total height of the title bar. Add // the visible height back in to account for the fact that if the title // bar is partially visible, the part of the visible rect which is // displaying our content is displaced by that amount. r.top = Math.max(viewToContentYf(ri.top + getVisibleTitleHeight()), 0.0f); r.right = Math.min(viewToContentXf(ri.right), (float)mContentWidth); r.bottom = Math.min(viewToContentYf(ri.bottom), (float)mContentHeight); } static class ViewSizeData { int mWidth; int mHeight; int mTextWrapWidth; int mAnchorX; int mAnchorY; float mScale; boolean mIgnoreHeight; } /** * Compute unzoomed width and height, and if they differ from the last * values we sent, send them to webkit (to be used has new viewport) * * @return true if new values were sent */ private boolean sendViewSizeZoom() { if (mPreviewZoomOnly) return false; int viewWidth = getViewWidth(); int newWidth = Math.round(viewWidth * mInvActualScale); int newHeight = Math.round(getViewHeight() * mInvActualScale); /* * Because the native side may have already done a layout before the * View system was able to measure us, we have to send a height of 0 to * remove excess whitespace when we grow our width. This will trigger a * layout and a change in content size. This content size change will * mean that contentSizeChanged will either call this method directly or * indirectly from onSizeChanged. */ if (newWidth > mLastWidthSent && mWrapContent) { newHeight = 0; } // Avoid sending another message if the dimensions have not changed. if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) { ViewSizeData data = new ViewSizeData(); data.mWidth = newWidth; data.mHeight = newHeight; data.mTextWrapWidth = Math.round(viewWidth / mTextWrapScale);; data.mScale = mActualScale; data.mIgnoreHeight = mZoomScale != 0 && !mHeightCanMeasure; data.mAnchorX = mAnchorX; data.mAnchorY = mAnchorY; mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data); mLastWidthSent = newWidth; mLastHeightSent = newHeight; mAnchorX = mAnchorY = 0; return true; } return false; } private int computeRealHorizontalScrollRange() { if (mDrawHistory) { return mHistoryWidth; } else if (mHorizontalScrollBarMode == SCROLLBAR_ALWAYSOFF && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { // only honor the scrollbar mode when it is at minimum zoom level return computeHorizontalScrollExtent(); } else { // to avoid rounding error caused unnecessary scrollbar, use floor return (int) Math.floor(mContentWidth * mActualScale); } } @Override protected int computeHorizontalScrollRange() { int range = computeRealHorizontalScrollRange(); // Adjust reported range if overscrolled to compress the scroll bars final int scrollX = mScrollX; final int overscrollRight = computeMaxScrollX(); if (scrollX < 0) { range -= scrollX; } else if (scrollX > overscrollRight) { range += scrollX - overscrollRight; } return range; } @Override protected int computeHorizontalScrollOffset() { return Math.max(mScrollX, 0); } private int computeRealVerticalScrollRange() { if (mDrawHistory) { return mHistoryHeight; } else if (mVerticalScrollBarMode == SCROLLBAR_ALWAYSOFF && (mActualScale - mMinZoomScale <= MINIMUM_SCALE_INCREMENT)) { // only honor the scrollbar mode when it is at minimum zoom level return computeVerticalScrollExtent(); } else { // to avoid rounding error caused unnecessary scrollbar, use floor return (int) Math.floor(mContentHeight * mActualScale); } } @Override protected int computeVerticalScrollRange() { int range = computeRealVerticalScrollRange(); // Adjust reported range if overscrolled to compress the scroll bars final int scrollY = mScrollY; final int overscrollBottom = computeMaxScrollY(); if (scrollY < 0) { range -= scrollY; } else if (scrollY > overscrollBottom) { range += scrollY - overscrollBottom; } return range; } @Override protected int computeVerticalScrollOffset() { return Math.max(mScrollY - getTitleHeight(), 0); } @Override protected int computeVerticalScrollExtent() { return getViewHeight(); } /** @hide */ @Override protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, int l, int t, int r, int b) { if (mScrollY < 0) { t -= mScrollY; } scrollBar.setBounds(l, t + getVisibleTitleHeight(), r, b); scrollBar.draw(canvas); } @Override protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { mInOverScrollMode = false; int maxX = computeMaxScrollX(); int maxY = computeMaxScrollY(); if (maxX == 0) { // do not over scroll x if the page just fits the screen scrollX = pinLocX(scrollX); } else if (scrollX < 0 || scrollX > maxX) { mInOverScrollMode = true; } if (scrollY < 0 || scrollY > maxY) { mInOverScrollMode = true; } int oldX = mScrollX; int oldY = mScrollY; super.scrollTo(scrollX, scrollY); // Only show overscroll bars if there was no movement in any direction // as a result of scrolling. if (mEdgeGlowTop != null && oldY == mScrollY && oldX == mScrollX) { // Don't show left/right glows if we fit the whole content. // Also don't show if there was vertical movement. if (maxX > 0) { final int pulledToX = oldX + mOverscrollDeltaX; if (pulledToX < 0) { mEdgeGlowLeft.onPull((float) mOverscrollDeltaX / getWidth()); if (!mEdgeGlowRight.isFinished()) { mEdgeGlowRight.onRelease(); } } else if (pulledToX > maxX) { mEdgeGlowRight.onPull((float) mOverscrollDeltaX / getWidth()); if (!mEdgeGlowLeft.isFinished()) { mEdgeGlowLeft.onRelease(); } } mOverscrollDeltaX = 0; } if (maxY > 0 || getOverScrollMode() == OVER_SCROLL_ALWAYS) { final int pulledToY = oldY + mOverscrollDeltaY; if (pulledToY < 0) { mEdgeGlowTop.onPull((float) mOverscrollDeltaY / getHeight()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } else if (pulledToY > maxY) { mEdgeGlowBottom.onPull((float) mOverscrollDeltaY / getHeight()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } } mOverscrollDeltaY = 0; } } } /** * Get the url for the current page. This is not always the same as the url * passed to WebViewClient.onPageStarted because although the load for * that url has begun, the current page may not have changed. * @return The url for the current page. */ public String getUrl() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getUrl() : null; } /** * Get the original url for the current page. This is not always the same * as the url passed to WebViewClient.onPageStarted because although the * load for that url has begun, the current page may not have changed. * Also, there may have been redirects resulting in a different url to that * originally requested. * @return The url that was originally requested for the current page. */ public String getOriginalUrl() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getOriginalUrl() : null; } /** * Get the title for the current page. This is the title of the current page * until WebViewClient.onReceivedTitle is called. * @return The title for the current page. */ public String getTitle() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getTitle() : null; } /** * Get the favicon for the current page. This is the favicon of the current * page until WebViewClient.onReceivedIcon is called. * @return The favicon for the current page. */ public Bitmap getFavicon() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getFavicon() : null; } /** * Get the touch icon url for the apple-touch-icon element. * @hide */ public String getTouchIconUrl() { WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); return h != null ? h.getTouchIconUrl() : null; } /** * Get the progress for the current page. * @return The progress for the current page between 0 and 100. */ public int getProgress() { return mCallbackProxy.getProgress(); } /** * @return the height of the HTML content. */ public int getContentHeight() { return mContentHeight; } /** * @return the width of the HTML content. * @hide */ public int getContentWidth() { return mContentWidth; } /** * Pause all layout, parsing, and javascript timers for all webviews. This * is a global requests, not restricted to just this webview. This can be * useful if the application has been paused. */ public void pauseTimers() { mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } /** * Resume all layout, parsing, and javascript timers for all webviews. * This will resume dispatching all timers. */ public void resumeTimers() { mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } /** * Call this to pause any extra processing associated with this view and * its associated DOM/plugins/javascript/etc. For example, if the view is * taken offscreen, this could be called to reduce unnecessary CPU and/or * network traffic. When the view is again "active", call onResume(). * * Note that this differs from pauseTimers(), which affects all views/DOMs * @hide */ public void onPause() { if (!mIsPaused) { mIsPaused = true; mWebViewCore.sendMessage(EventHub.ON_PAUSE); } } /** * Call this to balanace a previous call to onPause() * @hide */ public void onResume() { if (mIsPaused) { mIsPaused = false; mWebViewCore.sendMessage(EventHub.ON_RESUME); } } /** * Returns true if the view is paused, meaning onPause() was called. Calling * onResume() sets the paused state back to false. * @hide */ public boolean isPaused() { return mIsPaused; } /** * Call this to inform the view that memory is low so that it can * free any available memory. */ public void freeMemory() { mWebViewCore.sendMessage(EventHub.FREE_MEMORY); } /** * Clear the resource cache. Note that the cache is per-application, so * this will clear the cache for all WebViews used. * * @param includeDiskFiles If false, only the RAM cache is cleared. */ public void clearCache(boolean includeDiskFiles) { // Note: this really needs to be a static method as it clears cache for all // WebView. But we need mWebViewCore to send message to WebCore thread, so // we can't make this static. mWebViewCore.sendMessage(EventHub.CLEAR_CACHE, includeDiskFiles ? 1 : 0, 0); } /** * Make sure that clearing the form data removes the adapter from the * currently focused textfield if there is one. */ public void clearFormData() { if (inEditingMode()) { AutoCompleteAdapter adapter = null; mWebTextView.setAdapterCustom(adapter); } } /** * Tell the WebView to clear its internal back/forward list. */ public void clearHistory() { mCallbackProxy.getBackForwardList().setClearPending(); mWebViewCore.sendMessage(EventHub.CLEAR_HISTORY); } /** * Clear the SSL preferences table stored in response to proceeding with SSL * certificate errors. */ public void clearSslPreferences() { mWebViewCore.sendMessage(EventHub.CLEAR_SSL_PREF_TABLE); } /** * Return the WebBackForwardList for this WebView. This contains the * back/forward list for use in querying each item in the history stack. * This is a copy of the private WebBackForwardList so it contains only a * snapshot of the current state. Multiple calls to this method may return * different objects. The object returned from this method will not be * updated to reflect any new state. */ public WebBackForwardList copyBackForwardList() { return mCallbackProxy.getBackForwardList().clone(); } /* * Highlight and scroll to the next occurance of String in findAll. * Wraps the page infinitely, and scrolls. Must be called after * calling findAll. * * @param forward Direction to search. */ public void findNext(boolean forward) { if (0 == mNativeClass) return; // client isn't initialized nativeFindNext(forward); } /* * Find all instances of find on the page and highlight them. * @param find String to find. * @return int The number of occurances of the String "find" * that were found. */ public int findAll(String find) { if (0 == mNativeClass) return 0; // client isn't initialized int result = find != null ? nativeFindAll(find.toLowerCase(), find.toUpperCase()) : 0; invalidate(); mLastFind = find; return result; } /** * @hide */ public void setFindIsUp(boolean isUp) { mFindIsUp = isUp; if (isUp) { recordNewContentSize(mContentWidth, mContentHeight + mFindHeight, false); } if (0 == mNativeClass) return; // client isn't initialized nativeSetFindIsUp(isUp); } /** * @hide */ public int findIndex() { if (0 == mNativeClass) return -1; return nativeFindIndex(); } // Used to know whether the find dialog is open. Affects whether // or not we draw the highlights for matches. private boolean mFindIsUp; private int mFindHeight; // Keep track of the last string sent, so we can search again after an // orientation change or the dismissal of the soft keyboard. private String mLastFind; /** * Return the first substring consisting of the address of a physical * location. Currently, only addresses in the United States are detected, * and consist of: * - a house number * - a street name * - a street type (Road, Circle, etc), either spelled out or abbreviated * - a city name * - a state or territory, either spelled out or two-letter abbr. * - an optional 5 digit or 9 digit zip code. * * All names must be correctly capitalized, and the zip code, if present, * must be valid for the state. The street type must be a standard USPS * spelling or abbreviation. The state or territory must also be spelled * or abbreviated using USPS standards. The house number may not exceed * five digits. * @param addr The string to search for addresses. * * @return the address, or if no address is found, return null. */ public static String findAddress(String addr) { return findAddress(addr, false); } /** * @hide * Return the first substring consisting of the address of a physical * location. Currently, only addresses in the United States are detected, * and consist of: * - a house number * - a street name * - a street type (Road, Circle, etc), either spelled out or abbreviated * - a city name * - a state or territory, either spelled out or two-letter abbr. * - an optional 5 digit or 9 digit zip code. * * Names are optionally capitalized, and the zip code, if present, * must be valid for the state. The street type must be a standard USPS * spelling or abbreviation. The state or territory must also be spelled * or abbreviated using USPS standards. The house number may not exceed * five digits. * @param addr The string to search for addresses. * @param caseInsensitive addr Set to true to make search ignore case. * * @return the address, or if no address is found, return null. */ public static String findAddress(String addr, boolean caseInsensitive) { return WebViewCore.nativeFindAddress(addr, caseInsensitive); } /* * Clear the highlighting surrounding text matches created by findAll. */ public void clearMatches() { mLastFind = ""; if (mNativeClass == 0) return; nativeSetFindIsEmpty(); invalidate(); } /** * @hide */ public void notifyFindDialogDismissed() { if (mWebViewCore == null) { return; } clearMatches(); setFindIsUp(false); recordNewContentSize(mContentWidth, mContentHeight - mFindHeight, false); // Now that the dialog has been removed, ensure that we scroll to a // location that is not beyond the end of the page. pinScrollTo(mScrollX, mScrollY, false, 0); invalidate(); } /** * @hide */ public void setFindDialogHeight(int height) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "setFindDialogHeight height=" + height); } mFindHeight = height; } /** * Query the document to see if it contains any image references. The * message object will be dispatched with arg1 being set to 1 if images * were found and 0 if the document does not reference any images. * @param response The message that will be dispatched with the result. */ public void documentHasImages(Message response) { if (response == null) { return; } mWebViewCore.sendMessage(EventHub.DOC_HAS_IMAGES, response); } @Override public void computeScroll() { if (mScroller.computeScrollOffset()) { int oldX = mScrollX; int oldY = mScrollY; int x = mScroller.getCurrX(); int y = mScroller.getCurrY(); invalidate(); // So we draw again if (oldX != x || oldY != y) { final int rangeX = computeMaxScrollX(); final int rangeY = computeMaxScrollY(); overScrollBy(x - oldX, y - oldY, oldX, oldY, rangeX, rangeY, mOverflingDistance, mOverflingDistance, false); if (mEdgeGlowTop != null) { if (rangeY > 0 || getOverScrollMode() == OVER_SCROLL_ALWAYS) { if (y < 0 && oldY >= 0) { mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } else if (y > rangeY && oldY <= rangeY) { mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } } } if (rangeX > 0) { if (x < 0 && oldX >= 0) { mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); if (!mEdgeGlowRight.isFinished()) { mEdgeGlowRight.onRelease(); } } else if (x > rangeX && oldX <= rangeX) { mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); if (!mEdgeGlowLeft.isFinished()) { mEdgeGlowLeft.onRelease(); } } } } } if (mScroller.isFinished()) { mPrivateHandler.sendEmptyMessage(RESUME_WEBCORE_PRIORITY); } } else { super.computeScroll(); } } private static int computeDuration(int dx, int dy) { int distance = Math.max(Math.abs(dx), Math.abs(dy)); int duration = distance * 1000 / STD_SPEED; return Math.min(duration, MAX_DURATION); } // helper to pin the scrollBy parameters (already in view coordinates) // returns true if the scroll was changed private boolean pinScrollBy(int dx, int dy, boolean animate, int animationDuration) { return pinScrollTo(mScrollX + dx, mScrollY + dy, animate, animationDuration); } // helper to pin the scrollTo parameters (already in view coordinates) // returns true if the scroll was changed private boolean pinScrollTo(int x, int y, boolean animate, int animationDuration) { x = pinLocX(x); y = pinLocY(y); int dx = x - mScrollX; int dy = y - mScrollY; if ((dx | dy) == 0) { return false; } if (animate) { // Log.d(LOGTAG, "startScroll: " + dx + " " + dy); mScroller.startScroll(mScrollX, mScrollY, dx, dy, animationDuration > 0 ? animationDuration : computeDuration(dx, dy)); awakenScrollBars(mScroller.getDuration()); invalidate(); } else { abortAnimation(); // just in case scrollTo(x, y); } return true; } // Scale from content to view coordinates, and pin. // Also called by jni webview.cpp private boolean setContentScrollBy(int cx, int cy, boolean animate) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. // TODO: as we switchOutDrawHistory when trackball or navigation // keys are hit, this should be safe. Right? return false; } cx = contentToViewDimension(cx); cy = contentToViewDimension(cy); if (mHeightCanMeasure) { // move our visible rect according to scroll request if (cy != 0) { Rect tempRect = new Rect(); calcOurVisibleRect(tempRect); tempRect.offset(cx, cy); requestRectangleOnScreen(tempRect); } // FIXME: We scroll horizontally no matter what because currently // ScrollView and ListView will not scroll horizontally. // FIXME: Why do we only scroll horizontally if there is no // vertical scroll? // Log.d(LOGTAG, "setContentScrollBy cy=" + cy); return cy == 0 && cx != 0 && pinScrollBy(cx, 0, animate, 0); } else { return pinScrollBy(cx, cy, animate, 0); } } /** * Called by CallbackProxy when the page finishes loading. * @param url The URL of the page which has finished loading. */ /* package */ void onPageFinished(String url) { if (mPageThatNeedsToSlideTitleBarOffScreen != null) { // If the user is now on a different page, or has scrolled the page // past the point where the title bar is offscreen, ignore the // scroll request. if (mPageThatNeedsToSlideTitleBarOffScreen.equals(url) && mScrollX == 0 && mScrollY == 0) { pinScrollTo(0, mYDistanceToSlideTitleOffScreen, true, SLIDE_TITLE_DURATION); } mPageThatNeedsToSlideTitleBarOffScreen = null; } } /** * The URL of a page that sent a message to scroll the title bar off screen. * * Many mobile sites tell the page to scroll to (0,1) in order to scroll the * title bar off the screen. Sometimes, the scroll position is set before * the page finishes loading. Rather than scrolling while the page is still * loading, keep track of the URL and new scroll position so we can perform * the scroll once the page finishes loading. */ private String mPageThatNeedsToSlideTitleBarOffScreen; /** * The destination Y scroll position to be used when the page finishes * loading. See mPageThatNeedsToSlideTitleBarOffScreen. */ private int mYDistanceToSlideTitleOffScreen; // scale from content to view coordinates, and pin // return true if pin caused the final x/y different than the request cx/cy, // and a future scroll may reach the request cx/cy after our size has // changed // return false if the view scroll to the exact position as it is requested, // where negative numbers are taken to mean 0 private boolean setContentScrollTo(int cx, int cy) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. // One known case where this is called is that WebCore tries to // restore the scroll position. As history Picture already uses the // saved scroll position, it is ok to skip this. return false; } int vx; int vy; if ((cx | cy) == 0) { // If the page is being scrolled to (0,0), do not add in the title // bar's height, and simply scroll to (0,0). (The only other work // in contentToView_ is to multiply, so this would not change 0.) vx = 0; vy = 0; } else { vx = contentToViewX(cx); vy = contentToViewY(cy); } // Log.d(LOGTAG, "content scrollTo [" + cx + " " + cy + "] view=[" + // vx + " " + vy + "]"); // Some mobile sites attempt to scroll the title bar off the page by // scrolling to (0,1). If we are at the top left corner of the // page, assume this is an attempt to scroll off the title bar, and // animate the title bar off screen slowly enough that the user can see // it. if (cx == 0 && cy == 1 && mScrollX == 0 && mScrollY == 0 && mTitleBar != null) { // FIXME: 100 should be defined somewhere as our max progress. if (getProgress() < 100) { // Wait to scroll the title bar off screen until the page has // finished loading. Keep track of the URL and the destination // Y position mPageThatNeedsToSlideTitleBarOffScreen = getUrl(); mYDistanceToSlideTitleOffScreen = vy; } else { pinScrollTo(vx, vy, true, SLIDE_TITLE_DURATION); } // Since we are animating, we have not yet reached the desired // scroll position. Do not return true to request another attempt return false; } pinScrollTo(vx, vy, false, 0); // If the request was to scroll to a negative coordinate, treat it as if // it was a request to scroll to 0 if ((mScrollX != vx && cx >= 0) || (mScrollY != vy && cy >= 0)) { return true; } else { return false; } } // scale from content to view coordinates, and pin private void spawnContentScrollTo(int cx, int cy) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. return; } int vx = contentToViewX(cx); int vy = contentToViewY(cy); pinScrollTo(vx, vy, true, 0); } /** * These are from webkit, and are in content coordinate system (unzoomed) */ private void contentSizeChanged(boolean updateLayout) { // suppress 0,0 since we usually see real dimensions soon after // this avoids drawing the prev content in a funny place. If we find a // way to consolidate these notifications, this check may become // obsolete if ((mContentWidth | mContentHeight) == 0) { return; } if (mHeightCanMeasure) { if (getMeasuredHeight() != contentToViewDimension(mContentHeight) || updateLayout) { requestLayout(); } } else if (mWidthCanMeasure) { if (getMeasuredWidth() != contentToViewDimension(mContentWidth) || updateLayout) { requestLayout(); } } else { // If we don't request a layout, try to send our view size to the // native side to ensure that WebCore has the correct dimensions. sendViewSizeZoom(); } } /** * Set the WebViewClient that will receive various notifications and * requests. This will replace the current handler. * @param client An implementation of WebViewClient. */ public void setWebViewClient(WebViewClient client) { mCallbackProxy.setWebViewClient(client); } /** * Gets the WebViewClient * @return the current WebViewClient instance. * *@hide pending API council approval. */ public WebViewClient getWebViewClient() { return mCallbackProxy.getWebViewClient(); } /** * Register the interface to be used when content can not be handled by * the rendering engine, and should be downloaded instead. This will replace * the current handler. * @param listener An implementation of DownloadListener. */ public void setDownloadListener(DownloadListener listener) { mCallbackProxy.setDownloadListener(listener); } /** * Set the chrome handler. This is an implementation of WebChromeClient for * use in handling Javascript dialogs, favicons, titles, and the progress. * This will replace the current handler. * @param client An implementation of WebChromeClient. */ public void setWebChromeClient(WebChromeClient client) { mCallbackProxy.setWebChromeClient(client); } /** * Gets the chrome handler. * @return the current WebChromeClient instance. * * @hide API council approval. */ public WebChromeClient getWebChromeClient() { return mCallbackProxy.getWebChromeClient(); } /** * Set the back/forward list client. This is an implementation of * WebBackForwardListClient for handling new items and changes in the * history index. * @param client An implementation of WebBackForwardListClient. * {@hide} */ public void setWebBackForwardListClient(WebBackForwardListClient client) { mCallbackProxy.setWebBackForwardListClient(client); } /** * Gets the WebBackForwardListClient. * {@hide} */ public WebBackForwardListClient getWebBackForwardListClient() { return mCallbackProxy.getWebBackForwardListClient(); } /** * Set the Picture listener. This is an interface used to receive * notifications of a new Picture. * @param listener An implementation of WebView.PictureListener. */ public void setPictureListener(PictureListener listener) { mPictureListener = listener; } /** * {@hide} */ /* FIXME: Debug only! Remove for SDK! */ public void externalRepresentation(Message callback) { mWebViewCore.sendMessage(EventHub.REQUEST_EXT_REPRESENTATION, callback); } /** * {@hide} */ /* FIXME: Debug only! Remove for SDK! */ public void documentAsText(Message callback) { mWebViewCore.sendMessage(EventHub.REQUEST_DOC_AS_TEXT, callback); } /** * Use this function to bind an object to Javascript so that the * methods can be accessed from Javascript. *

IMPORTANT: *

    *
  • Using addJavascriptInterface() allows JavaScript to control your * application. This can be a very useful feature or a dangerous security * issue. When the HTML in the WebView is untrustworthy (for example, part * or all of the HTML is provided by some person or process), then an * attacker could inject HTML that will execute your code and possibly any * code of the attacker's choosing.
    * Do not use addJavascriptInterface() unless all of the HTML in this * WebView was written by you.
  • *
  • The Java object that is bound runs in another thread and not in * the thread that it was constructed in.
  • *

* @param obj The class instance to bind to Javascript * @param interfaceName The name to used to expose the class in Javascript */ public void addJavascriptInterface(Object obj, String interfaceName) { WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); arg.mObject = obj; arg.mInterfaceName = interfaceName; mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg); } /** * Return the WebSettings object used to control the settings for this * WebView. * @return A WebSettings object that can be used to control this WebView's * settings. */ public WebSettings getSettings() { return mWebViewCore.getSettings(); } /** * Use this method to inform the webview about packages that are installed * in the system. This information will be used by the * navigator.isApplicationInstalled() API. * @param packageNames is a set of package names that are known to be * installed in the system. * * @hide not a public API */ public void addPackageNames(Set packageNames) { mWebViewCore.sendMessage(EventHub.ADD_PACKAGE_NAMES, packageNames); } /** * Use this method to inform the webview about single packages that are * installed in the system. This information will be used by the * navigator.isApplicationInstalled() API. * @param packageName is the name of a package that is known to be * installed in the system. * * @hide not a public API */ public void addPackageName(String packageName) { mWebViewCore.sendMessage(EventHub.ADD_PACKAGE_NAME, packageName); } /** * Use this method to inform the webview about packages that are uninstalled * in the system. This information will be used by the * navigator.isApplicationInstalled() API. * @param packageName is the name of a package that has been uninstalled in * the system. * * @hide not a public API */ public void removePackageName(String packageName) { mWebViewCore.sendMessage(EventHub.REMOVE_PACKAGE_NAME, packageName); } /** * Return the list of currently loaded plugins. * @return The list of currently loaded plugins. * * @deprecated This was used for Gears, which has been deprecated. */ @Deprecated public static synchronized PluginList getPluginList() { return new PluginList(); } /** * @deprecated This was used for Gears, which has been deprecated. */ @Deprecated public void refreshPlugins(boolean reloadOpenPages) { } //------------------------------------------------------------------------- // Override View methods //------------------------------------------------------------------------- @Override protected void finalize() throws Throwable { try { destroy(); } finally { super.finalize(); } } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { if (child == mTitleBar) { // When drawing the title bar, move it horizontally to always show // at the top of the WebView. mTitleBar.offsetLeftAndRight(mScrollX - mTitleBar.getLeft()); } return super.drawChild(canvas, child, drawingTime); } private void drawContent(Canvas canvas) { // Update the buttons in the picture, so when we draw the picture // to the screen, they are in the correct state. // Tell the native side if user is a) touching the screen, // b) pressing the trackball down, or c) pressing the enter key // If the cursor is on a button, we need to draw it in the pressed // state. // If mNativeClass is 0, we should not reach here, so we do not // need to check it again. nativeRecordButtons(hasFocus() && hasWindowFocus(), mTouchMode == TOUCH_SHORTPRESS_START_MODE || mTrackballDown || mGotCenterDown, false); drawCoreAndCursorRing(canvas, mBackgroundColor, mDrawCursorRing); } @Override protected void onDraw(Canvas canvas) { // if mNativeClass is 0, the WebView has been destroyed. Do nothing. if (mNativeClass == 0) { return; } // if both mContentWidth and mContentHeight are 0, it means there is no // valid Picture passed to WebView yet. This can happen when WebView // just starts. Draw the background and return. if ((mContentWidth | mContentHeight) == 0 && mHistoryPicture == null) { canvas.drawColor(mBackgroundColor); return; } int saveCount = canvas.save(); if (mInOverScrollMode && !getSettings() .getUseWebViewBackgroundForOverscrollBackground()) { if (mOverScrollBackground == null) { mOverScrollBackground = new Paint(); Bitmap bm = BitmapFactory.decodeResource( mContext.getResources(), com.android.internal.R.drawable.status_bar_background); mOverScrollBackground.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)); mOverScrollBorder = new Paint(); mOverScrollBorder.setStyle(Paint.Style.STROKE); mOverScrollBorder.setStrokeWidth(0); mOverScrollBorder.setColor(0xffbbbbbb); } int top = 0; int right = computeRealHorizontalScrollRange(); int bottom = top + computeRealVerticalScrollRange(); // first draw the background and anchor to the top of the view canvas.save(); canvas.translate(mScrollX, mScrollY); canvas.clipRect(-mScrollX, top - mScrollY, right - mScrollX, bottom - mScrollY, Region.Op.DIFFERENCE); canvas.drawPaint(mOverScrollBackground); canvas.restore(); // then draw the border canvas.drawRect(-1, top - 1, right, bottom, mOverScrollBorder); // next clip the region for the content canvas.clipRect(0, top, right, bottom); } if (mTitleBar != null) { canvas.translate(0, (int) mTitleBar.getHeight()); } if (mDragTrackerHandler == null) { drawContent(canvas); } else { if (!mDragTrackerHandler.draw(canvas)) { // sometimes the tracker doesn't draw, even though its active drawContent(canvas); } if (mDragTrackerHandler.isFinished()) { mDragTrackerHandler = null; } } canvas.restoreToCount(saveCount); // Now draw the shadow. int titleH = getVisibleTitleHeight(); if (mTitleBar != null && titleH == 0) { int height = (int) (5f * getContext().getResources() .getDisplayMetrics().density); mTitleShadow.setBounds(mScrollX, mScrollY, mScrollX + getWidth(), mScrollY + height); mTitleShadow.draw(canvas); } if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } if (inEditingMode()) { mWebTextView.onDrawSubstitute(); } mWebViewCore.signalRepaintDone(); } @Override public void draw(Canvas canvas) { super.draw(canvas); if (mEdgeGlowTop != null && drawEdgeGlows(canvas)) { invalidate(); } } /** * Draw the glow effect along the sides of the widget. mEdgeGlow* must be non-null. * * @param canvas Canvas to draw into, transformed into view coordinates. * @return true if glow effects are still animating and the view should invalidate again. */ private boolean drawEdgeGlows(Canvas canvas) { final int scrollX = mScrollX; final int scrollY = mScrollY; final int width = getWidth(); int height = getHeight(); boolean invalidateForGlow = false; if (!mEdgeGlowTop.isFinished()) { final int restoreCount = canvas.save(); canvas.translate(-width / 2 + scrollX, Math.min(0, scrollY)); mEdgeGlowTop.setSize(width * 2, height); invalidateForGlow |= mEdgeGlowTop.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mEdgeGlowBottom.isFinished()) { final int restoreCount = canvas.save(); canvas.translate(-width / 2 + scrollX, Math.max(computeMaxScrollY(), scrollY) + height); canvas.rotate(180, width, 0); mEdgeGlowBottom.setSize(width * 2, height); invalidateForGlow |= mEdgeGlowBottom.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mEdgeGlowLeft.isFinished()) { final int restoreCount = canvas.save(); canvas.rotate(270); canvas.translate(-height * 1.5f - scrollY, Math.min(0, scrollX)); mEdgeGlowLeft.setSize(height * 2, width); invalidateForGlow |= mEdgeGlowLeft.draw(canvas); canvas.restoreToCount(restoreCount); } if (!mEdgeGlowRight.isFinished()) { final int restoreCount = canvas.save(); canvas.rotate(90); canvas.translate(-height / 2 + scrollY, -(Math.max(computeMaxScrollX(), scrollX) + width)); mEdgeGlowRight.setSize(height * 2, width); invalidateForGlow |= mEdgeGlowRight.draw(canvas); canvas.restoreToCount(restoreCount); } return invalidateForGlow; } @Override public void setLayoutParams(ViewGroup.LayoutParams params) { if (params.height == LayoutParams.WRAP_CONTENT) { mWrapContent = true; } super.setLayoutParams(params); } @Override public boolean performLongClick() { // performLongClick() is the result of a delayed message. If we switch // to windows overview, the WebView will be temporarily removed from the // view system. In that case, do nothing. if (getParent() == null) return false; if (mNativeClass != 0 && nativeCursorIsTextInput()) { // Send the click so that the textfield is in focus centerKeyPressOnTextField(); rebuildWebTextView(); } else { clearTextEntry(true); } if (inEditingMode()) { return mWebTextView.performLongClick(); } /* if long click brings up a context menu, the super function * returns true and we're done. Otherwise, nothing happened when * the user clicked. */ if (super.performLongClick()) { return true; } /* In the case where the application hasn't already handled the long * click action, look for a word under the click. If one is found, * animate the text selection into view. * FIXME: no animation code yet */ if (mSelectingText) return false; // long click does nothing on selection int x = viewToContentX((int) mLastTouchX + mScrollX); int y = viewToContentY((int) mLastTouchY + mScrollY); setUpSelect(); if (mNativeClass != 0 && nativeWordSelection(x, y)) { nativeSetExtendSelection(); WebChromeClient client = getWebChromeClient(); if (client != null) client.onSelectionStart(this); return true; } notifySelectDialogDismissed(); return false; } boolean inAnimateZoom() { return mZoomScale != 0; } /** * Need to adjust the WebTextView after a change in zoom, since mActualScale * has changed. This is especially important for password fields, which are * drawn by the WebTextView, since it conveys more information than what * webkit draws. Thus we need to reposition it to show in the correct * place. */ private boolean mNeedToAdjustWebTextView; private boolean didUpdateTextViewBounds(boolean allowIntersect) { Rect contentBounds = nativeFocusCandidateNodeBounds(); Rect vBox = contentToViewRect(contentBounds); Rect visibleRect = new Rect(); calcOurVisibleRect(visibleRect); // If the textfield is on screen, place the WebTextView in // its new place, accounting for our new scroll/zoom values, // and adjust its textsize. if (allowIntersect ? Rect.intersects(visibleRect, vBox) : visibleRect.contains(vBox)) { mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), vBox.height()); mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, contentToViewDimension( nativeFocusCandidateTextSize())); return true; } else { // The textfield is now off screen. The user probably // was not zooming to see the textfield better. Remove // the WebTextView. If the user types a key, and the // textfield is still in focus, we will reconstruct // the WebTextView and scroll it back on screen. mWebTextView.remove(); return false; } } private void drawExtras(Canvas canvas, int extras, boolean animationsRunning) { // If mNativeClass is 0, we should not reach here, so we do not // need to check it again. if (animationsRunning) { canvas.setDrawFilter(mWebViewCore.mZoomFilter); } nativeDrawExtras(canvas, extras); canvas.setDrawFilter(null); } private void drawCoreAndCursorRing(Canvas canvas, int color, boolean drawCursorRing) { if (mDrawHistory) { canvas.scale(mActualScale, mActualScale); canvas.drawPicture(mHistoryPicture); return; } boolean animateZoom = mZoomScale != 0; boolean animateScroll = ((!mScroller.isFinished() || mVelocityTracker != null) && (mTouchMode != TOUCH_DRAG_MODE || mHeldMotionless != MOTIONLESS_TRUE)) || mDeferTouchMode == TOUCH_DRAG_MODE; if (animateScroll) invalidate(); if (mTouchMode == TOUCH_DRAG_MODE) { if (mHeldMotionless == MOTIONLESS_PENDING) { mPrivateHandler.removeMessages(DRAG_HELD_MOTIONLESS); mPrivateHandler.removeMessages(AWAKEN_SCROLL_BARS); mHeldMotionless = MOTIONLESS_FALSE; } if (mHeldMotionless == MOTIONLESS_FALSE) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(DRAG_HELD_MOTIONLESS), MOTIONLESS_TIME); mHeldMotionless = MOTIONLESS_PENDING; } } if (animateZoom) { float zoomScale; int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); if (interval < ZOOM_ANIMATION_LENGTH) { float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; zoomScale = 1.0f / (mInvInitialZoomScale + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); invalidate(); } else { zoomScale = mZoomScale; // set mZoomScale to be 0 as we have done animation mZoomScale = 0; WebViewCore.resumeUpdatePicture(mWebViewCore); // call invalidate() again to draw with the final filters invalidate(); if (mNeedToAdjustWebTextView) { mNeedToAdjustWebTextView = false; if (didUpdateTextViewBounds(false) && nativeFocusCandidateIsPassword()) { // If it is a password field, start drawing the // WebTextView once again. mWebTextView.setInPassword(true); } } } // calculate the intermediate scroll position. As we need to use // zoomScale, we can't use pinLocX/Y directly. Copy the logic here. float scale = zoomScale * mInvInitialZoomScale; int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) - mZoomCenterX); tx = -pinLoc(tx, getViewWidth(), Math.round(mContentWidth * zoomScale)) + mScrollX; int titleHeight = getTitleHeight(); int ty = Math.round(scale * (mInitialScrollY + mZoomCenterY - titleHeight) - (mZoomCenterY - titleHeight)); ty = -(ty <= titleHeight ? Math.max(ty, 0) : pinLoc(ty - titleHeight, getViewHeight(), Math.round(mContentHeight * zoomScale)) + titleHeight) + mScrollY; canvas.translate(tx, ty); canvas.scale(zoomScale, zoomScale); if (inEditingMode() && !mNeedToAdjustWebTextView && mZoomScale != 0) { // The WebTextView is up. Keep track of this so we can adjust // its size and placement when we finish zooming mNeedToAdjustWebTextView = true; // If it is in password mode, turn it off so it does not draw // misplaced. if (nativeFocusCandidateIsPassword()) { mWebTextView.setInPassword(false); } } } else { canvas.scale(mActualScale, mActualScale); } boolean UIAnimationsRunning = false; // Currently for each draw we compute the animation values; // We may in the future decide to do that independently. if (mNativeClass != 0 && nativeEvaluateLayersAnimations()) { UIAnimationsRunning = true; // If we have unfinished (or unstarted) animations, // we ask for a repaint. invalidate(); } mWebViewCore.drawContentPicture(canvas, color, (animateZoom || mPreviewZoomOnly || UIAnimationsRunning), animateScroll); if (mNativeClass == 0) return; // decide which adornments to draw int extras = DRAW_EXTRAS_NONE; if (mFindIsUp) { extras = DRAW_EXTRAS_FIND; } else if (mSelectingText) { extras = DRAW_EXTRAS_SELECTION; nativeSetSelectionPointer(mDrawSelectionPointer, mInvActualScale, mSelectX, mSelectY - getTitleHeight()); } else if (drawCursorRing) { extras = DRAW_EXTRAS_CURSOR_RING; } drawExtras(canvas, extras, UIAnimationsRunning); if (extras == DRAW_EXTRAS_CURSOR_RING) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; } } if (mFocusSizeChanged) { mFocusSizeChanged = false; // If we are zooming, this will get handled above, when the zoom // finishes. We also do not need to do this unless the WebTextView // is showing. if (!animateZoom && inEditingMode()) { didUpdateTextViewBounds(true); } } } // draw history private boolean mDrawHistory = false; private Picture mHistoryPicture = null; private int mHistoryWidth = 0; private int mHistoryHeight = 0; // Only check the flag, can be called from WebCore thread boolean drawHistory() { return mDrawHistory; } // Should only be called in UI thread void switchOutDrawHistory() { if (null == mWebViewCore) return; // CallbackProxy may trigger this if (mDrawHistory && mWebViewCore.pictureReady()) { mDrawHistory = false; mHistoryPicture = null; invalidate(); int oldScrollX = mScrollX; int oldScrollY = mScrollY; mScrollX = pinLocX(mScrollX); mScrollY = pinLocY(mScrollY); if (oldScrollX != mScrollX || oldScrollY != mScrollY) { mUserScroll = false; mWebViewCore.sendMessage(EventHub.SYNC_SCROLL, oldScrollX, oldScrollY); onScrollChanged(mScrollX, mScrollY, oldScrollX, oldScrollY); } else { sendOurVisibleRect(); } } } WebViewCore.CursorData cursorData() { WebViewCore.CursorData result = new WebViewCore.CursorData(); result.mMoveGeneration = nativeMoveGeneration(); result.mFrame = nativeCursorFramePointer(); Point position = nativeCursorPosition(); result.mX = position.x; result.mY = position.y; return result; } /** * Delete text from start to end in the focused textfield. If there is no * focus, or if start == end, silently fail. If start and end are out of * order, swap them. * @param start Beginning of selection to delete. * @param end End of selection to delete. */ /* package */ void deleteSelection(int start, int end) { mTextGeneration++; WebViewCore.TextSelectionData data = new WebViewCore.TextSelectionData(start, end); mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, mTextGeneration, 0, data); } /** * Set the selection to (start, end) in the focused textfield. If start and * end are out of order, swap them. * @param start Beginning of selection. * @param end End of selection. */ /* package */ void setSelection(int start, int end) { mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); } @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { InputConnection connection = super.onCreateInputConnection(outAttrs); outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_FULLSCREEN; return connection; } /** * Called in response to a message from webkit telling us that the soft * keyboard should be launched. */ private void displaySoftKeyboard(boolean isTextView) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); // bring it back to the default scale so that user can enter text boolean zoom = mActualScale < mDefaultScale; if (zoom) { mInZoomOverview = false; mZoomCenterX = mLastTouchX; mZoomCenterY = mLastTouchY; // do not change text wrap scale so that there is no reflow setNewZoomScale(mDefaultScale, false, false); } if (isTextView) { rebuildWebTextView(); if (inEditingMode()) { imm.showSoftInput(mWebTextView, 0); if (zoom) { didUpdateTextViewBounds(true); } return; } } // Used by plugins. // Also used if the navigation cache is out of date, and // does not recognize that a textfield is in focus. In that // case, use WebView as the targeted view. // see http://b/issue?id=2457459 imm.showSoftInput(this, 0); } // Called by WebKit to instruct the UI to hide the keyboard private void hideSoftKeyboard() { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(this.getWindowToken(), 0); } /* * This method checks the current focus and cursor and potentially rebuilds * mWebTextView to have the appropriate properties, such as password, * multiline, and what text it contains. It also removes it if necessary. */ /* package */ void rebuildWebTextView() { // If the WebView does not have focus, do nothing until it gains focus. if (!hasFocus() && (null == mWebTextView || !mWebTextView.hasFocus())) { return; } boolean alreadyThere = inEditingMode(); // inEditingMode can only return true if mWebTextView is non-null, // so we can safely call remove() if (alreadyThere) if (0 == mNativeClass || !nativeFocusCandidateIsTextInput()) { if (alreadyThere) { mWebTextView.remove(); } return; } // At this point, we know we have found an input field, so go ahead // and create the WebTextView if necessary. if (mWebTextView == null) { mWebTextView = new WebTextView(mContext, WebView.this); // Initialize our generation number. mTextGeneration = 0; } mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, contentToViewDimension(nativeFocusCandidateTextSize())); Rect visibleRect = new Rect(); calcOurContentVisibleRect(visibleRect); // Note that sendOurVisibleRect calls viewToContent, so the coordinates // should be in content coordinates. Rect bounds = nativeFocusCandidateNodeBounds(); Rect vBox = contentToViewRect(bounds); mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), vBox.height()); if (!Rect.intersects(bounds, visibleRect)) { mWebTextView.bringIntoView(); } String text = nativeFocusCandidateText(); int nodePointer = nativeFocusCandidatePointer(); if (alreadyThere && mWebTextView.isSameTextField(nodePointer)) { // It is possible that we have the same textfield, but it has moved, // i.e. In the case of opening/closing the screen. // In that case, we need to set the dimensions, but not the other // aspects. // If the text has been changed by webkit, update it. However, if // there has been more UI text input, ignore it. We will receive // another update when that text is recognized. if (text != null && !text.equals(mWebTextView.getText().toString()) && nativeTextGeneration() == mTextGeneration) { mWebTextView.setTextAndKeepSelection(text); } } else { mWebTextView.setGravity(nativeFocusCandidateIsRtlText() ? Gravity.RIGHT : Gravity.NO_GRAVITY); // This needs to be called before setType, which may call // requestFormData, and it needs to have the correct nodePointer. mWebTextView.setNodePointer(nodePointer); mWebTextView.setType(nativeFocusCandidateType()); Rect paddingRect = nativeFocusCandidatePaddingRect(); if (paddingRect != null) { // Use contentToViewDimension since these are the dimensions of // the padding. mWebTextView.setPadding( contentToViewDimension(paddingRect.left), contentToViewDimension(paddingRect.top), contentToViewDimension(paddingRect.right), contentToViewDimension(paddingRect.bottom)); } if (null == text) { if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "rebuildWebTextView null == text"); } text = ""; } mWebTextView.setTextAndKeepSelection(text); InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && imm.isActive(mWebTextView)) { imm.restartInput(mWebTextView); } } mWebTextView.requestFocus(); } /** * Called by WebTextView to find saved form data associated with the * textfield * @param name Name of the textfield. * @param nodePointer Pointer to the node of the textfield, so it can be * compared to the currently focused textfield when the data is * retrieved. */ /* package */ void requestFormData(String name, int nodePointer) { if (mWebViewCore.getSettings().getSaveFormData()) { Message update = mPrivateHandler.obtainMessage(REQUEST_FORM_DATA); update.arg1 = nodePointer; RequestFormData updater = new RequestFormData(name, getUrl(), update); Thread t = new Thread(updater); t.start(); } } /** * Pass a message to find out the