/* * Copyright (C) 2006 The Android Open Source Project * * 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 android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.DialogInterface.OnCancelListener; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.net.http.SslCertificate; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.os.ServiceManager; import android.os.SystemClock; import android.provider.Checkin; import android.text.IClipboard; import android.text.Selection; import android.text.Spannable; import android.util.AttributeSet; import android.util.Config; import android.util.EventLog; import android.util.Log; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.SoundEffectConstants; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.inputmethod.InputMethodManager; import android.webkit.TextDialog.AutoCompleteAdapter; import android.webkit.WebViewCore.EventHub; import android.widget.AbsoluteLayout; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.Scroller; import android.widget.Toast; import android.widget.ZoomControls; import android.widget.ZoomRingController; import android.widget.FrameLayout; import android.widget.AdapterView.OnItemClickListener; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; /** *
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.
*Note that, in order for your Activity to access the Internet and load web pages * in a WebView, you must add the INTERNET permissions to your * Android Manifest file:
*<uses-permission android:name="android.permission.INTERNET" />*
This must be a child of the <manifest>
element.
* 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 failUrl URL to use if the content fails to load or null. */ public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String failUrl) { if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { loadData(data, mimeType, encoding); return; } switchOutDrawHistory(); HashMap arg = new HashMap(); arg.put("baseUrl", baseUrl); arg.put("data", data); arg.put("mimeType", mimeType); arg.put("encoding", encoding); arg.put("failUrl", failUrl); mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg); clearTextEntry(); } /** * 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() { 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) { // every time we go back or forward, we want to reset the // WebView certificate: // if the new site is secure, we will reload it and get a // new certificate set; // if the new site is not secure, the certificate must be // null, and that will be the case mCertificate = null; if (steps != 0) { clearTextEntry(); 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; } nativeClearFocus(-1, -1); 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; } nativeClearFocus(-1, -1); if (bottom) { return pinScrollTo(mScrollX, mContentHeight, 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 mTextEntry != null && mTextEntry.getParent() != null && mTextEntry.hasFocus(); } private void clearTextEntry() { if (inEditingMode()) { mTextEntry.remove(); } } /** * 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) { mInitialScale = 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(); ExtendedZoomControls zoomControls = (ExtendedZoomControls) getZoomControls(); zoomControls.show(true, canZoomScrollOut()); zoomControls.requestFocus(); mPrivateHandler.removeCallbacks(mZoomControlRunnable); mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); } /** * Return a HitTestResult based on the current focus 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 (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; if (node.mIsTextField || node.mIsTextArea) { result.setType(HitTestResult.EDIT_TEXT_TYPE); } else if (node.mText != null) { String text = node.mText; 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 (node.mIsAnchor) { 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 = viewToContent((int) mLastTouchX + mScrollX); int contentY = viewToContent((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; } /** * 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. */ public void requestFocusNodeHref(Message hrefMsg) { if (hrefMsg == null || mNativeClass == 0) { return; } if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; if (node.mIsAnchor) { // NOTE: We may already have the url of the anchor stored in // node.mText but it may be out of date or the caller may want // to know about javascript urls. mWebViewCore.sendMessage(EventHub.REQUEST_FOCUS_HREF, node.mFramePointer, node.mNodePointer, 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) { int contentX = viewToContent((int) mLastTouchX + mScrollX); int contentY = viewToContent((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" x = -(viewMax - docMax) >> 1; // 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) { return pinLoc(x, getViewWidth(), computeHorizontalScrollRange()); } // Expects y in view coordinates private int pinLocY(int y) { return pinLoc(y, getViewHeight(), computeVerticalScrollRange()); } /*package*/ int viewToContent(int x) { return Math.round(x * mInvActualScale); } private int contentToView(int x) { return Math.round(x * mActualScale); } // Called by JNI to invalidate the View, given rectangle coordinates in // content space private void viewInvalidate(int l, int t, int r, int b) { invalidate(contentToView(l), contentToView(t), contentToView(r), contentToView(b)); } // 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) { postInvalidateDelayed(delay, contentToView(l), contentToView(t), contentToView(r), contentToView(b)); } private Rect contentToView(Rect x) { return new Rect(contentToView(x.left), contentToView(x.top) , contentToView(x.right), contentToView(x.bottom)); } /* 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); // android.util.Log.d("skia", "recordNewContentSize - // abortAnimation"); mScroller.abortAnimation(); // just in case if (oldX != mScrollX || oldY != mScrollY) { sendOurVisibleRect(); } } } contentSizeChanged(updateLayout); } private void setNewZoomScale(float scale, boolean force) { if (scale < mMinZoomScale) { scale = mMinZoomScale; } else if (scale > mMaxZoomScale) { scale = mMaxZoomScale; } 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; if (!mPreviewZoomOnly) { 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; // now update our new scale and inverse if (scale != mActualScale && !mPreviewZoomOnly) { mCallbackProxy.onScaleChanged(mActualScale, scale); } mActualScale = scale; mInvActualScale = 1 / scale; // 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)); if (!mPreviewZoomOnly) { sendViewSizeZoom(); sendOurVisibleRect(); } } } } // Used to avoid sending many visible rect messages. private Rect mLastVisibleRectSent; private Rect mLastGlobalRect; private Rect sendOurVisibleRect() { Rect rect = new Rect(); calcOurContentVisibleRect(rect); if (mFindIsUp) { rect.bottom -= viewToContent(FIND_HEIGHT); } // Rect.equals() checks for null input. if (!rect.equals(mLastVisibleRectSent)) { mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, rect.left, rect.top); mLastVisibleRectSent = rect; } Rect globalRect = new Rect(); if (getGlobalVisibleRect(globalRect) && !globalRect.equals(mLastGlobalRect)) { // 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); } // Sets r to be our visible rectangle in content coordinates private void calcOurContentVisibleRect(Rect r) { calcOurVisibleRect(r); r.left = viewToContent(r.left); r.top = viewToContent(r.top); r.right = viewToContent(r.right); r.bottom = viewToContent(r.bottom); } /** * 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() { 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) { mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, newWidth, newHeight, new Integer(viewWidth)); mLastWidthSent = newWidth; mLastHeightSent = newHeight; return true; } return false; } @Override protected int computeHorizontalScrollRange() { if (mDrawHistory) { return mHistoryWidth; } else { return contentToView(mContentWidth); } } // Make sure this stays in sync with the actual height of the FindDialog. private static final int FIND_HEIGHT = 79; @Override protected int computeVerticalScrollRange() { if (mDrawHistory) { return mHistoryHeight; } else { int height = contentToView(mContentHeight); if (mFindIsUp) { height += FIND_HEIGHT; } return height; } } /** * 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. * * @hide pending API Council approval */ 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 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; } /** * Pause all layout, parsing, and javascript timers. This can be useful if * the WebView is not visible or the application has been paused. */ public void pauseTimers() { mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } /** * Resume all layout, parsing, and javascript timers. This will resume * dispatching all timers. */ public void resumeTimers() { mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } /** * Clear the resource cache. This will cause resources to be re-downloaded * if accessed again. *
* 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.
*/
public void clearCache(boolean includeDiskFiles) {
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;
mTextEntry.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) {
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) {
mFindIsUp = true;
int result = nativeFindAll(find.toLowerCase(), find.toUpperCase());
invalidate();
return result;
}
// Used to know whether the find dialog is open. Affects whether
// or not we draw the highlights for matches.
private boolean mFindIsUp;
private native int nativeFindAll(String findLower, String findUpper);
private native void nativeFindNext(boolean forward);
/**
* 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 WebViewCore.nativeFindAddress(addr);
}
/*
* Clear the highlighting surrounding text matches created by findAll.
*/
public void clearMatches() {
mFindIsUp = false;
nativeSetFindIsDown();
// 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();
}
/**
* 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;
mScrollX = mScroller.getCurrX();
mScrollY = mScroller.getCurrY();
postInvalidate(); // So we draw again
if (oldX != mScrollX || oldY != mScrollY) {
// as onScrollChanged() is not called, sendOurVisibleRect()
// needs to be call explicitly
sendOurVisibleRect();
}
} 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 (true && animate) {
// Log.d(LOGTAG, "startScroll: " + dx + " " + dy);
mScroller.startScroll(mScrollX, mScrollY, dx, dy,
animationDuration > 0 ? animationDuration : computeDuration(dx, dy));
invalidate();
} else {
mScroller.abortAnimation(); // just in case
scrollTo(x, y);
}
return true;
}
// Scale from content to view coordinates, and pin.
// Also called by jni webview.cpp
private void 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;
}
cx = contentToView(cx);
cy = contentToView(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);
if (cy == 0 && cx != 0) {
pinScrollBy(cx, 0, animate, 0);
}
} else {
pinScrollBy(cx, cy, animate, 0);
}
}
// scale from content to view coordinates, and pin
// return true if pin caused the final x/y different than the request cx/cy;
// return false if the view scroll to the exact position as it is requested.
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 = contentToView(cx);
int vy = contentToView(cy);
// Log.d(LOGTAG, "content scrollTo [" + cx + " " + cy + "] view=[" +
// vx + " " + vy + "]");
pinScrollTo(vx, vy, false, 0);
if (mScrollX != vx || mScrollY != vy) {
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 = contentToView(cx);
int vy = contentToView(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() != contentToView(mContentHeight)
&& updateLayout) {
requestLayout();
}
} else if (mWidthCanMeasure) {
if (getMeasuredWidth() != contentToView(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);
}
/**
* 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);
}
/**
* 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, the 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) {
// Use Hashmap rather than Bundle as Bundles can't cope with Objects
HashMap arg = new HashMap();
arg.put("object", obj);
arg.put("interfaceName", 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();
}
/**
* Return the list of currently loaded plugins.
* @return The list of currently loaded plugins.
*/
public static synchronized PluginList getPluginList() {
if (sPluginList == null) {
sPluginList = new PluginList();
}
return sPluginList;
}
/**
* Signal the WebCore thread to refresh its list of plugins. Use
* this if the directory contents of one of the plugin directories
* has been modified and needs its changes reflecting. May cause
* plugin load and/or unload.
* @param reloadOpenPages Set to true to reload all open pages.
*/
public void refreshPlugins(boolean reloadOpenPages) {
if (mWebViewCore != null) {
mWebViewCore.sendMessage(EventHub.REFRESH_PLUGINS, reloadOpenPages);
}
}
//-------------------------------------------------------------------------
// Override View methods
//-------------------------------------------------------------------------
@Override
protected void finalize() throws Throwable {
destroy();
}
@Override
protected void onDraw(Canvas canvas) {
// if mNativeClass is 0, the WebView has been destroyed. Do nothing.
if (mNativeClass == 0) {
return;
}
if (mWebViewCore.mEndScaleZoom) {
mWebViewCore.mEndScaleZoom = false;
if (mTouchMode >= FIRST_SCROLL_ZOOM
&& mTouchMode <= LAST_SCROLL_ZOOM) {
setHorizontalScrollBarEnabled(true);
setVerticalScrollBarEnabled(true);
mTouchMode = TOUCH_DONE_MODE;
}
}
int sc = canvas.save();
if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) {
scrollZoomDraw(canvas);
} else {
nativeRecomputeFocus();
// 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 focus is 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 || mGotEnterDown, false);
drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing);
}
canvas.restoreToCount(sc);
if (AUTO_REDRAW_HACK && mAutoRedraw) {
invalidate();
}
}
@Override
public void setLayoutParams(ViewGroup.LayoutParams params) {
if (params.height == LayoutParams.WRAP_CONTENT) {
mWrapContent = true;
}
super.setLayoutParams(params);
}
@Override
public boolean performLongClick() {
if (inEditingMode()) {
return mTextEntry.performLongClick();
} else {
return super.performLongClick();
}
}
private void drawCoreAndFocusRing(Canvas canvas, int color,
boolean drawFocus) {
if (mDrawHistory) {
canvas.scale(mActualScale, mActualScale);
canvas.drawPicture(mHistoryPicture);
return;
}
boolean animateZoom = mZoomScale != 0;
boolean animateScroll = !mScroller.isFinished()
|| mVelocityTracker != null;
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;
}
float scale = (mActualScale - zoomScale) * mInvActualScale;
float tx = scale * (mZoomCenterX + mScrollX);
float ty = scale * (mZoomCenterY + mScrollY);
// this block pins the translate to "legal" bounds. This makes the
// animation a bit non-obvious, but it means we won't pop when the
// "real" zoom takes effect
if (true) {
// canvas.translate(mScrollX, mScrollY);
tx -= mScrollX;
ty -= mScrollY;
tx = -pinLoc(-Math.round(tx), getViewWidth(), Math
.round(mContentWidth * zoomScale));
ty = -pinLoc(-Math.round(ty), getViewHeight(), Math
.round(mContentHeight * zoomScale));
tx += mScrollX;
ty += mScrollY;
}
canvas.translate(tx, ty);
canvas.scale(zoomScale, zoomScale);
} else {
canvas.scale(mActualScale, mActualScale);
}
mWebViewCore.drawContentPicture(canvas, color, animateZoom,
animateScroll);
if (mNativeClass == 0) return;
if (mShiftIsPressed) {
if (mTouchSelection) {
nativeDrawSelectionRegion(canvas);
} else {
nativeDrawSelection(canvas, mSelectX, mSelectY,
mExtendSelection);
}
} else if (drawFocus) {
if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) {
mTouchMode = TOUCH_SHORTPRESS_MODE;
HitTestResult hitTest = getHitTestResult();
if (hitTest != null &&
hitTest.mType != HitTestResult.UNKNOWN_TYPE) {
mPrivateHandler.sendMessageDelayed(mPrivateHandler
.obtainMessage(SWITCH_TO_LONGPRESS),
LONG_PRESS_TIMEOUT);
}
}
nativeDrawFocusRing(canvas);
}
// When the FindDialog is up, only draw the matches if we are not in
// the process of scrolling them into view.
if (mFindIsUp && !animateScroll) {
nativeDrawMatches(canvas);
}
}
private native void nativeDrawMatches(Canvas canvas);
private float scrollZoomGridScale(float invScale) {
float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID)
/ (float) SCROLL_ZOOM_GRID;
return 1.0f / griddedInvScale;
}
private float scrollZoomX(float scale) {
int width = getViewWidth();
float maxScrollZoomX = mContentWidth * scale - width;
int maxX = mContentWidth - width;
return -(maxScrollZoomX > 0 ? mZoomScrollX * maxScrollZoomX / maxX
: maxScrollZoomX / 2);
}
private float scrollZoomY(float scale) {
int height = getViewHeight();
float maxScrollZoomY = mContentHeight * scale - height;
int maxY = mContentHeight - height;
return -(maxScrollZoomY > 0 ? mZoomScrollY * maxScrollZoomY / maxY
: maxScrollZoomY / 2);
}
private void drawMagnifyFrame(Canvas canvas, Rect frame, Paint paint) {
final float ADORNMENT_LEN = 16.0f;
float width = frame.width();
float height = frame.height();
Path path = new Path();
path.moveTo(-ADORNMENT_LEN, -ADORNMENT_LEN);
path.lineTo(0, 0);
path.lineTo(width, 0);
path.lineTo(width + ADORNMENT_LEN, -ADORNMENT_LEN);
path.moveTo(-ADORNMENT_LEN, height + ADORNMENT_LEN);
path.lineTo(0, height);
path.lineTo(width, height);
path.lineTo(width + ADORNMENT_LEN, height + ADORNMENT_LEN);
path.moveTo(0, 0);
path.lineTo(0, height);
path.moveTo(width, 0);
path.lineTo(width, height);
path.offset(frame.left, frame.top);
canvas.drawPath(path, paint);
}
// Returns frame surrounding magified portion of screen while
// scroll-zoom is enabled. The frame is also used to center the
// zoom-in zoom-out points at the start and end of the animation.
private Rect scrollZoomFrame(int width, int height, float halfScale) {
Rect scrollFrame = new Rect();
scrollFrame.set(mZoomScrollX, mZoomScrollY,
mZoomScrollX + width, mZoomScrollY + height);
if (mContentWidth * mZoomScrollLimit < width) {
float scale = zoomFrameScaleX(width, halfScale, 1.0f);
float offsetX = (width * scale - width) * 0.5f;
scrollFrame.left -= offsetX;
scrollFrame.right += offsetX;
}
if (mContentHeight * mZoomScrollLimit < height) {
float scale = zoomFrameScaleY(height, halfScale, 1.0f);
float offsetY = (height * scale - height) * 0.5f;
scrollFrame.top -= offsetY;
scrollFrame.bottom += offsetY;
}
return scrollFrame;
}
private float zoomFrameScaleX(int width, float halfScale, float noScale) {
// mContentWidth > width > mContentWidth * mZoomScrollLimit
if (mContentWidth <= width) {
return halfScale;
}
float part = (width - mContentWidth * mZoomScrollLimit)
/ (width * (1 - mZoomScrollLimit));
return halfScale * part + noScale * (1.0f - part);
}
private float zoomFrameScaleY(int height, float halfScale, float noScale) {
if (mContentHeight <= height) {
return halfScale;
}
float part = (height - mContentHeight * mZoomScrollLimit)
/ (height * (1 - mZoomScrollLimit));
return halfScale * part + noScale * (1.0f - part);
}
private float scrollZoomMagScale(float invScale) {
return (invScale * 2 + mInvActualScale) / 3;
}
private void scrollZoomDraw(Canvas canvas) {
float invScale = mZoomScrollInvLimit;
int elapsed = 0;
if (mTouchMode != SCROLL_ZOOM_OUT) {
elapsed = (int) Math.min(System.currentTimeMillis()
- mZoomScrollStart, SCROLL_ZOOM_DURATION);
float transitionScale = (mZoomScrollInvLimit - mInvActualScale)
* elapsed / SCROLL_ZOOM_DURATION;
if (mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) {
invScale = mInvActualScale + transitionScale;
} else { /* if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) */
invScale = mZoomScrollInvLimit - transitionScale;
}
}
float scale = scrollZoomGridScale(invScale);
invScale = 1.0f / scale;
int width = getViewWidth();
int height = getViewHeight();
float halfScale = scrollZoomMagScale(invScale);
Rect scrollFrame = scrollZoomFrame(width, height, halfScale);
if (elapsed == SCROLL_ZOOM_DURATION) {
if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) {
setHorizontalScrollBarEnabled(true);
setVerticalScrollBarEnabled(true);
updateTextEntry();
scrollTo((int) (scrollFrame.centerX() * mActualScale)
- (width >> 1), (int) (scrollFrame.centerY()
* mActualScale) - (height >> 1));
mTouchMode = TOUCH_DONE_MODE;
} else {
mTouchMode = SCROLL_ZOOM_OUT;
}
}
float newX = scrollZoomX(scale);
float newY = scrollZoomY(scale);
if (LOGV_ENABLED) {
Log.v(LOGTAG, "scrollZoomDraw scale=" + scale + " + (" + newX
+ ", " + newY + ") mZoomScroll=(" + mZoomScrollX + ", "
+ mZoomScrollY + ")" + " invScale=" + invScale + " scale="
+ scale);
}
canvas.translate(newX, newY);
canvas.scale(scale, scale);
boolean animating = mTouchMode != SCROLL_ZOOM_OUT;
if (mDrawHistory) {
int sc = canvas.save(Canvas.CLIP_SAVE_FLAG);
Rect clip = new Rect(0, 0, mHistoryPicture.getWidth(),
mHistoryPicture.getHeight());
canvas.clipRect(clip, Region.Op.DIFFERENCE);
canvas.drawColor(mBackgroundColor);
canvas.restoreToCount(sc);
canvas.drawPicture(mHistoryPicture);
} else {
mWebViewCore.drawContentPicture(canvas, mBackgroundColor,
animating, true);
}
if (mTouchMode == TOUCH_DONE_MODE) {
return;
}
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(30.0f);
paint.setARGB(0x50, 0, 0, 0);
int maxX = mContentWidth - width;
int maxY = mContentHeight - height;
if (true) { // experiment: draw hint to place finger off magnify area
drawMagnifyFrame(canvas, scrollFrame, paint);
} else {
canvas.drawRect(scrollFrame, paint);
}
int sc = canvas.save();
canvas.clipRect(scrollFrame);
float halfX = (float) mZoomScrollX / maxX;
if (mContentWidth * mZoomScrollLimit < width) {
halfX = zoomFrameScaleX(width, 0.5f, halfX);
}
float halfY = (float) mZoomScrollY / maxY;
if (mContentHeight * mZoomScrollLimit < height) {
halfY = zoomFrameScaleY(height, 0.5f, halfY);
}
canvas.scale(halfScale, halfScale, mZoomScrollX + width * halfX
, mZoomScrollY + height * halfY);
if (LOGV_ENABLED) {
Log.v(LOGTAG, "scrollZoomDraw halfScale=" + halfScale + " w/h=("
+ width + ", " + height + ") half=(" + halfX + ", "
+ halfY + ")");
}
if (mDrawHistory) {
canvas.drawPicture(mHistoryPicture);
} else {
mWebViewCore.drawContentPicture(canvas, mBackgroundColor,
animating, false);
}
canvas.restoreToCount(sc);
if (mTouchMode != SCROLL_ZOOM_OUT) {
invalidate();
}
}
private void zoomScrollTap(float x, float y) {
float scale = scrollZoomGridScale(mZoomScrollInvLimit);
float left = scrollZoomX(scale);
float top = scrollZoomY(scale);
int width = getViewWidth();
int height = getViewHeight();
x -= width * scale / 2;
y -= height * scale / 2;
mZoomScrollX = Math.min(mContentWidth - width
, Math.max(0, (int) ((x - left) / scale)));
mZoomScrollY = Math.min(mContentHeight - height
, Math.max(0, (int) ((y - top) / scale)));
if (LOGV_ENABLED) {
Log.v(LOGTAG, "zoomScrollTap scale=" + scale + " + (" + left
+ ", " + top + ") mZoomScroll=(" + mZoomScrollX + ", "
+ mZoomScrollY + ")" + " x=" + x + " y=" + y);
}
}
private boolean canZoomScrollOut() {
if (mContentWidth == 0 || mContentHeight == 0) {
return false;
}
int width = getViewWidth();
int height = getViewHeight();
float x = (float) width / (float) mContentWidth;
float y = (float) height / (float) mContentHeight;
mZoomScrollLimit = Math.max(DEFAULT_MIN_ZOOM_SCALE, Math.min(x, y));
mZoomScrollInvLimit = 1.0f / mZoomScrollLimit;
if (LOGV_ENABLED) {
Log.v(LOGTAG, "canZoomScrollOut"
+ " mInvActualScale=" + mInvActualScale
+ " mZoomScrollLimit=" + mZoomScrollLimit
+ " mZoomScrollInvLimit=" + mZoomScrollInvLimit
+ " mContentWidth=" + mContentWidth
+ " mContentHeight=" + mContentHeight
);
}
// don't zoom out unless magnify area is at least half as wide
// or tall as content
float limit = mZoomScrollLimit * 2;
return mContentWidth >= width * limit
|| mContentHeight >= height * limit;
}
private void startZoomScrollOut() {
setHorizontalScrollBarEnabled(false);
setVerticalScrollBarEnabled(false);
if (mZoomControlRunnable != null) {
mPrivateHandler.removeCallbacks(mZoomControlRunnable);
}
if (mZoomControls != null) {
mZoomControls.hide();
}
int width = getViewWidth();
int height = getViewHeight();
int halfW = width >> 1;
mLastTouchX = halfW;
int halfH = height >> 1;
mLastTouchY = halfH;
mScroller.abortAnimation();
mZoomScrollStart = System.currentTimeMillis();
Rect zoomFrame = scrollZoomFrame(width, height
, scrollZoomMagScale(mZoomScrollInvLimit));
mZoomScrollX = Math.max(0, (int) ((mScrollX + halfW) * mInvActualScale)
- (zoomFrame.width() >> 1));
mZoomScrollY = Math.max(0, (int) ((mScrollY + halfH) * mInvActualScale)
- (zoomFrame.height() >> 1));
scrollTo(0, 0); // triggers inval, starts animation
clearTextEntry();
if (LOGV_ENABLED) {
Log.v(LOGTAG, "startZoomScrollOut mZoomScroll=("
+ mZoomScrollX + ", " + mZoomScrollY +")");
}
}
private void zoomScrollOut() {
if (canZoomScrollOut() == false) {
mTouchMode = TOUCH_DONE_MODE;
return;
}
startZoomScrollOut();
mTouchMode = SCROLL_ZOOM_ANIMATION_OUT;
invalidate();
}
private void moveZoomScrollWindow(float x, float y) {
if (Math.abs(x - mLastZoomScrollRawX) < 1.5f
&& Math.abs(y - mLastZoomScrollRawY) < 1.5f) {
return;
}
mLastZoomScrollRawX = x;
mLastZoomScrollRawY = y;
int oldX = mZoomScrollX;
int oldY = mZoomScrollY;
int width = getViewWidth();
int height = getViewHeight();
int maxZoomX = mContentWidth - width;
if (maxZoomX > 0) {
int maxScreenX = width - (int) Math.ceil(width
* mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER;
if (LOGV_ENABLED) {
Log.v(LOGTAG, "moveZoomScrollWindow-X"
+ " maxScreenX=" + maxScreenX + " width=" + width
+ " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x);
}
x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX;
x *= Math.max(maxZoomX / maxScreenX, mZoomScrollInvLimit);
mZoomScrollX = Math.max(0, Math.min(maxZoomX, (int) x));
}
int maxZoomY = mContentHeight - height;
if (maxZoomY > 0) {
int maxScreenY = height - (int) Math.ceil(height
* mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER;
if (LOGV_ENABLED) {
Log.v(LOGTAG, "moveZoomScrollWindow-Y"
+ " maxScreenY=" + maxScreenY + " height=" + height
+ " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y);
}
y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY;
y *= Math.max(maxZoomY / maxScreenY, mZoomScrollInvLimit);
mZoomScrollY = Math.max(0, Math.min(maxZoomY, (int) y));
}
if (oldX != mZoomScrollX || oldY != mZoomScrollY) {
invalidate();
}
if (LOGV_ENABLED) {
Log.v(LOGTAG, "moveZoomScrollWindow"
+ " scrollTo=(" + mZoomScrollX + ", " + mZoomScrollY + ")"
+ " mLastTouch=(" + mLastTouchX + ", " + mLastTouchY + ")"
+ " maxZoom=(" + maxZoomX + ", " + maxZoomY + ")"
+ " last=("+mLastScrollX+", "+mLastScrollY+")"
+ " x=" + x + " y=" + y);
}
}
private void setZoomScrollIn() {
mZoomScrollStart = System.currentTimeMillis();
}
private float mZoomScrollLimit;
private float mZoomScrollInvLimit;
private int mLastScrollX;
private int mLastScrollY;
private long mZoomScrollStart;
private int mZoomScrollX;
private int mZoomScrollY;
private float mLastZoomScrollRawX = -1000.0f;
private float mLastZoomScrollRawY = -1000.0f;
// The zoomed scale varies from 1.0 to DEFAULT_MIN_ZOOM_SCALE == 0.25.
// The zoom animation duration SCROLL_ZOOM_DURATION == 0.5.
// Two pressures compete for gridding; a high frame rate (e.g. 20 fps)
// and minimizing font cache allocations (fewer frames is better).
// A SCROLL_ZOOM_GRID of 6 permits about 20 zoom levels over 0.5 seconds:
// the inverse of: 1.0, 1.16, 1.33, 1.5, 1.67, 1.84, 2.0, etc. to 4.0
private static final int SCROLL_ZOOM_GRID = 6;
private static final int SCROLL_ZOOM_DURATION = 500;
// Make it easier to get to the bottom of a document by reserving a 32
// pixel buffer, for when the starting drag is a bit below the bottom of
// the magnify frame.
private static final int SCROLL_ZOOM_FINGER_BUFFER = 32;
// 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) {
mDrawHistory = false;
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);
}
sendOurVisibleRect();
}
}
/**
* Class representing the node which is focused.
*/
private class FocusNode {
public FocusNode() {
mBounds = new Rect();
}
// Only to be called by JNI
private void setAll(boolean isTextField, boolean isTextArea, boolean
isPassword, boolean isAnchor, boolean isRtlText, int maxLength,
int textSize, int boundsX, int boundsY, int boundsRight, int
boundsBottom, int nodePointer, int framePointer, String text,
String name, int rootTextGeneration) {
mIsTextField = isTextField;
mIsTextArea = isTextArea;
mIsPassword = isPassword;
mIsAnchor = isAnchor;
mIsRtlText = isRtlText;
mMaxLength = maxLength;
mTextSize = textSize;
mBounds.set(boundsX, boundsY, boundsRight, boundsBottom);
mNodePointer = nodePointer;
mFramePointer = framePointer;
mText = text;
mName = name;
mRootTextGeneration = rootTextGeneration;
}
public boolean mIsTextField;
public boolean mIsTextArea;
public boolean mIsPassword;
public boolean mIsAnchor;
public boolean mIsRtlText;
public int mSelectionStart;
public int mSelectionEnd;
public int mMaxLength;
public int mTextSize;
public Rect mBounds;
public int mNodePointer;
public int mFramePointer;
public String mText;
public String mName;
public int mRootTextGeneration;
}
// Warning: ONLY use mFocusNode AFTER calling nativeUpdateFocusNode(),
// and ONLY if it returns true;
private FocusNode mFocusNode = new FocusNode();
/**
* 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) {
mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end,
new WebViewCore.FocusData(mFocusData));
}
/**
* 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,
new WebViewCore.FocusData(mFocusData));
}
// Called by JNI when a touch event puts a textfield into focus.
private void displaySoftKeyboard() {
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(mTextEntry, 0);
mTextEntry.enableScrollOnScreen(true);
// Now we need to fake a touch event to place the cursor where the
// user touched.
AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams)
mTextEntry.getLayoutParams();
if (lp != null) {
// Take the last touch and adjust for the location of the
// TextDialog.
float x = mLastTouchX - lp.x;
float y = mLastTouchY - lp.y;
mTextEntry.fakeTouchEvent(x, y);
}
}
private void updateTextEntry() {
if (mTextEntry == null) {
mTextEntry = new TextDialog(mContext, WebView.this);
// Initialize our generation number.
mTextGeneration = 0;
}
// If we do not have focus, do nothing until we gain focus.
if (!hasFocus() && !mTextEntry.hasFocus()
|| (mTouchMode >= FIRST_SCROLL_ZOOM
&& mTouchMode <= LAST_SCROLL_ZOOM)) {
mNeedsUpdateTextEntry = true;
return;
}
boolean alreadyThere = inEditingMode();
if (0 == mNativeClass || !nativeUpdateFocusNode()) {
if (alreadyThere) {
mTextEntry.remove();
}
return;
}
FocusNode node = mFocusNode;
if (!node.mIsTextField && !node.mIsTextArea) {
if (alreadyThere) {
mTextEntry.remove();
}
return;
}
mTextEntry.setTextSize(contentToView(node.mTextSize));
Rect visibleRect = sendOurVisibleRect();
// Note that sendOurVisibleRect calls viewToContent, so the coordinates
// should be in content coordinates.
if (!Rect.intersects(node.mBounds, visibleRect)) {
// Node is not on screen, so do not bother.
return;
}
int x = node.mBounds.left;
int y = node.mBounds.top;
int width = node.mBounds.width();
int height = node.mBounds.height();
if (alreadyThere && mTextEntry.isSameTextField(node.mNodePointer)) {
// 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.
// We also need to restore the selection, which gets wrecked by
// calling setTextEntryRect.
Spannable spannable = (Spannable) mTextEntry.getText();
int start = Selection.getSelectionStart(spannable);
int end = Selection.getSelectionEnd(spannable);
setTextEntryRect(x, y, width, height);
// 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 (node.mText != null && !node.mText.equals(spannable.toString())
&& node.mRootTextGeneration == mTextGeneration) {
mTextEntry.setTextAndKeepSelection(node.mText);
} else {
Selection.setSelection(spannable, start, end);
}
} else {
String text = node.mText;
setTextEntryRect(x, y, width, height);
mTextEntry.setGravity(node.mIsRtlText ? Gravity.RIGHT :
Gravity.NO_GRAVITY);
// this needs to be called before update adapter thread starts to
// ensure the mTextEntry has the same node pointer
mTextEntry.setNodePointer(node.mNodePointer);
int maxLength = -1;
if (node.mIsTextField) {
maxLength = node.mMaxLength;
if (mWebViewCore.getSettings().getSaveFormData()
&& node.mName != null) {
HashMap data = new HashMap();
data.put("text", node.mText);
Message update = mPrivateHandler.obtainMessage(
UPDATE_TEXT_ENTRY_ADAPTER, node.mNodePointer, 0,
data);
UpdateTextEntryAdapter updater = new UpdateTextEntryAdapter(
node.mName, getUrl(), update);
Thread t = new Thread(updater);
t.start();
}
}
mTextEntry.setMaxLength(maxLength);
AutoCompleteAdapter adapter = null;
mTextEntry.setAdapterCustom(adapter);
mTextEntry.setSingleLine(node.mIsTextField);
mTextEntry.setInPassword(node.mIsPassword);
if (null == text) {
mTextEntry.setText("", 0, 0);
} else {
// Change to true to enable the old style behavior, where
// entering a textfield/textarea always set the selection to the
// whole field. This was desirable for the case where the user
// intends to scroll past the field using the trackball.
// However, it causes a problem when replying to emails - the
// user expects the cursor to be at the beginning of the
// textarea. Testing out a new behavior, where textfields set
// selection at the end, and textareas at the beginning.
if (false) {
mTextEntry.setText(text, 0, text.length());
} else if (node.mIsTextField) {
int length = text.length();
mTextEntry.setText(text, length, length);
} else {
mTextEntry.setText(text, 0, 0);
}
}
mTextEntry.requestFocus();
}
}
private class UpdateTextEntryAdapter implements Runnable {
private String mName;
private String mUrl;
private Message mUpdateMessage;
public UpdateTextEntryAdapter(String name, String url, Message msg) {
mName = name;
mUrl = url;
mUpdateMessage = msg;
}
public void run() {
ArrayList