diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2009-03-03 19:31:44 -0800 |
commit | 9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch) | |
tree | d88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/webkit | |
parent | d83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff) | |
download | frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.zip frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.gz frameworks_base-9066cfe9886ac131c34d59ed0e2d287b0e3c0087.tar.bz2 |
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/webkit')
52 files changed, 22758 insertions, 0 deletions
diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java new file mode 100644 index 0000000..451af6d --- /dev/null +++ b/core/java/android/webkit/BrowserFrame.java @@ -0,0 +1,786 @@ +/* + * 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.content.Context; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.net.ParseException; +import android.net.WebAddress; +import android.net.http.SslCertificate; +import android.os.Handler; +import android.os.Message; +import android.util.Config; +import android.util.Log; +import android.util.TypedValue; + +import junit.framework.Assert; + +import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Iterator; + +class BrowserFrame extends Handler { + + private static final String LOGTAG = "webkit"; + + /** + * Cap the number of LoadListeners that will be instantiated, so + * we don't blow the GREF count. Attempting to queue more than + * this many requests will prompt an error() callback on the + * request's LoadListener + */ + private final static int MAX_OUTSTANDING_REQUESTS = 300; + + private final CallbackProxy mCallbackProxy; + private final WebSettings mSettings; + private final Context mContext; + private final WebViewDatabase mDatabase; + private final WebViewCore mWebViewCore; + /* package */ boolean mLoadInitFromJava; + private int mLoadType; + private boolean mFirstLayoutDone = true; + private boolean mCommitted = true; + + // Is this frame the main frame? + private boolean mIsMainFrame; + + // Attached Javascript interfaces + private HashMap mJSInterfaceMap; + + // message ids + // a message posted when a frame loading is completed + static final int FRAME_COMPLETED = 1001; + // a message posted when the user decides the policy + static final int POLICY_FUNCTION = 1003; + + // Note: need to keep these in sync with FrameLoaderTypes.h in native + static final int FRAME_LOADTYPE_STANDARD = 0; + static final int FRAME_LOADTYPE_BACK = 1; + static final int FRAME_LOADTYPE_FORWARD = 2; + static final int FRAME_LOADTYPE_INDEXEDBACKFORWARD = 3; + static final int FRAME_LOADTYPE_RELOAD = 4; + static final int FRAME_LOADTYPE_RELOADALLOWINGSTALEDATA = 5; + static final int FRAME_LOADTYPE_SAME = 6; + static final int FRAME_LOADTYPE_REDIRECT = 7; + static final int FRAME_LOADTYPE_REPLACE = 8; + + // A progress threshold to switch from history Picture to live Picture + private static final int TRANSITION_SWITCH_THRESHOLD = 75; + + // This is a field accessed by native code as well as package classes. + /*package*/ int mNativeFrame; + + // Static instance of a JWebCoreJavaBridge to handle timer and cookie + // requests from WebCore. + static JWebCoreJavaBridge sJavaBridge; + + /** + * Create a new BrowserFrame to be used in an application. + * @param context An application context to use when retrieving assets. + * @param w A WebViewCore used as the view for this frame. + * @param proxy A CallbackProxy for posting messages to the UI thread and + * querying a client for information. + * @param settings A WebSettings object that holds all settings. + * XXX: Called by WebCore thread. + */ + public BrowserFrame(Context context, WebViewCore w, CallbackProxy proxy, + WebSettings settings) { + // Create a global JWebCoreJavaBridge to handle timers and + // cookies in the WebCore thread. + if (sJavaBridge == null) { + sJavaBridge = new JWebCoreJavaBridge(); + // set WebCore native cache size + sJavaBridge.setCacheSize(4 * 1024 * 1024); + // initialize CacheManager + CacheManager.init(context); + // create CookieSyncManager with current Context + CookieSyncManager.createInstance(context); + } + AssetManager am = context.getAssets(); + nativeCreateFrame(w, am, proxy.getBackForwardList()); + + mSettings = settings; + mContext = context; + mCallbackProxy = proxy; + mDatabase = WebViewDatabase.getInstance(context); + mWebViewCore = w; + + if (Config.LOGV) { + Log.v(LOGTAG, "BrowserFrame constructor: this=" + this); + } + } + + /** + * Load a url from the network or the filesystem into the main frame. + * Following the same behaviour as Safari, javascript: URLs are not + * passed to the main frame, instead they are evaluated immediately. + * @param url The url to load. + */ + public void loadUrl(String url) { + mLoadInitFromJava = true; + if (URLUtil.isJavaScriptUrl(url)) { + // strip off the scheme and evaluate the string + stringByEvaluatingJavaScriptFromString( + url.substring("javascript:".length())); + } else { + nativeLoadUrl(url); + } + mLoadInitFromJava = false; + } + + /** + * Load the content as if it was loaded by the provided base URL. The + * failUrl is used as the history entry for the load data. If null or + * an empty string is passed for the failUrl, then no history entry is + * created. + * + * @param baseUrl Base URL used to resolve relative paths in the content + * @param data Content to render in the browser + * @param mimeType Mimetype of the data being passed in + * @param encoding Character set encoding of the provided data. + * @param failUrl URL to use if the content fails to load or null. + */ + public void loadData(String baseUrl, String data, String mimeType, + String encoding, String failUrl) { + mLoadInitFromJava = true; + if (failUrl == null) { + failUrl = ""; + } + if (data == null) { + data = ""; + } + + // Setup defaults for missing values. These defaults where taken from + // WebKit's WebFrame.mm + if (baseUrl == null || baseUrl.length() == 0) { + baseUrl = "about:blank"; + } + if (mimeType == null || mimeType.length() == 0) { + mimeType = "text/html"; + } + nativeLoadData(baseUrl, data, mimeType, encoding, failUrl); + mLoadInitFromJava = false; + } + + /** + * Go back or forward the number of steps given. + * @param steps A negative or positive number indicating the direction + * and number of steps to move. + */ + public void goBackOrForward(int steps) { + mLoadInitFromJava = true; + nativeGoBackOrForward(steps); + mLoadInitFromJava = false; + } + + /** + * native callback + * Report an error to an activity. + * @param errorCode The HTTP error code. + * @param description A String description. + * TODO: Report all errors including resource errors but include some kind + * of domain identifier. Change errorCode to an enum for a cleaner + * interface. + */ + private void reportError(final int errorCode, final String description, + final String failingUrl) { + // As this is called for the main resource and loading will be stopped + // after, reset the state variables. + mCommitted = true; + mWebViewCore.mEndScaleZoom = mFirstLayoutDone == false; + mFirstLayoutDone = true; + mCallbackProxy.onReceivedError(errorCode, description, failingUrl); + } + + /* package */boolean committed() { + return mCommitted; + } + + /* package */boolean firstLayoutDone() { + return mFirstLayoutDone; + } + + /* package */int loadType() { + return mLoadType; + } + + /* package */void didFirstLayout() { + if (!mFirstLayoutDone) { + mFirstLayoutDone = true; + // ensure {@link WebViewCore#webkitDraw} is called as we were + // blocking the update in {@link #loadStarted} + mWebViewCore.contentDraw(); + } + mWebViewCore.mEndScaleZoom = true; + } + + /** + * native callback + * Indicates the beginning of a new load. + * This method will be called once for the main frame. + */ + private void loadStarted(String url, Bitmap favicon, int loadType, + boolean isMainFrame) { + mIsMainFrame = isMainFrame; + + if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { + mLoadType = loadType; + + if (isMainFrame) { + // Call onPageStarted for main frames. + mCallbackProxy.onPageStarted(url, favicon); + // as didFirstLayout() is only called for the main frame, reset + // mFirstLayoutDone only for the main frames + mFirstLayoutDone = false; + mCommitted = false; + // remove pending draw to block update until mFirstLayoutDone is + // set to true in didFirstLayout() + mWebViewCore.removeMessages(WebViewCore.EventHub.WEBKIT_DRAW); + } + + // Note: only saves committed form data in standard load + if (loadType == FRAME_LOADTYPE_STANDARD + && mSettings.getSaveFormData()) { + final WebHistoryItem h = mCallbackProxy.getBackForwardList() + .getCurrentItem(); + if (h != null) { + String currentUrl = h.getUrl(); + if (currentUrl != null) { + mDatabase.setFormData(currentUrl, getFormTextData()); + } + } + } + } + } + + /** + * native callback + * Indicates the WebKit has committed to the new load + */ + private void transitionToCommitted(int loadType, boolean isMainFrame) { + // loadType is not used yet + if (isMainFrame) { + mCommitted = true; + } + } + + /** + * native callback + * <p> + * Indicates the end of a new load. + * This method will be called once for the main frame. + */ + private void loadFinished(String url, int loadType, boolean isMainFrame) { + // mIsMainFrame and isMainFrame are better be equal!!! + + if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { + if (isMainFrame) { + mCallbackProxy.switchOutDrawHistory(); + mCallbackProxy.onPageFinished(url); + } + } + } + + /** + * We have received an SSL certificate for the main top-level page. + * + * !!!Called from the network thread!!! + */ + void certificate(SslCertificate certificate) { + if (mIsMainFrame) { + // we want to make this call even if the certificate is null + // (ie, the site is not secure) + mCallbackProxy.onReceivedCertificate(certificate); + } + } + + /** + * Destroy all native components of the BrowserFrame. + */ + public void destroy() { + nativeDestroyFrame(); + removeCallbacksAndMessages(null); + } + + /** + * Handle messages posted to us. + * @param msg The message to handle. + */ + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case FRAME_COMPLETED: { + if (mSettings.getSavePassword() && hasPasswordField()) { + if (Config.DEBUG) { + Assert.assertNotNull(mCallbackProxy.getBackForwardList() + .getCurrentItem()); + } + WebAddress uri = new WebAddress( + mCallbackProxy.getBackForwardList().getCurrentItem() + .getUrl()); + String schemePlusHost = uri.mScheme + uri.mHost; + String[] up = mDatabase.getUsernamePassword(schemePlusHost); + if (up != null && up[0] != null) { + setUsernamePassword(up[0], up[1]); + } + } + CacheManager.trimCacheIfNeeded(); + break; + } + + case POLICY_FUNCTION: { + nativeCallPolicyFunction(msg.arg1, msg.arg2); + break; + } + + default: + break; + } + } + + /** + * Punch-through for WebCore to set the document + * title. Inform the Activity of the new title. + * @param title The new title of the document. + */ + private void setTitle(String title) { + // FIXME: The activity must call getTitle (a native method) to get the + // title. We should try and cache the title if we can also keep it in + // sync with the document. + mCallbackProxy.onReceivedTitle(title); + } + + /** + * Retrieves the render tree of this frame and puts it as the object for + * the message and sends the message. + * @param callback the message to use to send the render tree + */ + public void externalRepresentation(Message callback) { + callback.obj = externalRepresentation();; + callback.sendToTarget(); + } + + /** + * Return the render tree as a string + */ + private native String externalRepresentation(); + + /** + * Retrieves the visual text of the current frame, puts it as the object for + * the message and sends the message. + * @param callback the message to use to send the visual text + */ + public void documentAsText(Message callback) { + callback.obj = documentAsText();; + callback.sendToTarget(); + } + + /** + * Return the text drawn on the screen as a string + */ + private native String documentAsText(); + + /* + * This method is called by WebCore to inform the frame that + * the Javascript window object has been cleared. + * We should re-attach any attached js interfaces. + */ + private void windowObjectCleared(int nativeFramePointer) { + if (mJSInterfaceMap != null) { + Iterator iter = mJSInterfaceMap.keySet().iterator(); + while (iter.hasNext()) { + String interfaceName = (String) iter.next(); + nativeAddJavascriptInterface(nativeFramePointer, + mJSInterfaceMap.get(interfaceName), interfaceName); + } + } + } + + /** + * This method is called by WebCore to check whether application + * wants to hijack url loading + */ + public boolean handleUrl(String url) { + if (mLoadInitFromJava == true) { + return false; + } + if (mCallbackProxy.shouldOverrideUrlLoading(url)) { + // if the url is hijacked, reset the state of the BrowserFrame + didFirstLayout(); + return true; + } else { + return false; + } + } + + public void addJavascriptInterface(Object obj, String interfaceName) { + if (mJSInterfaceMap == null) { + mJSInterfaceMap = new HashMap<String, Object>(); + } + if (mJSInterfaceMap.containsKey(interfaceName)) { + mJSInterfaceMap.remove(interfaceName); + } + mJSInterfaceMap.put(interfaceName, obj); + } + + /** + * Start loading a resource. + * @param loaderHandle The native ResourceLoader that is the target of the + * data. + * @param url The url to load. + * @param method The http method. + * @param headers The http headers. + * @param postData If the method is "POST" postData is sent as the request + * body. Is null when empty. + * @param cacheMode The cache mode to use when loading this resource. + * @param isHighPriority True if this resource needs to be put at the front + * of the network queue. + * @param synchronous True if the load is synchronous. + * @return A newly created LoadListener object. + */ + private LoadListener startLoadingResource(int loaderHandle, + String url, + String method, + HashMap headers, + byte[] postData, + int cacheMode, + boolean isHighPriority, + boolean synchronous) { + PerfChecker checker = new PerfChecker(); + + if (mSettings.getCacheMode() != WebSettings.LOAD_DEFAULT) { + cacheMode = mSettings.getCacheMode(); + } + + if (method.equals("POST")) { + // Don't use the cache on POSTs when issuing a normal POST + // request. + if (cacheMode == WebSettings.LOAD_NORMAL) { + cacheMode = WebSettings.LOAD_NO_CACHE; + } + if (mSettings.getSavePassword() && hasPasswordField()) { + try { + if (Config.DEBUG) { + Assert.assertNotNull(mCallbackProxy.getBackForwardList() + .getCurrentItem()); + } + WebAddress uri = new WebAddress(mCallbackProxy + .getBackForwardList().getCurrentItem().getUrl()); + String schemePlusHost = uri.mScheme + uri.mHost; + String[] ret = getUsernamePassword(); + // Has the user entered a username/password pair and is + // there some POST data + if (ret != null && postData != null && + ret[0].length() > 0 && ret[1].length() > 0) { + // Check to see if the username & password appear in + // the post data (there could be another form on the + // page and that was posted instead. + String postString = new String(postData); + if (postString.contains(URLEncoder.encode(ret[0])) && + postString.contains(URLEncoder.encode(ret[1]))) { + String[] saved = mDatabase.getUsernamePassword( + schemePlusHost); + if (saved != null) { + // null username implies that user has chosen not to + // save password + if (saved[0] != null) { + // non-null username implies that user has + // chosen to save password, so update the + // recorded password + mDatabase.setUsernamePassword( + schemePlusHost, ret[0], ret[1]); + } + } else { + // CallbackProxy will handle creating the resume + // message + mCallbackProxy.onSavePassword(schemePlusHost, ret[0], + ret[1], null); + } + } + } + } catch (ParseException ex) { + // if it is bad uri, don't save its password + } + + } + } + + // is this resource the main-frame top-level page? + boolean isMainFramePage = mIsMainFrame; + + if (Config.LOGV) { + Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method=" + + method + ", postData=" + postData + ", isHighPriority=" + + isHighPriority + ", isMainFramePage=" + isMainFramePage); + } + + // Create a LoadListener + LoadListener loadListener = LoadListener.getLoadListener(mContext, this, url, + loaderHandle, synchronous, isMainFramePage); + + mCallbackProxy.onLoadResource(url); + + if (LoadListener.getNativeLoaderCount() > MAX_OUTSTANDING_REQUESTS) { + loadListener.error( + android.net.http.EventHandler.ERROR, mContext.getString( + com.android.internal.R.string.httpErrorTooManyRequests)); + loadListener.notifyError(); + loadListener.tearDown(); + return null; + } + + // during synchronous load, the WebViewCore thread is blocked, so we + // need to endCacheTransaction first so that http thread won't be + // blocked in setupFile() when createCacheFile. + if (synchronous) { + CacheManager.endCacheTransaction(); + } + + FrameLoader loader = new FrameLoader(loadListener, mSettings, + method, isHighPriority); + loader.setHeaders(headers); + loader.setPostData(postData); + loader.setCacheMode(cacheMode); // Set the load mode to the mode used + // for the current page. + // Set referrer to current URL? + if (!loader.executeLoad()) { + checker.responseAlert("startLoadingResource fail"); + } + checker.responseAlert("startLoadingResource succeed"); + + if (synchronous) { + CacheManager.startCacheTransaction(); + } + + return !synchronous ? loadListener : null; + } + + /** + * Set the progress for the browser activity. Called by native code. + * Uses a delay so it does not happen too often. + * @param newProgress An int between zero and one hundred representing + * the current progress percentage of loading the page. + */ + private void setProgress(int newProgress) { + mCallbackProxy.onProgressChanged(newProgress); + if (newProgress == 100) { + sendMessageDelayed(obtainMessage(FRAME_COMPLETED), 100); + } + // FIXME: Need to figure out a better way to switch out of the history + // drawing mode. Maybe we can somehow compare the history picture with + // the current picture, and switch when it contains more content. + if (mFirstLayoutDone && newProgress > TRANSITION_SWITCH_THRESHOLD) { + mCallbackProxy.switchOutDrawHistory(); + } + } + + /** + * Send the icon to the activity for display. + * @param icon A Bitmap representing a page's favicon. + */ + private void didReceiveIcon(Bitmap icon) { + mCallbackProxy.onReceivedIcon(icon); + } + + /** + * Request a new window from the client. + * @return The BrowserFrame object stored in the new WebView. + */ + private BrowserFrame createWindow(boolean dialog, boolean userGesture) { + WebView w = mCallbackProxy.createWindow(dialog, userGesture); + if (w != null) { + return w.getWebViewCore().getBrowserFrame(); + } + return null; + } + + /** + * Try to focus this WebView. + */ + private void requestFocus() { + mCallbackProxy.onRequestFocus(); + } + + /** + * Close this frame and window. + */ + private void closeWindow(WebViewCore w) { + mCallbackProxy.onCloseWindow(w.getWebView()); + } + + // XXX: Must match PolicyAction in FrameLoaderTypes.h in webcore + static final int POLICY_USE = 0; + static final int POLICY_IGNORE = 2; + + private void decidePolicyForFormResubmission(int policyFunction) { + Message dontResend = obtainMessage(POLICY_FUNCTION, policyFunction, + POLICY_IGNORE); + Message resend = obtainMessage(POLICY_FUNCTION, policyFunction, + POLICY_USE); + mCallbackProxy.onFormResubmission(dontResend, resend); + } + + /** + * Tell the activity to update its global history. + */ + private void updateVisitedHistory(String url, boolean isReload) { + mCallbackProxy.doUpdateVisitedHistory(url, isReload); + } + + /** + * Get the CallbackProxy for sending messages to the UI thread. + */ + /* package */ CallbackProxy getCallbackProxy() { + return mCallbackProxy; + } + + /** + * Returns the User Agent used by this frame + */ + String getUserAgentString() { + return mSettings.getUserAgentString(); + } + + // these ids need to be in sync with enum RAW_RES_ID in WebFrame + private static final int NODOMAIN = 1; + private static final int LOADERROR = 2; + + String getRawResFilename(int id) { + int resid; + switch (id) { + case NODOMAIN: + resid = com.android.internal.R.raw.nodomain; + break; + + case LOADERROR: + resid = com.android.internal.R.raw.loaderror; + break; + + default: + Log.e(LOGTAG, "getRawResFilename got incompatible resource ID"); + return new String(); + } + TypedValue value = new TypedValue(); + mContext.getResources().getValue(resid, value, true); + return value.string.toString(); + } + + //========================================================================== + // native functions + //========================================================================== + + /** + * Create a new native frame for a given WebView + * @param w A WebView that the frame draws into. + * @param am AssetManager to use to get assets. + * @param list The native side will add and remove items from this list as + * the native list changes. + */ + private native void nativeCreateFrame(WebViewCore w, AssetManager am, + WebBackForwardList list); + + /** + * Destroy the native frame. + */ + public native void nativeDestroyFrame(); + + private native void nativeCallPolicyFunction(int policyFunction, + int decision); + + /** + * Reload the current main frame. + */ + public native void reload(boolean allowStale); + + /** + * Go back or forward the number of steps given. + * @param steps A negative or positive number indicating the direction + * and number of steps to move. + */ + private native void nativeGoBackOrForward(int steps); + + /** + * stringByEvaluatingJavaScriptFromString will execute the + * JS passed in in the context of this browser frame. + * @param script A javascript string to execute + * + * @return string result of execution or null + */ + public native String stringByEvaluatingJavaScriptFromString(String script); + + /** + * Add a javascript interface to the main frame. + */ + private native void nativeAddJavascriptInterface(int nativeFramePointer, + Object obj, String interfaceName); + + /** + * Enable or disable the native cache. + */ + /* FIXME: The native cache is always on for now until we have a better + * solution for our 2 caches. */ + private native void setCacheDisabled(boolean disabled); + + public native boolean cacheDisabled(); + + public native void clearCache(); + + /** + * Returns false if the url is bad. + */ + private native void nativeLoadUrl(String url); + + private native void nativeLoadData(String baseUrl, String data, + String mimeType, String encoding, String failUrl); + + /** + * Stop loading the current page. + */ + public native void stopLoading(); + + /** + * Return true if the document has images. + */ + public native boolean documentHasImages(); + + /** + * @return TRUE if there is a password field in the current frame + */ + private native boolean hasPasswordField(); + + /** + * Get username and password in the current frame. If found, String[0] is + * username and String[1] is password. Otherwise return NULL. + * @return String[] + */ + private native String[] getUsernamePassword(); + + /** + * Set username and password to the proper fields in the current frame + * @param username + * @param password + */ + private native void setUsernamePassword(String username, String password); + + /** + * Get form's "text" type data associated with the current frame. + * @return HashMap If succeed, returns a list of name/value pair. Otherwise + * returns null. + */ + private native HashMap getFormTextData(); +} diff --git a/core/java/android/webkit/ByteArrayBuilder.java b/core/java/android/webkit/ByteArrayBuilder.java new file mode 100644 index 0000000..806b458 --- /dev/null +++ b/core/java/android/webkit/ByteArrayBuilder.java @@ -0,0 +1,142 @@ +/* + * 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 java.util.LinkedList; + +/** Utility class optimized for accumulating bytes, and then spitting + them back out. It does not optimize for returning the result in a + single array, though this is supported in the API. It is fastest + if the retrieval can be done via iterating through chunks. + + Things to add: + - consider dynamically increasing our min_capacity, + as we see mTotalSize increase +*/ +class ByteArrayBuilder { + + private static final int DEFAULT_CAPACITY = 8192; + + private LinkedList<Chunk> mChunks; + + /** free pool */ + private LinkedList<Chunk> mPool; + + private int mMinCapacity; + + public ByteArrayBuilder() { + init(0); + } + + public ByteArrayBuilder(int minCapacity) { + init(minCapacity); + } + + private void init(int minCapacity) { + mChunks = new LinkedList<Chunk>(); + mPool = new LinkedList<Chunk>(); + + if (minCapacity <= 0) { + minCapacity = DEFAULT_CAPACITY; + } + mMinCapacity = minCapacity; + } + + public void append(byte[] array) { + append(array, 0, array.length); + } + + public synchronized void append(byte[] array, int offset, int length) { + while (length > 0) { + Chunk c = appendChunk(length); + int amount = Math.min(length, c.mArray.length - c.mLength); + System.arraycopy(array, offset, c.mArray, c.mLength, amount); + c.mLength += amount; + length -= amount; + offset += amount; + } + } + + /** + * The fastest way to retrieve the data is to iterate through the + * chunks. This returns the first chunk. Note: this pulls the + * chunk out of the queue. The caller must call releaseChunk() to + * dispose of it. + */ + public synchronized Chunk getFirstChunk() { + if (mChunks.isEmpty()) return null; + return mChunks.removeFirst(); + } + + /** + * recycles chunk + */ + public synchronized void releaseChunk(Chunk c) { + c.mLength = 0; + mPool.addLast(c); + } + + public boolean isEmpty() { + return mChunks.isEmpty(); + } + + public synchronized void clear() { + Chunk c = getFirstChunk(); + while (c != null) { + releaseChunk(c); + c = getFirstChunk(); + } + } + + private Chunk appendChunk(int length) { + if (length < mMinCapacity) { + length = mMinCapacity; + } + + Chunk c; + if (mChunks.isEmpty()) { + c = obtainChunk(length); + } else { + c = mChunks.getLast(); + if (c.mLength == c.mArray.length) { + c = obtainChunk(length); + } + } + return c; + } + + private Chunk obtainChunk(int length) { + Chunk c; + if (mPool.isEmpty()) { + c = new Chunk(length); + } else { + c = mPool.removeFirst(); + } + mChunks.addLast(c); + return c; + } + + public static class Chunk { + public byte[] mArray; + public int mLength; + + public Chunk(int length) { + mArray = new byte[length]; + mLength = 0; + } + } +} diff --git a/core/java/android/webkit/CacheLoader.java b/core/java/android/webkit/CacheLoader.java new file mode 100644 index 0000000..3e1b602 --- /dev/null +++ b/core/java/android/webkit/CacheLoader.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2007 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.net.http.Headers; + +/** + * This class is a concrete implementation of StreamLoader that uses a + * CacheResult as the source for the stream. The CacheResult stored mimetype + * and encoding is added to the HTTP response headers. + */ +class CacheLoader extends StreamLoader { + + CacheManager.CacheResult mCacheResult; // Content source + + /** + * Constructs a CacheLoader for use when loading content from the cache. + * + * @param loadListener LoadListener to pass the content to + * @param result CacheResult used as the source for the content. + */ + CacheLoader(LoadListener loadListener, CacheManager.CacheResult result) { + super(loadListener); + mCacheResult = result; + } + + @Override + protected boolean setupStreamAndSendStatus() { + mDataStream = mCacheResult.inStream; + mContentLength = mCacheResult.contentLength; + mHandler.status(1, 1, mCacheResult.httpStatusCode, "OK"); + return true; + } + + @Override + protected void buildHeaders(Headers headers) { + StringBuilder sb = new StringBuilder(mCacheResult.mimeType); + if (mCacheResult.encoding != null && + mCacheResult.encoding.length() > 0) { + sb.append(';'); + sb.append(mCacheResult.encoding); + } + headers.setContentType(sb.toString()); + + if (mCacheResult.location != null && + mCacheResult.location.length() > 0) { + headers.setLocation(mCacheResult.location); + } + } + +} diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java new file mode 100644 index 0000000..d12940d --- /dev/null +++ b/core/java/android/webkit/CacheManager.java @@ -0,0 +1,703 @@ +/* + * 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.content.Context; +import android.net.http.Headers; +import android.os.FileUtils; +import android.util.Config; +import android.util.Log; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Map; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; + +/** + * The class CacheManager provides the persistent cache of content that is + * received over the network. The component handles parsing of HTTP headers and + * utilizes the relevant cache headers to determine if the content should be + * stored and if so, how long it is valid for. Network requests are provided to + * this component and if they can not be resolved by the cache, the HTTP headers + * are attached, as appropriate, to the request for revalidation of content. The + * class also manages the cache size. + */ +public final class CacheManager { + + private static final String LOGTAG = "cache"; + + static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since"; + static final String HEADER_KEY_IFNONEMATCH = "if-none-match"; + + private static final String NO_STORE = "no-store"; + private static final String NO_CACHE = "no-cache"; + private static final String MAX_AGE = "max-age"; + + private static long CACHE_THRESHOLD = 6 * 1024 * 1024; + private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024; + + private static boolean mDisabled; + + // Reference count the enable/disable transaction + private static int mRefCount; + + // trimCacheIfNeeded() is called when a page is fully loaded. But JavaScript + // can load the content, e.g. in a slideshow, continuously, so we need to + // trim the cache on a timer base too. endCacheTransaction() is called on a + // timer base. We share the same timer with less frequent update. + private static int mTrimCacheCount = 0; + private static final int TRIM_CACHE_INTERVAL = 5; + + private static WebViewDatabase mDataBase; + private static File mBaseDir; + + // Flag to clear the cache when the CacheManager is initialized + private static boolean mClearCacheOnInit = false; + + public static class CacheResult { + // these fields are saved to the database + int httpStatusCode; + long contentLength; + long expires; + String localPath; + String lastModified; + String etag; + String mimeType; + String location; + String encoding; + + // these fields are NOT saved to the database + InputStream inStream; + OutputStream outStream; + File outFile; + + public int getHttpStatusCode() { + return httpStatusCode; + } + + public long getContentLength() { + return contentLength; + } + + public String getLocalPath() { + return localPath; + } + + public long getExpires() { + return expires; + } + + public String getLastModified() { + return lastModified; + } + + public String getETag() { + return etag; + } + + public String getMimeType() { + return mimeType; + } + + public String getLocation() { + return location; + } + + public String getEncoding() { + return encoding; + } + + // For out-of-package access to the underlying streams. + public InputStream getInputStream() { + return inStream; + } + + public OutputStream getOutputStream() { + return outStream; + } + + // These fields can be set manually. + public void setInputStream(InputStream stream) { + this.inStream = stream; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + } + + /** + * initialize the CacheManager. WebView should handle this for each process. + * + * @param context The application context. + */ + static void init(Context context) { + mDataBase = WebViewDatabase.getInstance(context); + mBaseDir = new File(context.getCacheDir(), "webviewCache"); + if (createCacheDirectory() && mClearCacheOnInit) { + removeAllCacheFiles(); + mClearCacheOnInit = false; + } + } + + /** + * Create the cache directory if it does not already exist. + * + * @return true if the cache directory didn't exist and was created. + */ + static private boolean createCacheDirectory() { + if (!mBaseDir.exists()) { + if(!mBaseDir.mkdirs()) { + Log.w(LOGTAG, "Unable to create webviewCache directory"); + return false; + } + FileUtils.setPermissions( + mBaseDir.toString(), + FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH, + -1, -1); + // If we did create the directory, we need to flush + // the cache database. The directory could be recreated + // because the system flushed all the data/cache directories + // to free up disk space. + WebViewCore.endCacheTransaction(); + mDataBase.clearCache(); + WebViewCore.startCacheTransaction(); + return true; + } + return false; + } + + /** + * get the base directory of the cache. With localPath of the CacheResult, + * it identifies the cache file. + * + * @return File The base directory of the cache. + */ + public static File getCacheFileBaseDir() { + return mBaseDir; + } + + /** + * set the flag to control whether cache is enabled or disabled + * + * @param disabled true to disable the cache + */ + // only called from WebCore thread + static void setCacheDisabled(boolean disabled) { + if (disabled == mDisabled) { + return; + } + mDisabled = disabled; + if (mDisabled) { + removeAllCacheFiles(); + } + } + + /** + * get the state of the current cache, enabled or disabled + * + * @return return if it is disabled + */ + public static boolean cacheDisabled() { + return mDisabled; + } + + // only called from WebCore thread + // make sure to call enableTransaction/disableTransaction in pair + static boolean enableTransaction() { + if (++mRefCount == 1) { + mDataBase.startCacheTransaction(); + return true; + } + return false; + } + + // only called from WebCore thread + // make sure to call enableTransaction/disableTransaction in pair + static boolean disableTransaction() { + if (mRefCount == 0) { + Log.e(LOGTAG, "disableTransaction is out of sync"); + } + if (--mRefCount == 0) { + mDataBase.endCacheTransaction(); + return true; + } + return false; + } + + // only called from WebCore thread + // make sure to call startCacheTransaction/endCacheTransaction in pair + public static boolean startCacheTransaction() { + return mDataBase.startCacheTransaction(); + } + + // only called from WebCore thread + // make sure to call startCacheTransaction/endCacheTransaction in pair + public static boolean endCacheTransaction() { + boolean ret = mDataBase.endCacheTransaction(); + if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) { + mTrimCacheCount = 0; + trimCacheIfNeeded(); + } + return ret; + } + + /** + * Given a url, returns the CacheResult if exists. Otherwise returns null. + * If headers are provided and a cache needs validation, + * HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE will be set in the + * cached headers. + * + * @return the CacheResult for a given url + */ + // only called from WebCore thread + public static CacheResult getCacheFile(String url, + Map<String, String> headers) { + if (mDisabled) { + return null; + } + + CacheResult result = mDataBase.getCache(url); + if (result != null) { + if (result.contentLength == 0) { + if (result.httpStatusCode != 301 + && result.httpStatusCode != 302 + && result.httpStatusCode != 307) { + // this should not happen. If it does, remove it. + mDataBase.removeCache(url); + return null; + } + } else { + File src = new File(mBaseDir, result.localPath); + try { + // open here so that even the file is deleted, the content + // is still readable by the caller until close() is called + result.inStream = new FileInputStream(src); + } catch (FileNotFoundException e) { + // the files in the cache directory can be removed by the + // system. If it is gone, clean up the database + mDataBase.removeCache(url); + return null; + } + } + } else { + return null; + } + + // null headers request coming from CACHE_MODE_CACHE_ONLY + // which implies that it needs cache even it is expired. + // negative expires means time in the far future. + if (headers != null && result.expires >= 0 + && result.expires <= System.currentTimeMillis()) { + if (result.lastModified == null && result.etag == null) { + return null; + } + // return HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE + // for requesting validation + if (result.etag != null) { + headers.put(HEADER_KEY_IFNONEMATCH, result.etag); + } + if (result.lastModified != null) { + headers.put(HEADER_KEY_IFMODIFIEDSINCE, result.lastModified); + } + } + + if (Config.LOGV) { + Log.v(LOGTAG, "getCacheFile for url " + url); + } + + return result; + } + + /** + * Given a url and its full headers, returns CacheResult if a local cache + * can be stored. Otherwise returns null. The mimetype is passed in so that + * the function can use the mimetype that will be passed to WebCore which + * could be different from the mimetype defined in the headers. + * forceCache is for out-of-package callers to force creation of a + * CacheResult, and is used to supply surrogate responses for URL + * interception. + * @return CacheResult for a given url + * @hide - hide createCacheFile since it has a parameter of type headers, which is + * in a hidden package. + */ + // can be called from any thread + public static CacheResult createCacheFile(String url, int statusCode, + Headers headers, String mimeType, boolean forceCache) { + if (!forceCache && mDisabled) { + return null; + } + + CacheResult ret = parseHeaders(statusCode, headers, mimeType); + if (ret != null) { + setupFiles(url, ret); + try { + ret.outStream = new FileOutputStream(ret.outFile); + } catch (FileNotFoundException e) { + // This can happen with the system did a purge and our + // subdirectory has gone, so lets try to create it again + if (createCacheDirectory()) { + try { + ret.outStream = new FileOutputStream(ret.outFile); + } catch (FileNotFoundException e2) { + // We failed to create the file again, so there + // is something else wrong. Return null. + return null; + } + } else { + // Failed to create cache directory + return null; + } + } + ret.mimeType = mimeType; + } + + return ret; + } + + /** + * Save the info of a cache file for a given url to the CacheMap so that it + * can be reused later + */ + // only called from WebCore thread + public static void saveCacheFile(String url, CacheResult cacheRet) { + try { + cacheRet.outStream.close(); + } catch (IOException e) { + return; + } + + if (!cacheRet.outFile.exists()) { + // the file in the cache directory can be removed by the system + return; + } + + cacheRet.contentLength = cacheRet.outFile.length(); + if (cacheRet.httpStatusCode == 301 + || cacheRet.httpStatusCode == 302 + || cacheRet.httpStatusCode == 307) { + // location is in database, no need to keep the file + cacheRet.contentLength = 0; + cacheRet.localPath = new String(); + cacheRet.outFile.delete(); + } else if (cacheRet.contentLength == 0) { + cacheRet.outFile.delete(); + return; + } + + mDataBase.addCache(url, cacheRet); + + if (Config.LOGV) { + Log.v(LOGTAG, "saveCacheFile for url " + url); + } + } + + /** + * remove all cache files + * + * @return true if it succeeds + */ + // only called from WebCore thread + static boolean removeAllCacheFiles() { + // Note, this is called before init() when the database is + // created or upgraded. + if (mBaseDir == null) { + // Init() has not been called yet, so just flag that + // we need to clear the cache when init() is called. + mClearCacheOnInit = true; + return true; + } + // delete cache in a separate thread to not block UI. + final Runnable clearCache = new Runnable() { + public void run() { + // delete all cache files + try { + String[] files = mBaseDir.list(); + // if mBaseDir doesn't exist, files can be null. + if (files != null) { + for (int i = 0; i < files.length; i++) { + new File(mBaseDir, files[i]).delete(); + } + } + } catch (SecurityException e) { + // Ignore SecurityExceptions. + } + // delete database + mDataBase.clearCache(); + } + }; + new Thread(clearCache).start(); + return true; + } + + /** + * Return true if the cache is empty. + */ + // only called from WebCore thread + static boolean cacheEmpty() { + return mDataBase.hasCache(); + } + + // only called from WebCore thread + static void trimCacheIfNeeded() { + if (mDataBase.getCacheTotalSize() > CACHE_THRESHOLD) { + ArrayList<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT); + int size = pathList.size(); + for (int i = 0; i < size; i++) { + new File(mBaseDir, pathList.get(i)).delete(); + } + } + } + + @SuppressWarnings("deprecation") + private static void setupFiles(String url, CacheResult cacheRet) { + if (true) { + // Note: SHA1 is much stronger hash. But the cost of setupFiles() is + // 3.2% cpu time for a fresh load of nytimes.com. While a simple + // String.hashCode() is only 0.6%. If adding the collision resolving + // to String.hashCode(), it makes the cpu time to be 1.6% for a + // fresh load, but 5.3% for the worst case where all the files + // already exist in the file system, but database is gone. So it + // needs to resolve collision for every file at least once. + int hashCode = url.hashCode(); + StringBuffer ret = new StringBuffer(8); + appendAsHex(hashCode, ret); + String path = ret.toString(); + File file = new File(mBaseDir, path); + if (true) { + boolean checkOldPath = true; + // Check hash collision. If the hash file doesn't exist, just + // continue. There is a chance that the old cache file is not + // same as the hash file. As mDataBase.getCache() is more + // expansive than "leak" a file until clear cache, don't bother. + // If the hash file exists, make sure that it is same as the + // cache file. If it is not, resolve the collision. + while (file.exists()) { + if (checkOldPath) { + // as this is called from http thread through + // createCacheFile, we need endCacheTransaction before + // database access. + WebViewCore.endCacheTransaction(); + CacheResult oldResult = mDataBase.getCache(url); + WebViewCore.startCacheTransaction(); + if (oldResult != null && oldResult.contentLength > 0) { + if (path.equals(oldResult.localPath)) { + path = oldResult.localPath; + } else { + path = oldResult.localPath; + file = new File(mBaseDir, path); + } + break; + } + checkOldPath = false; + } + ret = new StringBuffer(8); + appendAsHex(++hashCode, ret); + path = ret.toString(); + file = new File(mBaseDir, path); + } + } + cacheRet.localPath = path; + cacheRet.outFile = file; + } else { + // get hash in byte[] + Digest digest = new SHA1Digest(); + int digestLen = digest.getDigestSize(); + byte[] hash = new byte[digestLen]; + int urlLen = url.length(); + byte[] data = new byte[urlLen]; + url.getBytes(0, urlLen, data, 0); + digest.update(data, 0, urlLen); + digest.doFinal(hash, 0); + // convert byte[] to hex String + StringBuffer result = new StringBuffer(2 * digestLen); + for (int i = 0; i < digestLen; i = i + 4) { + int h = (0x00ff & hash[i]) << 24 | (0x00ff & hash[i + 1]) << 16 + | (0x00ff & hash[i + 2]) << 8 | (0x00ff & hash[i + 3]); + appendAsHex(h, result); + } + cacheRet.localPath = result.toString(); + cacheRet.outFile = new File(mBaseDir, cacheRet.localPath); + } + } + + private static void appendAsHex(int i, StringBuffer ret) { + String hex = Integer.toHexString(i); + switch (hex.length()) { + case 1: + ret.append("0000000"); + break; + case 2: + ret.append("000000"); + break; + case 3: + ret.append("00000"); + break; + case 4: + ret.append("0000"); + break; + case 5: + ret.append("000"); + break; + case 6: + ret.append("00"); + break; + case 7: + ret.append("0"); + break; + } + ret.append(hex); + } + + private static CacheResult parseHeaders(int statusCode, Headers headers, + String mimeType) { + // TODO: if authenticated or secure, return null + CacheResult ret = new CacheResult(); + ret.httpStatusCode = statusCode; + + String location = headers.getLocation(); + if (location != null) ret.location = location; + + ret.expires = -1; + String expires = headers.getExpires(); + if (expires != null) { + try { + ret.expires = HttpDateTime.parse(expires); + } catch (IllegalArgumentException ex) { + // Take care of the special "-1" and "0" cases + if ("-1".equals(expires) || "0".equals(expires)) { + // make it expired, but can be used for history navigation + ret.expires = 0; + } else { + Log.e(LOGTAG, "illegal expires: " + expires); + } + } + } + + String lastModified = headers.getLastModified(); + if (lastModified != null) ret.lastModified = lastModified; + + String etag = headers.getEtag(); + if (etag != null) ret.etag = etag; + + String cacheControl = headers.getCacheControl(); + if (cacheControl != null) { + String[] controls = cacheControl.toLowerCase().split("[ ,;]"); + for (int i = 0; i < controls.length; i++) { + if (NO_STORE.equals(controls[i])) { + return null; + } + // According to the spec, 'no-cache' means that the content + // must be re-validated on every load. It does not mean that + // the content can not be cached. set to expire 0 means it + // can only be used in CACHE_MODE_CACHE_ONLY case + if (NO_CACHE.equals(controls[i])) { + ret.expires = 0; + } else if (controls[i].startsWith(MAX_AGE)) { + int separator = controls[i].indexOf('='); + if (separator < 0) { + separator = controls[i].indexOf(':'); + } + if (separator > 0) { + String s = controls[i].substring(separator + 1); + try { + long sec = Long.parseLong(s); + if (sec >= 0) { + ret.expires = System.currentTimeMillis() + 1000 + * sec; + } + } catch (NumberFormatException ex) { + if ("1d".equals(s)) { + // Take care of the special "1d" case + ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000 + } else { + Log.e(LOGTAG, "exception in parseHeaders for " + + "max-age:" + + controls[i].substring(separator + 1)); + ret.expires = 0; + } + } + } + } + } + } + + // According to RFC 2616 section 14.32: + // HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the + // client had sent "Cache-Control: no-cache" + if (NO_CACHE.equals(headers.getPragma())) { + ret.expires = 0; + } + + // According to RFC 2616 section 13.2.4, if an expiration has not been + // explicitly defined a heuristic to set an expiration may be used. + if (ret.expires == -1) { + if (ret.httpStatusCode == 301) { + // If it is a permanent redirect, and it did not have an + // explicit cache directive, then it never expires + ret.expires = Long.MAX_VALUE; + } else if (ret.httpStatusCode == 302 || ret.httpStatusCode == 307) { + // If it is temporary redirect, expires + ret.expires = 0; + } else if (ret.lastModified == null) { + // When we have no last-modified, then expire the content with + // in 24hrs as, according to the RFC, longer time requires a + // warning 113 to be added to the response. + + // Only add the default expiration for non-html markup. Some + // sites like news.google.com have no cache directives. + if (!mimeType.startsWith("text/html")) { + ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000 + } else { + // Setting a expires as zero will cache the result for + // forward/back nav. + ret.expires = 0; + } + } else { + // If we have a last-modified value, we could use it to set the + // expiration. Suggestion from RFC is 10% of time since + // last-modified. As we are on mobile, loads are expensive, + // increasing this to 20%. + + // 24 * 60 * 60 * 1000 + long lastmod = System.currentTimeMillis() + 86400000; + try { + lastmod = HttpDateTime.parse(ret.lastModified); + } catch (IllegalArgumentException ex) { + Log.e(LOGTAG, "illegal lastModified: " + ret.lastModified); + } + long difference = System.currentTimeMillis() - lastmod; + if (difference > 0) { + ret.expires = System.currentTimeMillis() + difference / 5; + } else { + // last modified is in the future, expire the content + // on the last modified + ret.expires = lastmod; + } + } + } + + return ret; + } +} diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java new file mode 100644 index 0000000..84aeb83 --- /dev/null +++ b/core/java/android/webkit/CallbackProxy.java @@ -0,0 +1,1020 @@ +/* + * Copyright (C) 2007 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.ActivityNotFoundException; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.util.Config; +import android.util.Log; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; +import com.android.internal.R; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; + +/** + * This class is a proxy class for handling WebCore -> UI thread messaging. All + * the callback functions are called from the WebCore thread and messages are + * posted to the UI thread for the actual client callback. + */ +/* + * This class is created in the UI thread so its handler and any private classes + * that extend Handler will operate in the UI thread. + */ +class CallbackProxy extends Handler { + // Logging tag + private static final String LOGTAG = "CallbackProxy"; + // Instance of WebViewClient that is the client callback. + private volatile WebViewClient mWebViewClient; + // Instance of WebChromeClient for handling all chrome functions. + private volatile WebChromeClient mWebChromeClient; + // Instance of WebView for handling UI requests. + private final WebView mWebView; + // Client registered callback listener for download events + private volatile DownloadListener mDownloadListener; + // Keep track of multiple progress updates. + private boolean mProgressUpdatePending; + // Keep track of the last progress amount. + private volatile int mLatestProgress; + // Back/Forward list + private final WebBackForwardList mBackForwardList; + // Used to call startActivity during url override. + private final Context mContext; + + // Message Ids + private static final int PAGE_STARTED = 100; + private static final int RECEIVED_ICON = 101; + private static final int RECEIVED_TITLE = 102; + private static final int OVERRIDE_URL = 103; + private static final int AUTH_REQUEST = 104; + private static final int SSL_ERROR = 105; + private static final int PROGRESS = 106; + private static final int UPDATE_VISITED = 107; + private static final int LOAD_RESOURCE = 108; + private static final int CREATE_WINDOW = 109; + private static final int CLOSE_WINDOW = 110; + private static final int SAVE_PASSWORD = 111; + private static final int JS_ALERT = 112; + private static final int JS_CONFIRM = 113; + private static final int JS_PROMPT = 114; + private static final int JS_UNLOAD = 115; + private static final int ASYNC_KEYEVENTS = 116; + private static final int TOO_MANY_REDIRECTS = 117; + private static final int DOWNLOAD_FILE = 118; + private static final int REPORT_ERROR = 119; + private static final int RESEND_POST_DATA = 120; + private static final int PAGE_FINISHED = 121; + private static final int REQUEST_FOCUS = 122; + private static final int SCALE_CHANGED = 123; + private static final int RECEIVED_CERTIFICATE = 124; + private static final int SWITCH_OUT_HISTORY = 125; + + // Message triggered by the client to resume execution + private static final int NOTIFY = 200; + + // Result transportation object for returning results across thread + // boundaries. + private class ResultTransport<E> { + // Private result object + private E mResult; + + public synchronized void setResult(E result) { + mResult = result; + } + + public synchronized E getResult() { + return mResult; + } + } + + /** + * Construct a new CallbackProxy. + */ + public CallbackProxy(Context context, WebView w) { + // Used to start a default activity. + mContext = context; + mWebView = w; + mBackForwardList = new WebBackForwardList(); + } + + /** + * Set the WebViewClient. + * @param client An implementation of WebViewClient. + */ + public void setWebViewClient(WebViewClient client) { + mWebViewClient = client; + } + + /** + * Set the WebChromeClient. + * @param client An implementation of WebChromeClient. + */ + public void setWebChromeClient(WebChromeClient client) { + mWebChromeClient = client; + } + + /** + * Set the client DownloadListener. + * @param client An implementation of DownloadListener. + */ + public void setDownloadListener(DownloadListener client) { + mDownloadListener = client; + } + + /** + * Get the Back/Forward list to return to the user or to update the cached + * history list. + */ + public WebBackForwardList getBackForwardList() { + return mBackForwardList; + } + + /** + * Called by the UI side. Calling overrideUrlLoading from the WebCore + * side will post a message to call this method. + */ + public boolean uiOverrideUrlLoading(String overrideUrl) { + if (overrideUrl == null || overrideUrl.length() == 0) { + return false; + } + boolean override = false; + if (mWebViewClient != null) { + override = mWebViewClient.shouldOverrideUrlLoading(mWebView, + overrideUrl); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW, + Uri.parse(overrideUrl)); + intent.addCategory(Intent.CATEGORY_BROWSABLE); + try { + mContext.startActivity(intent); + override = true; + } catch (ActivityNotFoundException ex) { + // If no application can handle the URL, assume that the + // browser can handle it. + } + } + return override; + } + + /** + * Called by UI side. + */ + public boolean uiOverrideKeyEvent(KeyEvent event) { + if (mWebViewClient != null) { + return mWebViewClient.shouldOverrideKeyEvent(mWebView, event); + } + return false; + } + + @Override + public void handleMessage(Message msg) { + // We don't have to do synchronization because this function operates + // in the UI thread. The WebViewClient and WebChromeClient functions + // that check for a non-null callback are ok because java ensures atomic + // 32-bit reads and writes. + switch (msg.what) { + case PAGE_STARTED: + if (mWebViewClient != null) { + mWebViewClient.onPageStarted(mWebView, + msg.getData().getString("url"), + (Bitmap) msg.obj); + } + break; + + case PAGE_FINISHED: + if (mWebViewClient != null) { + mWebViewClient.onPageFinished(mWebView, (String) msg.obj); + } + break; + + case RECEIVED_ICON: + if (mWebChromeClient != null) { + mWebChromeClient.onReceivedIcon(mWebView, (Bitmap) msg.obj); + } + break; + + case RECEIVED_TITLE: + if (mWebChromeClient != null) { + mWebChromeClient.onReceivedTitle(mWebView, + (String) msg.obj); + } + break; + + case TOO_MANY_REDIRECTS: + Message cancelMsg = + (Message) msg.getData().getParcelable("cancelMsg"); + Message continueMsg = + (Message) msg.getData().getParcelable("continueMsg"); + if (mWebViewClient != null) { + mWebViewClient.onTooManyRedirects(mWebView, cancelMsg, + continueMsg); + } else { + cancelMsg.sendToTarget(); + } + break; + + case REPORT_ERROR: + if (mWebViewClient != null) { + int reasonCode = msg.arg1; + final String description = msg.getData().getString("description"); + final String failUrl = msg.getData().getString("failingUrl"); + mWebViewClient.onReceivedError(mWebView, reasonCode, + description, failUrl); + } + break; + + case RESEND_POST_DATA: + Message resend = + (Message) msg.getData().getParcelable("resend"); + Message dontResend = + (Message) msg.getData().getParcelable("dontResend"); + if (mWebViewClient != null) { + mWebViewClient.onFormResubmission(mWebView, dontResend, + resend); + } else { + dontResend.sendToTarget(); + } + break; + + case OVERRIDE_URL: + String overrideUrl = msg.getData().getString("url"); + boolean override = uiOverrideUrlLoading(overrideUrl); + ResultTransport<Boolean> result = + (ResultTransport<Boolean>) msg.obj; + synchronized (this) { + result.setResult(override); + notify(); + } + break; + + case AUTH_REQUEST: + if (mWebViewClient != null) { + HttpAuthHandler handler = (HttpAuthHandler) msg.obj; + String host = msg.getData().getString("host"); + String realm = msg.getData().getString("realm"); + mWebViewClient.onReceivedHttpAuthRequest(mWebView, handler, + host, realm); + } + break; + + case SSL_ERROR: + if (mWebViewClient != null) { + HashMap<String, Object> map = + (HashMap<String, Object>) msg.obj; + mWebViewClient.onReceivedSslError(mWebView, + (SslErrorHandler) map.get("handler"), + (SslError) map.get("error")); + } + break; + + case PROGRESS: + // Synchronize to ensure mLatestProgress is not modified after + // setProgress is called and before mProgressUpdatePending is + // changed. + synchronized (this) { + if (mWebChromeClient != null) { + mWebChromeClient.onProgressChanged(mWebView, + mLatestProgress); + } + mProgressUpdatePending = false; + } + break; + + case UPDATE_VISITED: + if (mWebViewClient != null) { + mWebViewClient.doUpdateVisitedHistory(mWebView, + (String) msg.obj, msg.arg1 != 0); + } + break; + + case LOAD_RESOURCE: + if (mWebViewClient != null) { + mWebViewClient.onLoadResource(mWebView, (String) msg.obj); + } + break; + + case DOWNLOAD_FILE: + if (mDownloadListener != null) { + String url = msg.getData().getString("url"); + String userAgent = msg.getData().getString("userAgent"); + String contentDisposition = + msg.getData().getString("contentDisposition"); + String mimetype = msg.getData().getString("mimetype"); + Long contentLength = msg.getData().getLong("contentLength"); + + mDownloadListener.onDownloadStart(url, userAgent, + contentDisposition, mimetype, contentLength); + } + break; + + case CREATE_WINDOW: + if (mWebChromeClient != null) { + if (!mWebChromeClient.onCreateWindow(mWebView, + msg.arg1 == 1, msg.arg2 == 1, + (Message) msg.obj)) { + synchronized (this) { + notify(); + } + } + } + break; + + case REQUEST_FOCUS: + if (mWebChromeClient != null) { + mWebChromeClient.onRequestFocus(mWebView); + } + break; + + case CLOSE_WINDOW: + if (mWebChromeClient != null) { + mWebChromeClient.onCloseWindow((WebView) msg.obj); + } + break; + + case SAVE_PASSWORD: + Bundle bundle = msg.getData(); + String schemePlusHost = bundle.getString("host"); + String username = bundle.getString("username"); + String password = bundle.getString("password"); + // If the client returned false it means that the notify message + // will not be sent and we should notify WebCore ourselves. + if (!mWebView.onSavePassword(schemePlusHost, username, password, + (Message) msg.obj)) { + synchronized (this) { + notify(); + } + } + break; + + case ASYNC_KEYEVENTS: + if (mWebViewClient != null) { + mWebViewClient.onUnhandledKeyEvent(mWebView, + (KeyEvent) msg.obj); + } + break; + + case JS_ALERT: + if (mWebChromeClient != null) { + final JsResult res = (JsResult) msg.obj; + String message = msg.getData().getString("message"); + String url = msg.getData().getString("url"); + if (!mWebChromeClient.onJsAlert(mWebView, url, message, + res)) { + new AlertDialog.Builder(mContext) + .setTitle(getJsDialogTitle(url)) + .setMessage(message) + .setPositiveButton(R.string.ok, + new AlertDialog.OnClickListener() { + public void onClick( + DialogInterface dialog, + int which) { + res.confirm(); + } + }) + .setCancelable(false) + .show(); + } + res.setReady(); + } + break; + + case JS_CONFIRM: + if (mWebChromeClient != null) { + final JsResult res = (JsResult) msg.obj; + String message = msg.getData().getString("message"); + String url = msg.getData().getString("url"); + if (!mWebChromeClient.onJsConfirm(mWebView, url, message, + res)) { + new AlertDialog.Builder(mContext) + .setTitle(getJsDialogTitle(url)) + .setMessage(message) + .setPositiveButton(R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick( + DialogInterface dialog, + int which) { + res.confirm(); + }}) + .setNegativeButton(R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick( + DialogInterface dialog, + int which) { + res.cancel(); + }}) + .show(); + } + // Tell the JsResult that it is ready for client + // interaction. + res.setReady(); + } + break; + + case JS_PROMPT: + if (mWebChromeClient != null) { + final JsPromptResult res = (JsPromptResult) msg.obj; + String message = msg.getData().getString("message"); + String defaultVal = msg.getData().getString("default"); + String url = msg.getData().getString("url"); + if (!mWebChromeClient.onJsPrompt(mWebView, url, message, + defaultVal, res)) { + final LayoutInflater factory = LayoutInflater + .from(mContext); + final View view = factory.inflate(R.layout.js_prompt, + null); + final EditText v = (EditText) view + .findViewById(R.id.value); + v.setText(defaultVal); + ((TextView) view.findViewById(R.id.message)) + .setText(message); + new AlertDialog.Builder(mContext) + .setTitle(getJsDialogTitle(url)) + .setView(view) + .setPositiveButton(R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick( + DialogInterface dialog, + int whichButton) { + res.confirm(v.getText() + .toString()); + } + }) + .setNegativeButton(R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick( + DialogInterface dialog, + int whichButton) { + res.cancel(); + } + }) + .setOnCancelListener( + new DialogInterface.OnCancelListener() { + public void onCancel( + DialogInterface dialog) { + res.cancel(); + } + }) + .show(); + } + // Tell the JsResult that it is ready for client + // interaction. + res.setReady(); + } + break; + + case JS_UNLOAD: + if (mWebChromeClient != null) { + final JsResult res = (JsResult) msg.obj; + String message = msg.getData().getString("message"); + String url = msg.getData().getString("url"); + if (!mWebChromeClient.onJsBeforeUnload(mWebView, url, + message, res)) { + final String m = mContext.getString( + R.string.js_dialog_before_unload, message); + new AlertDialog.Builder(mContext) + .setMessage(m) + .setPositiveButton(R.string.ok, + new DialogInterface.OnClickListener() { + public void onClick( + DialogInterface dialog, + int which) { + res.confirm(); + } + }) + .setNegativeButton(R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick( + DialogInterface dialog, + int which) { + res.cancel(); + } + }) + .show(); + } + res.setReady(); + } + break; + + case RECEIVED_CERTIFICATE: + mWebView.setCertificate((SslCertificate) msg.obj); + break; + + case NOTIFY: + synchronized (this) { + notify(); + } + break; + + case SCALE_CHANGED: + if (mWebViewClient != null) { + mWebViewClient.onScaleChanged(mWebView, msg.getData() + .getFloat("old"), msg.getData().getFloat("new")); + } + break; + + case SWITCH_OUT_HISTORY: + mWebView.switchOutDrawHistory(); + break; + } + } + + /** + * Return the latest progress. + */ + public int getProgress() { + return mLatestProgress; + } + + /** + * Called by WebCore side to switch out of history Picture drawing mode + */ + void switchOutDrawHistory() { + sendMessage(obtainMessage(SWITCH_OUT_HISTORY)); + } + + private String getJsDialogTitle(String url) { + String title = url; + if (URLUtil.isDataUrl(url)) { + // For data: urls, we just display 'JavaScript' similar to Safari. + title = mContext.getString(R.string.js_dialog_title_default); + } else { + try { + URL aUrl = new URL(url); + // For example: "The page at 'http://www.mit.edu' says:" + title = mContext.getString(R.string.js_dialog_title, + aUrl.getProtocol() + "://" + aUrl.getHost()); + } catch (MalformedURLException ex) { + // do nothing. just use the url as the title + } + } + return title; + } + + //-------------------------------------------------------------------------- + // WebViewClient functions. + // NOTE: shouldOverrideKeyEvent is never called from the WebCore thread so + // it is not necessary to include it here. + //-------------------------------------------------------------------------- + + // Performance probe + private long mWebCoreThreadTime; + + public void onPageStarted(String url, Bitmap favicon) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + // Performance probe + if (false) { + mWebCoreThreadTime = SystemClock.currentThreadTimeMillis(); + Network.getInstance(mContext).startTiming(); + } + Message msg = obtainMessage(PAGE_STARTED); + msg.obj = favicon; + msg.getData().putString("url", url); + sendMessage(msg); + } + + public void onPageFinished(String url) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + // Performance probe + if (false) { + Log.d("WebCore", "WebCore thread used " + + (SystemClock.currentThreadTimeMillis() - mWebCoreThreadTime) + + " ms"); + Network.getInstance(mContext).stopTiming(); + } + Message msg = obtainMessage(PAGE_FINISHED, url); + sendMessage(msg); + } + + public void onTooManyRedirects(Message cancelMsg, Message continueMsg) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + cancelMsg.sendToTarget(); + return; + } + + Message msg = obtainMessage(TOO_MANY_REDIRECTS); + Bundle bundle = msg.getData(); + bundle.putParcelable("cancelMsg", cancelMsg); + bundle.putParcelable("continueMsg", continueMsg); + sendMessage(msg); + } + + public void onReceivedError(int errorCode, String description, + String failingUrl) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + + Message msg = obtainMessage(REPORT_ERROR); + msg.arg1 = errorCode; + msg.getData().putString("description", description); + msg.getData().putString("failingUrl", failingUrl); + sendMessage(msg); + } + + public void onFormResubmission(Message dontResend, + Message resend) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + dontResend.sendToTarget(); + return; + } + + Message msg = obtainMessage(RESEND_POST_DATA); + Bundle bundle = msg.getData(); + bundle.putParcelable("resend", resend); + bundle.putParcelable("dontResend", dontResend); + sendMessage(msg); + } + + /** + * Called by the WebCore side + */ + public boolean shouldOverrideUrlLoading(String url) { + // We have a default behavior if no client exists so always send the + // message. + ResultTransport<Boolean> res = new ResultTransport<Boolean>(); + Message msg = obtainMessage(OVERRIDE_URL); + msg.getData().putString("url", url); + msg.obj = res; + synchronized (this) { + sendMessage(msg); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while waiting for overrideUrl"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + return res.getResult().booleanValue(); + } + + public void onReceivedHttpAuthRequest(HttpAuthHandler handler, + String hostName, String realmName) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + handler.cancel(); + return; + } + Message msg = obtainMessage(AUTH_REQUEST, handler); + msg.getData().putString("host", hostName); + msg.getData().putString("realm", realmName); + sendMessage(msg); + } + /** + * @hide - hide this because it contains a parameter of type SslError. + * SslError is located in a hidden package. + */ + public void onReceivedSslError(SslErrorHandler handler, SslError error) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + handler.cancel(); + return; + } + Message msg = obtainMessage(SSL_ERROR); + //, handler); + HashMap<String, Object> map = new HashMap(); + map.put("handler", handler); + map.put("error", error); + msg.obj = map; + sendMessage(msg); + } + /** + * @hide - hide this because it contains a parameter of type SslCertificate, + * which is located in a hidden package. + */ + + public void onReceivedCertificate(SslCertificate certificate) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + // here, certificate can be null (if the site is not secure) + sendMessage(obtainMessage(RECEIVED_CERTIFICATE, certificate)); + } + + public void doUpdateVisitedHistory(String url, boolean isReload) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + sendMessage(obtainMessage(UPDATE_VISITED, isReload ? 1 : 0, 0, url)); + } + + public void onLoadResource(String url) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + sendMessage(obtainMessage(LOAD_RESOURCE, url)); + } + + public void onUnhandledKeyEvent(KeyEvent event) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + sendMessage(obtainMessage(ASYNC_KEYEVENTS, event)); + } + + public void onScaleChanged(float oldScale, float newScale) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebViewClient == null) { + return; + } + Message msg = obtainMessage(SCALE_CHANGED); + Bundle bundle = msg.getData(); + bundle.putFloat("old", oldScale); + bundle.putFloat("new", newScale); + sendMessage(msg); + } + + //-------------------------------------------------------------------------- + // DownloadListener functions. + //-------------------------------------------------------------------------- + + /** + * Starts a download if a download listener has been registered, otherwise + * return false. + */ + public boolean onDownloadStart(String url, String userAgent, + String contentDisposition, String mimetype, long contentLength) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mDownloadListener == null) { + // Cancel the download if there is no browser client. + return false; + } + + Message msg = obtainMessage(DOWNLOAD_FILE); + Bundle bundle = msg.getData(); + bundle.putString("url", url); + bundle.putString("userAgent", userAgent); + bundle.putString("mimetype", mimetype); + bundle.putLong("contentLength", contentLength); + bundle.putString("contentDisposition", contentDisposition); + sendMessage(msg); + return true; + } + + + //-------------------------------------------------------------------------- + // WebView specific functions that do not interact with a client. These + // functions just need to operate within the UI thread. + //-------------------------------------------------------------------------- + + public boolean onSavePassword(String schemePlusHost, String username, + String password, Message resumeMsg) { + // resumeMsg should be null at this point because we want to create it + // within the CallbackProxy. + if (Config.DEBUG) { + junit.framework.Assert.assertNull(resumeMsg); + } + resumeMsg = obtainMessage(NOTIFY); + + Message msg = obtainMessage(SAVE_PASSWORD, resumeMsg); + Bundle bundle = msg.getData(); + bundle.putString("host", schemePlusHost); + bundle.putString("username", username); + bundle.putString("password", password); + synchronized (this) { + sendMessage(msg); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, + "Caught exception while waiting for onSavePassword"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + // Doesn't matter here + return false; + } + + //-------------------------------------------------------------------------- + // WebChromeClient methods + //-------------------------------------------------------------------------- + + public void onProgressChanged(int newProgress) { + // Synchronize so that mLatestProgress is up-to-date. + synchronized (this) { + mLatestProgress = newProgress; + if (mWebChromeClient == null) { + return; + } + if (!mProgressUpdatePending) { + sendEmptyMessage(PROGRESS); + mProgressUpdatePending = true; + } + } + } + + public WebView createWindow(boolean dialog, boolean userGesture) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return null; + } + + WebView.WebViewTransport transport = mWebView.new WebViewTransport(); + final Message msg = obtainMessage(NOTIFY); + msg.obj = transport; + synchronized (this) { + sendMessage(obtainMessage(CREATE_WINDOW, dialog ? 1 : 0, + userGesture ? 1 : 0, msg)); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, + "Caught exception while waiting for createWindow"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + + WebView w = transport.getWebView(); + if (w != null) { + w.getWebViewCore().initializeSubwindow(); + } + return w; + } + + public void onRequestFocus() { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return; + } + + sendEmptyMessage(REQUEST_FOCUS); + } + + public void onCloseWindow(WebView window) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return; + } + sendMessage(obtainMessage(CLOSE_WINDOW, window)); + } + + public void onReceivedIcon(Bitmap icon) { + // The current item might be null if the icon was already stored in the + // database and this is a new WebView. + WebHistoryItem i = mBackForwardList.getCurrentItem(); + if (i != null) { + i.setFavicon(icon); + } + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return; + } + sendMessage(obtainMessage(RECEIVED_ICON, icon)); + } + + public void onReceivedTitle(String title) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return; + } + sendMessage(obtainMessage(RECEIVED_TITLE, title)); + } + + public void onJsAlert(String url, String message) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return; + } + JsResult result = new JsResult(this, false); + Message alert = obtainMessage(JS_ALERT, result); + alert.getData().putString("message", message); + alert.getData().putString("url", url); + synchronized (this) { + sendMessage(alert); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while waiting for jsAlert"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + } + + public boolean onJsConfirm(String url, String message) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return false; + } + JsResult result = new JsResult(this, false); + Message confirm = obtainMessage(JS_CONFIRM, result); + confirm.getData().putString("message", message); + confirm.getData().putString("url", url); + synchronized (this) { + sendMessage(confirm); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while waiting for jsConfirm"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + return result.getResult(); + } + + public String onJsPrompt(String url, String message, String defaultValue) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return null; + } + JsPromptResult result = new JsPromptResult(this); + Message prompt = obtainMessage(JS_PROMPT, result); + prompt.getData().putString("message", message); + prompt.getData().putString("default", defaultValue); + prompt.getData().putString("url", url); + synchronized (this) { + sendMessage(prompt); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while waiting for jsPrompt"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + return result.getStringResult(); + } + + public boolean onJsBeforeUnload(String url, String message) { + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return true; + } + JsResult result = new JsResult(this, true); + Message confirm = obtainMessage(JS_UNLOAD, result); + confirm.getData().putString("message", message); + confirm.getData().putString("url", url); + synchronized (this) { + sendMessage(confirm); + try { + wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while waiting for jsUnload"); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + return result.getResult(); + } +} diff --git a/core/java/android/webkit/ContentLoader.java b/core/java/android/webkit/ContentLoader.java new file mode 100644 index 0000000..fb01c8c --- /dev/null +++ b/core/java/android/webkit/ContentLoader.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2008 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.content.Context; +import android.net.http.EventHandler; +import android.net.http.Headers; +import android.net.Uri; + +import java.io.File; +import java.io.FileInputStream; + +/** + * This class is a concrete implementation of StreamLoader that loads + * "content:" URIs + */ +class ContentLoader extends StreamLoader { + + private String mUrl; + private Context mContext; + private String mContentType; + + /** + * Construct a ContentLoader with the specified content URI + * + * @param rawUrl "content:" url pointing to content to be loaded. This url + * is the same url passed in to the WebView. + * @param loadListener LoadListener to pass the content to + * @param context Context to use to access the asset. + */ + ContentLoader(String rawUrl, LoadListener loadListener, Context context) { + super(loadListener); + mContext = context; + + /* strip off mimetype */ + int mimeIndex = rawUrl.lastIndexOf('?'); + if (mimeIndex != -1) { + mUrl = rawUrl.substring(0, mimeIndex); + mContentType = rawUrl.substring(mimeIndex + 1); + } else { + mUrl = rawUrl; + } + + } + + @Override + protected boolean setupStreamAndSendStatus() { + Uri uri = Uri.parse(mUrl); + if (uri == null) { + mHandler.error( + EventHandler.FILE_NOT_FOUND_ERROR, + mContext.getString( + com.android.internal.R.string.httpErrorBadUrl) + + " " + mUrl); + return false; + } + + try { + mDataStream = mContext.getContentResolver().openInputStream(uri); + mHandler.status(1, 1, 0, "OK"); + } catch (java.io.FileNotFoundException ex) { + mHandler.error( + EventHandler.FILE_NOT_FOUND_ERROR, + mContext.getString( + com.android.internal.R.string.httpErrorFileNotFound) + + " " + ex.getMessage()); + return false; + + } catch (java.io.IOException ex) { + mHandler.error( + EventHandler.FILE_ERROR, + mContext.getString( + com.android.internal.R.string.httpErrorFileNotFound) + + " " + ex.getMessage()); + return false; + } catch (RuntimeException ex) { + // readExceptionWithFileNotFoundExceptionFromParcel in DatabaseUtils + // can throw a serial of RuntimeException. Catch them all here. + mHandler.error( + EventHandler.FILE_ERROR, + mContext.getString( + com.android.internal.R.string.httpErrorFileNotFound) + + " " + ex.getMessage()); + return false; + } + return true; + } + + @Override + protected void buildHeaders(Headers headers) { + if (mContentType != null) { + headers.setContentType("text/html"); + } + } + + /** + * Construct a ContentLoader and instruct it to start loading. + * + * @param url "content:" url pointing to content to be loaded + * @param loadListener LoadListener to pass the content to + * @param context Context to use to access the asset. + */ + public static void requestUrl(String url, LoadListener loadListener, + Context context) { + ContentLoader loader = new ContentLoader(url, loadListener, context); + loader.load(); + } + +} diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java new file mode 100644 index 0000000..07c1a5d --- /dev/null +++ b/core/java/android/webkit/CookieManager.java @@ -0,0 +1,934 @@ +/* + * 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.net.ParseException; +import android.net.WebAddress; +import android.util.Config; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * CookieManager manages cookies according to RFC2109 spec. + */ +public final class CookieManager { + + private static CookieManager sRef; + + private static final String LOGTAG = "webkit"; + + private static final String DOMAIN = "domain"; + + private static final String PATH = "path"; + + private static final String EXPIRES = "expires"; + + private static final String SECURE = "secure"; + + private static final String MAX_AGE = "max-age"; + + private static final String HTTP_ONLY = "httponly"; + + private static final String HTTPS = "https"; + + private static final char PERIOD = '.'; + + private static final char COMMA = ','; + + private static final char SEMICOLON = ';'; + + private static final char EQUAL = '='; + + private static final char PATH_DELIM = '/'; + + private static final char QUESTION_MARK = '?'; + + private static final char WHITE_SPACE = ' '; + + private static final char QUOTATION = '\"'; + + private static final int SECURE_LENGTH = SECURE.length(); + + private static final int HTTP_ONLY_LENGTH = HTTP_ONLY.length(); + + // RFC2109 defines 4k as maximum size of a cookie + private static final int MAX_COOKIE_LENGTH = 4 * 1024; + + // RFC2109 defines 20 as max cookie count per domain. As we track with base + // domain, we allow 50 per base domain + private static final int MAX_COOKIE_COUNT_PER_BASE_DOMAIN = 50; + + // RFC2109 defines 300 as max count of domains. As we track with base + // domain, we set 200 as max base domain count + private static final int MAX_DOMAIN_COUNT = 200; + + // max cookie count to limit RAM cookie takes less than 100k, it is based on + // average cookie entry size is less than 100 bytes + private static final int MAX_RAM_COOKIES_COUNT = 1000; + + // max domain count to limit RAM cookie takes less than 100k, + private static final int MAX_RAM_DOMAIN_COUNT = 15; + + private Map<String, ArrayList<Cookie>> mCookieMap = new LinkedHashMap + <String, ArrayList<Cookie>>(MAX_DOMAIN_COUNT, 0.75f, true); + + private boolean mAcceptCookie = true; + + /** + * This contains a list of 2nd-level domains that aren't allowed to have + * wildcards when combined with country-codes. For example: [.co.uk]. + */ + private final static String[] BAD_COUNTRY_2LDS = + { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", + "lg", "ne", "net", "or", "org" }; + + static { + Arrays.sort(BAD_COUNTRY_2LDS); + } + + /** + * Package level class to be accessed by cookie sync manager + */ + static class Cookie { + static final byte MODE_NEW = 0; + + static final byte MODE_NORMAL = 1; + + static final byte MODE_DELETED = 2; + + static final byte MODE_REPLACED = 3; + + String domain; + + String path; + + String name; + + String value; + + long expires; + + long lastAcessTime; + + long lastUpdateTime; + + boolean secure; + + byte mode; + + Cookie() { + } + + Cookie(String defaultDomain, String defaultPath) { + domain = defaultDomain; + path = defaultPath; + expires = -1; + } + + boolean exactMatch(Cookie in) { + return domain.equals(in.domain) && path.equals(in.path) && + name.equals(in.name); + } + + boolean domainMatch(String urlHost) { + if (domain.startsWith(".")) { + if (urlHost.endsWith(domain.substring(1))) { + int len = domain.length(); + int urlLen = urlHost.length(); + if (urlLen > len - 1) { + // make sure bar.com doesn't match .ar.com + return urlHost.charAt(urlLen - len) == PERIOD; + } + return true; + } + return false; + } else { + // exact match if domain is not leading w/ dot + return urlHost.equals(domain); + } + } + + boolean pathMatch(String urlPath) { + if (urlPath.startsWith(path)) { + int len = path.length(); + if (len == 0) { + Log.w(LOGTAG, "Empty cookie path"); + return false; + } + int urlLen = urlPath.length(); + if (path.charAt(len-1) != PATH_DELIM && urlLen > len) { + // make sure /wee doesn't match /we + return urlPath.charAt(len) == PATH_DELIM; + } + return true; + } + return false; + } + + public String toString() { + return "domain: " + domain + "; path: " + path + "; name: " + name + + "; value: " + value; + } + } + + private CookieManager() { + } + + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException("doesn't implement Cloneable"); + } + + /** + * Get a singleton CookieManager. If this is called before any + * {@link WebView} is created or outside of {@link WebView} context, the + * caller needs to call {@link CookieSyncManager#createInstance(Context)} + * first. + * + * @return CookieManager += */ + public static synchronized CookieManager getInstance() { + if (sRef == null) { + sRef = new CookieManager(); + } + return sRef; + } + + /** + * Control whether cookie is enabled or disabled + * @param accept TRUE if accept cookie + */ + public synchronized void setAcceptCookie(boolean accept) { + mAcceptCookie = accept; + } + + /** + * Return whether cookie is enabled + * @return TRUE if accept cookie + */ + public synchronized boolean acceptCookie() { + return mAcceptCookie; + } + + /** + * Set cookie for a given url. The old cookie with same host/path/name will + * be removed. The new cookie will be added if it is not expired or it does + * not have expiration which implies it is session cookie. + * @param url The url which cookie is set for + * @param value The value for set-cookie: in http response header + */ + public void setCookie(String url, String value) { + WebAddress uri; + try { + uri = new WebAddress(url); + } catch (ParseException ex) { + Log.e(LOGTAG, "Bad address: " + url); + return; + } + setCookie(uri, value); + } + + /** + * Set cookie for a given uri. The old cookie with same host/path/name will + * be removed. The new cookie will be added if it is not expired or it does + * not have expiration which implies it is session cookie. + * @param uri The uri which cookie is set for + * @param value The value for set-cookie: in http response header + * @hide - hide this because it takes in a parameter of type WebAddress, + * a system private class. + */ + public synchronized void setCookie(WebAddress uri, String value) { + if (value != null && value.length() > MAX_COOKIE_LENGTH) { + return; + } + if (!mAcceptCookie || uri == null) { + return; + } + if (Config.LOGV) { + Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value); + } + + String[] hostAndPath = getHostAndPath(uri); + if (hostAndPath == null) { + return; + } + + // For default path, when setting a cookie, the spec says: + //Path: Defaults to the path of the request URL that generated the + // Set-Cookie response, up to, but not including, the + // right-most /. + if (hostAndPath[1].length() > 1) { + int index = hostAndPath[1].lastIndexOf(PATH_DELIM); + hostAndPath[1] = hostAndPath[1].substring(0, + index > 0 ? index : index + 1); + } + + ArrayList<Cookie> cookies = null; + try { + cookies = parseCookie(hostAndPath[0], hostAndPath[1], value); + } catch (RuntimeException ex) { + Log.e(LOGTAG, "parse cookie failed for: " + value); + } + + if (cookies == null || cookies.size() == 0) { + return; + } + + String baseDomain = getBaseDomain(hostAndPath[0]); + ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain); + if (cookieList == null) { + cookieList = CookieSyncManager.getInstance() + .getCookiesForDomain(baseDomain); + mCookieMap.put(baseDomain, cookieList); + } + + long now = System.currentTimeMillis(); + int size = cookies.size(); + for (int i = 0; i < size; i++) { + Cookie cookie = cookies.get(i); + + boolean done = false; + Iterator<Cookie> iter = cookieList.iterator(); + while (iter.hasNext()) { + Cookie cookieEntry = iter.next(); + if (cookie.exactMatch(cookieEntry)) { + // expires == -1 means no expires defined. Otherwise + // negative means far future + if (cookie.expires < 0 || cookie.expires > now) { + // secure cookies can't be overwritten by non-HTTPS url + if (!cookieEntry.secure || HTTPS.equals(uri.mScheme)) { + cookieEntry.value = cookie.value; + cookieEntry.expires = cookie.expires; + cookieEntry.secure = cookie.secure; + cookieEntry.lastAcessTime = now; + cookieEntry.lastUpdateTime = now; + cookieEntry.mode = Cookie.MODE_REPLACED; + } + } else { + cookieEntry.lastUpdateTime = now; + cookieEntry.mode = Cookie.MODE_DELETED; + } + done = true; + break; + } + } + + // expires == -1 means no expires defined. Otherwise negative means + // far future + if (!done && (cookie.expires < 0 || cookie.expires > now)) { + cookie.lastAcessTime = now; + cookie.lastUpdateTime = now; + cookie.mode = Cookie.MODE_NEW; + if (cookieList.size() > MAX_COOKIE_COUNT_PER_BASE_DOMAIN) { + Cookie toDelete = new Cookie(); + toDelete.lastAcessTime = now; + Iterator<Cookie> iter2 = cookieList.iterator(); + while (iter2.hasNext()) { + Cookie cookieEntry2 = iter2.next(); + if ((cookieEntry2.lastAcessTime < toDelete.lastAcessTime) + && cookieEntry2.mode != Cookie.MODE_DELETED) { + toDelete = cookieEntry2; + } + } + toDelete.mode = Cookie.MODE_DELETED; + } + cookieList.add(cookie); + } + } + } + + /** + * Get cookie(s) for a given url so that it can be set to "cookie:" in http + * request header. + * @param url The url needs cookie + * @return The cookies in the format of NAME=VALUE [; NAME=VALUE] + */ + public String getCookie(String url) { + WebAddress uri; + try { + uri = new WebAddress(url); + } catch (ParseException ex) { + Log.e(LOGTAG, "Bad address: " + url); + return null; + } + return getCookie(uri); + } + + /** + * Get cookie(s) for a given uri so that it can be set to "cookie:" in http + * request header. + * @param uri The uri needs cookie + * @return The cookies in the format of NAME=VALUE [; NAME=VALUE] + * @hide - hide this because it has a parameter of type WebAddress, which + * is a system private class. + */ + public synchronized String getCookie(WebAddress uri) { + if (!mAcceptCookie || uri == null) { + return null; + } + + String[] hostAndPath = getHostAndPath(uri); + if (hostAndPath == null) { + return null; + } + + String baseDomain = getBaseDomain(hostAndPath[0]); + ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain); + if (cookieList == null) { + cookieList = CookieSyncManager.getInstance() + .getCookiesForDomain(baseDomain); + mCookieMap.put(baseDomain, cookieList); + } + + long now = System.currentTimeMillis(); + boolean secure = HTTPS.equals(uri.mScheme); + Iterator<Cookie> iter = cookieList.iterator(); + StringBuilder ret = new StringBuilder(256); + + while (iter.hasNext()) { + Cookie cookie = iter.next(); + if (cookie.domainMatch(hostAndPath[0]) && + cookie.pathMatch(hostAndPath[1]) + // expires == -1 means no expires defined. Otherwise + // negative means far future + && (cookie.expires < 0 || cookie.expires > now) + && (!cookie.secure || secure) + && cookie.mode != Cookie.MODE_DELETED) { + cookie.lastAcessTime = now; + + if (ret.length() > 0) { + ret.append(SEMICOLON); + // according to RC2109, SEMICOLON is office separator, + // but when log in yahoo.com, it needs WHITE_SPACE too. + ret.append(WHITE_SPACE); + } + + ret.append(cookie.name); + ret.append(EQUAL); + ret.append(cookie.value); + } + } + if (ret.length() > 0) { + if (Config.LOGV) { + Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret); + } + return ret.toString(); + } else { + if (Config.LOGV) { + Log.v(LOGTAG, "getCookie: uri: " + uri + + " But can't find cookie."); + } + return null; + } + } + + /** + * Remove all session cookies, which are cookies without expiration date + */ + public void removeSessionCookie() { + final Runnable clearCache = new Runnable() { + public void run() { + synchronized(CookieManager.this) { + Collection<ArrayList<Cookie>> cookieList = mCookieMap.values(); + Iterator<ArrayList<Cookie>> listIter = cookieList.iterator(); + while (listIter.hasNext()) { + ArrayList<Cookie> list = listIter.next(); + Iterator<Cookie> iter = list.iterator(); + while (iter.hasNext()) { + Cookie cookie = iter.next(); + if (cookie.expires == -1) { + iter.remove(); + } + } + } + CookieSyncManager.getInstance().clearSessionCookies(); + } + } + }; + new Thread(clearCache).start(); + } + + /** + * Remove all cookies + */ + public void removeAllCookie() { + final Runnable clearCache = new Runnable() { + public void run() { + synchronized(CookieManager.this) { + mCookieMap = new LinkedHashMap<String, ArrayList<Cookie>>( + MAX_DOMAIN_COUNT, 0.75f, true); + CookieSyncManager.getInstance().clearAllCookies(); + } + } + }; + new Thread(clearCache).start(); + } + + /** + * Return true if there are stored cookies. + */ + public synchronized boolean hasCookies() { + return CookieSyncManager.getInstance().hasCookies(); + } + + /** + * Remove all expired cookies + */ + public void removeExpiredCookie() { + final Runnable clearCache = new Runnable() { + public void run() { + synchronized(CookieManager.this) { + long now = System.currentTimeMillis(); + Collection<ArrayList<Cookie>> cookieList = mCookieMap.values(); + Iterator<ArrayList<Cookie>> listIter = cookieList.iterator(); + while (listIter.hasNext()) { + ArrayList<Cookie> list = listIter.next(); + Iterator<Cookie> iter = list.iterator(); + while (iter.hasNext()) { + Cookie cookie = iter.next(); + // expires == -1 means no expires defined. Otherwise + // negative means far future + if (cookie.expires > 0 && cookie.expires < now) { + iter.remove(); + } + } + } + CookieSyncManager.getInstance().clearExpiredCookies(now); + } + } + }; + new Thread(clearCache).start(); + } + + /** + * Package level api, called from CookieSyncManager + * + * Get a list of cookies which are updated since a given time. + * @param last The given time in millisec + * @return A list of cookies + */ + synchronized ArrayList<Cookie> getUpdatedCookiesSince(long last) { + ArrayList<Cookie> cookies = new ArrayList<Cookie>(); + Collection<ArrayList<Cookie>> cookieList = mCookieMap.values(); + Iterator<ArrayList<Cookie>> listIter = cookieList.iterator(); + while (listIter.hasNext()) { + ArrayList<Cookie> list = listIter.next(); + Iterator<Cookie> iter = list.iterator(); + while (iter.hasNext()) { + Cookie cookie = iter.next(); + if (cookie.lastUpdateTime > last) { + cookies.add(cookie); + } + } + } + return cookies; + } + + /** + * Package level api, called from CookieSyncManager + * + * Delete a Cookie in the RAM + * @param cookie Cookie to be deleted + */ + synchronized void deleteACookie(Cookie cookie) { + if (cookie.mode == Cookie.MODE_DELETED) { + String baseDomain = getBaseDomain(cookie.domain); + ArrayList<Cookie> cookieList = mCookieMap.get(baseDomain); + if (cookieList != null) { + cookieList.remove(cookie); + if (cookieList.isEmpty()) { + mCookieMap.remove(baseDomain); + } + } + } + } + + /** + * Package level api, called from CookieSyncManager + * + * Called after a cookie is synced to FLASH + * @param cookie Cookie to be synced + */ + synchronized void syncedACookie(Cookie cookie) { + cookie.mode = Cookie.MODE_NORMAL; + } + + /** + * Package level api, called from CookieSyncManager + * + * Delete the least recent used domains if the total cookie count in RAM + * exceeds the limit + * @return A list of cookies which are removed from RAM + */ + synchronized ArrayList<Cookie> deleteLRUDomain() { + int count = 0; + int byteCount = 0; + int mapSize = mCookieMap.size(); + + if (mapSize < MAX_RAM_DOMAIN_COUNT) { + Collection<ArrayList<Cookie>> cookieLists = mCookieMap.values(); + Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator(); + while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { + ArrayList<Cookie> list = listIter.next(); + if (Config.DEBUG) { + Iterator<Cookie> iter = list.iterator(); + while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { + Cookie cookie = iter.next(); + // 14 is 3 * sizeof(long) + sizeof(boolean) + // + sizeof(byte) + byteCount += cookie.domain.length() + + cookie.path.length() + + cookie.name.length() + + cookie.value.length() + 14; + count++; + } + } else { + count += list.size(); + } + } + } + + ArrayList<Cookie> retlist = new ArrayList<Cookie>(); + if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) { + if (Config.DEBUG) { + Log.v(LOGTAG, count + " cookies used " + byteCount + + " bytes with " + mapSize + " domains"); + } + Object[] domains = mCookieMap.keySet().toArray(); + int toGo = mapSize / 10 + 1; + while (toGo-- > 0){ + String domain = domains[toGo].toString(); + if (Config.LOGV) { + Log.v(LOGTAG, "delete domain: " + domain + + " from RAM cache"); + } + retlist.addAll(mCookieMap.get(domain)); + mCookieMap.remove(domain); + } + } + return retlist; + } + + /** + * Extract the host and path out of a uri + * @param uri The given WebAddress + * @return The host and path in the format of String[], String[0] is host + * which has at least two periods, String[1] is path which always + * ended with "/" + */ + private String[] getHostAndPath(WebAddress uri) { + if (uri.mHost != null && uri.mPath != null) { + String[] ret = new String[2]; + ret[0] = uri.mHost; + ret[1] = uri.mPath; + + int index = ret[0].indexOf(PERIOD); + if (index == -1) { + if (uri.mScheme.equalsIgnoreCase("file")) { + // There is a potential bug where a local file path matches + // another file in the local web server directory. Still + // "localhost" is the best pseudo domain name. + ret[0] = "localhost"; + } else if (!ret[0].equals("localhost")) { + return null; + } + } else if (index == ret[0].lastIndexOf(PERIOD)) { + // cookie host must have at least two periods + ret[0] = PERIOD + ret[0]; + } + + if (ret[1].charAt(0) != PATH_DELIM) { + return null; + } + + /* + * find cookie path, e.g. for http://www.google.com, the path is "/" + * for http://www.google.com/lab/, the path is "/lab" + * for http://www.google.com/lab/foo, the path is "/lab/foo" + * for http://www.google.com/lab?hl=en, the path is "/lab" + * for http://www.google.com/lab.asp?hl=en, the path is "/lab.asp" + * Note: the path from URI has at least one "/" + * See: + * http://www.unix.com.ua/rfc/rfc2109.html + */ + index = ret[1].indexOf(QUESTION_MARK); + if (index != -1) { + ret[1] = ret[1].substring(0, index); + } + return ret; + } else + return null; + } + + /** + * Get the base domain for a give host. E.g. mail.google.com will return + * google.com + * @param host The give host + * @return the base domain + */ + private String getBaseDomain(String host) { + int startIndex = 0; + int nextIndex = host.indexOf(PERIOD); + int lastIndex = host.lastIndexOf(PERIOD); + while (nextIndex < lastIndex) { + startIndex = nextIndex + 1; + nextIndex = host.indexOf(PERIOD, startIndex); + } + if (startIndex > 0) { + return host.substring(startIndex); + } else { + return host; + } + } + + /** + * parseCookie() parses the cookieString which is a comma-separated list of + * one or more cookies in the format of "NAME=VALUE; expires=DATE; + * path=PATH; domain=DOMAIN_NAME; secure httponly" to a list of Cookies. + * Here is a sample: IGDND=1, IGPC=ET=UB8TSNwtDmQ:AF=0; expires=Sun, + * 17-Jan-2038 19:14:07 GMT; path=/ig; domain=.google.com, =, + * PREF=ID=408909b1b304593d:TM=1156459854:LM=1156459854:S=V-vCAU6Sh-gobCfO; + * expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com which + * contains 3 cookies IGDND, IGPC, PREF and an empty cookie + * @param host The default host + * @param path The default path + * @param cookieString The string coming from "Set-Cookie:" + * @return A list of Cookies + */ + private ArrayList<Cookie> parseCookie(String host, String path, + String cookieString) { + ArrayList<Cookie> ret = new ArrayList<Cookie>(); + + int index = 0; + int length = cookieString.length(); + while (true) { + Cookie cookie = null; + + // done + if (index < 0 || index >= length) { + break; + } + + // skip white space + if (cookieString.charAt(index) == WHITE_SPACE) { + index++; + continue; + } + + /* + * get NAME=VALUE; pair. detecting the end of a pair is tricky, it + * can be the end of a string, like "foo=bluh", it can be semicolon + * like "foo=bluh;path=/"; or it can be enclosed by \", like + * "foo=\"bluh bluh\";path=/" + * + * Note: in the case of "foo=bluh, bar=bluh;path=/", we interpret + * it as one cookie instead of two cookies. + */ + int equalIndex = cookieString.indexOf(EQUAL, index); + if (equalIndex == -1) { + // bad format, force return + break; + } + cookie = new Cookie(host, path); + cookie.name = cookieString.substring(index, equalIndex); + if (cookieString.charAt(equalIndex + 1) == QUOTATION) { + index = cookieString.indexOf(QUOTATION, equalIndex + 2); + if (index == -1) { + // bad format, force return + break; + } + } + int semicolonIndex = cookieString.indexOf(SEMICOLON, index); + if (semicolonIndex == -1) { + semicolonIndex = length; + } + if (semicolonIndex - equalIndex > MAX_COOKIE_LENGTH) { + // cookie is too big, trim it + cookie.value = cookieString.substring(equalIndex + 1, + equalIndex + MAX_COOKIE_LENGTH); + } else if (equalIndex + 1 == semicolonIndex + || semicolonIndex < equalIndex) { + // these are unusual case like foo=; and foo; path=/ + cookie.value = ""; + } else { + cookie.value = cookieString.substring(equalIndex + 1, + semicolonIndex); + } + // get attributes + index = semicolonIndex; + while (true) { + // done + if (index < 0 || index >= length) { + break; + } + + // skip white space and semicolon + if (cookieString.charAt(index) == WHITE_SPACE + || cookieString.charAt(index) == SEMICOLON) { + index++; + continue; + } + + // comma means next cookie + if (cookieString.charAt(index) == COMMA) { + index++; + break; + } + + // "secure" is a known attribute doesn't use "="; + // while sites like live.com uses "secure=" + if (length - index > SECURE_LENGTH + && cookieString.substring(index, index + SECURE_LENGTH). + equalsIgnoreCase(SECURE)) { + index += SECURE_LENGTH; + cookie.secure = true; + if (cookieString.charAt(index) == EQUAL) index++; + continue; + } + + // "httponly" is a known attribute doesn't use "="; + // while sites like live.com uses "httponly=" + if (length - index > HTTP_ONLY_LENGTH + && cookieString.substring(index, + index + HTTP_ONLY_LENGTH). + equalsIgnoreCase(HTTP_ONLY)) { + index += HTTP_ONLY_LENGTH; + if (cookieString.charAt(index) == EQUAL) index++; + // FIXME: currently only parse the attribute + continue; + } + equalIndex = cookieString.indexOf(EQUAL, index); + if (equalIndex > 0) { + String name = cookieString.substring(index, equalIndex) + .toLowerCase(); + if (name.equals(EXPIRES)) { + int comaIndex = cookieString.indexOf(COMMA, equalIndex); + + // skip ',' in (Wdy, DD-Mon-YYYY HH:MM:SS GMT) or + // (Weekday, DD-Mon-YY HH:MM:SS GMT) if it applies. + // "Wednesday" is the longest Weekday which has length 9 + if ((comaIndex != -1) && + (comaIndex - equalIndex <= 10)) { + index = comaIndex + 1; + } + } + semicolonIndex = cookieString.indexOf(SEMICOLON, index); + int commaIndex = cookieString.indexOf(COMMA, index); + if (semicolonIndex == -1 && commaIndex == -1) { + index = length; + } else if (semicolonIndex == -1) { + index = commaIndex; + } else if (commaIndex == -1) { + index = semicolonIndex; + } else { + index = Math.min(semicolonIndex, commaIndex); + } + String value = + cookieString.substring(equalIndex + 1, index); + + // Strip quotes if they exist + if (value.length() > 2 && value.charAt(0) == QUOTATION) { + int endQuote = value.indexOf(QUOTATION, 1); + if (endQuote > 0) { + value = value.substring(1, endQuote); + } + } + if (name.equals(EXPIRES)) { + try { + cookie.expires = HttpDateTime.parse(value); + } catch (IllegalArgumentException ex) { + Log.e(LOGTAG, + "illegal format for expires: " + value); + } + } else if (name.equals(MAX_AGE)) { + try { + cookie.expires = System.currentTimeMillis() + 1000 + * Long.parseLong(value); + } catch (NumberFormatException ex) { + Log.e(LOGTAG, + "illegal format for max-age: " + value); + } + } else if (name.equals(PATH)) { + // only allow non-empty path value + if (value.length() > 0) { + cookie.path = value; + } + } else if (name.equals(DOMAIN)) { + int lastPeriod = value.lastIndexOf(PERIOD); + if (lastPeriod == 0) { + // disallow cookies set for TLDs like [.com] + cookie.domain = null; + continue; + } + try { + Integer.parseInt(value.substring(lastPeriod + 1)); + // no wildcard for ip address match + if (!value.equals(host)) { + // no cross-site cookie + cookie.domain = null; + } + continue; + } catch (NumberFormatException ex) { + // ignore the exception, value is a host name + } + value = value.toLowerCase(); + if (value.charAt(0) != PERIOD) { + // pre-pended dot to make it as a domain cookie + value = PERIOD + value; + lastPeriod++; + } + if (host.endsWith(value.substring(1))) { + int len = value.length(); + int hostLen = host.length(); + if (hostLen > (len - 1) + && host.charAt(hostLen - len) != PERIOD) { + // make sure the bar.com doesn't match .ar.com + cookie.domain = null; + continue; + } + // disallow cookies set on ccTLDs like [.co.uk] + if ((len == lastPeriod + 3) + && (len >= 6 && len <= 8)) { + String s = value.substring(1, lastPeriod); + if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) { + cookie.domain = null; + continue; + } + } + cookie.domain = value; + } else { + // no cross-site or more specific sub-domain cookie + cookie.domain = null; + } + } + } else { + // bad format, force return + index = length; + } + } + if (cookie != null && cookie.domain != null) { + ret.add(cookie); + } + } + return ret; + } +} diff --git a/core/java/android/webkit/CookieSyncManager.java b/core/java/android/webkit/CookieSyncManager.java new file mode 100644 index 0000000..f2511d8 --- /dev/null +++ b/core/java/android/webkit/CookieSyncManager.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2007 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.content.Context; +import android.util.Config; +import android.util.Log; +import android.webkit.CookieManager.Cookie; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * The class CookieSyncManager is used to synchronize the browser cookies + * between RAM and FLASH. To get the best performance, browser cookie is saved + * in RAM. We use a separate thread to sync the cookies between RAM and FLASH on + * a timer base. + * <p> + * To use the CookieSyncManager, the host application has to call the following + * when the application starts. + * <p> + * CookieSyncManager.createInstance(context) + * <p> + * To set up for sync, the host application has to call + * <p> + * CookieSyncManager.getInstance().startSync() + * <p> + * in its Activity.onResume(), and call + * <p> + * CookieSyncManager.getInstance().stopSync() + * <p> + * in its Activity.onStop(). + * <p> + * To get instant sync instead of waiting for the timer to trigger, the host can + * call + * <p> + * CookieSyncManager.getInstance().sync() + */ +public final class CookieSyncManager extends WebSyncManager { + + private static CookieSyncManager sRef; + + // time when last update happened + private long mLastUpdate; + + private CookieSyncManager(Context context) { + super(context, "CookieSyncManager"); + } + + /** + * Singleton access to a {@link CookieSyncManager}. An + * IllegalStateException will be thrown if + * {@link CookieSyncManager#createInstance(Context)} is not called before. + * + * @return CookieSyncManager + */ + public static synchronized CookieSyncManager getInstance() { + if (sRef == null) { + throw new IllegalStateException( + "CookieSyncManager::createInstance() needs to be called " + + "before CookieSyncManager::getInstance()"); + } + return sRef; + } + + /** + * Create a singleton CookieSyncManager within a context + * @param context + * @return CookieSyncManager + */ + public static synchronized CookieSyncManager createInstance( + Context context) { + if (sRef == null) { + sRef = new CookieSyncManager(context); + } + return sRef; + } + + /** + * Package level api, called from CookieManager Get all the cookies which + * matches a given base domain. + * @param domain + * @return A list of Cookie + */ + ArrayList<Cookie> getCookiesForDomain(String domain) { + // null mDataBase implies that the host application doesn't support + // persistent cookie. No sync needed. + if (mDataBase == null) { + return new ArrayList<Cookie>(); + } + + return mDataBase.getCookiesForDomain(domain); + } + + /** + * Package level api, called from CookieManager Clear all cookies in the + * database + */ + void clearAllCookies() { + // null mDataBase implies that the host application doesn't support + // persistent cookie. + if (mDataBase == null) { + return; + } + + mDataBase.clearCookies(); + } + + /** + * Returns true if there are any saved cookies. + */ + boolean hasCookies() { + // null mDataBase implies that the host application doesn't support + // persistent cookie. + if (mDataBase == null) { + return false; + } + + return mDataBase.hasCookies(); + } + + /** + * Package level api, called from CookieManager Clear all session cookies in + * the database + */ + void clearSessionCookies() { + // null mDataBase implies that the host application doesn't support + // persistent cookie. + if (mDataBase == null) { + return; + } + + mDataBase.clearSessionCookies(); + } + + /** + * Package level api, called from CookieManager Clear all expired cookies in + * the database + */ + void clearExpiredCookies(long now) { + // null mDataBase implies that the host application doesn't support + // persistent cookie. + if (mDataBase == null) { + return; + } + + mDataBase.clearExpiredCookies(now); + } + + protected void syncFromRamToFlash() { + if (Config.LOGV) { + Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash STARTS"); + } + + if (!CookieManager.getInstance().acceptCookie()) { + return; + } + + ArrayList<Cookie> cookieList = CookieManager.getInstance() + .getUpdatedCookiesSince(mLastUpdate); + mLastUpdate = System.currentTimeMillis(); + syncFromRamToFlash(cookieList); + + ArrayList<Cookie> lruList = + CookieManager.getInstance().deleteLRUDomain(); + syncFromRamToFlash(lruList); + + if (Config.LOGV) { + Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash DONE"); + } + } + + private void syncFromRamToFlash(ArrayList<Cookie> list) { + Iterator<Cookie> iter = list.iterator(); + while (iter.hasNext()) { + Cookie cookie = iter.next(); + if (cookie.mode != Cookie.MODE_NORMAL) { + if (cookie.mode != Cookie.MODE_NEW) { + mDataBase.deleteCookies(cookie.domain, cookie.path, + cookie.name); + } + if (cookie.mode != Cookie.MODE_DELETED) { + mDataBase.addCookie(cookie); + CookieManager.getInstance().syncedACookie(cookie); + } else { + CookieManager.getInstance().deleteACookie(cookie); + } + } + } + } +} diff --git a/core/java/android/webkit/DataLoader.java b/core/java/android/webkit/DataLoader.java new file mode 100644 index 0000000..dcdc949 --- /dev/null +++ b/core/java/android/webkit/DataLoader.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2007 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 org.apache.http.protocol.HTTP; + +import android.net.http.Headers; + +import java.io.ByteArrayInputStream; + +/** + * This class is a concrete implementation of StreamLoader that uses the + * content supplied as a URL as the source for the stream. The mimetype + * optionally provided in the URL is extracted and inserted into the HTTP + * response headers. + */ +class DataLoader extends StreamLoader { + + private String mContentType; // Content mimetype, if supplied in URL + + /** + * Constructor uses the dataURL as the source for an InputStream + * @param dataUrl data: URL string optionally containing a mimetype + * @param loadListener LoadListener to pass the content to + */ + DataLoader(String dataUrl, LoadListener loadListener) { + super(loadListener); + + String url = dataUrl.substring("data:".length()); + String content; + int commaIndex = url.indexOf(','); + if (commaIndex != -1) { + mContentType = url.substring(0, commaIndex); + content = url.substring(commaIndex + 1); + } else { + content = url; + } + mDataStream = new ByteArrayInputStream(content.getBytes()); + mContentLength = content.length(); + } + + @Override + protected boolean setupStreamAndSendStatus() { + mHandler.status(1, 1, 0, "OK"); + return true; + } + + @Override + protected void buildHeaders(Headers headers) { + if (mContentType != null) { + headers.setContentType(mContentType); + } + } + + /** + * Construct a DataLoader and instruct it to start loading. + * + * @param url data: URL string optionally containing a mimetype + * @param loadListener LoadListener to pass the content to + */ + public static void requestUrl(String url, LoadListener loadListener) { + DataLoader loader = new DataLoader(url, loadListener); + loader.load(); + } + +} diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java new file mode 100644 index 0000000..750403b --- /dev/null +++ b/core/java/android/webkit/DateSorter.java @@ -0,0 +1,124 @@ +/* + * 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.content.Context; +import android.content.res.Resources; + +import java.util.Calendar; +import java.util.Date; + +/** + * Sorts dates into the following groups: + * Today + * Yesterday + * five days ago + * one month ago + * older than a month ago + */ + +public class DateSorter { + + private static final String LOGTAG = "webkit"; + + /** must be >= 3 */ + public static final int DAY_COUNT = 5; + + private long [] mBins = new long[DAY_COUNT]; + private String [] mLabels = new String[DAY_COUNT]; + + private static final int NUM_DAYS_AGO = 5; + + Date mDate = new Date(); + Calendar mCal = Calendar.getInstance(); + + /** + * @param context Application context + */ + public DateSorter(Context context) { + Resources resources = context.getResources(); + + Calendar c = Calendar.getInstance(); + beginningOfDay(c); + + // Create the bins + mBins[0] = c.getTimeInMillis(); // Today + c.roll(Calendar.DAY_OF_YEAR, -1); + mBins[1] = c.getTimeInMillis(); // Yesterday + c.roll(Calendar.DAY_OF_YEAR, -(NUM_DAYS_AGO - 1)); + mBins[2] = c.getTimeInMillis(); // Five days ago + c.roll(Calendar.DAY_OF_YEAR, NUM_DAYS_AGO); // move back to today + c.roll(Calendar.MONTH, -1); + mBins[3] = c.getTimeInMillis(); // One month ago + c.roll(Calendar.MONTH, -1); + mBins[4] = c.getTimeInMillis(); // Over one month ago + + // build labels + mLabels[0] = context.getText(com.android.internal.R.string.today).toString(); + mLabels[1] = context.getText(com.android.internal.R.string.yesterday).toString(); + + int resId = com.android.internal.R.plurals.num_days_ago; + String format = resources.getQuantityString(resId, NUM_DAYS_AGO); + mLabels[2] = String.format(format, NUM_DAYS_AGO); + + mLabels[3] = context.getText(com.android.internal.R.string.oneMonthDurationPast).toString(); + mLabels[4] = context.getText(com.android.internal.R.string.beforeOneMonthDurationPast) + .toString(); + } + + /** + * @param time time since the Epoch in milliseconds, such as that + * returned by Calendar.getTimeInMillis() + * @return an index from 0 to (DAY_COUNT - 1) that identifies which + * date bin this date belongs to + */ + public int getIndex(long time) { + // Lame linear search + for (int i = 0; i < DAY_COUNT; i++) { + if (time > mBins[i]) return i; + } + return DAY_COUNT - 1; + } + + /** + * @param index date bin index as returned by getIndex() + * @return string label suitable for display to user + */ + public String getLabel(int index) { + return mLabels[index]; + } + + + /** + * @param index date bin index as returned by getIndex() + * @return date boundary at given index + */ + public long getBoundary(int index) { + return mBins[index]; + } + + /** + * Calcuate 12:00am by zeroing out hour, minute, second, millisecond + */ + private Calendar beginningOfDay(Calendar c) { + c.set(Calendar.HOUR_OF_DAY, 0); + c.set(Calendar.MINUTE, 0); + c.set(Calendar.SECOND, 0); + c.set(Calendar.MILLISECOND, 0); + return c; + } +} diff --git a/core/java/android/webkit/DownloadListener.java b/core/java/android/webkit/DownloadListener.java new file mode 100644 index 0000000..dfaa1b9 --- /dev/null +++ b/core/java/android/webkit/DownloadListener.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2007 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; + +public interface DownloadListener { + + /** + * Notify the host application that a file should be downloaded + * @param url The full url to the content that should be downloaded + * @param userAgent the user agent to be used for the download. + * @param contentDisposition Content-disposition http header, if + * present. + * @param mimetype The mimetype of the content reported by the server + * @param contentLength The file size reported by the server + */ + public void onDownloadStart(String url, String userAgent, + String contentDisposition, String mimetype, long contentLength); + +} diff --git a/core/java/android/webkit/FileLoader.java b/core/java/android/webkit/FileLoader.java new file mode 100644 index 0000000..54a4c1d --- /dev/null +++ b/core/java/android/webkit/FileLoader.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2007 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 com.android.internal.R; + +import android.content.Context; +import android.content.res.AssetManager; +import android.net.http.EventHandler; +import android.net.http.Headers; +import android.os.Environment; + +import java.io.File; +import java.io.FileInputStream; + +/** + * This class is a concrete implementation of StreamLoader that uses a + * file or asset as the source for the stream. + * + */ +class FileLoader extends StreamLoader { + + private String mPath; // Full path to the file to load + private Context mContext; // Application context, used for asset loads + private boolean mIsAsset; // Indicates if the load is an asset or not + private boolean mAllowFileAccess; // Allow/block file system access + + /** + * Construct a FileLoader with the file URL specified as the content + * source. + * + * @param url Full file url pointing to content to be loaded + * @param loadListener LoadListener to pass the content to + * @param context Context to use to access the asset. + * @param asset true if url points to an asset. + * @param allowFileAccess true if this WebView is allowed to access files + * on the file system. + */ + FileLoader(String url, LoadListener loadListener, Context context, + boolean asset, boolean allowFileAccess) { + super(loadListener); + mIsAsset = asset; + mContext = context; + mAllowFileAccess = allowFileAccess; + + // clean the Url + int index = url.indexOf('?'); + if (mIsAsset) { + mPath = index > 0 ? URLUtil.stripAnchor( + url.substring(URLUtil.ASSET_BASE.length(), index)) : + URLUtil.stripAnchor(url.substring( + URLUtil.ASSET_BASE.length())); + } else { + mPath = index > 0 ? URLUtil.stripAnchor( + url.substring(URLUtil.FILE_BASE.length(), index)) : + URLUtil.stripAnchor(url.substring( + URLUtil.FILE_BASE.length())); + } + } + + @Override + protected boolean setupStreamAndSendStatus() { + try { + if (mIsAsset) { + try { + mDataStream = mContext.getAssets().open(mPath); + } catch (java.io.FileNotFoundException ex) { + // try the rest files included in the package + mDataStream = mContext.getAssets().openNonAsset(mPath); + } + } else { + if (!mAllowFileAccess) { + mHandler.error(EventHandler.FILE_ERROR, + mContext.getString(R.string.httpErrorFileNotFound)); + return false; + } + + mDataStream = new FileInputStream(mPath); + mContentLength = (new File(mPath)).length(); + } + mHandler.status(1, 1, 0, "OK"); + + } catch (java.io.FileNotFoundException ex) { + mHandler.error( + EventHandler.FILE_NOT_FOUND_ERROR, + mContext.getString(R.string.httpErrorFileNotFound) + + " " + ex.getMessage()); + return false; + + } catch (java.io.IOException ex) { + mHandler.error(EventHandler.FILE_ERROR, + mContext.getString(R.string.httpErrorFileNotFound) + + " " + ex.getMessage()); + return false; + } + return true; + } + + @Override + protected void buildHeaders(Headers headers) { + // do nothing. + } + + + /** + * Construct a FileLoader and instruct it to start loading. + * + * @param url Full file url pointing to content to be loaded + * @param loadListener LoadListener to pass the content to + * @param context Context to use to access the asset. + * @param asset true if url points to an asset. + * @param allowFileAccess true if this FileLoader can load files from the + * file system. + */ + public static void requestUrl(String url, LoadListener loadListener, + Context context, boolean asset, boolean allowFileAccess) { + FileLoader loader = new FileLoader(url, loadListener, context, asset, + allowFileAccess); + loader.load(); + } + +} diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java new file mode 100644 index 0000000..5e323eb --- /dev/null +++ b/core/java/android/webkit/FrameLoader.java @@ -0,0 +1,370 @@ +/* + * 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.net.http.EventHandler; +import android.net.http.RequestHandle; +import android.util.Config; +import android.util.Log; +import android.webkit.CacheManager.CacheResult; +import android.webkit.UrlInterceptRegistry; + +import java.util.HashMap; +import java.util.Map; + +class FrameLoader { + + private final LoadListener mListener; + private final String mMethod; + private final boolean mIsHighPriority; + private final WebSettings mSettings; + private Map<String, String> mHeaders; + private byte[] mPostData; + private Network mNetwork; + private int mCacheMode; + private String mReferrer; + private String mContentType; + + private static final int URI_PROTOCOL = 0x100; + + private static final String CONTENT_TYPE = "content-type"; + + // Contents of an about:blank page + private static final String mAboutBlank = + "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EB\">" + + "<html><head><title>about:blank</title></head><body></body></html>"; + + static final String HEADER_STR = "text/xml, text/html, " + + "application/xhtml+xml, image/png, text/plain, */*;q=0.8"; + + private static final String LOGTAG = "webkit"; + + FrameLoader(LoadListener listener, WebSettings settings, + String method, boolean highPriority) { + mListener = listener; + mHeaders = null; + mMethod = method; + mIsHighPriority = highPriority; + mCacheMode = WebSettings.LOAD_NORMAL; + mSettings = settings; + } + + public void setReferrer(String ref) { + // only set referrer for http or https + if (URLUtil.isNetworkUrl(ref)) mReferrer = ref; + } + + public void setPostData(byte[] postData) { + mPostData = postData; + } + + public void setContentTypeForPost(String postContentType) { + mContentType = postContentType; + } + + public void setCacheMode(int cacheMode) { + mCacheMode = cacheMode; + } + + public void setHeaders(HashMap headers) { + mHeaders = headers; + } + + public LoadListener getLoadListener() { + return mListener; + } + + /** + * Issues the load request. + * + * Return value does not indicate if the load was successful or not. It + * simply indicates that the load request is reasonable. + * + * @return true if the load is reasonable. + */ + public boolean executeLoad() { + String url = mListener.url(); + + // Attempt to decode the percent-encoded url. + try { + url = new String(URLUtil.decode(url.getBytes())); + } catch (IllegalArgumentException e) { + // Fail with a bad url error if the decode fails. + mListener.error(EventHandler.ERROR_BAD_URL, + mListener.getContext().getString( + com.android.internal.R.string.httpErrorBadUrl)); + return false; + } + + if (URLUtil.isNetworkUrl(url)){ + if (mSettings.getBlockNetworkLoads()) { + mListener.error(EventHandler.ERROR_BAD_URL, + mListener.getContext().getString( + com.android.internal.R.string.httpErrorBadUrl)); + return false; + } + mNetwork = Network.getInstance(mListener.getContext()); + return handleHTTPLoad(); + } else if (handleLocalFile(url, mListener, mSettings)) { + return true; + } + if (Config.LOGV) { + Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:" + + mListener.url()); + } + mListener.error(EventHandler.ERROR_UNSUPPORTED_SCHEME, + mListener.getContext().getText( + com.android.internal.R.string.httpErrorUnsupportedScheme).toString()); + return false; + + } + + /* package */ + static boolean handleLocalFile(String url, LoadListener loadListener, + WebSettings settings) { + if (URLUtil.isAssetUrl(url)) { + FileLoader.requestUrl(url, loadListener, loadListener.getContext(), + true, settings.getAllowFileAccess()); + return true; + } else if (URLUtil.isFileUrl(url)) { + FileLoader.requestUrl(url, loadListener, loadListener.getContext(), + false, settings.getAllowFileAccess()); + return true; + } else if (URLUtil.isContentUrl(url)) { + // Send the raw url to the ContentLoader because it will do a + // permission check and the url has to match.. + ContentLoader.requestUrl(loadListener.url(), loadListener, + loadListener.getContext()); + return true; + } else if (URLUtil.isDataUrl(url)) { + DataLoader.requestUrl(url, loadListener); + return true; + } else if (URLUtil.isAboutUrl(url)) { + loadListener.data(mAboutBlank.getBytes(), mAboutBlank.length()); + loadListener.endData(); + return true; + } + return false; + } + + private boolean handleHTTPLoad() { + if (mHeaders == null) { + mHeaders = new HashMap<String, String>(); + } + populateStaticHeaders(); + populateHeaders(); + + // response was handled by UrlIntercept, don't issue HTTP request + if (handleUrlIntercept()) return true; + + // response was handled by Cache, don't issue HTTP request + if (handleCache()) { + // push the request data down to the LoadListener + // as response from the cache could be a redirect + // and we may need to initiate a network request if the cache + // can't satisfy redirect URL + mListener.setRequestData(mMethod, mHeaders, mPostData, + mIsHighPriority); + return true; + } + + if (Config.LOGV) { + Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: " + + mListener.url()); + } + + boolean ret = false; + int error = EventHandler.ERROR_UNSUPPORTED_SCHEME; + + try { + ret = mNetwork.requestURL(mMethod, mHeaders, + mPostData, mListener, mIsHighPriority); + } catch (android.net.ParseException ex) { + error = EventHandler.ERROR_BAD_URL; + } catch (java.lang.RuntimeException ex) { + /* probably an empty header set by javascript. We want + the same result as bad URL */ + error = EventHandler.ERROR_BAD_URL; + } + if (!ret) { + mListener.error(error, mListener.getContext().getText( + EventHandler.errorStringResources[Math.abs(error)]).toString()); + return false; + } + return true; + } + + /* + * This function is used by handleUrlInterecpt and handleCache to + * setup a load from the byte stream in a CacheResult. + */ + private void startCacheLoad(CacheResult result) { + if (Config.LOGV) { + Log.v(LOGTAG, "FrameLoader: loading from cache: " + + mListener.url()); + } + // Tell the Listener respond with the cache file + CacheLoader cacheLoader = + new CacheLoader(mListener, result); + mListener.setCacheLoader(cacheLoader); + cacheLoader.load(); + } + + /* + * This function is used by handleHTTPLoad to allow URL + * interception. This can be used to provide alternative load + * methods such as locally stored versions or for debugging. + * + * Returns true if the response was handled by UrlIntercept. + */ + private boolean handleUrlIntercept() { + // Check if the URL can be served from UrlIntercept. If + // successful, return the data just like a cache hit. + CacheResult result = UrlInterceptRegistry.getSurrogate( + mListener.url(), mHeaders); + if(result != null) { + // Intercepted. The data is stored in result.stream. Setup + // a load from the CacheResult. + startCacheLoad(result); + return true; + } + // Not intercepted. Carry on as normal. + return false; + } + + /* + * This function is used by the handleHTTPLoad to setup the cache headers + * correctly. + * Returns true if the response was handled from the cache + */ + private boolean handleCache() { + switch (mCacheMode) { + // This mode is normally used for a reload, it instructs the http + // loader to not use the cached content. + case WebSettings.LOAD_NO_CACHE: + break; + + + // This mode is used when the content should only be loaded from + // the cache. If it is not there, then fail the load. This is used + // to load POST content in a history navigation. + case WebSettings.LOAD_CACHE_ONLY: { + CacheResult result = CacheManager.getCacheFile(mListener.url(), + null); + if (result != null) { + startCacheLoad(result); + } else { + // This happens if WebCore was first told that the POST + // response was in the cache, then when we try to use it + // it has gone. + // Generate a file not found error + int err = EventHandler.FILE_NOT_FOUND_ERROR; + mListener.error(err, mListener.getContext().getText( + EventHandler.errorStringResources[Math.abs(err)]) + .toString()); + } + return true; + } + + // This mode is for when the user is doing a history navigation + // in the browser and should returned cached content regardless + // of it's state. If it is not in the cache, then go to the + // network. + case WebSettings.LOAD_CACHE_ELSE_NETWORK: { + if (Config.LOGV) { + Log.v(LOGTAG, "FrameLoader: checking cache: " + + mListener.url()); + } + // Get the cache file name for the current URL, passing null for + // the validation headers causes no validation to occur + CacheResult result = CacheManager.getCacheFile(mListener.url(), + null); + if (result != null) { + startCacheLoad(result); + return true; + } + break; + } + + // This is the default case, which is to check to see if the + // content in the cache can be used. If it can be used, then + // use it. If it needs revalidation then the relevant headers + // are added to the request. + default: + case WebSettings.LOAD_NORMAL: + return mListener.checkCache(mHeaders); + }// end of switch + + return false; + } + + /** + * Add the static headers that don't change with each request. + */ + private void populateStaticHeaders() { + // Accept header should already be there as they are built by WebCore, + // but in the case they are missing, add some. + String accept = mHeaders.get("Accept"); + if (accept == null || accept.length() == 0) { + mHeaders.put("Accept", HEADER_STR); + } + mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7"); + + String acceptLanguage = mSettings.getAcceptLanguage(); + if (acceptLanguage.length() > 0) { + mHeaders.put("Accept-Language", acceptLanguage); + } + + mHeaders.put("User-Agent", mSettings.getUserAgentString()); + } + + /** + * Add the content related headers. These headers contain user private data + * and is not used when we are proxying an untrusted request. + */ + private void populateHeaders() { + + if (mReferrer != null) mHeaders.put("Referer", mReferrer); + if (mContentType != null) mHeaders.put(CONTENT_TYPE, mContentType); + + // if we have an active proxy and have proxy credentials, do pre-emptive + // authentication to avoid an extra round-trip: + if (mNetwork.isValidProxySet()) { + String username; + String password; + /* The proxy credentials can be set in the Network thread */ + synchronized (mNetwork) { + username = mNetwork.getProxyUsername(); + password = mNetwork.getProxyPassword(); + } + if (username != null && password != null) { + // we collect credentials ONLY if the proxy scheme is BASIC!!! + String proxyHeader = RequestHandle.authorizationHeader(true); + mHeaders.put(proxyHeader, + "Basic " + RequestHandle.computeBasicAuthResponse( + username, password)); + } + } + + // Set cookie header + String cookie = CookieManager.getInstance().getCookie( + mListener.getWebAddress()); + if (cookie != null && cookie.length() > 0) { + mHeaders.put("cookie", cookie); + } + } +} diff --git a/core/java/android/webkit/HttpAuthHandler.java b/core/java/android/webkit/HttpAuthHandler.java new file mode 100644 index 0000000..48b9eec --- /dev/null +++ b/core/java/android/webkit/HttpAuthHandler.java @@ -0,0 +1,186 @@ +/* + * 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.os.Bundle; +import android.os.Handler; +import android.os.Message; + +import java.util.ListIterator; +import java.util.LinkedList; + +/** + * HTTP authentication handler: local handler that takes care + * of HTTP authentication requests. This class is passed as a + * parameter to BrowserCallback.displayHttpAuthDialog and is + * meant to receive the user's response. + */ +public class HttpAuthHandler extends Handler { + /* It is important that the handler is in Network, because + * we want to share it accross multiple loaders and windows + * (like our subwindow and the main window). + */ + + private static final String LOGTAG = "network"; + + /** + * Network. + */ + private Network mNetwork; + + /** + * Loader queue. + */ + private LinkedList<LoadListener> mLoaderQueue; + + + // Message id for handling the user response + private final int AUTH_PROCEED = 100; + private final int AUTH_CANCEL = 200; + + /** + * Creates a new HTTP authentication handler with an empty + * loader queue + * + * @param network The parent network object + */ + /* package */ HttpAuthHandler(Network network) { + mNetwork = network; + mLoaderQueue = new LinkedList<LoadListener>(); + } + + + @Override + public void handleMessage(Message msg) { + LoadListener loader = null; + synchronized (mLoaderQueue) { + loader = mLoaderQueue.poll(); + } + + switch (msg.what) { + case AUTH_PROCEED: + String username = msg.getData().getString("username"); + String password = msg.getData().getString("password"); + + loader.handleAuthResponse(username, password); + break; + + case AUTH_CANCEL: + + mNetwork.resetHandlersAndStopLoading(loader.getFrame()); + break; + } + + processNextLoader(); + } + + + /** + * Proceed with the authorization with the given credentials + * + * @param username The username to use for authentication + * @param password The password to use for authentication + */ + public void proceed(String username, String password) { + Message msg = obtainMessage(AUTH_PROCEED); + msg.getData().putString("username", username); + msg.getData().putString("password", password); + sendMessage(msg); + } + + /** + * Cancel the authorization request + */ + public void cancel() { + sendMessage(obtainMessage(AUTH_CANCEL)); + } + + /** + * @return True if we can use user credentials on record + * (ie, if we did not fail trying to use them last time) + */ + public boolean useHttpAuthUsernamePassword() { + LoadListener loader = null; + synchronized (mLoaderQueue) { + loader = mLoaderQueue.peek(); + } + if (loader != null) { + return !loader.authCredentialsInvalid(); + } + + return false; + } + + /** + * Resets the HTTP-authentication request handler, removes + * all loaders that share the same BrowserFrame + * + * @param frame The browser frame + */ + /* package */ void reset(BrowserFrame frame) { + synchronized (mLoaderQueue) { + ListIterator<LoadListener> i = mLoaderQueue.listIterator(0); + while (i.hasNext()) { + LoadListener loader = i.next(); + if (frame == loader.getFrame()) { + i.remove(); + } + } + } + } + + /** + * Enqueues the loader, if the loader is the only element + * in the queue, starts processing the loader + * + * @param loader The loader that resulted in this http + * authentication request + */ + /* package */ void handleAuthRequest(LoadListener loader) { + boolean processNext = false; + + synchronized (mLoaderQueue) { + mLoaderQueue.offer(loader); + processNext = + (mLoaderQueue.size() == 1); + } + + if (processNext) { + processNextLoader(); + } + } + + /** + * Process the next loader in the queue (helper method) + */ + private void processNextLoader() { + LoadListener loader = null; + synchronized (mLoaderQueue) { + loader = mLoaderQueue.peek(); + } + if (loader != null) { + CallbackProxy proxy = loader.getFrame().getCallbackProxy(); + + String hostname = loader.proxyAuthenticate() ? + mNetwork.getProxyHostname() : loader.host(); + + String realm = loader.realm(); + + proxy.onReceivedHttpAuthRequest(this, hostname, realm); + } + } +} diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java new file mode 100644 index 0000000..c6ec2d2 --- /dev/null +++ b/core/java/android/webkit/HttpDateTime.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2007 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.text.format.Time; + +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + + +class HttpDateTime { + + /* + * Regular expression for parsing HTTP-date. + * + * Wdy, DD Mon YYYY HH:MM:SS GMT + * RFC 822, updated by RFC 1123 + * + * Weekday, DD-Mon-YY HH:MM:SS GMT + * RFC 850, obsoleted by RFC 1036 + * + * Wdy Mon DD HH:MM:SS YYYY + * ANSI C's asctime() format + * + * with following variations + * + * Wdy, DD-Mon-YYYY HH:MM:SS GMT + * Wdy, (SP)D Mon YYYY HH:MM:SS GMT + * Wdy,DD Mon YYYY HH:MM:SS GMT + * Wdy, DD-Mon-YY HH:MM:SS GMT + * Wdy, DD Mon YYYY HH:MM:SS -HHMM + * Wdy, DD Mon YYYY HH:MM:SS + * Wdy Mon (SP)D HH:MM:SS YYYY + * Wdy Mon DD HH:MM:SS YYYY GMT + */ + private static final String HTTP_DATE_RFC_REGEXP = + "([0-9]{1,2})[- ]([A-Za-z]{3,3})[- ]([0-9]{2,4})[ ]" + + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])"; + + private static final String HTTP_DATE_ANSIC_REGEXP = + "[ ]([A-Za-z]{3,3})[ ]+([0-9]{1,2})[ ]" + + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; + + /** + * The compiled version of the HTTP-date regular expressions. + */ + private static final Pattern HTTP_DATE_RFC_PATTERN = + Pattern.compile(HTTP_DATE_RFC_REGEXP); + private static final Pattern HTTP_DATE_ANSIC_PATTERN = + Pattern.compile(HTTP_DATE_ANSIC_REGEXP); + + private static class TimeOfDay { + int hour; + int minute; + int second; + } + + public static Long parse(String timeString) + throws IllegalArgumentException { + + int date = 1; + int month = Calendar.JANUARY; + int year = 1970; + TimeOfDay timeOfDay = new TimeOfDay(); + + Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); + if (rfcMatcher.find()) { + date = getDate(rfcMatcher.group(1)); + month = getMonth(rfcMatcher.group(2)); + year = getYear(rfcMatcher.group(3)); + timeOfDay = getTime(rfcMatcher.group(4)); + } else { + Matcher ansicMatcher = HTTP_DATE_ANSIC_PATTERN.matcher(timeString); + if (ansicMatcher.find()) { + month = getMonth(ansicMatcher.group(1)); + date = getDate(ansicMatcher.group(2)); + timeOfDay = getTime(ansicMatcher.group(3)); + year = getYear(ansicMatcher.group(4)); + } else { + throw new IllegalArgumentException(); + } + } + + // FIXME: Y2038 BUG! + if (year >= 2038) { + year = 2038; + month = Calendar.JANUARY; + date = 1; + } + + Time time = new Time(Time.TIMEZONE_UTC); + time.set(timeOfDay.second, timeOfDay.minute, timeOfDay.hour, date, + month, year); + return time.toMillis(false /* use isDst */); + } + + private static int getDate(String dateString) { + if (dateString.length() == 2) { + return (dateString.charAt(0) - '0') * 10 + + (dateString.charAt(1) - '0'); + } else { + return (dateString.charAt(0) - '0'); + } + } + + /* + * jan = 9 + 0 + 13 = 22 + * feb = 5 + 4 + 1 = 10 + * mar = 12 + 0 + 17 = 29 + * apr = 0 + 15 + 17 = 32 + * may = 12 + 0 + 24 = 36 + * jun = 9 + 20 + 13 = 42 + * jul = 9 + 20 + 11 = 40 + * aug = 0 + 20 + 6 = 26 + * sep = 18 + 4 + 15 = 37 + * oct = 14 + 2 + 19 = 35 + * nov = 13 + 14 + 21 = 48 + * dec = 3 + 4 + 2 = 9 + */ + private static int getMonth(String monthString) { + int hash = Character.toLowerCase(monthString.charAt(0)) + + Character.toLowerCase(monthString.charAt(1)) + + Character.toLowerCase(monthString.charAt(2)) - 3 * 'a'; + switch (hash) { + case 22: + return Calendar.JANUARY; + case 10: + return Calendar.FEBRUARY; + case 29: + return Calendar.MARCH; + case 32: + return Calendar.APRIL; + case 36: + return Calendar.MAY; + case 42: + return Calendar.JUNE; + case 40: + return Calendar.JULY; + case 26: + return Calendar.AUGUST; + case 37: + return Calendar.SEPTEMBER; + case 35: + return Calendar.OCTOBER; + case 48: + return Calendar.NOVEMBER; + case 9: + return Calendar.DECEMBER; + default: + throw new IllegalArgumentException(); + } + } + + private static int getYear(String yearString) { + if (yearString.length() == 2) { + int year = (yearString.charAt(0) - '0') * 10 + + (yearString.charAt(1) - '0'); + if (year >= 70) { + return year + 1900; + } else { + return year + 2000; + } + } else + return (yearString.charAt(0) - '0') * 1000 + + (yearString.charAt(1) - '0') * 100 + + (yearString.charAt(2) - '0') * 10 + + (yearString.charAt(3) - '0'); + } + + private static TimeOfDay getTime(String timeString) { + TimeOfDay time = new TimeOfDay(); + time.hour = (timeString.charAt(0) - '0') * 10 + + (timeString.charAt(1) - '0'); + time.minute = (timeString.charAt(3) - '0') * 10 + + (timeString.charAt(4) - '0'); + time.second = (timeString.charAt(6) - '0') * 10 + + (timeString.charAt(7) - '0'); + return time; + } +} diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java new file mode 100644 index 0000000..a0049ac --- /dev/null +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -0,0 +1,195 @@ +/* + * 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.os.Handler; +import android.os.Message; +import android.util.Config; +import android.util.Log; + +final class JWebCoreJavaBridge extends Handler { + // Identifier for the timer message. + private static final int TIMER_MESSAGE = 1; + // ID for servicing functionptr queue + private static final int FUNCPTR_MESSAGE = 2; + // Log system identifier. + private static final String LOGTAG = "webkit-timers"; + + // Native object pointer for interacting in native code. + private int mNativeBridge; + // Instant timer is used to implement a timer that needs to fire almost + // immediately. + private boolean mHasInstantTimer; + // Reference count the pause/resume of timers + private int mPauseTimerRefCount; + + /** + * Construct a new JWebCoreJavaBridge to interface with + * WebCore timers and cookies. + */ + public JWebCoreJavaBridge() { + nativeConstructor(); + } + + @Override + protected void finalize() { + nativeFinalize(); + } + + /** + * handleMessage + * @param msg The dispatched message. + * + * The only accepted message currently is TIMER_MESSAGE + */ + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case TIMER_MESSAGE: { + PerfChecker checker = new PerfChecker(); + // clear the flag so that sharedTimerFired() can set a new timer + mHasInstantTimer = false; + sharedTimerFired(); + checker.responseAlert("sharedTimer"); + break; + } + case FUNCPTR_MESSAGE: + nativeServiceFuncPtrQueue(); + break; + } + } + + // called from JNI side + private void signalServiceFuncPtrQueue() { + Message msg = obtainMessage(FUNCPTR_MESSAGE); + sendMessage(msg); + } + + private native void nativeServiceFuncPtrQueue(); + + /** + * Pause all timers. + */ + public void pause() { + if (--mPauseTimerRefCount == 0) { + setDeferringTimers(true); + } + } + + /** + * Resume all timers. + */ + public void resume() { + if (++mPauseTimerRefCount == 1) { + setDeferringTimers(false); + } + } + + /** + * Set WebCore cache size. + * @param bytes The cache size in bytes. + */ + public native void setCacheSize(int bytes); + + /** + * Store a cookie string associated with a url. + * @param url The url to be used as a key for the cookie. + * @param docUrl The policy base url used by WebCore. + * @param value The cookie string to be stored. + */ + private void setCookies(String url, String docUrl, String value) { + if (value.contains("\r") || value.contains("\n")) { + // for security reason, filter out '\r' and '\n' from the cookie + int size = value.length(); + StringBuilder buffer = new StringBuilder(size); + int i = 0; + while (i != -1 && i < size) { + int ir = value.indexOf('\r', i); + int in = value.indexOf('\n', i); + int newi = (ir == -1) ? in : (in == -1 ? ir : (ir < in ? ir + : in)); + if (newi > i) { + buffer.append(value.subSequence(i, newi)); + } else if (newi == -1) { + buffer.append(value.subSequence(i, size)); + break; + } + i = newi + 1; + } + value = buffer.toString(); + } + CookieManager.getInstance().setCookie(url, value); + } + + /** + * Retrieve the cookie string for the given url. + * @param url The resource's url. + * @return A String representing the cookies for the given resource url. + */ + private String cookies(String url) { + return CookieManager.getInstance().getCookie(url); + } + + /** + * Returns whether cookies are enabled or not. + */ + private boolean cookiesEnabled() { + return CookieManager.getInstance().acceptCookie(); + } + + /** + * setSharedTimer + * @param timemillis The relative time when the timer should fire + */ + private void setSharedTimer(long timemillis) { + if (Config.LOGV) Log.v(LOGTAG, "setSharedTimer " + timemillis); + + if (timemillis <= 0) { + // we don't accumulate the sharedTimer unless it is a delayed + // request. This way we won't flood the message queue with + // WebKit messages. This should improve the browser's + // responsiveness to key events. + if (mHasInstantTimer) { + return; + } else { + mHasInstantTimer = true; + Message msg = obtainMessage(TIMER_MESSAGE); + sendMessageDelayed(msg, timemillis); + } + } else { + Message msg = obtainMessage(TIMER_MESSAGE); + sendMessageDelayed(msg, timemillis); + } + } + + /** + * Stop the shared timer. + */ + private void stopSharedTimer() { + if (Config.LOGV) { + Log.v(LOGTAG, "stopSharedTimer removing all timers"); + } + removeMessages(TIMER_MESSAGE); + mHasInstantTimer = false; + } + + private native void nativeConstructor(); + private native void nativeFinalize(); + private native void sharedTimerFired(); + private native void setDeferringTimers(boolean defer); + public native void setNetworkOnLine(boolean online); +} diff --git a/core/java/android/webkit/JsPromptResult.java b/core/java/android/webkit/JsPromptResult.java new file mode 100644 index 0000000..9fcd1bc --- /dev/null +++ b/core/java/android/webkit/JsPromptResult.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2007 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; + + +/** + * Public class for handling javascript prompt requests. A + * JsDialogHandlerInterface implentation will receive a jsPrompt call with a + * JsPromptResult parameter. This parameter is used to return a result to + * WebView. The client can call cancel() to cancel the dialog or confirm() with + * the user's input to confirm the dialog. + */ +public class JsPromptResult extends JsResult { + // String result of the prompt + private String mStringResult; + + /** + * Handle a confirmation response from the user. + */ + public void confirm(String result) { + mStringResult = result; + confirm(); + } + + /*package*/ JsPromptResult(CallbackProxy proxy) { + super(proxy, /* unused */ false); + } + + /*package*/ String getStringResult() { + return mStringResult; + } + + @Override + /*package*/ void handleDefault() { + mStringResult = null; + super.handleDefault(); + } +} diff --git a/core/java/android/webkit/JsResult.java b/core/java/android/webkit/JsResult.java new file mode 100644 index 0000000..0c86e0a --- /dev/null +++ b/core/java/android/webkit/JsResult.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2007 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; + + +public class JsResult { + // This prevents a user from interacting with the result before WebCore is + // ready to handle it. + private boolean mReady; + // Tells us if the user tried to confirm or cancel the result before WebCore + // is ready. + private boolean mTriedToNotifyBeforeReady; + // This is a basic result of a confirm or prompt dialog. + protected boolean mResult; + // This is the caller of the prompt and is the object that is waiting. + protected final CallbackProxy mProxy; + // This is the default value of the result. + private final boolean mDefaultValue; + + /** + * Handle the result if the user cancelled the dialog. + */ + public final void cancel() { + mResult = false; + wakeUp(); + } + + /** + * Handle a confirmation response from the user. + */ + public final void confirm() { + mResult = true; + wakeUp(); + } + + /*package*/ JsResult(CallbackProxy proxy, boolean defaultVal) { + mProxy = proxy; + mDefaultValue = defaultVal; + } + + /*package*/ final boolean getResult() { + return mResult; + } + + /*package*/ final void setReady() { + mReady = true; + if (mTriedToNotifyBeforeReady) { + wakeUp(); + } + } + + /*package*/ void handleDefault() { + setReady(); + mResult = mDefaultValue; + wakeUp(); + } + + /* Wake up the WebCore thread. */ + protected final void wakeUp() { + if (mReady) { + synchronized (mProxy) { + mProxy.notify(); + } + } else { + mTriedToNotifyBeforeReady = true; + } + } +} diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java new file mode 100644 index 0000000..dfae17d --- /dev/null +++ b/core/java/android/webkit/LoadListener.java @@ -0,0 +1,1499 @@ +/* + * 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.content.Context; +import android.net.WebAddress; +import android.net.ParseException; +import android.net.http.EventHandler; +import android.net.http.Headers; +import android.net.http.HttpAuthHeader; +import android.net.http.RequestHandle; +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.net.http.SslCertificate; + +import android.os.Handler; +import android.os.Message; +import android.util.Config; +import android.util.Log; +import android.webkit.CacheManager.CacheResult; + +import com.android.internal.R; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import org.apache.commons.codec.binary.Base64; + +class LoadListener extends Handler implements EventHandler { + + private static final String LOGTAG = "webkit"; + + // Messages used internally to communicate state between the + // Network thread and the WebCore thread. + private static final int MSG_CONTENT_HEADERS = 100; + private static final int MSG_CONTENT_DATA = 110; + private static final int MSG_CONTENT_FINISHED = 120; + private static final int MSG_CONTENT_ERROR = 130; + private static final int MSG_LOCATION_CHANGED = 140; + private static final int MSG_LOCATION_CHANGED_REQUEST = 150; + private static final int MSG_STATUS = 160; + private static final int MSG_SSL_CERTIFICATE = 170; + private static final int MSG_SSL_ERROR = 180; + + // Standard HTTP status codes in a more representative format + private static final int HTTP_OK = 200; + private static final int HTTP_MOVED_PERMANENTLY = 301; + private static final int HTTP_FOUND = 302; + private static final int HTTP_SEE_OTHER = 303; + private static final int HTTP_NOT_MODIFIED = 304; + private static final int HTTP_TEMPORARY_REDIRECT = 307; + private static final int HTTP_AUTH = 401; + private static final int HTTP_NOT_FOUND = 404; + private static final int HTTP_PROXY_AUTH = 407; + + private static int sNativeLoaderCount; + + private final ByteArrayBuilder mDataBuilder = new ByteArrayBuilder(8192); + + private String mUrl; + private WebAddress mUri; + private boolean mPermanent; + private String mOriginalUrl; + private Context mContext; + private BrowserFrame mBrowserFrame; + private int mNativeLoader; + private String mMimeType; + private String mEncoding; + private String mTransferEncoding; + private int mStatusCode; + private String mStatusText; + public long mContentLength; // Content length of the incoming data + private boolean mCancelled; // The request has been cancelled. + private boolean mAuthFailed; // indicates that the prev. auth failed + private CacheLoader mCacheLoader; + private CacheManager.CacheResult mCacheResult; + private HttpAuthHeader mAuthHeader; + private int mErrorID = OK; + private String mErrorDescription; + private SslError mSslError; + private RequestHandle mRequestHandle; + + // Request data. It is only valid when we are doing a load from the + // cache. It is needed if the cache returns a redirect + private String mMethod; + private Map<String, String> mRequestHeaders; + private byte[] mPostData; + private boolean mIsHighPriority; + // Flag to indicate that this load is synchronous. + private boolean mSynchronous; + private Vector<Message> mMessageQueue; + + // Does this loader correspond to the main-frame top-level page? + private boolean mIsMainPageLoader; + + private Headers mHeaders; + + // ========================================================================= + // Public functions + // ========================================================================= + + public static LoadListener getLoadListener( + Context context, BrowserFrame frame, String url, + int nativeLoader, boolean synchronous, boolean isMainPageLoader) { + + sNativeLoaderCount += 1; + return new LoadListener( + context, frame, url, nativeLoader, synchronous, isMainPageLoader); + } + + public static int getNativeLoaderCount() { + return sNativeLoaderCount; + } + + LoadListener(Context context, BrowserFrame frame, String url, + int nativeLoader, boolean synchronous, boolean isMainPageLoader) { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener constructor url=" + url); + } + mContext = context; + mBrowserFrame = frame; + setUrl(url); + mNativeLoader = nativeLoader; + mMimeType = ""; + mEncoding = ""; + mSynchronous = synchronous; + if (synchronous) { + mMessageQueue = new Vector<Message>(); + } + mIsMainPageLoader = isMainPageLoader; + } + + /** + * We keep a count of refs to the nativeLoader so we do not create + * so many LoadListeners that the GREFs blow up + */ + private void clearNativeLoader() { + sNativeLoaderCount -= 1; + mNativeLoader = 0; + } + + /* + * This message handler is to facilitate communication between the network + * thread and the browser thread. + */ + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_CONTENT_HEADERS: + /* + * This message is sent when the LoadListener has headers + * available. The headers are sent onto WebCore to see what we + * should do with them. + */ + handleHeaders((Headers) msg.obj); + break; + + case MSG_CONTENT_DATA: + /* + * This message is sent when the LoadListener has data available + * in it's data buffer. This data buffer could be filled from a + * file (this thread) or from http (Network thread). + */ + if (mNativeLoader != 0 && !ignoreCallbacks()) { + commitLoad(); + } + break; + + case MSG_CONTENT_FINISHED: + /* + * This message is sent when the LoadListener knows that the + * load is finished. This message is not sent in the case of an + * error. + * + */ + handleEndData(); + break; + + case MSG_CONTENT_ERROR: + /* + * This message is sent when a load error has occured. The + * LoadListener will clean itself up. + */ + handleError(msg.arg1, (String) msg.obj); + break; + + case MSG_LOCATION_CHANGED: + /* + * This message is sent from LoadListener.endData to inform the + * browser activity that the location of the top level page + * changed. + */ + doRedirect(); + break; + + case MSG_LOCATION_CHANGED_REQUEST: + /* + * This message is sent from endData on receipt of a 307 + * Temporary Redirect in response to a POST -- the user must + * confirm whether to continue loading. If the user says Yes, + * we simply call MSG_LOCATION_CHANGED. If the user says No, + * we call MSG_CONTENT_FINISHED. + */ + Message contMsg = obtainMessage(MSG_LOCATION_CHANGED); + Message stopMsg = obtainMessage(MSG_CONTENT_FINISHED); + mBrowserFrame.getCallbackProxy().onFormResubmission( + stopMsg, contMsg); + break; + + case MSG_STATUS: + /* + * This message is sent from the network thread when the http + * stack has received the status response from the server. + */ + HashMap status = (HashMap) msg.obj; + handleStatus(((Integer) status.get("major")).intValue(), + ((Integer) status.get("minor")).intValue(), + ((Integer) status.get("code")).intValue(), + (String) status.get("reason")); + break; + + case MSG_SSL_CERTIFICATE: + /* + * This message is sent when the network thread receives a ssl + * certificate. + */ + handleCertificate((SslCertificate) msg.obj); + break; + + case MSG_SSL_ERROR: + /* + * This message is sent when the network thread encounters a + * ssl error. + */ + handleSslError((SslError) msg.obj); + break; + } + } + + /** + * @return The loader's BrowserFrame. + */ + BrowserFrame getFrame() { + return mBrowserFrame; + } + + Context getContext() { + return mContext; + } + + /* package */ boolean isSynchronous() { + return mSynchronous; + } + + /** + * @return True iff the load has been cancelled + */ + public boolean cancelled() { + return mCancelled; + } + + /** + * Parse the headers sent from the server. + * @param headers gives up the HeaderGroup + * IMPORTANT: as this is called from network thread, can't call native + * directly + */ + public void headers(Headers headers) { + if (Config.LOGV) Log.v(LOGTAG, "LoadListener.headers"); + sendMessageInternal(obtainMessage(MSG_CONTENT_HEADERS, headers)); + } + + // Does the header parsing work on the WebCore thread. + private void handleHeaders(Headers headers) { + if (mCancelled) return; + mHeaders = headers; + mMimeType = ""; + mEncoding = ""; + + ArrayList<String> cookies = headers.getSetCookie(); + for (int i = 0; i < cookies.size(); ++i) { + CookieManager.getInstance().setCookie(mUri, cookies.get(i)); + } + + long contentLength = headers.getContentLength(); + if (contentLength != Headers.NO_CONTENT_LENGTH) { + mContentLength = contentLength; + } else { + mContentLength = 0; + } + + String contentType = headers.getContentType(); + if (contentType != null) { + parseContentTypeHeader(contentType); + + // If we have one of "generic" MIME types, try to deduce + // the right MIME type from the file extension (if any): + if (mMimeType.equalsIgnoreCase("text/plain") || + mMimeType.equalsIgnoreCase("application/octet-stream")) { + + String newMimeType = guessMimeTypeFromExtension(); + if (newMimeType != null) { + mMimeType = newMimeType; + } + } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml")) { + // As we don't support wml, render it as plain text + mMimeType = "text/plain"; + } else { + // XXX: Until the servers send us either correct xhtml or + // text/html, treat application/xhtml+xml as text/html. + // It seems that xhtml+xml and vnd.wap.xhtml+xml mime + // subtypes are used interchangeably. So treat them the same. + if (mMimeType.equalsIgnoreCase("application/xhtml+xml") || + mMimeType.equals("application/vnd.wap.xhtml+xml")) { + mMimeType = "text/html"; + } + } + } else { + /* Often when servers respond with 304 Not Modified or a + Redirect, then they don't specify a MIMEType. When this + occurs, the function below is called. In the case of + 304 Not Modified, the cached headers are used rather + than the headers that are returned from the server. */ + guessMimeType(); + } + + // is it an authentication request? + boolean mustAuthenticate = (mStatusCode == HTTP_AUTH || + mStatusCode == HTTP_PROXY_AUTH); + // is it a proxy authentication request? + boolean isProxyAuthRequest = (mStatusCode == HTTP_PROXY_AUTH); + // is this authentication request due to a failed attempt to + // authenticate ealier? + mAuthFailed = false; + + // if we tried to authenticate ourselves last time + if (mAuthHeader != null) { + // we failed, if we must to authenticate again now and + // we have a proxy-ness match + mAuthFailed = (mustAuthenticate && + isProxyAuthRequest == mAuthHeader.isProxy()); + + // if we did NOT fail and last authentication request was a + // proxy-authentication request + if (!mAuthFailed && mAuthHeader.isProxy()) { + Network network = Network.getInstance(mContext); + // if we have a valid proxy set + if (network.isValidProxySet()) { + /* The proxy credentials can be read in the WebCore thread + */ + synchronized (network) { + // save authentication credentials for pre-emptive proxy + // authentication + network.setProxyUsername(mAuthHeader.getUsername()); + network.setProxyPassword(mAuthHeader.getPassword()); + } + } + } + } + // it is only here that we can reset the last mAuthHeader object + // (if existed) and start a new one!!! + mAuthHeader = null; + if (mustAuthenticate) { + if (mStatusCode == HTTP_AUTH) { + mAuthHeader = parseAuthHeader( + headers.getWwwAuthenticate()); + } else { + mAuthHeader = parseAuthHeader( + headers.getProxyAuthenticate()); + // if successfully parsed the header + if (mAuthHeader != null) { + // mark the auth-header object as a proxy + mAuthHeader.setProxy(); + } + } + } + + // Only create a cache file if the server has responded positively. + if ((mStatusCode == HTTP_OK || + mStatusCode == HTTP_FOUND || + mStatusCode == HTTP_MOVED_PERMANENTLY || + mStatusCode == HTTP_TEMPORARY_REDIRECT) && + mNativeLoader != 0) { + // Content arriving from a StreamLoader (eg File, Cache or Data) + // will not be cached as they have the header: + // cache-control: no-store + mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, + headers, mMimeType, false); + if (mCacheResult != null) { + mCacheResult.encoding = mEncoding; + } + } + commitHeadersCheckRedirect(); + } + + /** + * @return True iff this loader is in the proxy-authenticate state. + */ + boolean proxyAuthenticate() { + if (mAuthHeader != null) { + return mAuthHeader.isProxy(); + } + + return false; + } + + /** + * Report the status of the response. + * TODO: Comments about each parameter. + * IMPORTANT: as this is called from network thread, can't call native + * directly + */ + public void status(int majorVersion, int minorVersion, + int code, /* Status-Code value */ String reasonPhrase) { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener: from: " + mUrl + + " major: " + majorVersion + + " minor: " + minorVersion + + " code: " + code + + " reason: " + reasonPhrase); + } + HashMap status = new HashMap(); + status.put("major", majorVersion); + status.put("minor", minorVersion); + status.put("code", code); + status.put("reason", reasonPhrase); + sendMessageInternal(obtainMessage(MSG_STATUS, status)); + } + + // Handle the status callback on the WebCore thread. + private void handleStatus(int major, int minor, int code, String reason) { + if (mCancelled) return; + + mStatusCode = code; + mStatusText = reason; + mPermanent = false; + } + + /** + * Implementation of certificate handler for EventHandler. + * Called every time a resource is loaded via a secure + * connection. In this context, can be called multiple + * times if we have redirects + * @param certificate The SSL certifcate + * IMPORTANT: as this is called from network thread, can't call native + * directly + */ + public void certificate(SslCertificate certificate) { + sendMessageInternal(obtainMessage(MSG_SSL_CERTIFICATE, certificate)); + } + + // Handle the certificate on the WebCore thread. + private void handleCertificate(SslCertificate certificate) { + // if this is the top-most main-frame page loader + if (mIsMainPageLoader) { + // update the browser frame (ie, the main frame) + mBrowserFrame.certificate(certificate); + } + } + + /** + * Implementation of error handler for EventHandler. + * Subclasses should call this method to have error fields set. + * @param id The error id described by EventHandler. + * @param description A string description of the error. + * IMPORTANT: as this is called from network thread, can't call native + * directly + */ + public void error(int id, String description) { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.error url:" + + url() + " id:" + id + " description:" + description); + } + sendMessageInternal(obtainMessage(MSG_CONTENT_ERROR, id, 0, description)); + } + + // Handle the error on the WebCore thread. + private void handleError(int id, String description) { + mErrorID = id; + mErrorDescription = description; + detachRequestHandle(); + notifyError(); + tearDown(); + } + + /** + * Add data to the internal collection of data. This function is used by + * the data: scheme, about: scheme and http/https schemes. + * @param data A byte array containing the content. + * @param length The length of data. + * IMPORTANT: as this is called from network thread, can't call native + * directly + * XXX: Unlike the other network thread methods, this method can do the + * work of decoding the data and appending it to the data builder because + * mDataBuilder is a thread-safe structure. + */ + public void data(byte[] data, int length) { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.data(): url: " + url()); + } + + // Decode base64 data + // Note: It's fine that we only decode base64 here and not in the other + // data call because the only caller of the stream version is not + // base64 encoded. + if ("base64".equalsIgnoreCase(mTransferEncoding)) { + if (length < data.length) { + byte[] trimmedData = new byte[length]; + System.arraycopy(data, 0, trimmedData, 0, length); + data = trimmedData; + } + data = Base64.decodeBase64(data); + length = data.length; + } + // Synchronize on mData because commitLoad may write mData to WebCore + // and we don't want to replace mData or mDataLength at the same time + // as a write. + boolean sendMessage = false; + synchronized (mDataBuilder) { + sendMessage = mDataBuilder.isEmpty(); + mDataBuilder.append(data, 0, length); + } + if (sendMessage) { + // Send a message whenever data comes in after a write to WebCore + sendMessageInternal(obtainMessage(MSG_CONTENT_DATA)); + } + } + + /** + * Event handler's endData call. Send a message to the handler notifying + * them that the data has finished. + * IMPORTANT: as this is called from network thread, can't call native + * directly + */ + public void endData() { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.endData(): url: " + url()); + } + sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); + } + + // Handle the end of data. + private void handleEndData() { + if (mCancelled) return; + + switch (mStatusCode) { + case HTTP_MOVED_PERMANENTLY: + // 301 - permanent redirect + mPermanent = true; + case HTTP_FOUND: + case HTTP_SEE_OTHER: + case HTTP_TEMPORARY_REDIRECT: + // 301, 302, 303, and 307 - redirect + if (mStatusCode == HTTP_TEMPORARY_REDIRECT) { + if (mRequestHandle != null && + mRequestHandle.getMethod().equals("POST")) { + sendMessageInternal(obtainMessage( + MSG_LOCATION_CHANGED_REQUEST)); + } else if (mMethod != null && mMethod.equals("POST")) { + sendMessageInternal(obtainMessage( + MSG_LOCATION_CHANGED_REQUEST)); + } else { + sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); + } + } else { + sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); + } + return; + + case HTTP_AUTH: + case HTTP_PROXY_AUTH: + // According to rfc2616, the response for HTTP_AUTH must include + // WWW-Authenticate header field and the response for + // HTTP_PROXY_AUTH must include Proxy-Authenticate header field. + if (mAuthHeader != null && + (Network.getInstance(mContext).isValidProxySet() || + !mAuthHeader.isProxy())) { + Network.getInstance(mContext).handleAuthRequest(this); + return; + } + break; // use default + + case HTTP_NOT_MODIFIED: + // Server could send back NOT_MODIFIED even if we didn't + // ask for it, so make sure we have a valid CacheLoader + // before calling it. + if (mCacheLoader != null) { + detachRequestHandle(); + mCacheLoader.load(); + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener cache load url=" + url()); + } + return; + } + break; // use default + + case HTTP_NOT_FOUND: + // Not an error, the server can send back content. + default: + break; + } + detachRequestHandle(); + tearDown(); + } + + /* This method is called from CacheLoader when the initial request is + * serviced by the Cache. */ + /* package */ void setCacheLoader(CacheLoader c) { + mCacheLoader = c; + } + + /** + * Check the cache for the current URL, and load it if it is valid. + * + * @param headers for the request + * @return true if cached response is used. + */ + boolean checkCache(Map<String, String> headers) { + // Get the cache file name for the current URL + CacheResult result = CacheManager.getCacheFile(url(), + headers); + + // Go ahead and set the cache loader to null in case the result is + // null. + mCacheLoader = null; + + if (result != null) { + // The contents of the cache may need to be revalidated so just + // remember the cache loader in the case that the server responds + // positively to the cached content. This is also used to detect if + // a redirect came from the cache. + mCacheLoader = new CacheLoader(this, result); + + // If I got a cachedUrl and the revalidation header was not + // added, then the cached content valid, we should use it. + if (!headers.containsKey( + CacheManager.HEADER_KEY_IFNONEMATCH) && + !headers.containsKey( + CacheManager.HEADER_KEY_IFMODIFIEDSINCE)) { + if (Config.LOGV) { + Log.v(LOGTAG, "FrameLoader: HTTP URL in cache " + + "and usable: " + url()); + } + // Load the cached file + mCacheLoader.load(); + return true; + } + } + return false; + } + + /** + * SSL certificate error callback. Handles SSL error(s) on the way up + * to the user. + * IMPORTANT: as this is called from network thread, can't call native + * directly + */ + public void handleSslErrorRequest(SslError error) { + if (Config.LOGV) { + Log.v(LOGTAG, + "LoadListener.handleSslErrorRequest(): url:" + url() + + " primary error: " + error.getPrimaryError() + + " certificate: " + error.getCertificate()); + } + sendMessageInternal(obtainMessage(MSG_SSL_ERROR, error)); + } + + // Handle the ssl error on the WebCore thread. + private void handleSslError(SslError error) { + if (!mCancelled) { + mSslError = error; + Network.getInstance(mContext).handleSslErrorRequest(this); + } + } + + /** + * @return HTTP authentication realm or null if none. + */ + String realm() { + if (mAuthHeader == null) { + return null; + } else { + return mAuthHeader.getRealm(); + } + } + + /** + * Returns true iff an HTTP authentication problem has + * occured (credentials invalid). + */ + boolean authCredentialsInvalid() { + // if it is digest and the nonce is stale, we just + // resubmit with a new nonce + return (mAuthFailed && + !(mAuthHeader.isDigest() && mAuthHeader.getStale())); + } + + /** + * @return The last SSL error or null if there is none + */ + SslError sslError() { + return mSslError; + } + + /** + * Handles SSL error(s) on the way down from the user + * (the user has already provided their feedback). + */ + void handleSslErrorResponse(boolean proceed) { + if (mRequestHandle != null) { + mRequestHandle.handleSslErrorResponse(proceed); + } + } + + /** + * Uses user-supplied credentials to restar a request. + */ + void handleAuthResponse(String username, String password) { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.handleAuthResponse: url: " + mUrl + + " username: " + username + + " password: " + password); + } + + // create and queue an authentication-response + if (username != null && password != null) { + if (mAuthHeader != null && mRequestHandle != null) { + mAuthHeader.setUsername(username); + mAuthHeader.setPassword(password); + + int scheme = mAuthHeader.getScheme(); + if (scheme == HttpAuthHeader.BASIC) { + // create a basic response + boolean isProxy = mAuthHeader.isProxy(); + + mRequestHandle.setupBasicAuthResponse(isProxy, + username, password); + } else { + if (scheme == HttpAuthHeader.DIGEST) { + // create a digest response + boolean isProxy = mAuthHeader.isProxy(); + + String realm = mAuthHeader.getRealm(); + String nonce = mAuthHeader.getNonce(); + String qop = mAuthHeader.getQop(); + String algorithm = mAuthHeader.getAlgorithm(); + String opaque = mAuthHeader.getOpaque(); + + mRequestHandle.setupDigestAuthResponse + (isProxy, username, password, realm, + nonce, qop, algorithm, opaque); + } + } + } + } + } + + /** + * This is called when a request can be satisfied by the cache, however, + * the cache result could be a redirect. In this case we need to issue + * the network request. + * @param method + * @param headers + * @param postData + * @param isHighPriority + */ + void setRequestData(String method, Map<String, String> headers, + byte[] postData, boolean isHighPriority) { + mMethod = method; + mRequestHeaders = headers; + mPostData = postData; + mIsHighPriority = isHighPriority; + } + + /** + * @return The current URL associated with this load. + */ + String url() { + return mUrl; + } + + /** + * @return The current WebAddress associated with this load. + */ + WebAddress getWebAddress() { + return mUri; + } + + /** + * @return URL hostname (current URL). + */ + String host() { + if (mUri != null) { + return mUri.mHost; + } + + return null; + } + + /** + * @return The original URL associated with this load. + */ + String originalUrl() { + if (mOriginalUrl != null) { + return mOriginalUrl; + } else { + return mUrl; + } + } + + void attachRequestHandle(RequestHandle requestHandle) { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + + "requestHandle: " + requestHandle); + } + mRequestHandle = requestHandle; + } + + void detachRequestHandle() { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.detachRequestHandle(): " + + "requestHandle: " + mRequestHandle); + } + mRequestHandle = null; + } + + /* + * This function is called from native WebCore code to + * notify this LoadListener that the content it is currently + * downloading should be saved to a file and not sent to + * WebCore. + */ + void downloadFile() { + // Setting the Cache Result to null ensures that this + // content is not added to the cache + mCacheResult = null; + + // Inform the client that they should download a file + mBrowserFrame.getCallbackProxy().onDownloadStart(url(), + mBrowserFrame.getUserAgentString(), + mHeaders.getContentDisposition(), + mMimeType, mContentLength); + + // Cancel the download. We need to stop the http load. + // The native loader object will get cleared by the call to + // cancel() but will also be cleared on the WebCore side + // when this function returns. + cancel(); + } + + /* + * This function is called from native WebCore code to + * find out if the given URL is in the cache, and if it can + * be used. This is just for forward/back navigation to a POST + * URL. + */ + static boolean willLoadFromCache(String url) { + boolean inCache = CacheManager.getCacheFile(url, null) != null; + if (Config.LOGV) { + Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + + inCache); + } + return inCache; + } + + /* + * Reset the cancel flag. This is used when we are resuming a stopped + * download. To suspend a download, we cancel it. It can also be cancelled + * when it has run out of disk space. In this situation, the download + * can be resumed. + */ + void resetCancel() { + mCancelled = false; + } + + String mimeType() { + return mMimeType; + } + + /* + * Return the size of the content being downloaded. This represents the + * full content size, even under the situation where the download has been + * resumed after interruption. + * + * @ return full content size + */ + long contentLength() { + return mContentLength; + } + + // Commit the headers if the status code is not a redirect. + private void commitHeadersCheckRedirect() { + if (mCancelled) return; + + // do not call webcore if it is redirect. According to the code in + // InspectorController::willSendRequest(), the response is only updated + // when it is not redirect. + if ((mStatusCode >= 301 && mStatusCode <= 303) || mStatusCode == 307) { + return; + } + + commitHeaders(); + } + + // This commits the headers without checking the response status code. + private void commitHeaders() { + // Commit the headers to WebCore + int nativeResponse = createNativeResponse(); + // The native code deletes the native response object. + nativeReceivedResponse(nativeResponse); + } + + /** + * Create a WebCore response object so that it can be used by + * nativeReceivedResponse or nativeRedirectedToUrl + * @return native response pointer + */ + private int createNativeResponse() { + // The reason we change HTTP_NOT_MODIFIED to HTTP_OK is because we know + // that WebCore never sends the if-modified-since header. Our + // CacheManager does it for us. If the server responds with a 304, then + // we treat it like it was a 200 code and proceed with loading the file + // from the cache. + int statusCode = mStatusCode == HTTP_NOT_MODIFIED + ? HTTP_OK : mStatusCode; + // pass content-type content-length and content-encoding + final int nativeResponse = nativeCreateResponse( + mUrl, statusCode, mStatusText, + mMimeType, mContentLength, mEncoding, + mCacheResult == null ? 0 : mCacheResult.expires / 1000); + if (mHeaders != null) { + mHeaders.getHeaders(new Headers.HeaderCallback() { + public void header(String name, String value) { + nativeSetResponseHeader(nativeResponse, name, value); + } + }); + } + return nativeResponse; + } + + /** + * Commit the load. It should be ok to call repeatedly but only before + * tearDown is called. + */ + private void commitLoad() { + if (mCancelled) return; + + // Give the data to WebKit now + PerfChecker checker = new PerfChecker(); + ByteArrayBuilder.Chunk c; + while (true) { + c = mDataBuilder.getFirstChunk(); + if (c == null) break; + + if (c.mLength != 0) { + if (mCacheResult != null) { + try { + mCacheResult.outStream.write(c.mArray, 0, c.mLength); + } catch (IOException e) { + mCacheResult = null; + } + } + nativeAddData(c.mArray, c.mLength); + } + mDataBuilder.releaseChunk(c); + checker.responseAlert("res nativeAddData"); + } + } + + /** + * Tear down the load. Subclasses should clean up any mess because of + * cancellation or errors during the load. + */ + void tearDown() { + if (mCacheResult != null) { + if (getErrorID() == OK) { + CacheManager.saveCacheFile(mUrl, mCacheResult); + } + + // we need to reset mCacheResult to be null + // resource loader's tearDown will call into WebCore's + // nativeFinish, which in turn calls loader.cancel(). + // If we don't reset mCacheFile, the file will be deleted. + mCacheResult = null; + } + if (mNativeLoader != 0) { + PerfChecker checker = new PerfChecker(); + nativeFinished(); + checker.responseAlert("res nativeFinished"); + clearNativeLoader(); + } + } + + /** + * Helper for getting the error ID. + * @return errorID. + */ + private int getErrorID() { + return mErrorID; + } + + /** + * Return the error description. + * @return errorDescription. + */ + private String getErrorDescription() { + return mErrorDescription; + } + + /** + * Notify the loader we encountered an error. + */ + void notifyError() { + if (mNativeLoader != 0) { + String description = getErrorDescription(); + if (description == null) description = ""; + nativeError(getErrorID(), description, url()); + clearNativeLoader(); + } + } + + /** + * Cancel a request. + * FIXME: This will only work if the request has yet to be handled. This + * is in no way guarenteed if requests are served in a separate thread. + * It also causes major problems if cancel is called during an + * EventHandler's method call. + */ + public void cancel() { + if (Config.LOGV) { + if (mRequestHandle == null) { + Log.v(LOGTAG, "LoadListener.cancel(): no requestHandle"); + } else { + Log.v(LOGTAG, "LoadListener.cancel()"); + } + } + if (mRequestHandle != null) { + mRequestHandle.cancel(); + mRequestHandle = null; + } + + mCacheResult = null; + mCancelled = true; + + clearNativeLoader(); + } + + // This count is transferred from RequestHandle to LoadListener when + // loading from the cache so that we can detect redirect loops that switch + // between the network and the cache. + private int mCacheRedirectCount; + + /* + * Perform the actual redirection. This involves setting up the new URL, + * informing WebCore and then telling the Network to start loading again. + */ + private void doRedirect() { + // as cancel() can cancel the load before doRedirect() is + // called through handleMessage, needs to check to see if we + // are canceled before proceed + if (mCancelled) { + return; + } + + // Do the same check for a redirect loop that + // RequestHandle.setupRedirect does. + if (mCacheRedirectCount >= RequestHandle.MAX_REDIRECT_COUNT) { + handleError(EventHandler.ERROR_REDIRECT_LOOP, mContext.getString( + R.string.httpErrorRedirectLoop)); + return; + } + + String redirectTo = mHeaders.getLocation(); + if (redirectTo != null) { + int nativeResponse = createNativeResponse(); + redirectTo = + nativeRedirectedToUrl(mUrl, redirectTo, nativeResponse); + // nativeRedirectedToUrl() may call cancel(), e.g. when redirect + // from a https site to a http site, check mCancelled again + if (mCancelled) { + return; + } + if (redirectTo == null) { + Log.d(LOGTAG, "Redirection failed for " + + mHeaders.getLocation()); + cancel(); + return; + } else if (!URLUtil.isNetworkUrl(redirectTo)) { + final String text = mContext + .getString(R.string.open_permission_deny) + + "\n" + redirectTo; + nativeAddData(text.getBytes(), text.length()); + nativeFinished(); + clearNativeLoader(); + return; + } + + if (mOriginalUrl == null) { + mOriginalUrl = mUrl; + } + + // Cache the redirect response + if (mCacheResult != null) { + if (getErrorID() == OK) { + CacheManager.saveCacheFile(mUrl, mCacheResult); + } + mCacheResult = null; + } + + setUrl(redirectTo); + + // Redirect may be in the cache + if (mRequestHeaders == null) { + mRequestHeaders = new HashMap<String, String>(); + } + boolean fromCache = false; + if (mCacheLoader != null) { + // This is a redirect from the cache loader. Increment the + // redirect count to avoid redirect loops. + mCacheRedirectCount++; + fromCache = true; + } + if (!checkCache(mRequestHeaders)) { + // mRequestHandle can be null when the request was satisfied + // by the cache, and the cache returned a redirect + if (mRequestHandle != null) { + mRequestHandle.setupRedirect(redirectTo, mStatusCode, + mRequestHeaders); + } else { + // If the original request came from the cache, there is no + // RequestHandle, we have to create a new one through + // Network.requestURL. + Network network = Network.getInstance(getContext()); + if (!network.requestURL(mMethod, mRequestHeaders, + mPostData, this, mIsHighPriority)) { + // Signal a bad url error if we could not load the + // redirection. + handleError(EventHandler.ERROR_BAD_URL, + mContext.getString(R.string.httpErrorBadUrl)); + return; + } + } + if (fromCache) { + // If we are coming from a cache load, we need to transfer + // the redirect count to the new (or old) RequestHandle to + // keep the redirect count in sync. + mRequestHandle.setRedirectCount(mCacheRedirectCount); + } + } else if (!fromCache) { + // Switching from network to cache means we need to grab the + // redirect count from the RequestHandle to keep the count in + // sync. Add 1 to account for the current redirect. + mCacheRedirectCount = mRequestHandle.getRedirectCount() + 1; + } + // Clear the buffered data since the redirect is valid. + mDataBuilder.clear(); + } else { + commitHeaders(); + commitLoad(); + tearDown(); + } + + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.onRedirect(): redirect to: " + + redirectTo); + } + } + + /** + * Parses the content-type header. + */ + private static final Pattern CONTENT_TYPE_PATTERN = + Pattern.compile("^([a-zA-Z\\*]+/[\\w\\+\\*-]+[\\.[\\w\\+-]+]*)$"); + + private void parseContentTypeHeader(String contentType) { + if (Config.LOGV) { + Log.v(LOGTAG, "LoadListener.parseContentTypeHeader: " + + "contentType: " + contentType); + } + + if (contentType != null) { + int i = contentType.indexOf(';'); + if (i >= 0) { + mMimeType = contentType.substring(0, i); + + int j = contentType.indexOf('=', i); + if (j > 0) { + i = contentType.indexOf(';', j); + if (i < j) { + i = contentType.length(); + } + mEncoding = contentType.substring(j + 1, i); + } else { + mEncoding = contentType.substring(i + 1); + } + // Trim excess whitespace. + mEncoding = mEncoding.trim(); + + if (i < contentType.length() - 1) { + // for data: uri the mimeType and encoding have + // the form image/jpeg;base64 or text/plain;charset=utf-8 + // or text/html;charset=utf-8;base64 + mTransferEncoding = contentType.substring(i + 1).trim(); + } + } else { + mMimeType = contentType; + } + + // Trim leading and trailing whitespace + mMimeType = mMimeType.trim(); + + try { + Matcher m = CONTENT_TYPE_PATTERN.matcher(mMimeType); + if (m.find()) { + mMimeType = m.group(1); + } else { + guessMimeType(); + } + } catch (IllegalStateException ex) { + guessMimeType(); + } + } + } + + /** + * @return The HTTP-authentication object or null if there + * is no supported scheme in the header. + * If there are several valid schemes present, we pick the + * strongest one. If there are several schemes of the same + * strength, we pick the one that comes first. + */ + private HttpAuthHeader parseAuthHeader(String header) { + if (header != null) { + int posMax = 256; + int posLen = 0; + int[] pos = new int [posMax]; + + int headerLen = header.length(); + if (headerLen > 0) { + // first, we find all unquoted instances of 'Basic' and 'Digest' + boolean quoted = false; + for (int i = 0; i < headerLen && posLen < posMax; ++i) { + if (header.charAt(i) == '\"') { + quoted = !quoted; + } else { + if (!quoted) { + if (header.regionMatches(true, i, + HttpAuthHeader.BASIC_TOKEN, 0, + HttpAuthHeader.BASIC_TOKEN.length())) { + pos[posLen++] = i; + continue; + } + + if (header.regionMatches(true, i, + HttpAuthHeader.DIGEST_TOKEN, 0, + HttpAuthHeader.DIGEST_TOKEN.length())) { + pos[posLen++] = i; + continue; + } + } + } + } + } + + if (posLen > 0) { + // consider all digest schemes first (if any) + for (int i = 0; i < posLen; i++) { + if (header.regionMatches(true, pos[i], + HttpAuthHeader.DIGEST_TOKEN, 0, + HttpAuthHeader.DIGEST_TOKEN.length())) { + String sub = header.substring(pos[i], + (i + 1 < posLen ? pos[i + 1] : headerLen)); + + HttpAuthHeader rval = new HttpAuthHeader(sub); + if (rval.isSupportedScheme()) { + // take the first match + return rval; + } + } + } + + // ...then consider all basic schemes (if any) + for (int i = 0; i < posLen; i++) { + if (header.regionMatches(true, pos[i], + HttpAuthHeader.BASIC_TOKEN, 0, + HttpAuthHeader.BASIC_TOKEN.length())) { + String sub = header.substring(pos[i], + (i + 1 < posLen ? pos[i + 1] : headerLen)); + + HttpAuthHeader rval = new HttpAuthHeader(sub); + if (rval.isSupportedScheme()) { + // take the first match + return rval; + } + } + } + } + } + + return null; + } + + /** + * If the content is a redirect or not modified we should not send + * any data into WebCore as that will cause it create a document with + * the data, then when we try to provide the real content, it will assert. + * + * @return True iff the callback should be ignored. + */ + private boolean ignoreCallbacks() { + return (mCancelled || mAuthHeader != null || + (mStatusCode > 300 && mStatusCode < 400)); + } + + /** + * Sets the current URL associated with this load. + */ + void setUrl(String url) { + if (url != null) { + if (URLUtil.isDataUrl(url)) { + // Don't strip anchor as that is a valid part of the URL + mUrl = url; + } else { + mUrl = URLUtil.stripAnchor(url); + } + mUri = null; + if (URLUtil.isNetworkUrl(mUrl)) { + try { + mUri = new WebAddress(mUrl); + } catch (ParseException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Guesses MIME type if one was not specified. Defaults to 'text/html'. In + * addition, tries to guess the MIME type based on the extension. + * + */ + private void guessMimeType() { + // Data urls must have a valid mime type or a blank string for the mime + // type (implying text/plain). + if (URLUtil.isDataUrl(mUrl) && mMimeType.length() != 0) { + cancel(); + final String text = mContext.getString(R.string.httpErrorBadUrl); + handleError(EventHandler.ERROR_BAD_URL, text); + } else { + // Note: This is ok because this is used only for the main content + // of frames. If no content-type was specified, it is fine to + // default to text/html. + mMimeType = "text/html"; + String newMimeType = guessMimeTypeFromExtension(); + if (newMimeType != null) { + mMimeType = newMimeType; + } + } + } + + /** + * guess MIME type based on the file extension. + */ + private String guessMimeTypeFromExtension() { + // PENDING: need to normalize url + if (Config.LOGV) { + Log.v(LOGTAG, "guessMimeTypeFromExtension: mURL = " + mUrl); + } + + String mimeType = + MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(mUrl)); + + if (mimeType != null) { + // XXX: Until the servers send us either correct xhtml or + // text/html, treat application/xhtml+xml as text/html. + if (mimeType.equals("application/xhtml+xml")) { + mimeType = "text/html"; + } + } + + return mimeType; + } + + /** + * Either send a message to ourselves or queue the message if this is a + * synchronous load. + */ + private void sendMessageInternal(Message msg) { + if (mSynchronous) { + mMessageQueue.add(msg); + } else { + sendMessage(msg); + } + } + + /** + * Cycle through our messages for synchronous loads. + */ + /* package */ void loadSynchronousMessages() { + if (Config.DEBUG && !mSynchronous) { + throw new AssertionError(); + } + // Note: this can be called twice if it is a synchronous network load, + // and there is a cache, but it needs to go to network to validate. If + // validation succeed, the CacheLoader is used so this is first called + // from http thread. Then it is called again from WebViewCore thread + // after the load is completed. So make sure the queue is cleared but + // don't set it to null. + for (int size = mMessageQueue.size(); size > 0; size--) { + handleMessage(mMessageQueue.remove(0)); + } + } + + //========================================================================= + // native functions + //========================================================================= + + /** + * Create a new native response object. + * @param url The url of the resource. + * @param statusCode The HTTP status code. + * @param statusText The HTTP status text. + * @param mimeType HTTP content-type. + * @param expectedLength An estimate of the content length or the length + * given by the server. + * @param encoding HTTP encoding. + * @param expireTime HTTP expires converted to seconds since the epoch. + * @return The native response pointer. + */ + private native int nativeCreateResponse(String url, int statusCode, + String statusText, String mimeType, long expectedLength, + String encoding, long expireTime); + + /** + * Add a response header to the native object. + * @param nativeResponse The native pointer. + * @param key String key. + * @param val String value. + */ + private native void nativeSetResponseHeader(int nativeResponse, String key, + String val); + + /** + * Dispatch the response. + * @param nativeResponse The native pointer. + */ + private native void nativeReceivedResponse(int nativeResponse); + + /** + * Add data to the loader. + * @param data Byte array of data. + * @param length Number of objects in data. + */ + private native void nativeAddData(byte[] data, int length); + + /** + * Tell the loader it has finished. + */ + private native void nativeFinished(); + + /** + * tell the loader to redirect + * @param baseUrl The base url. + * @param redirectTo The url to redirect to. + * @param nativeResponse The native pointer. + * @return The new url that the resource redirected to. + */ + private native String nativeRedirectedToUrl(String baseUrl, + String redirectTo, int nativeResponse); + + /** + * Tell the loader there is error + * @param id + * @param desc + * @param failingUrl The url that failed. + */ + private native void nativeError(int id, String desc, String failingUrl); + +} diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java new file mode 100644 index 0000000..c9cc208 --- /dev/null +++ b/core/java/android/webkit/MimeTypeMap.java @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2007 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 java.util.HashMap; +import java.util.regex.Pattern; + +/** + * Two-way map that maps MIME-types to file extensions and vice versa. + */ +public /* package */ class MimeTypeMap { + + /** + * Singleton MIME-type map instance: + */ + private static MimeTypeMap sMimeTypeMap; + + /** + * MIME-type to file extension mapping: + */ + private HashMap<String, String> mMimeTypeToExtensionMap; + + /** + * File extension to MIME type mapping: + */ + private HashMap<String, String> mExtensionToMimeTypeMap; + + + /** + * Creates a new MIME-type map. + */ + private MimeTypeMap() { + mMimeTypeToExtensionMap = new HashMap<String, String>(); + mExtensionToMimeTypeMap = new HashMap<String, String>(); + } + + /** + * Returns the file extension or an empty string iff there is no + * extension. + */ + public static String getFileExtensionFromUrl(String url) { + if (url != null && url.length() > 0) { + int query = url.lastIndexOf('?'); + if (query > 0) { + url = url.substring(0, query); + } + int filenamePos = url.lastIndexOf('/'); + String filename = + 0 <= filenamePos ? url.substring(filenamePos + 1) : url; + + // if the filename contains special characters, we don't + // consider it valid for our matching purposes: + if (filename.length() > 0 && + Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)]+", filename)) { + int dotPos = filename.lastIndexOf('.'); + if (0 <= dotPos) { + return filename.substring(dotPos + 1); + } + } + } + + return ""; + } + + /** + * Load an entry into the map. This does not check if the item already + * exists, it trusts the caller! + */ + private void loadEntry(String mimeType, String extension, + boolean textType) { + // + // if we have an existing x --> y mapping, we do not want to + // override it with another mapping x --> ? + // this is mostly because of the way the mime-type map below + // is constructed (if a mime type maps to several extensions + // the first extension is considered the most popular and is + // added first; we do not want to overwrite it later). + // + if (!mMimeTypeToExtensionMap.containsKey(mimeType)) { + mMimeTypeToExtensionMap.put(mimeType, extension); + } + + // + // here, we don't want to map extensions to text MIME types; + // otherwise, we will start replacing generic text/plain and + // text/html with text MIME types that our platform does not + // understand. + // + if (!textType) { + mExtensionToMimeTypeMap.put(extension, mimeType); + } + } + + /** + * @return True iff there is a mimeType entry in the map. + */ + public boolean hasMimeType(String mimeType) { + if (mimeType != null && mimeType.length() > 0) { + return mMimeTypeToExtensionMap.containsKey(mimeType); + } + + return false; + } + + /** + * @return The extension for the MIME type or null iff there is none. + */ + public String getMimeTypeFromExtension(String extension) { + if (extension != null && extension.length() > 0) { + return mExtensionToMimeTypeMap.get(extension); + } + + return null; + } + + /** + * @return True iff there is an extension entry in the map. + */ + public boolean hasExtension(String extension) { + if (extension != null && extension.length() > 0) { + return mExtensionToMimeTypeMap.containsKey(extension); + } + + return false; + } + + /** + * @return The MIME type for the extension or null iff there is none. + */ + public String getExtensionFromMimeType(String mimeType) { + if (mimeType != null && mimeType.length() > 0) { + return mMimeTypeToExtensionMap.get(mimeType); + } + + return null; + } + + /** + * @return The singleton instance of the MIME-type map. + */ + public static MimeTypeMap getSingleton() { + if (sMimeTypeMap == null) { + sMimeTypeMap = new MimeTypeMap(); + + // The following table is based on /etc/mime.types data minus + // chemical/* MIME types and MIME types that don't map to any + // file extensions. We also exclude top-level domain names to + // deal with cases like: + // + // mail.google.com/a/google.com + // + // and "active" MIME types (due to potential security issues). + // + // Also, notice that not all data from this table is actually + // added (see loadEntry method for more details). + + sMimeTypeMap.loadEntry("application/andrew-inset", "ez", false); + sMimeTypeMap.loadEntry("application/dsptype", "tsp", false); + sMimeTypeMap.loadEntry("application/futuresplash", "spl", false); + sMimeTypeMap.loadEntry("application/hta", "hta", false); + sMimeTypeMap.loadEntry("application/mac-binhex40", "hqx", false); + sMimeTypeMap.loadEntry("application/mac-compactpro", "cpt", false); + sMimeTypeMap.loadEntry("application/mathematica", "nb", false); + sMimeTypeMap.loadEntry("application/msaccess", "mdb", false); + sMimeTypeMap.loadEntry("application/oda", "oda", false); + sMimeTypeMap.loadEntry("application/ogg", "ogg", false); + sMimeTypeMap.loadEntry("application/pdf", "pdf", false); + sMimeTypeMap.loadEntry("application/pgp-keys", "key", false); + sMimeTypeMap.loadEntry("application/pgp-signature", "pgp", false); + sMimeTypeMap.loadEntry("application/pics-rules", "prf", false); + sMimeTypeMap.loadEntry("application/rar", "rar", false); + sMimeTypeMap.loadEntry("application/rdf+xml", "rdf", false); + sMimeTypeMap.loadEntry("application/rss+xml", "rss", false); + sMimeTypeMap.loadEntry("application/zip", "zip", false); + sMimeTypeMap.loadEntry("application/vnd.android.package-archive", + "apk", false); + sMimeTypeMap.loadEntry("application/vnd.cinderella", "cdy", false); + sMimeTypeMap.loadEntry("application/vnd.ms-pki.stl", "stl", false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.database", "odb", + false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.formula", "odf", + false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.graphics", "odg", + false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.graphics-template", + "otg", false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.image", "odi", false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.spreadsheet", "ods", + false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.spreadsheet-template", + "ots", false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text", "odt", false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-master", "odm", + false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-template", "ott", + false); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-web", "oth", + false); + sMimeTypeMap.loadEntry("application/vnd.rim.cod", "cod", false); + sMimeTypeMap.loadEntry("application/vnd.smaf", "mmf", false); + sMimeTypeMap.loadEntry("application/vnd.stardivision.calc", "sdc", + false); + sMimeTypeMap.loadEntry("application/vnd.stardivision.draw", "sda", + false); + sMimeTypeMap.loadEntry( + "application/vnd.stardivision.impress", "sdd", false); + sMimeTypeMap.loadEntry( + "application/vnd.stardivision.impress", "sdp", false); + sMimeTypeMap.loadEntry("application/vnd.stardivision.math", "smf", + false); + sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "sdw", + false); + sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "vor", + false); + sMimeTypeMap.loadEntry( + "application/vnd.stardivision.writer-global", "sgl", false); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.calc", "sxc", + false); + sMimeTypeMap.loadEntry( + "application/vnd.sun.xml.calc.template", "stc", false); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.draw", "sxd", + false); + sMimeTypeMap.loadEntry( + "application/vnd.sun.xml.draw.template", "std", false); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.impress", "sxi", + false); + sMimeTypeMap.loadEntry( + "application/vnd.sun.xml.impress.template", "sti", false); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.math", "sxm", + false); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.writer", "sxw", + false); + sMimeTypeMap.loadEntry( + "application/vnd.sun.xml.writer.global", "sxg", false); + sMimeTypeMap.loadEntry( + "application/vnd.sun.xml.writer.template", "stw", false); + sMimeTypeMap.loadEntry("application/vnd.visio", "vsd", false); + sMimeTypeMap.loadEntry("application/x-abiword", "abw", false); + sMimeTypeMap.loadEntry("application/x-apple-diskimage", "dmg", + false); + sMimeTypeMap.loadEntry("application/x-bcpio", "bcpio", false); + sMimeTypeMap.loadEntry("application/x-bittorrent", "torrent", + false); + sMimeTypeMap.loadEntry("application/x-cdf", "cdf", false); + sMimeTypeMap.loadEntry("application/x-cdlink", "vcd", false); + sMimeTypeMap.loadEntry("application/x-chess-pgn", "pgn", false); + sMimeTypeMap.loadEntry("application/x-cpio", "cpio", false); + sMimeTypeMap.loadEntry("application/x-debian-package", "deb", + false); + sMimeTypeMap.loadEntry("application/x-debian-package", "udeb", + false); + sMimeTypeMap.loadEntry("application/x-director", "dcr", false); + sMimeTypeMap.loadEntry("application/x-director", "dir", false); + sMimeTypeMap.loadEntry("application/x-director", "dxr", false); + sMimeTypeMap.loadEntry("application/x-dms", "dms", false); + sMimeTypeMap.loadEntry("application/x-doom", "wad", false); + sMimeTypeMap.loadEntry("application/x-dvi", "dvi", false); + sMimeTypeMap.loadEntry("application/x-flac", "flac", false); + sMimeTypeMap.loadEntry("application/x-font", "pfa", false); + sMimeTypeMap.loadEntry("application/x-font", "pfb", false); + sMimeTypeMap.loadEntry("application/x-font", "gsf", false); + sMimeTypeMap.loadEntry("application/x-font", "pcf", false); + sMimeTypeMap.loadEntry("application/x-font", "pcf.Z", false); + sMimeTypeMap.loadEntry("application/x-freemind", "mm", false); + sMimeTypeMap.loadEntry("application/x-futuresplash", "spl", false); + sMimeTypeMap.loadEntry("application/x-gnumeric", "gnumeric", false); + sMimeTypeMap.loadEntry("application/x-go-sgf", "sgf", false); + sMimeTypeMap.loadEntry("application/x-graphing-calculator", "gcf", + false); + sMimeTypeMap.loadEntry("application/x-gtar", "gtar", false); + sMimeTypeMap.loadEntry("application/x-gtar", "tgz", false); + sMimeTypeMap.loadEntry("application/x-gtar", "taz", false); + sMimeTypeMap.loadEntry("application/x-hdf", "hdf", false); + sMimeTypeMap.loadEntry("application/x-ica", "ica", false); + sMimeTypeMap.loadEntry("application/x-internet-signup", "ins", + false); + sMimeTypeMap.loadEntry("application/x-internet-signup", "isp", + false); + sMimeTypeMap.loadEntry("application/x-iphone", "iii", false); + sMimeTypeMap.loadEntry("application/x-iso9660-image", "iso", false); + sMimeTypeMap.loadEntry("application/x-jmol", "jmz", false); + sMimeTypeMap.loadEntry("application/x-kchart", "chrt", false); + sMimeTypeMap.loadEntry("application/x-killustrator", "kil", false); + sMimeTypeMap.loadEntry("application/x-koan", "skp", false); + sMimeTypeMap.loadEntry("application/x-koan", "skd", false); + sMimeTypeMap.loadEntry("application/x-koan", "skt", false); + sMimeTypeMap.loadEntry("application/x-koan", "skm", false); + sMimeTypeMap.loadEntry("application/x-kpresenter", "kpr", false); + sMimeTypeMap.loadEntry("application/x-kpresenter", "kpt", false); + sMimeTypeMap.loadEntry("application/x-kspread", "ksp", false); + sMimeTypeMap.loadEntry("application/x-kword", "kwd", false); + sMimeTypeMap.loadEntry("application/x-kword", "kwt", false); + sMimeTypeMap.loadEntry("application/x-latex", "latex", false); + sMimeTypeMap.loadEntry("application/x-lha", "lha", false); + sMimeTypeMap.loadEntry("application/x-lzh", "lzh", false); + sMimeTypeMap.loadEntry("application/x-lzx", "lzx", false); + sMimeTypeMap.loadEntry("application/x-maker", "frm", false); + sMimeTypeMap.loadEntry("application/x-maker", "maker", false); + sMimeTypeMap.loadEntry("application/x-maker", "frame", false); + sMimeTypeMap.loadEntry("application/x-maker", "fb", false); + sMimeTypeMap.loadEntry("application/x-maker", "book", false); + sMimeTypeMap.loadEntry("application/x-maker", "fbdoc", false); + sMimeTypeMap.loadEntry("application/x-mif", "mif", false); + sMimeTypeMap.loadEntry("application/x-ms-wmd", "wmd", false); + sMimeTypeMap.loadEntry("application/x-ms-wmz", "wmz", false); + sMimeTypeMap.loadEntry("application/x-msi", "msi", false); + sMimeTypeMap.loadEntry("application/x-ns-proxy-autoconfig", "pac", + false); + sMimeTypeMap.loadEntry("application/x-nwc", "nwc", false); + sMimeTypeMap.loadEntry("application/x-object", "o", false); + sMimeTypeMap.loadEntry("application/x-oz-application", "oza", + false); + sMimeTypeMap.loadEntry("application/x-pkcs7-certreqresp", "p7r", + false); + sMimeTypeMap.loadEntry("application/x-pkcs7-crl", "crl", false); + sMimeTypeMap.loadEntry("application/x-quicktimeplayer", "qtl", + false); + sMimeTypeMap.loadEntry("application/x-shar", "shar", false); + sMimeTypeMap.loadEntry("application/x-stuffit", "sit", false); + sMimeTypeMap.loadEntry("application/x-sv4cpio", "sv4cpio", false); + sMimeTypeMap.loadEntry("application/x-sv4crc", "sv4crc", false); + sMimeTypeMap.loadEntry("application/x-tar", "tar", false); + sMimeTypeMap.loadEntry("application/x-texinfo", "texinfo", false); + sMimeTypeMap.loadEntry("application/x-texinfo", "texi", false); + sMimeTypeMap.loadEntry("application/x-troff", "t", false); + sMimeTypeMap.loadEntry("application/x-troff", "roff", false); + sMimeTypeMap.loadEntry("application/x-troff-man", "man", false); + sMimeTypeMap.loadEntry("application/x-ustar", "ustar", false); + sMimeTypeMap.loadEntry("application/x-wais-source", "src", false); + sMimeTypeMap.loadEntry("application/x-wingz", "wz", false); + sMimeTypeMap.loadEntry( + "application/x-webarchive", "webarchive", false); // added + sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt", false); + sMimeTypeMap.loadEntry("application/x-xcf", "xcf", false); + sMimeTypeMap.loadEntry("application/x-xfig", "fig", false); + sMimeTypeMap.loadEntry("audio/basic", "snd", false); + sMimeTypeMap.loadEntry("audio/midi", "mid", false); + sMimeTypeMap.loadEntry("audio/midi", "midi", false); + sMimeTypeMap.loadEntry("audio/midi", "kar", false); + sMimeTypeMap.loadEntry("audio/mpeg", "mpga", false); + sMimeTypeMap.loadEntry("audio/mpeg", "mpega", false); + sMimeTypeMap.loadEntry("audio/mpeg", "mp2", false); + sMimeTypeMap.loadEntry("audio/mpeg", "mp3", false); + sMimeTypeMap.loadEntry("audio/mpeg", "m4a", false); + sMimeTypeMap.loadEntry("audio/mpegurl", "m3u", false); + sMimeTypeMap.loadEntry("audio/prs.sid", "sid", false); + sMimeTypeMap.loadEntry("audio/x-aiff", "aif", false); + sMimeTypeMap.loadEntry("audio/x-aiff", "aiff", false); + sMimeTypeMap.loadEntry("audio/x-aiff", "aifc", false); + sMimeTypeMap.loadEntry("audio/x-gsm", "gsm", false); + sMimeTypeMap.loadEntry("audio/x-mpegurl", "m3u", false); + sMimeTypeMap.loadEntry("audio/x-ms-wma", "wma", false); + sMimeTypeMap.loadEntry("audio/x-ms-wax", "wax", false); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ra", false); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "rm", false); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ram", false); + sMimeTypeMap.loadEntry("audio/x-realaudio", "ra", false); + sMimeTypeMap.loadEntry("audio/x-scpls", "pls", false); + sMimeTypeMap.loadEntry("audio/x-sd2", "sd2", false); + sMimeTypeMap.loadEntry("audio/x-wav", "wav", false); + sMimeTypeMap.loadEntry("image/bmp", "bmp", false); // added + sMimeTypeMap.loadEntry("image/gif", "gif", false); + sMimeTypeMap.loadEntry("image/ico", "cur", false); // added + sMimeTypeMap.loadEntry("image/ico", "ico", false); // added + sMimeTypeMap.loadEntry("image/ief", "ief", false); + sMimeTypeMap.loadEntry("image/jpeg", "jpeg", false); + sMimeTypeMap.loadEntry("image/jpeg", "jpg", false); + sMimeTypeMap.loadEntry("image/jpeg", "jpe", false); + sMimeTypeMap.loadEntry("image/pcx", "pcx", false); + sMimeTypeMap.loadEntry("image/png", "png", false); + sMimeTypeMap.loadEntry("image/svg+xml", "svg", false); + sMimeTypeMap.loadEntry("image/svg+xml", "svgz", false); + sMimeTypeMap.loadEntry("image/tiff", "tiff", false); + sMimeTypeMap.loadEntry("image/tiff", "tif", false); + sMimeTypeMap.loadEntry("image/vnd.djvu", "djvu", false); + sMimeTypeMap.loadEntry("image/vnd.djvu", "djv", false); + sMimeTypeMap.loadEntry("image/vnd.wap.wbmp", "wbmp", false); + sMimeTypeMap.loadEntry("image/x-cmu-raster", "ras", false); + sMimeTypeMap.loadEntry("image/x-coreldraw", "cdr", false); + sMimeTypeMap.loadEntry("image/x-coreldrawpattern", "pat", false); + sMimeTypeMap.loadEntry("image/x-coreldrawtemplate", "cdt", false); + sMimeTypeMap.loadEntry("image/x-corelphotopaint", "cpt", false); + sMimeTypeMap.loadEntry("image/x-icon", "ico", false); + sMimeTypeMap.loadEntry("image/x-jg", "art", false); + sMimeTypeMap.loadEntry("image/x-jng", "jng", false); + sMimeTypeMap.loadEntry("image/x-ms-bmp", "bmp", false); + sMimeTypeMap.loadEntry("image/x-photoshop", "psd", false); + sMimeTypeMap.loadEntry("image/x-portable-anymap", "pnm", false); + sMimeTypeMap.loadEntry("image/x-portable-bitmap", "pbm", false); + sMimeTypeMap.loadEntry("image/x-portable-graymap", "pgm", false); + sMimeTypeMap.loadEntry("image/x-portable-pixmap", "ppm", false); + sMimeTypeMap.loadEntry("image/x-rgb", "rgb", false); + sMimeTypeMap.loadEntry("image/x-xbitmap", "xbm", false); + sMimeTypeMap.loadEntry("image/x-xpixmap", "xpm", false); + sMimeTypeMap.loadEntry("image/x-xwindowdump", "xwd", false); + sMimeTypeMap.loadEntry("model/iges", "igs", false); + sMimeTypeMap.loadEntry("model/iges", "iges", false); + sMimeTypeMap.loadEntry("model/mesh", "msh", false); + sMimeTypeMap.loadEntry("model/mesh", "mesh", false); + sMimeTypeMap.loadEntry("model/mesh", "silo", false); + sMimeTypeMap.loadEntry("text/calendar", "ics", true); + sMimeTypeMap.loadEntry("text/calendar", "icz", true); + sMimeTypeMap.loadEntry("text/comma-separated-values", "csv", true); + sMimeTypeMap.loadEntry("text/css", "css", true); + sMimeTypeMap.loadEntry("text/h323", "323", true); + sMimeTypeMap.loadEntry("text/iuls", "uls", true); + sMimeTypeMap.loadEntry("text/mathml", "mml", true); + // add it first so it will be the default for ExtensionFromMimeType + sMimeTypeMap.loadEntry("text/plain", "txt", true); + sMimeTypeMap.loadEntry("text/plain", "asc", true); + sMimeTypeMap.loadEntry("text/plain", "text", true); + sMimeTypeMap.loadEntry("text/plain", "diff", true); + sMimeTypeMap.loadEntry("text/plain", "pot", true); + sMimeTypeMap.loadEntry("text/richtext", "rtx", true); + sMimeTypeMap.loadEntry("text/rtf", "rtf", true); + sMimeTypeMap.loadEntry("text/texmacs", "ts", true); + sMimeTypeMap.loadEntry("text/text", "phps", true); + sMimeTypeMap.loadEntry("text/tab-separated-values", "tsv", true); + sMimeTypeMap.loadEntry("text/x-bibtex", "bib", true); + sMimeTypeMap.loadEntry("text/x-boo", "boo", true); + sMimeTypeMap.loadEntry("text/x-c++hdr", "h++", true); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hpp", true); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hxx", true); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hh", true); + sMimeTypeMap.loadEntry("text/x-c++src", "c++", true); + sMimeTypeMap.loadEntry("text/x-c++src", "cpp", true); + sMimeTypeMap.loadEntry("text/x-c++src", "cxx", true); + sMimeTypeMap.loadEntry("text/x-chdr", "h", true); + sMimeTypeMap.loadEntry("text/x-component", "htc", true); + sMimeTypeMap.loadEntry("text/x-csh", "csh", true); + sMimeTypeMap.loadEntry("text/x-csrc", "c", true); + sMimeTypeMap.loadEntry("text/x-dsrc", "d", true); + sMimeTypeMap.loadEntry("text/x-haskell", "hs", true); + sMimeTypeMap.loadEntry("text/x-java", "java", true); + sMimeTypeMap.loadEntry("text/x-literate-haskell", "lhs", true); + sMimeTypeMap.loadEntry("text/x-moc", "moc", true); + sMimeTypeMap.loadEntry("text/x-pascal", "p", true); + sMimeTypeMap.loadEntry("text/x-pascal", "pas", true); + sMimeTypeMap.loadEntry("text/x-pcs-gcd", "gcd", true); + sMimeTypeMap.loadEntry("text/x-setext", "etx", true); + sMimeTypeMap.loadEntry("text/x-tcl", "tcl", true); + sMimeTypeMap.loadEntry("text/x-tex", "tex", true); + sMimeTypeMap.loadEntry("text/x-tex", "ltx", true); + sMimeTypeMap.loadEntry("text/x-tex", "sty", true); + sMimeTypeMap.loadEntry("text/x-tex", "cls", true); + sMimeTypeMap.loadEntry("text/x-vcalendar", "vcs", true); + sMimeTypeMap.loadEntry("text/x-vcard", "vcf", true); + sMimeTypeMap.loadEntry("video/3gpp", "3gp", false); + sMimeTypeMap.loadEntry("video/3gpp", "3g2", false); + sMimeTypeMap.loadEntry("video/dl", "dl", false); + sMimeTypeMap.loadEntry("video/dv", "dif", false); + sMimeTypeMap.loadEntry("video/dv", "dv", false); + sMimeTypeMap.loadEntry("video/fli", "fli", false); + sMimeTypeMap.loadEntry("video/mpeg", "mpeg", false); + sMimeTypeMap.loadEntry("video/mpeg", "mpg", false); + sMimeTypeMap.loadEntry("video/mpeg", "mpe", false); + sMimeTypeMap.loadEntry("video/mp4", "mp4", false); + sMimeTypeMap.loadEntry("video/mpeg", "VOB", false); + sMimeTypeMap.loadEntry("video/quicktime", "qt", false); + sMimeTypeMap.loadEntry("video/quicktime", "mov", false); + sMimeTypeMap.loadEntry("video/vnd.mpegurl", "mxu", false); + sMimeTypeMap.loadEntry("video/x-la-asf", "lsf", false); + sMimeTypeMap.loadEntry("video/x-la-asf", "lsx", false); + sMimeTypeMap.loadEntry("video/x-mng", "mng", false); + sMimeTypeMap.loadEntry("video/x-ms-asf", "asf", false); + sMimeTypeMap.loadEntry("video/x-ms-asf", "asx", false); + sMimeTypeMap.loadEntry("video/x-ms-wm", "wm", false); + sMimeTypeMap.loadEntry("video/x-ms-wmv", "wmv", false); + sMimeTypeMap.loadEntry("video/x-ms-wmx", "wmx", false); + sMimeTypeMap.loadEntry("video/x-ms-wvx", "wvx", false); + sMimeTypeMap.loadEntry("video/x-msvideo", "avi", false); + sMimeTypeMap.loadEntry("video/x-sgi-movie", "movie", false); + sMimeTypeMap.loadEntry("x-conference/x-cooltalk", "ice", false); + } + + return sMimeTypeMap; + } +} diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java new file mode 100644 index 0000000..74622b3 --- /dev/null +++ b/core/java/android/webkit/Network.java @@ -0,0 +1,349 @@ +/* + * 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.content.Context; +import android.net.http.*; +import android.os.*; +import android.util.Log; +import android.util.Config; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Map; + +import junit.framework.Assert; + +class Network { + + private static final String LOGTAG = "network"; + + /** + * Static instance of a Network object. + */ + private static Network sNetwork; + + /** + * Flag to store the state of platform notifications, for the case + * when the Network object has not been constructed yet + */ + private static boolean sPlatformNotifications; + + /** + * Reference count for platform notifications as the network class is a + * static and can exist over multiple activities, thus over multiple + * onPause/onResume pairs. + */ + private static int sPlatformNotificationEnableRefCount; + + /** + * Proxy username if known (used for pre-emptive proxy authentication). + */ + private String mProxyUsername; + + /** + * Proxy password if known (used for pre-emptive proxy authentication). + */ + private String mProxyPassword; + + /** + * Network request queue (requests are added from the browser thread). + */ + private RequestQueue mRequestQueue; + + /** + * SSL error handler: takes care of synchronization of multiple async + * loaders with SSL-related problems. + */ + private SslErrorHandler mSslErrorHandler; + + /** + * HTTP authentication handler: takes care of synchronization of HTTP + * authentication requests. + */ + private HttpAuthHandler mHttpAuthHandler; + + /** + * @return The singleton instance of the network. + */ + public static synchronized Network getInstance(Context context) { + if (sNetwork == null) { + // Note Context of the Application is used here, rather than + // the what is passed in (usually a Context derived from an + // Activity) so the intent receivers belong to the application + // rather than an activity - this fixes the issue where + // Activities are created and destroyed during the lifetime of + // an Application + sNetwork = new Network(context.getApplicationContext()); + if (sPlatformNotifications) { + // Adjust the ref count before calling enable as it is already + // taken into account when the static function was called + // directly + --sPlatformNotificationEnableRefCount; + enablePlatformNotifications(); + } + } + return sNetwork; + } + + + /** + * Enables data state and proxy tracking + */ + public static void enablePlatformNotifications() { + if (++sPlatformNotificationEnableRefCount == 1) { + if (sNetwork != null) { + sNetwork.mRequestQueue.enablePlatformNotifications(); + } else { + sPlatformNotifications = true; + } + } + } + + /** + * If platform notifications are enabled, this should be called + * from onPause() or onStop() + */ + public static void disablePlatformNotifications() { + if (--sPlatformNotificationEnableRefCount == 0) { + if (sNetwork != null) { + sNetwork.mRequestQueue.disablePlatformNotifications(); + } else { + sPlatformNotifications = false; + } + } + } + + /** + * Creates a new Network object. + * XXX: Must be created in the same thread as WebCore!!!!! + */ + private Network(Context context) { + if (Config.DEBUG) { + Assert.assertTrue(Thread.currentThread(). + getName().equals(WebViewCore.THREAD_NAME)); + } + mSslErrorHandler = new SslErrorHandler(this); + mHttpAuthHandler = new HttpAuthHandler(this); + + mRequestQueue = new RequestQueue(context); + } + + /** + * Request a url from either the network or the file system. + * @param url The url to load. + * @param method The http method. + * @param headers The http headers. + * @param postData The body of the request. + * @param loader A LoadListener for receiving the results of the request. + * @param isHighPriority True if this is high priority request. + * @return True if the request was successfully queued. + */ + public boolean requestURL(String method, + Map<String, String> headers, + byte [] postData, + LoadListener loader, + boolean isHighPriority) { + + String url = loader.url(); + + // Not a valid url, return false because we won't service the request! + if (!URLUtil.isValidUrl(url)) { + return false; + } + + // asset, file system or data stream are handled in the other code path. + // This only handles network request. + if (URLUtil.isAssetUrl(url) || URLUtil.isFileUrl(url) || + URLUtil.isDataUrl(url)) { + return false; + } + + /* FIXME: this is lame. Pass an InputStream in, rather than + making this lame one here */ + InputStream bodyProvider = null; + int bodyLength = 0; + if (postData != null) { + bodyLength = postData.length; + bodyProvider = new ByteArrayInputStream(postData); + } + + RequestQueue q = mRequestQueue; + if (loader.isSynchronous()) { + q = new RequestQueue(loader.getContext(), 1); + } + + RequestHandle handle = q.queueRequest( + url, loader.getWebAddress(), method, headers, loader, + bodyProvider, bodyLength, isHighPriority); + loader.attachRequestHandle(handle); + + if (loader.isSynchronous()) { + handle.waitUntilComplete(); + loader.loadSynchronousMessages(); + q.shutdown(); + } + return true; + } + + /** + * @return True iff there is a valid proxy set. + */ + public boolean isValidProxySet() { + // The proxy host and port can be set within a different thread during + // an Intent broadcast. + synchronized (mRequestQueue) { + return mRequestQueue.getProxyHost() != null; + } + } + + /** + * Get the proxy hostname. + * @return The proxy hostname obtained from the network queue and proxy + * settings. + */ + public String getProxyHostname() { + return mRequestQueue.getProxyHost().getHostName(); + } + + /** + * @return The proxy username or null if none. + */ + public synchronized String getProxyUsername() { + return mProxyUsername; + } + + /** + * Sets the proxy username. + * @param proxyUsername Username to use when + * connecting through the proxy. + */ + public synchronized void setProxyUsername(String proxyUsername) { + if (Config.DEBUG) { + Assert.assertTrue(isValidProxySet()); + } + + mProxyUsername = proxyUsername; + } + + /** + * @return The proxy password or null if none. + */ + public synchronized String getProxyPassword() { + return mProxyPassword; + } + + /** + * Sets the proxy password. + * @param proxyPassword Password to use when + * connecting through the proxy. + */ + public synchronized void setProxyPassword(String proxyPassword) { + if (Config.DEBUG) { + Assert.assertTrue(isValidProxySet()); + } + + mProxyPassword = proxyPassword; + } + + /** + * If we need to stop loading done in a handler (here, browser frame), we + * send a message to the handler to stop loading, and remove all loaders + * that share the same CallbackProxy in question from all local + * handlers (such as ssl-error and http-authentication handler). + * @param proxy The CallbackProxy responsible for cancelling the current + * load. + */ + public void resetHandlersAndStopLoading(BrowserFrame frame) { + if (Config.LOGV) { + Log.v(LOGTAG, "Network.resetHandlersAndStopLoading()"); + } + + frame.stopLoading(); + mSslErrorHandler.reset(frame); + mHttpAuthHandler.reset(frame); + } + + /** + * Saves the state of network handlers (user SSL and HTTP-authentication + * preferences). + * @param outState The out-state to save (write) to. + * @return True iff succeeds. + */ + public boolean saveState(Bundle outState) { + if (Config.LOGV) { + Log.v(LOGTAG, "Network.saveState()"); + } + + return mSslErrorHandler.saveState(outState); + } + + /** + * Restores the state of network handlers (user SSL and HTTP-authentication + * preferences). + * @param inState The in-state to load (read) from. + * @return True iff succeeds. + */ + public boolean restoreState(Bundle inState) { + if (Config.LOGV) { + Log.v(LOGTAG, "Network.restoreState()"); + } + + return mSslErrorHandler.restoreState(inState); + } + + /** + * Clears user SSL-error preference table. + */ + public void clearUserSslPrefTable() { + mSslErrorHandler.clear(); + } + + /** + * Handles SSL error(s) on the way up to the user: the user must decide + * whether errors should be ignored or not. + * @param loader The loader that resulted in SSL errors. + */ + public void handleSslErrorRequest(LoadListener loader) { + if (Config.DEBUG) Assert.assertNotNull(loader); + if (loader != null) { + mSslErrorHandler.handleSslErrorRequest(loader); + } + } + + /** + * Handles authentication requests on their way up to the user (the user + * must provide credentials). + * @param loader The loader that resulted in an HTTP + * authentication request. + */ + public void handleAuthRequest(LoadListener loader) { + if (Config.DEBUG) Assert.assertNotNull(loader); + if (loader != null) { + mHttpAuthHandler.handleAuthRequest(loader); + } + } + + // Performance probe + public void startTiming() { + mRequestQueue.startTiming(); + } + + public void stopTiming() { + mRequestQueue.stopTiming(); + } +} diff --git a/core/java/android/webkit/PerfChecker.java b/core/java/android/webkit/PerfChecker.java new file mode 100644 index 0000000..8c5f86e --- /dev/null +++ b/core/java/android/webkit/PerfChecker.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2007 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.os.SystemClock; +import android.util.Log; + +class PerfChecker { + + private long mTime; + private static final long mResponseThreshold = 2000; // 2s + + public PerfChecker() { + if (false) { + mTime = SystemClock.uptimeMillis(); + } + } + + /** + * @param what log string + * Logs given string if mResponseThreshold time passed between either + * instantiation or previous responseAlert call + */ + public void responseAlert(String what) { + if (false) { + long upTime = SystemClock.uptimeMillis(); + long time = upTime - mTime; + if (time > mResponseThreshold) { + Log.w("webkit", what + " used " + time + " ms"); + } + // Reset mTime, to permit reuse + mTime = upTime; + } + } +} diff --git a/core/java/android/webkit/Plugin.java b/core/java/android/webkit/Plugin.java new file mode 100644 index 0000000..f83da99 --- /dev/null +++ b/core/java/android/webkit/Plugin.java @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2007 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 com.android.internal.R; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.webkit.WebView; + +/** + * Represents a plugin (Java equivalent of the PluginPackageAndroid + * C++ class in libs/WebKitLib/WebKit/WebCore/plugins/android/) + */ +public class Plugin { + public interface PreferencesClickHandler { + public void handleClickEvent(Context context); + } + + private String mName; + private String mPath; + private String mFileName; + private String mDescription; + private PreferencesClickHandler mHandler; + + public Plugin(String name, + String path, + String fileName, + String description) { + mName = name; + mPath = path; + mFileName = fileName; + mDescription = description; + mHandler = new DefaultClickHandler(); + } + + public String toString() { + return mName; + } + + public String getName() { + return mName; + } + + public String getPath() { + return mPath; + } + + public String getFileName() { + return mFileName; + } + + public String getDescription() { + return mDescription; + } + + public void setName(String name) { + mName = name; + } + + public void setPath(String path) { + mPath = path; + } + + public void setFileName(String fileName) { + mFileName = fileName; + } + + public void setDescription(String description) { + mDescription = description; + } + + public void setClickHandler(PreferencesClickHandler handler) { + mHandler = handler; + } + + /** + * Invokes the click handler for this plugin. + */ + public void dispatchClickEvent(Context context) { + if (mHandler != null) { + mHandler.handleClickEvent(context); + } + } + + /** + * Default click handler. The plugins should implement their own. + */ + private class DefaultClickHandler implements PreferencesClickHandler, + DialogInterface.OnClickListener { + private AlertDialog mDialog; + + public void handleClickEvent(Context context) { + // Show a simple popup dialog containing the description + // string of the plugin. + if (mDialog == null) { + mDialog = new AlertDialog.Builder(context) + .setTitle(mName) + .setMessage(mDescription) + .setPositiveButton(R.string.ok, this) + .setCancelable(false) + .show(); + } + } + + public void onClick(DialogInterface dialog, int which) { + mDialog.dismiss(); + mDialog = null; + } + } +} diff --git a/core/java/android/webkit/PluginList.java b/core/java/android/webkit/PluginList.java new file mode 100644 index 0000000..a9d3d8c --- /dev/null +++ b/core/java/android/webkit/PluginList.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2007 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.content.Context; +import java.util.ArrayList; +import java.util.List; + +/** + * A simple list of initialized plugins. This list gets + * populated when the plugins are initialized (at + * browser startup, at the moment). + */ +public class PluginList { + private ArrayList<Plugin> mPlugins; + + /** + * Public constructor. Initializes the list of plugins. + */ + public PluginList() { + mPlugins = new ArrayList<Plugin>(); + } + + /** + * Returns the list of plugins as a java.util.List. + */ + public synchronized List getList() { + return mPlugins; + } + + /** + * Adds a plugin to the list. + */ + public synchronized void addPlugin(Plugin plugin) { + if (!mPlugins.contains(plugin)) { + mPlugins.add(plugin); + } + } + + /** + * Removes a plugin from the list. + */ + public synchronized void removePlugin(Plugin plugin) { + int location = mPlugins.indexOf(plugin); + if (location != -1) { + mPlugins.remove(location); + } + } + + /** + * Clears the plugin list. + */ + public synchronized void clear() { + mPlugins.clear(); + } + + /** + * Dispatches the click event to the appropriate plugin. + */ + public synchronized void pluginClicked(Context context, int position) { + try { + Plugin plugin = mPlugins.get(position); + plugin.dispatchClickEvent(context); + } catch (IndexOutOfBoundsException e) { + // This can happen if the list of plugins + // gets changed while the pref menu is up. + } + } +} diff --git a/core/java/android/webkit/SslErrorHandler.java b/core/java/android/webkit/SslErrorHandler.java new file mode 100644 index 0000000..115434a --- /dev/null +++ b/core/java/android/webkit/SslErrorHandler.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2007 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 junit.framework.Assert; + +import android.net.http.SslError; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Config; +import android.util.Log; + +import java.util.LinkedList; +import java.util.ListIterator; + +/** + * SslErrorHandler: class responsible for handling SSL errors. This class is + * passed as a parameter to BrowserCallback.displaySslErrorDialog and is meant + * to receive the user's response. + */ +public class SslErrorHandler extends Handler { + /* One problem here is that there may potentially be multiple SSL errors + * coming from mutiple loaders. Therefore, we keep a queue of loaders + * that have SSL-related problems and process errors one by one in the + * order they were received. + */ + + private static final String LOGTAG = "network"; + + /** + * Network. + */ + private Network mNetwork; + + /** + * Queue of loaders that experience SSL-related problems. + */ + private LinkedList<LoadListener> mLoaderQueue; + + /** + * SSL error preference table. + */ + private Bundle mSslPrefTable; + + // Message id for handling the response + private final int HANDLE_RESPONSE = 100; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case HANDLE_RESPONSE: + handleSslErrorResponse(msg.arg1 == 1); + fastProcessQueuedSslErrors(); + break; + } + } + + /** + * Creates a new error handler with an empty loader queue. + */ + /* package */ SslErrorHandler(Network network) { + mNetwork = network; + + mLoaderQueue = new LinkedList<LoadListener>(); + mSslPrefTable = new Bundle(); + } + + /** + * Saves this handler's state into a map. + * @return True iff succeeds. + */ + /* package */ boolean saveState(Bundle outState) { + boolean success = (outState != null); + if (success) { + // TODO? + outState.putBundle("ssl-error-handler", mSslPrefTable); + } + + return success; + } + + /** + * Restores this handler's state from a map. + * @return True iff succeeds. + */ + /* package */ boolean restoreState(Bundle inState) { + boolean success = (inState != null); + if (success) { + success = inState.containsKey("ssl-error-handler"); + if (success) { + mSslPrefTable = inState.getBundle("ssl-error-handler"); + } + } + + return success; + } + + /** + * Clears SSL error preference table. + */ + /* package */ synchronized void clear() { + mSslPrefTable.clear(); + } + + /** + * Resets the SSL error handler, removes all loaders that + * share the same BrowserFrame. + */ + /* package */ synchronized void reset(BrowserFrame frame) { + ListIterator<LoadListener> i = mLoaderQueue.listIterator(0); + while (i.hasNext()) { + LoadListener loader = i.next(); + if (frame == loader.getFrame()) { + i.remove(); + } + } + } + + /** + * Handles SSL error(s) on the way up to the user. + */ + /* package */ synchronized void handleSslErrorRequest(LoadListener loader) { + if (Config.LOGV) { + Log.v(LOGTAG, "SslErrorHandler.handleSslErrorRequest(): " + + "url=" + loader.url()); + } + + if (!loader.cancelled()) { + mLoaderQueue.offer(loader); + if (loader == mLoaderQueue.peek()) { + fastProcessQueuedSslErrors(); + } + } + } + + /** + * Processes queued SSL-error confirmation requests in + * a tight loop while there is no need to ask the user. + */ + /* package */void fastProcessQueuedSslErrors() { + while (processNextLoader()); + } + + /** + * Processes the next loader in the queue. + * @return True iff should proceed to processing the + * following loader in the queue + */ + private synchronized boolean processNextLoader() { + LoadListener loader = mLoaderQueue.peek(); + if (loader != null) { + // if this loader has been cancelled + if (loader.cancelled()) { + // go to the following loader in the queue + return true; + } + + SslError error = loader.sslError(); + + if (Config.DEBUG) { + Assert.assertNotNull(error); + } + + int primary = error.getPrimaryError(); + String host = loader.host(); + + if (Config.DEBUG) { + Assert.assertTrue(host != null && primary != 0); + } + + if (mSslPrefTable.containsKey(host)) { + if (primary <= mSslPrefTable.getInt(host)) { + handleSslErrorResponse(true); + return true; + } + } + + // if we do not have information on record, ask + // the user (display a dialog) + CallbackProxy proxy = loader.getFrame().getCallbackProxy(); + proxy.onReceivedSslError(this, error); + } + + // the queue must be empty, stop + return false; + } + + /** + * Proceed with the SSL certificate. + */ + public void proceed() { + sendMessage(obtainMessage(HANDLE_RESPONSE, 1, 0)); + } + + /** + * Cancel this request and all pending requests for the WebView that had + * the error. + */ + public void cancel() { + sendMessage(obtainMessage(HANDLE_RESPONSE, 0, 0)); + } + + /** + * Handles SSL error(s) on the way down from the user. + */ + /* package */ synchronized void handleSslErrorResponse(boolean proceed) { + LoadListener loader = mLoaderQueue.poll(); + if (Config.DEBUG) { + Assert.assertNotNull(loader); + } + + if (Config.LOGV) { + Log.v(LOGTAG, "SslErrorHandler.handleSslErrorResponse():" + + " proceed: " + proceed + + " url:" + loader.url()); + } + + if (!loader.cancelled()) { + if (proceed) { + // update the user's SSL error preference table + int primary = loader.sslError().getPrimaryError(); + String host = loader.host(); + + if (Config.DEBUG) { + Assert.assertTrue(host != null && primary != 0); + } + boolean hasKey = mSslPrefTable.containsKey(host); + if (!hasKey || + primary > mSslPrefTable.getInt(host)) { + mSslPrefTable.putInt(host, new Integer(primary)); + } + + loader.handleSslErrorResponse(proceed); + } else { + loader.handleSslErrorResponse(proceed); + mNetwork.resetHandlersAndStopLoading(loader.getFrame()); + } + } + } +} diff --git a/core/java/android/webkit/StreamLoader.java b/core/java/android/webkit/StreamLoader.java new file mode 100644 index 0000000..9098307 --- /dev/null +++ b/core/java/android/webkit/StreamLoader.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2007 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.net.http.EventHandler; +import android.net.http.Headers; +import android.os.Handler; +import android.os.Message; +import android.util.Config; + +import java.io.IOException; +import java.io.InputStream; + + +/** + * This abstract class is used for all content loaders that rely on streaming + * content into the rendering engine loading framework. + * + * The class implements a state machine to load the content into the frame in + * a similar manor to the way content arrives from the network. The class uses + * messages to move from one state to the next, which enables async. loading of + * the streamed content. + * + * Classes that inherit from this class must implement two methods, the first + * method is used to setup the InputStream and notify the loading framework if + * it can load it's content. The other method allows the derived class to add + * additional HTTP headers to the response. + * + * By default, content loaded with a StreamLoader is marked with a HTTP header + * that indicates the content should not be cached. + * + */ +abstract class StreamLoader extends Handler { + + public static final String NO_STORE = "no-store"; + + private static final int MSG_STATUS = 100; // Send status to loader + private static final int MSG_HEADERS = 101; // Send headers to loader + private static final int MSG_DATA = 102; // Send data to loader + private static final int MSG_END = 103; // Send endData to loader + + protected LoadListener mHandler; // loader class + protected InputStream mDataStream; // stream to read data from + protected long mContentLength; // content length of data + private byte [] mData; // buffer to pass data to loader with. + + /** + * Constructor. Although this class calls the LoadListener, it only calls + * the EventHandler Interface methods. LoadListener concrete class is used + * to avoid the penality of calling an interface. + * + * @param loadlistener The LoadListener to call with the data. + */ + StreamLoader(LoadListener loadlistener) { + mHandler = loadlistener; + } + + /** + * This method is called when the derived class should setup mDataStream, + * and call mHandler.status() to indicate that the load can occur. If it + * fails to setup, it should still call status() with the error code. + * + * @return true if stream was successfully setup + */ + protected abstract boolean setupStreamAndSendStatus(); + + /** + * This method is called when the headers are about to be sent to the + * load framework. The derived class has the opportunity to add addition + * headers. + * + * @param headers Map of HTTP headers that will be sent to the loader. + */ + abstract protected void buildHeaders(Headers headers); + + + /** + * Calling this method starts the load of the content for this StreamLoader. + * This method simply posts a message to send the status and returns + * immediately. + */ + public void load() { + if (!mHandler.isSynchronous()) { + sendMessage(obtainMessage(MSG_STATUS)); + } else { + // Load the stream synchronously. + if (setupStreamAndSendStatus()) { + // We were able to open the stream, create the array + // to pass data to the loader + mData = new byte[8192]; + sendHeaders(); + while (!sendData()); + closeStreamAndSendEndData(); + mHandler.loadSynchronousMessages(); + } + } + } + + /* (non-Javadoc) + * @see android.os.Handler#handleMessage(android.os.Message) + */ + public void handleMessage(Message msg) { + if (Config.DEBUG && mHandler.isSynchronous()) { + throw new AssertionError(); + } + switch(msg.what) { + case MSG_STATUS: + if (setupStreamAndSendStatus()) { + // We were able to open the stream, create the array + // to pass data to the loader + mData = new byte[8192]; + sendMessage(obtainMessage(MSG_HEADERS)); + } + break; + case MSG_HEADERS: + sendHeaders(); + sendMessage(obtainMessage(MSG_DATA)); + break; + case MSG_DATA: + if (sendData()) { + sendMessage(obtainMessage(MSG_END)); + } else { + sendMessage(obtainMessage(MSG_DATA)); + } + break; + case MSG_END: + closeStreamAndSendEndData(); + break; + default: + super.handleMessage(msg); + break; + } + } + + /** + * Construct the headers and pass them to the EventHandler. + */ + private void sendHeaders() { + Headers headers = new Headers(); + if (mContentLength > 0) { + headers.setContentLength(mContentLength); + } + headers.setCacheControl(NO_STORE); + buildHeaders(headers); + mHandler.headers(headers); + } + + /** + * Read data from the stream and pass it to the EventHandler. + * If an error occurs reading the stream, then an error is sent to the + * EventHandler, and moves onto the next state - end of data. + * @return True if all the data has been read. False if sendData should be + * called again. + */ + private boolean sendData() { + if (mDataStream != null) { + try { + int amount = mDataStream.read(mData); + if (amount > 0) { + mHandler.data(mData, amount); + return false; + } + } catch (IOException ex) { + mHandler.error(EventHandler.FILE_ERROR, + ex.getMessage()); + } + } + return true; + } + + /** + * Close the stream and inform the EventHandler that load is complete. + */ + private void closeStreamAndSendEndData() { + if (mDataStream != null) { + try { + mDataStream.close(); + } catch (IOException ex) { + // ignore. + } + } + mHandler.endData(); + } + +} diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java new file mode 100644 index 0000000..8a82411 --- /dev/null +++ b/core/java/android/webkit/TextDialog.java @@ -0,0 +1,610 @@ +/* + * Copyright (C) 2007 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.content.Context; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RectShape; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Selection; +import android.text.Spannable; +import android.text.TextPaint; +import android.text.TextUtils; +import android.text.method.MovementMethod; +import android.text.method.PasswordTransformationMethod; +import android.text.method.TextKeyListener; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsoluteLayout.LayoutParams; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * TextDialog is a specialized version of EditText used by WebView + * to overlay html textfields (and textareas) to use our standard + * text editing. + */ +/* package */ class TextDialog extends AutoCompleteTextView { + + private WebView mWebView; + private boolean mSingle; + private int mWidthSpec; + private int mHeightSpec; + private int mNodePointer; + // FIXME: This is a hack for blocking unmatched key ups, in particular + // on the enter key. The method for blocking unmatched key ups prevents + // the shift key from working properly. + private boolean mGotEnterDown; + // mScrollToAccommodateCursor being set to false prevents us from scrolling + // the cursor on screen when using the trackball to select a textfield. + private boolean mScrollToAccommodateCursor; + private int mMaxLength; + // Keep track of the text before the change so we know whether we actually + // need to send down the DOM events. + private String mPreChange; + // Array to store the final character added in onTextChanged, so that its + // KeyEvents may be determined. + private char[] mCharacter = new char[1]; + // This is used to reset the length filter when on a textfield + // with no max length. + // FIXME: This can be replaced with TextView.NO_FILTERS if that + // is made public/protected. + private static final InputFilter[] NO_FILTERS = new InputFilter[0]; + + /** + * Create a new TextDialog. + * @param context The Context for this TextDialog. + * @param webView The WebView that created this. + */ + /* package */ TextDialog(Context context, WebView webView) { + super(context); + mWebView = webView; + ShapeDrawable background = new ShapeDrawable(new RectShape()); + Paint shapePaint = background.getPaint(); + shapePaint.setStyle(Paint.Style.STROKE); + ColorDrawable color = new ColorDrawable(Color.WHITE); + Drawable[] array = new Drawable[2]; + array[0] = color; + array[1] = background; + LayerDrawable layers = new LayerDrawable(array); + // Hide WebCore's text behind this and allow the WebView + // to draw its own focusring. + setBackgroundDrawable(layers); + // Align the text better with the text behind it, so moving + // off of the textfield will not appear to move the text. + setPadding(3, 2, 0, 0); + mMaxLength = -1; + // Turn on subpixel text, and turn off kerning, so it better matches + // the text in webkit. + TextPaint paint = getPaint(); + int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG | + Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG; + paint.setFlags(flags); + // Set the text color to black, regardless of the theme. This ensures + // that other applications that use embedded WebViews will properly + // display the text in textfields. + setTextColor(Color.BLACK); + } + + @Override + protected boolean shouldAdvanceFocusOnEnter() { + // In the browser, single line textfields use enter as a form submit, + // so we never want to advance the focus on enter. + return false; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (event.isSystem()) { + return super.dispatchKeyEvent(event); + } + // Treat ACTION_DOWN and ACTION MULTIPLE the same + boolean down = event.getAction() != KeyEvent.ACTION_UP; + int keyCode = event.getKeyCode(); + Spannable text = (Spannable) getText(); + int oldLength = text.length(); + // Normally the delete key's dom events are sent via onTextChanged. + // However, if the length is zero, the text did not change, so we + // go ahead and pass the key down immediately. + if (KeyEvent.KEYCODE_DEL == keyCode && 0 == oldLength) { + sendDomEvent(event); + return true; + } + + if ((mSingle && KeyEvent.KEYCODE_ENTER == keyCode)) { + if (isPopupShowing()) { + return super.dispatchKeyEvent(event); + } + if (!down) { + // Hide the keyboard, since the user has just submitted this + // form. The submission happens thanks to the two calls + // to sendDomEvent. + InputMethodManager.getInstance(mContext) + .hideSoftInputFromWindow(getWindowToken(), 0); + sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + sendDomEvent(event); + } + return super.dispatchKeyEvent(event); + } else if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { + // Note that this handles center key and trackball. + if (isPopupShowing()) { + return super.dispatchKeyEvent(event); + } + // Center key should be passed to a potential onClick + if (!down) { + mWebView.shortPressOnTextField(); + } + // Pass to super to handle longpress. + return super.dispatchKeyEvent(event); + } + + // Ensure there is a layout so arrow keys are handled properly. + if (getLayout() == null) { + measure(mWidthSpec, mHeightSpec); + } + int oldStart = Selection.getSelectionStart(text); + int oldEnd = Selection.getSelectionEnd(text); + + boolean maxedOut = mMaxLength != -1 && oldLength == mMaxLength; + // If we are at max length, and there is a selection rather than a + // cursor, we need to store the text to compare later, since the key + // may have changed the string. + String oldText; + if (maxedOut && oldEnd != oldStart) { + oldText = text.toString(); + } else { + oldText = ""; + } + if (super.dispatchKeyEvent(event)) { + // If the TextDialog handled the key it was either an alphanumeric + // key, a delete, or a movement within the text. All of those are + // ok to pass to javascript. + + // UNLESS there is a max length determined by the html. In that + // case, if the string was already at the max length, an + // alphanumeric key will be erased by the LengthFilter, + // so do not pass down to javascript, and instead + // return true. If it is an arrow key or a delete key, we can go + // ahead and pass it down. + boolean isArrowKey; + switch(keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + isArrowKey = true; + break; + case KeyEvent.KEYCODE_ENTER: + // For multi-line text boxes, newlines will + // trigger onTextChanged for key down (which will send both + // key up and key down) but not key up. + mGotEnterDown = true; + default: + isArrowKey = false; + break; + } + if (maxedOut && !isArrowKey && keyCode != KeyEvent.KEYCODE_DEL) { + if (oldEnd == oldStart) { + // Return true so the key gets dropped. + mScrollToAccommodateCursor = true; + return true; + } else if (!oldText.equals(getText().toString())) { + // FIXME: This makes the text work properly, but it + // does not pass down the key event, so it may not + // work for a textfield that has the type of + // behavior of GoogleSuggest. That said, it is + // unlikely that a site would combine the two in + // one textfield. + Spannable span = (Spannable) getText(); + int newStart = Selection.getSelectionStart(span); + int newEnd = Selection.getSelectionEnd(span); + mWebView.replaceTextfieldText(0, oldLength, span.toString(), + newStart, newEnd); + mScrollToAccommodateCursor = true; + return true; + } + } + if (isArrowKey) { + // Arrow key does not change the text, but we still want to send + // the DOM events. + sendDomEvent(event); + } + mScrollToAccommodateCursor = true; + return true; + } + // FIXME: TextViews return false for up and down key events even though + // they change the selection. Since we don't want the get out of sync + // with WebCore's notion of the current selection, reset the selection + // to what it was before the key event. + Selection.setSelection(text, oldStart, oldEnd); + // Ignore the key up event for newlines. This prevents + // multiple newlines in the native textarea. + if (mGotEnterDown && !down) { + return true; + } + // if it is a navigation key, pass it to WebView + if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT + || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT + || keyCode == KeyEvent.KEYCODE_DPAD_UP + || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + // WebView check the trackballtime in onKeyDown to avoid calling + // native from both trackball and key handling. As this is called + // from TextDialog, we always want WebView to check with native. + // Reset trackballtime to ensure it. + mWebView.resetTrackballTime(); + return down ? mWebView.onKeyDown(keyCode, event) : mWebView + .onKeyUp(keyCode, event); + } + return false; + } + + /** + * Create a fake touch up event at (x,y) with respect to this TextDialog. + * This is used by WebView to act as though a touch event which happened + * before we placed the TextDialog actually hit it, so that it can place + * the cursor accordingly. + */ + /* package */ void fakeTouchEvent(float x, float y) { + // We need to ensure that there is a Layout, since the Layout is used + // in determining where to place the cursor. + if (getLayout() == null) { + measure(mWidthSpec, mHeightSpec); + } + // Create a fake touch up, which is used to place the cursor. + MotionEvent ev = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, + x, y, 0); + onTouchEvent(ev); + ev.recycle(); + } + + /** + * Determine whether this TextDialog currently represents the node + * represented by ptr. + * @param ptr Pointer to a node to compare to. + * @return boolean Whether this TextDialog already represents the node + * pointed to by ptr. + */ + /* package */ boolean isSameTextField(int ptr) { + return ptr == mNodePointer; + } + + @Override + public boolean onPreDraw() { + if (getLayout() == null) { + measure(mWidthSpec, mHeightSpec); + } + return super.onPreDraw(); + } + + @Override + protected void onTextChanged(CharSequence s,int start,int before,int count){ + super.onTextChanged(s, start, before, count); + String postChange = s.toString(); + // Prevent calls to setText from invoking onTextChanged (since this will + // mean we are on a different textfield). Also prevent the change when + // going from a textfield with a string of text to one with a smaller + // limit on text length from registering the onTextChanged event. + if (mPreChange == null || mPreChange.equals(postChange) || + (mMaxLength > -1 && mPreChange.length() > mMaxLength && + mPreChange.substring(0, mMaxLength).equals(postChange))) { + return; + } + mPreChange = postChange; + // This was simply a delete or a cut, so just delete the + // selection. + if (before > 0 && 0 == count) { + mWebView.deleteSelection(start, start + before); + // For this and all changes to the text, update our cache + updateCachedTextfield(); + return; + } + // Find the last character being replaced. If it can be represented by + // events, we will pass them to native (after replacing the beginning + // of the changed text), so we can see javascript events. + // Otherwise, replace the text being changed (including the last + // character) in the textfield. + TextUtils.getChars(s, start + count - 1, start + count, mCharacter, 0); + KeyCharacterMap kmap = + KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); + KeyEvent[] events = kmap.getEvents(mCharacter); + boolean cannotUseKeyEvents = null == events; + int charactersFromKeyEvents = cannotUseKeyEvents ? 0 : 1; + if (count > 1 || cannotUseKeyEvents) { + String replace = s.subSequence(start, + start + count - charactersFromKeyEvents).toString(); + mWebView.replaceTextfieldText(start, start + before, replace, + start + count - charactersFromKeyEvents, + start + count - charactersFromKeyEvents); + } else { + // This corrects the selection which may have been affected by the + // trackball or auto-correct. + mWebView.setSelection(start, start + before); + } + updateCachedTextfield(); + if (cannotUseKeyEvents) { + return; + } + int length = events.length; + for (int i = 0; i < length; i++) { + // We never send modifier keys to native code so don't send them + // here either. + if (!KeyEvent.isModifierKey(events[i].getKeyCode())) { + sendDomEvent(events[i]); + } + } + } + + @Override + public boolean onTrackballEvent(MotionEvent event) { + if (isPopupShowing()) { + return super.onTrackballEvent(event); + } + if (event.getAction() != MotionEvent.ACTION_MOVE) { + return false; + } + Spannable text = (Spannable) getText(); + MovementMethod move = getMovementMethod(); + if (move != null && getLayout() != null && + move.onTrackballEvent(this, text, event)) { + // Need to pass down the selection, which has changed. + // FIXME: This should work, but does not, so we set the selection + // in onTextChanged. + //int start = Selection.getSelectionStart(text); + //int end = Selection.getSelectionEnd(text); + //mWebView.setSelection(start, end); + return true; + } + // If the user is in a textfield, and the movement method is not + // handling the trackball events, it means they are at the end of the + // field and continuing to move the trackball. In this case, we should + // not scroll the cursor on screen bc the user may be attempting to + // scroll the page, possibly in the opposite direction of the cursor. + mScrollToAccommodateCursor = false; + return false; + } + + /** + * Remove this TextDialog from its host WebView, and return + * focus to the host. + */ + /* package */ void remove() { + // hide the soft keyboard when the edit text is out of focus + InputMethodManager.getInstance(mContext).hideSoftInputFromWindow( + getWindowToken(), 0); + mWebView.removeView(this); + mWebView.requestFocus(); + mScrollToAccommodateCursor = false; + } + + /* package */ void enableScrollOnScreen(boolean enable) { + mScrollToAccommodateCursor = enable; + } + + /* package */ void bringIntoView() { + if (getLayout() != null) { + bringPointIntoView(Selection.getSelectionEnd(getText())); + } + } + + @Override + public boolean requestRectangleOnScreen(Rect rectangle) { + if (mScrollToAccommodateCursor) { + return super.requestRectangleOnScreen(rectangle); + } + return false; + } + + /** + * Send the DOM events for the specified event. + * @param event KeyEvent to be translated into a DOM event. + */ + private void sendDomEvent(KeyEvent event) { + mWebView.passToJavaScript(getText().toString(), event); + } + + /** + * Always use this instead of setAdapter, as this has features specific to + * the TextDialog. + */ + public void setAdapterCustom(AutoCompleteAdapter adapter) { + if (adapter != null) { + setInputType(EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE); + adapter.setTextView(this); + } + super.setAdapter(adapter); + } + + /** + * This is a special version of ArrayAdapter which changes its text size + * to match the text size of its host TextView. + */ + public static class AutoCompleteAdapter extends ArrayAdapter<String> { + private TextView mTextView; + + public AutoCompleteAdapter(Context context, ArrayList<String> entries) { + super(context, com.android.internal.R.layout + .search_dropdown_item_1line, entries); + } + + /** + * {@inheritDoc} + */ + public View getView(int position, View convertView, ViewGroup parent) { + TextView tv = + (TextView) super.getView(position, convertView, parent); + if (tv != null && mTextView != null) { + tv.setTextSize(mTextView.getTextSize()); + } + return tv; + } + + /** + * Set the TextView so we can match its text size. + */ + private void setTextView(TextView tv) { + mTextView = tv; + } + } + + /** + * Determine whether to use the system-wide password disguising method, + * or to use none. + * @param inPassword True if the textfield is a password field. + */ + /* package */ void setInPassword(boolean inPassword) { + PasswordTransformationMethod method; + if (inPassword) { + method = PasswordTransformationMethod.getInstance(); + setInputType(EditorInfo.TYPE_CLASS_TEXT|EditorInfo. + TYPE_TEXT_VARIATION_PASSWORD); + } else { + method = null; + } + setTransformationMethod(method); + } + + /* package */ void setMaxLength(int maxLength) { + mMaxLength = maxLength; + if (-1 == maxLength) { + setFilters(NO_FILTERS); + } else { + setFilters(new InputFilter[] { + new InputFilter.LengthFilter(maxLength) }); + } + } + + /** + * Set the pointer for this node so it can be determined which node this + * TextDialog represents. + * @param ptr Integer representing the pointer to the node which this + * TextDialog represents. + */ + /* package */ void setNodePointer(int ptr) { + mNodePointer = ptr; + } + + /** + * Determine the position and size of TextDialog, and add it to the + * WebView's view heirarchy. All parameters are presumed to be in + * view coordinates. Also requests Focus and sets the cursor to not + * request to be in view. + * @param x x-position of the textfield. + * @param y y-position of the textfield. + * @param width width of the textfield. + * @param height height of the textfield. + */ + /* package */ void setRect(int x, int y, int width, int height) { + LayoutParams lp = (LayoutParams) getLayoutParams(); + if (null == lp) { + lp = new LayoutParams(width, height, x, y); + } else { + lp.x = x; + lp.y = y; + lp.width = width; + lp.height = height; + } + if (getParent() == null) { + mWebView.addView(this, lp); + } else { + setLayoutParams(lp); + } + // Set up a measure spec so a layout can always be recreated. + mWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + mHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + requestFocus(); + } + + /** + * Set whether this is a single-line textfield or a multi-line textarea. + * Textfields scroll horizontally, and do not handle the enter key. + * Textareas behave oppositely. + * Do NOT call this after calling setInPassword(true). This will result in + * removing the password input type. + */ + public void setSingleLine(boolean single) { + if (mSingle != single) { + TextKeyListener.Capitalize cap; + int inputType = EditorInfo.TYPE_CLASS_TEXT; + if (single) { + cap = TextKeyListener.Capitalize.NONE; + } else { + cap = TextKeyListener.Capitalize.SENTENCES; + inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE; + } + setKeyListener(TextKeyListener.getInstance(!single, cap)); + mSingle = single; + setHorizontallyScrolling(single); + setInputType(inputType); + } + } + + /** + * Set the text for this TextDialog, and set the selection to (start, end) + * @param text Text to go into this TextDialog. + * @param start Beginning of the selection. + * @param end End of the selection. + */ + /* package */ void setText(CharSequence text, int start, int end) { + mPreChange = text.toString(); + setText(text); + Spannable span = (Spannable) getText(); + int length = span.length(); + if (end > length) { + end = length; + } + if (start < 0) { + start = 0; + } else if (start > length) { + start = length; + } + Selection.setSelection(span, start, end); + } + + /** + * Set the text to the new string, but use the old selection, making sure + * to keep it within the new string. + * @param text The new text to place in the textfield. + */ + /* package */ void setTextAndKeepSelection(String text) { + mPreChange = text.toString(); + Editable edit = (Editable) getText(); + edit.replace(0, edit.length(), text); + updateCachedTextfield(); + } + + /** + * Update the cache to reflect the current text. + */ + /* package */ void updateCachedTextfield() { + mWebView.updateCachedTextfield(getText().toString()); + } +} diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java new file mode 100644 index 0000000..0e8144e --- /dev/null +++ b/core/java/android/webkit/URLUtil.java @@ -0,0 +1,363 @@ +/* + * 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 java.io.UnsupportedEncodingException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.net.Uri; +import android.net.ParseException; +import android.net.WebAddress; +import android.util.Config; +import android.util.Log; + +public final class URLUtil { + + private static final String LOGTAG = "webkit"; + + static final String ASSET_BASE = "file:///android_asset/"; + static final String FILE_BASE = "file://"; + static final String PROXY_BASE = "file:///cookieless_proxy/"; + + /** + * Cleans up (if possible) user-entered web addresses + */ + public static String guessUrl(String inUrl) { + + String retVal = inUrl; + WebAddress webAddress; + + Log.v(LOGTAG, "guessURL before queueRequest: " + inUrl); + + if (inUrl.length() == 0) return inUrl; + if (inUrl.startsWith("about:")) return inUrl; + // Do not try to interpret data scheme URLs + if (inUrl.startsWith("data:")) return inUrl; + // Do not try to interpret file scheme URLs + if (inUrl.startsWith("file:")) return inUrl; + // Do not try to interpret javascript scheme URLs + if (inUrl.startsWith("javascript:")) return inUrl; + + // bug 762454: strip period off end of url + if (inUrl.endsWith(".") == true) { + inUrl = inUrl.substring(0, inUrl.length() - 1); + } + + try { + webAddress = new WebAddress(inUrl); + } catch (ParseException ex) { + + if (Config.LOGV) { + Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl); + } + return retVal; + } + + // Check host + if (webAddress.mHost.indexOf('.') == -1) { + // no dot: user probably entered a bare domain. try .com + webAddress.mHost = "www." + webAddress.mHost + ".com"; + } + return webAddress.toString(); + } + + public static String composeSearchUrl(String inQuery, String template, + String queryPlaceHolder) { + int placeHolderIndex = template.indexOf(queryPlaceHolder); + if (placeHolderIndex < 0) { + return null; + } + + String query; + StringBuilder buffer = new StringBuilder(); + buffer.append(template.substring(0, placeHolderIndex)); + + try { + query = java.net.URLEncoder.encode(inQuery, "utf-8"); + buffer.append(query); + } catch (UnsupportedEncodingException ex) { + return null; + } + + buffer.append(template.substring( + placeHolderIndex + queryPlaceHolder.length())); + + return buffer.toString(); + } + + public static byte[] decode(byte[] url) throws IllegalArgumentException { + if (url.length == 0) { + return new byte[0]; + } + + // Create a new byte array with the same length to ensure capacity + byte[] tempData = new byte[url.length]; + + int tempCount = 0; + for (int i = 0; i < url.length; i++) { + byte b = url[i]; + if (b == '%') { + if (url.length - i > 2) { + b = (byte) (parseHex(url[i + 1]) * 16 + + parseHex(url[i + 2])); + i += 2; + } else { + throw new IllegalArgumentException("Invalid format"); + } + } + tempData[tempCount++] = b; + } + byte[] retData = new byte[tempCount]; + System.arraycopy(tempData, 0, retData, 0, tempCount); + return retData; + } + + private static int parseHex(byte b) { + if (b >= '0' && b <= '9') return (b - '0'); + if (b >= 'A' && b <= 'F') return (b - 'A' + 10); + if (b >= 'a' && b <= 'f') return (b - 'a' + 10); + + throw new IllegalArgumentException("Invalid hex char '" + b + "'"); + } + + /** + * @return True iff the url is an asset file. + */ + public static boolean isAssetUrl(String url) { + return (null != url) && url.startsWith(ASSET_BASE); + } + + /** + * @return True iff the url is an proxy url to allow cookieless network + * requests from a file url. + * @deprecated Cookieless proxy is no longer supported. + */ + public static boolean isCookielessProxyUrl(String url) { + return (null != url) && url.startsWith(PROXY_BASE); + } + + /** + * @return True iff the url is a local file. + */ + public static boolean isFileUrl(String url) { + return (null != url) && (url.startsWith(FILE_BASE) && + !url.startsWith(ASSET_BASE) && + !url.startsWith(PROXY_BASE)); + } + + /** + * @return True iff the url is an about: url. + */ + public static boolean isAboutUrl(String url) { + return (null != url) && url.startsWith("about:"); + } + + /** + * @return True iff the url is a data: url. + */ + public static boolean isDataUrl(String url) { + return (null != url) && url.startsWith("data:"); + } + + /** + * @return True iff the url is a javascript: url. + */ + public static boolean isJavaScriptUrl(String url) { + return (null != url) && url.startsWith("javascript:"); + } + + /** + * @return True iff the url is an http: url. + */ + public static boolean isHttpUrl(String url) { + return (null != url) && + (url.length() > 6) && + url.substring(0, 7).equalsIgnoreCase("http://"); + } + + /** + * @return True iff the url is an https: url. + */ + public static boolean isHttpsUrl(String url) { + return (null != url) && + (url.length() > 7) && + url.substring(0, 8).equalsIgnoreCase("https://"); + } + + /** + * @return True iff the url is a network url. + */ + public static boolean isNetworkUrl(String url) { + if (url == null || url.length() == 0) { + return false; + } + return isHttpUrl(url) || isHttpsUrl(url); + } + + /** + * @return True iff the url is a content: url. + */ + public static boolean isContentUrl(String url) { + return (null != url) && url.startsWith("content:"); + } + + /** + * @return True iff the url is valid. + */ + public static boolean isValidUrl(String url) { + if (url == null || url.length() == 0) { + return false; + } + + return (isAssetUrl(url) || + isFileUrl(url) || + isAboutUrl(url) || + isHttpUrl(url) || + isHttpsUrl(url) || + isJavaScriptUrl(url) || + isContentUrl(url)); + } + + /** + * Strips the url of the anchor. + */ + public static String stripAnchor(String url) { + int anchorIndex = url.indexOf('#'); + if (anchorIndex != -1) { + return url.substring(0, anchorIndex); + } + return url; + } + + /** + * Guesses canonical filename that a download would have, using + * the URL and contentDisposition. File extension, if not defined, + * is added based on the mimetype + * @param url Url to the content + * @param contentDisposition Content-Disposition HTTP header or null + * @param mimeType Mime-type of the content or null + * + * @return suggested filename + */ + public static final String guessFileName( + String url, + String contentDisposition, + String mimeType) { + String filename = null; + String extension = null; + + // If we couldn't do anything with the hint, move toward the content disposition + if (filename == null && contentDisposition != null) { + filename = parseContentDisposition(contentDisposition); + if (filename != null) { + int index = filename.lastIndexOf('/') + 1; + if (index > 0) { + filename = filename.substring(index); + } + } + } + + // If all the other http-related approaches failed, use the plain uri + if (filename == null) { + String decodedUrl = Uri.decode(url); + if (decodedUrl != null) { + int queryIndex = decodedUrl.indexOf('?'); + // If there is a query string strip it, same as desktop browsers + if (queryIndex > 0) { + decodedUrl = decodedUrl.substring(0, queryIndex); + } + if (!decodedUrl.endsWith("/")) { + int index = decodedUrl.lastIndexOf('/') + 1; + if (index > 0) { + filename = decodedUrl.substring(index); + } + } + } + } + + // Finally, if couldn't get filename from URI, get a generic filename + if (filename == null) { + filename = "downloadfile"; + } + + // Split filename between base and extension + // Add an extension if filename does not have one + int dotIndex = filename.indexOf('.'); + if (dotIndex < 0) { + if (mimeType != null) { + extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + if (extension != null) { + extension = "." + extension; + } + } + if (extension == null) { + if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) { + if (mimeType.equalsIgnoreCase("text/html")) { + extension = ".html"; + } else { + extension = ".txt"; + } + } else { + extension = ".bin"; + } + } + } else { + if (mimeType != null) { + // Compare the last segment of the extension against the mime type. + // If there's a mismatch, discard the entire extension. + int lastDotIndex = filename.lastIndexOf('.'); + String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension( + filename.substring(lastDotIndex + 1)); + if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) { + extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + if (extension != null) { + extension = "." + extension; + } + } + } + if (extension == null) { + extension = filename.substring(dotIndex); + } + filename = filename.substring(0, dotIndex); + } + + return filename + extension; + } + + /** Regex used to parse content-disposition headers */ + private static final Pattern CONTENT_DISPOSITION_PATTERN = + Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\""); + + /* + * Parse the Content-Disposition HTTP Header. The format of the header + * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html + * This header provides a filename for content that is going to be + * downloaded to the file system. We only support the attachment type. + */ + private static String parseContentDisposition(String contentDisposition) { + try { + Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition); + if (m.find()) { + return m.group(1); + } + } catch (IllegalStateException ex) { + // This function is defined as returning null when it can't parse the header + } + return null; + } +} diff --git a/core/java/android/webkit/UrlInterceptHandler.java b/core/java/android/webkit/UrlInterceptHandler.java new file mode 100644 index 0000000..e1c9d61 --- /dev/null +++ b/core/java/android/webkit/UrlInterceptHandler.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2008 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.webkit.CacheManager.CacheResult; +import java.util.Map; + +public interface UrlInterceptHandler { + + /** + * Given an URL, returns the CacheResult which contains the + * surrogate response for the request, or null if the handler is + * not interested. + * + * @param url URL string. + * @param headers The headers associated with the request. May be null. + * @return The CacheResult containing the surrogate response. + */ + public CacheResult service(String url, Map<String, String> headers); +} diff --git a/core/java/android/webkit/UrlInterceptRegistry.java b/core/java/android/webkit/UrlInterceptRegistry.java new file mode 100644 index 0000000..a218191 --- /dev/null +++ b/core/java/android/webkit/UrlInterceptRegistry.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2008 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.webkit.CacheManager.CacheResult; +import android.webkit.UrlInterceptHandler; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; + +public final class UrlInterceptRegistry { + + private final static String LOGTAG = "intercept"; + + private static boolean mDisabled = false; + + private static LinkedList mHandlerList; + + private static synchronized LinkedList getHandlers() { + if(mHandlerList == null) + mHandlerList = new LinkedList<UrlInterceptHandler>(); + return mHandlerList; + } + + /** + * set the flag to control whether url intercept is enabled or disabled + * + * @param disabled true to disable the cache + */ + public static synchronized void setUrlInterceptDisabled(boolean disabled) { + mDisabled = disabled; + } + + /** + * get the state of the url intercept, enabled or disabled + * + * @return return if it is disabled + */ + public static synchronized boolean urlInterceptDisabled() { + return mDisabled; + } + + /** + * Register a new UrlInterceptHandler. This handler will be called + * before any that were previously registered. + * + * @param handler The new UrlInterceptHandler object + * @return true if the handler was not previously registered. + */ + public static synchronized boolean registerHandler( + UrlInterceptHandler handler) { + if (!getHandlers().contains(handler)) { + getHandlers().addFirst(handler); + return true; + } else { + return false; + } + } + + /** + * Unregister a previously registered UrlInterceptHandler. + * + * @param handler A previously registered UrlInterceptHandler. + * @return true if the handler was found and removed from the list. + */ + public static synchronized boolean unregisterHandler( + UrlInterceptHandler handler) { + return getHandlers().remove(handler); + } + + /** + * Given an url, returns the CacheResult of the first + * UrlInterceptHandler interested, or null if none are. + * + * @return A CacheResult containing surrogate content. + */ + public static synchronized CacheResult getSurrogate( + String url, Map<String, String> headers) { + if (urlInterceptDisabled()) + return null; + Iterator iter = getHandlers().listIterator(); + while (iter.hasNext()) { + UrlInterceptHandler handler = (UrlInterceptHandler) iter.next(); + CacheResult result = handler.service(url, headers); + if (result != null) { + return result; + } + } + return null; + } +} diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java new file mode 100644 index 0000000..9dea5ec --- /dev/null +++ b/core/java/android/webkit/WebBackForwardList.java @@ -0,0 +1,188 @@ +/* + * 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.util.Config; +import java.io.Serializable; +import java.util.ArrayList; + +/** + * This class contains the back/forward list for a WebView. + * WebView.copyBackForwardList() will return a copy of this class used to + * inspect the entries in the list. + */ +public class WebBackForwardList implements Cloneable, Serializable { + // Current position in the list. + private int mCurrentIndex; + // ArrayList of WebHistoryItems for maintaining our copy. + private ArrayList<WebHistoryItem> mArray; + // Flag to indicate that the list is invalid + private boolean mClearPending; + + /** + * Construct a back/forward list used by clients of WebView. + */ + /*package*/ WebBackForwardList() { + mCurrentIndex = -1; + mArray = new ArrayList<WebHistoryItem>(); + } + + /** + * Return the current history item. This method returns null if the list is + * empty. + * @return The current history item. + */ + public synchronized WebHistoryItem getCurrentItem() { + return getItemAtIndex(mCurrentIndex); + } + + /** + * Get the index of the current history item. This index can be used to + * directly index into the array list. + * @return The current index from 0...n or -1 if the list is empty. + */ + public synchronized int getCurrentIndex() { + return mCurrentIndex; + } + + /** + * Get the history item at the given index. The index range is from 0...n + * where 0 is the first item and n is the last item. + * @param index The index to retrieve. + */ + public synchronized WebHistoryItem getItemAtIndex(int index) { + if (index < 0 || index >= getSize()) { + return null; + } + return mArray.get(index); + } + + /** + * Get the total size of the back/forward list. + * @return The size of the list. + */ + public synchronized int getSize() { + return mArray.size(); + } + + /** + * Mark the back/forward list as having a pending clear. This is used on the + * UI side to mark the list as being invalid during the clearHistory method. + */ + /*package*/ synchronized void setClearPending() { + mClearPending = true; + } + + /** + * Return the status of the clear flag. This is used on the UI side to + * determine if the list is valid for checking things like canGoBack. + */ + /*package*/ synchronized boolean getClearPending() { + return mClearPending; + } + + /** + * Add a new history item to the list. This will remove all items after the + * current item and append the new item to the end of the list. Called from + * the WebCore thread only. Synchronized because the UI thread may be + * reading the array or the current index. + * @param item A new history item. + */ + /*package*/ synchronized void addHistoryItem(WebHistoryItem item) { + // Update the current position because we are going to add the new item + // in that slot. + ++mCurrentIndex; + // If the current position is not at the end, remove all history items + // after the current item. + final int size = mArray.size(); + final int newPos = mCurrentIndex; + if (newPos != size) { + for (int i = size - 1; i >= newPos; i--) { + final WebHistoryItem h = mArray.remove(i); + } + } + // Add the item to the list. + mArray.add(item); + } + + /** + * Clear the back/forward list. Called from the WebCore thread. + */ + /*package*/ synchronized void close(int nativeFrame) { + // Clear the array first because nativeClose will call addHistoryItem + // with the current item. + mArray.clear(); + mCurrentIndex = -1; + nativeClose(nativeFrame); + // Reset the clear flag + mClearPending = false; + } + + /* Remove the item at the given index. Called by JNI only. */ + private synchronized void removeHistoryItem(int index) { + // XXX: This is a special case. Since the callback is only triggered + // when removing the first item, we can assert that the index is 0. + // This lets us change the current index without having to query the + // native BackForwardList. + if (Config.DEBUG && (index != 0)) { + throw new AssertionError(); + } + final WebHistoryItem h = mArray.remove(index); + // XXX: If we ever add another callback for removing history items at + // any index, this will no longer be valid. + mCurrentIndex--; + } + + /** + * Clone the entire object to be used in the UI thread by clients of + * WebView. This creates a copy that should never be modified by any of the + * webkit package classes. + */ + protected synchronized WebBackForwardList clone() { + WebBackForwardList l = new WebBackForwardList(); + if (mClearPending) { + // If a clear is pending, return a copy with only the current item. + l.addHistoryItem(getCurrentItem()); + return l; + } + l.mCurrentIndex = mCurrentIndex; + int size = getSize(); + l.mArray = new ArrayList<WebHistoryItem>(size); + for (int i = 0; i < size; i++) { + // Add a copy of each WebHistoryItem + l.mArray.add(mArray.get(i).clone()); + } + return l; + } + + /** + * Set the new history index. + * @param newIndex The new history index. + */ + /*package*/ synchronized void setCurrentIndex(int newIndex) { + mCurrentIndex = newIndex; + } + + /** + * Restore the history index. + */ + /*package*/ static native synchronized void restoreIndex(int nativeFrame, + int index); + + /* Close the native list. */ + private static native void nativeClose(int nativeFrame); +} diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java new file mode 100644 index 0000000..f940006 --- /dev/null +++ b/core/java/android/webkit/WebChromeClient.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2008 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.graphics.Bitmap; +import android.os.Message; + +public class WebChromeClient { + + /** + * Tell the host application the current progress of loading a page. + * @param view The WebView that initiated the callback. + * @param newProgress Current page loading progress, represented by + * an integer between 0 and 100. + */ + public void onProgressChanged(WebView view, int newProgress) {} + + /** + * Notify the host application of a change in the document title. + * @param view The WebView that initiated the callback. + * @param title A String containing the new title of the document. + */ + public void onReceivedTitle(WebView view, String title) {} + + /** + * Notify the host application of a new favicon for the current page. + * @param view The WebView that initiated the callback. + * @param icon A Bitmap containing the favicon for the current page. + */ + public void onReceivedIcon(WebView view, Bitmap icon) {} + + /** + * Request the host application to create a new Webview. The host + * application should handle placement of the new WebView in the view + * system. The default behavior returns null. + * @param view The WebView that initiated the callback. + * @param dialog True if the new window is meant to be a small dialog + * window. + * @param userGesture True if the request was initiated by a user gesture + * such as clicking a link. + * @param resultMsg The message to send when done creating a new WebView. + * Set the new WebView through resultMsg.obj which is + * WebView.WebViewTransport() and then call + * resultMsg.sendToTarget(); + * @return Similar to javscript dialogs, this method should return true if + * the client is going to handle creating a new WebView. Note that + * the WebView will halt processing if this method returns true so + * make sure to call resultMsg.sendToTarget(). It is undefined + * behavior to call resultMsg.sendToTarget() after returning false + * from this method. + */ + public boolean onCreateWindow(WebView view, boolean dialog, + boolean userGesture, Message resultMsg) { + return false; + } + + /** + * Request display and focus for this WebView. This may happen due to + * another WebView opening a link in this WebView and requesting that this + * WebView be displayed. + * @param view The WebView that needs to be focused. + */ + public void onRequestFocus(WebView view) {} + + /** + * Notify the host application to close the given WebView and remove it + * from the view system if necessary. At this point, WebCore has stopped + * any loading in this window and has removed any cross-scripting ability + * in javascript. + * @param window The WebView that needs to be closed. + */ + public void onCloseWindow(WebView window) {} + + /** + * Tell the client to display a javascript alert dialog. If the client + * returns true, WebView will assume that the client will handle the + * dialog. If the client returns false, it will continue execution. + * @param view The WebView that initiated the callback. + * @param url The url of the page requesting the dialog. + * @param message Message to be displayed in the window. + * @param result A JsResult to confirm that the user hit enter. + * @return boolean Whether the client will handle the alert dialog. + */ + public boolean onJsAlert(WebView view, String url, String message, + JsResult result) { + return false; + } + + /** + * Tell the client to display a confirm dialog to the user. If the client + * returns true, WebView will assume that the client will handle the + * confirm dialog and call the appropriate JsResult method. If the + * client returns false, a default value of false will be returned to + * javascript. The default behavior is to return false. + * @param view The WebView that initiated the callback. + * @param url The url of the page requesting the dialog. + * @param message Message to be displayed in the window. + * @param result A JsResult used to send the user's response to + * javascript. + * @return boolean Whether the client will handle the confirm dialog. + */ + public boolean onJsConfirm(WebView view, String url, String message, + JsResult result) { + return false; + } + + /** + * Tell the client to display a prompt dialog to the user. If the client + * returns true, WebView will assume that the client will handle the + * prompt dialog and call the appropriate JsPromptResult method. If the + * client returns false, a default value of false will be returned to to + * javascript. The default behavior is to return false. + * @param view The WebView that initiated the callback. + * @param url The url of the page requesting the dialog. + * @param message Message to be displayed in the window. + * @param defaultValue The default value displayed in the prompt dialog. + * @param result A JsPromptResult used to send the user's reponse to + * javascript. + * @return boolean Whether the client will handle the prompt dialog. + */ + public boolean onJsPrompt(WebView view, String url, String message, + String defaultValue, JsPromptResult result) { + return false; + } + + /** + * Tell the client to display a dialog to confirm navigation away from the + * current page. This is the result of the onbeforeunload javascript event. + * If the client returns true, WebView will assume that the client will + * handle the confirm dialog and call the appropriate JsResult method. If + * the client returns false, a default value of true will be returned to + * javascript to accept navigation away from the current page. The default + * behavior is to return false. Setting the JsResult to true will navigate + * away from the current page, false will cancel the navigation. + * @param view The WebView that initiated the callback. + * @param url The url of the page requesting the dialog. + * @param message Message to be displayed in the window. + * @param result A JsResult used to send the user's response to + * javascript. + * @return boolean Whether the client will handle the confirm dialog. + */ + public boolean onJsBeforeUnload(WebView view, String url, String message, + JsResult result) { + return false; + } +} diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java new file mode 100644 index 0000000..a408e06 --- /dev/null +++ b/core/java/android/webkit/WebHistoryItem.java @@ -0,0 +1,179 @@ +/* + * 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.graphics.Bitmap; + +/** + * A convenience class for accessing fields in an entry in the back/forward list + * of a WebView. Each WebHistoryItem is a snapshot of the requested history + * item. Each history item may be updated during the load of a page. + * @see WebBackForwardList + */ +public class WebHistoryItem implements Cloneable { + // Global identifier count. + private static int sNextId = 0; + // Unique identifier. + private final int mId; + // The title of this item's document. + private String mTitle; + // The base url of this item. + private String mUrl; + // The original requested url of this item. + private String mOriginalUrl; + // The favicon for this item. + private Bitmap mFavicon; + // The pre-flattened data used for saving the state. + private byte[] mFlattenedData; + + /** + * Basic constructor that assigns a unique id to the item. Called by JNI + * only. + */ + private WebHistoryItem() { + synchronized (WebHistoryItem.class) { + mId = sNextId++; + } + } + + /** + * Construct a new WebHistoryItem with initial flattened data. + * @param data The pre-flattened data coming from restoreState. + */ + /*package*/ WebHistoryItem(byte[] data) { + mUrl = null; // This will be updated natively + mFlattenedData = data; + synchronized (WebHistoryItem.class) { + mId = sNextId++; + } + } + + /** + * Construct a clone of a WebHistoryItem from the given item. + * @param item The history item to clone. + */ + private WebHistoryItem(WebHistoryItem item) { + mUrl = item.mUrl; + mTitle = item.mTitle; + mFlattenedData = item.mFlattenedData; + mFavicon = item.mFavicon; + mId = item.mId; +} + + /** + * Return an identifier for this history item. If an item is a copy of + * another item, the identifiers will be the same even if they are not the + * same object. + * @return The id for this item. + */ + public int getId() { + return mId; + } + + /** + * Return the url of this history item. The url is the base url of this + * history item. See getTargetUrl() for the url that is the actual target of + * this history item. + * @return The base url of this history item. + * Note: The VM ensures 32-bit atomic read/write operations so we don't have + * to synchronize this method. + */ + public String getUrl() { + return mUrl; + } + + /** + * Return the original url of this history item. This was the requested + * url, the final url may be different as there might have been + * redirects while loading the site. + * @return The original url of this history item. + * + * @hide pending API Council approval + */ + public String getOriginalUrl() { + return mOriginalUrl; + } + + /** + * Return the document title of this history item. + * @return The document title of this history item. + * Note: The VM ensures 32-bit atomic read/write operations so we don't have + * to synchronize this method. + */ + public String getTitle() { + return mTitle; + } + + /** + * Return the favicon of this history item or null if no favicon was found. + * @return A Bitmap containing the favicon for this history item or null. + * Note: The VM ensures 32-bit atomic read/write operations so we don't have + * to synchronize this method. + */ + public Bitmap getFavicon() { + return mFavicon; + } + + /** + * Set the favicon. + * @param icon A Bitmap containing the favicon for this history item. + * Note: The VM ensures 32-bit atomic read/write operations so we don't have + * to synchronize this method. + */ + /*package*/ void setFavicon(Bitmap icon) { + mFavicon = icon; + } + + /** + * Get the pre-flattened data. + * Note: The VM ensures 32-bit atomic read/write operations so we don't have + * to synchronize this method. + */ + /*package*/ byte[] getFlattenedData() { + return mFlattenedData; + } + + /** + * Inflate this item. + * Note: The VM ensures 32-bit atomic read/write operations so we don't have + * to synchronize this method. + */ + /*package*/ void inflate(int nativeFrame) { + inflate(nativeFrame, mFlattenedData); + } + + /** + * Clone the history item for use by clients of WebView. + */ + protected synchronized WebHistoryItem clone() { + return new WebHistoryItem(this); + } + + /* Natively inflate this item, this method is called in the WebCore thread. + */ + private native void inflate(int nativeFrame, byte[] data); + + /* Called by jni when the item is updated */ + private void update(String url, String originalUrl, String title, + Bitmap favicon, byte[] data) { + mUrl = url; + mOriginalUrl = originalUrl; + mTitle = title; + mFavicon = favicon; + mFlattenedData = data; + } +} diff --git a/core/java/android/webkit/WebIconDatabase.java b/core/java/android/webkit/WebIconDatabase.java new file mode 100644 index 0000000..d284f5e --- /dev/null +++ b/core/java/android/webkit/WebIconDatabase.java @@ -0,0 +1,251 @@ +/* + * 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.os.Handler; +import android.os.Message; +import android.graphics.Bitmap; + +import java.util.Vector; + +/** + * Functions for manipulating the icon database used by WebView. + * These functions require that a WebView be constructed before being invoked + * and WebView.getIconDatabase() will return a WebIconDatabase object. This + * WebIconDatabase object is a single instance and all methods operate on that + * single object. + */ +public final class WebIconDatabase { + // Global instance of a WebIconDatabase + private static WebIconDatabase sIconDatabase; + // EventHandler for handling messages before and after the WebCore thread is + // ready. + private final EventHandler mEventHandler = new EventHandler(); + + // Class to handle messages before WebCore is ready + private class EventHandler extends Handler { + // Message ids + static final int OPEN = 0; + static final int CLOSE = 1; + static final int REMOVE_ALL = 2; + static final int REQUEST_ICON = 3; + static final int RETAIN_ICON = 4; + static final int RELEASE_ICON = 5; + // Message for dispatching icon request results + private static final int ICON_RESULT = 10; + // Actual handler that runs in WebCore thread + private Handler mHandler; + // Vector of messages before the WebCore thread is ready + private Vector<Message> mMessages = new Vector<Message>(); + // Class to handle a result dispatch + private class IconResult { + private final String mUrl; + private final Bitmap mIcon; + private final IconListener mListener; + IconResult(String url, Bitmap icon, IconListener l) { + mUrl = url; + mIcon = icon; + mListener = l; + } + void dispatch() { + mListener.onReceivedIcon(mUrl, mIcon); + } + } + + @Override + public void handleMessage(Message msg) { + // Note: This is the message handler for the UI thread. + switch (msg.what) { + case ICON_RESULT: + ((IconResult) msg.obj).dispatch(); + break; + } + } + + // Called by WebCore thread to create the actual handler + private synchronized void createHandler() { + if (mHandler == null) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // Note: This is the message handler for the WebCore + // thread. + switch (msg.what) { + case OPEN: + nativeOpen((String) msg.obj); + break; + + case CLOSE: + nativeClose(); + break; + + case REMOVE_ALL: + nativeRemoveAllIcons(); + break; + + case REQUEST_ICON: + IconListener l = (IconListener) msg.obj; + String url = msg.getData().getString("url"); + Bitmap icon = nativeIconForPageUrl(url); + if (icon != null) { + EventHandler.this.sendMessage( + Message.obtain(null, ICON_RESULT, + new IconResult(url, icon, l))); + } + break; + + case RETAIN_ICON: + nativeRetainIconForPageUrl((String) msg.obj); + break; + + case RELEASE_ICON: + nativeReleaseIconForPageUrl((String) msg.obj); + break; + } + } + }; + // Transfer all pending messages + for (int size = mMessages.size(); size > 0; size--) { + mHandler.sendMessage(mMessages.remove(0)); + } + mMessages = null; + } + } + + private synchronized void postMessage(Message msg) { + if (mMessages != null) { + mMessages.add(msg); + } else { + mHandler.sendMessage(msg); + } + } + } + + /** + * Interface for receiving icons from the database. + */ + public interface IconListener { + /** + * Called when the icon has been retrieved from the database and the + * result is non-null. + * @param url The url passed in the request. + * @param icon The favicon for the given url. + */ + public void onReceivedIcon(String url, Bitmap icon); + } + + /** + * Open a the icon database and store the icons in the given path. + * @param path The directory path where the icon database will be stored. + * @return True if the database was successfully opened or created in + * the given path. + */ + public void open(String path) { + if (path != null) { + mEventHandler.postMessage( + Message.obtain(null, EventHandler.OPEN, path)); + } + } + + /** + * Close the shared instance of the icon database. + */ + public void close() { + mEventHandler.postMessage( + Message.obtain(null, EventHandler.CLOSE)); + } + + /** + * Removes all the icons in the database. + */ + public void removeAllIcons() { + mEventHandler.postMessage( + Message.obtain(null, EventHandler.REMOVE_ALL)); + } + + /** + * Request the Bitmap representing the icon for the given page + * url. If the icon exists, the listener will be called with the result. + * @param url The page's url. + * @param listener An implementation on IconListener to receive the result. + */ + public void requestIconForPageUrl(String url, IconListener listener) { + if (listener == null || url == null) { + return; + } + Message msg = Message.obtain(null, EventHandler.REQUEST_ICON, listener); + msg.getData().putString("url", url); + mEventHandler.postMessage(msg); + } + + /** + * Retain the icon for the given page url. + * @param url The page's url. + */ + public void retainIconForPageUrl(String url) { + if (url != null) { + mEventHandler.postMessage( + Message.obtain(null, EventHandler.RETAIN_ICON, url)); + } + } + + /** + * Release the icon for the given page url. + * @param url The page's url. + */ + public void releaseIconForPageUrl(String url) { + if (url != null) { + mEventHandler.postMessage( + Message.obtain(null, EventHandler.RELEASE_ICON, url)); + } + } + + /** + * Get the global instance of WebIconDatabase. + * @return A single instance of WebIconDatabase. It will be the same + * instance for the current process each time this method is + * called. + */ + public static WebIconDatabase getInstance() { + // XXX: Must be created in the UI thread. + if (sIconDatabase == null) { + sIconDatabase = new WebIconDatabase(); + } + return sIconDatabase; + } + + /** + * Create the internal handler and transfer all pending messages. + * XXX: Called by WebCore thread only! + */ + /*package*/ void createHandler() { + mEventHandler.createHandler(); + } + + /** + * Private constructor to avoid anyone else creating an instance. + */ + private WebIconDatabase() {} + + // Native functions + private static native void nativeOpen(String path); + private static native void nativeClose(); + private static native void nativeRemoveAllIcons(); + private static native Bitmap nativeIconForPageUrl(String url); + private static native void nativeRetainIconForPageUrl(String url); + private static native void nativeReleaseIconForPageUrl(String url); +} diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java new file mode 100644 index 0000000..4e2b2ab --- /dev/null +++ b/core/java/android/webkit/WebSettings.java @@ -0,0 +1,1112 @@ +/* + * Copyright (C) 2007 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.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.provider.Checkin; + +import java.lang.SecurityException; +import android.content.pm.PackageManager; + +import java.util.Locale; + +/** + * Manages settings state for a WebView. When a WebView is first created, it + * obtains a set of default settings. These default settings will be returned + * from any getter call. A WebSettings object obtained from + * WebView.getSettings() is tied to the life of the WebView. If a WebView has + * been destroyed, any method call on WebSettings will throw an + * IllegalStateException. + */ +public class WebSettings { + /** + * Enum for controlling the layout of html. + * NORMAL means no rendering changes. + * SINGLE_COLUMN moves all content into one column that is the width of the + * view. + * NARROW_COLUMNS makes all columns no wider than the screen if possible. + */ + // XXX: These must match LayoutAlgorithm in Settings.h in WebCore. + public enum LayoutAlgorithm { + NORMAL, + SINGLE_COLUMN, + NARROW_COLUMNS + } + + /** + * Enum for specifying the text size. + * SMALLEST is 50% + * SMALLER is 75% + * NORMAL is 100% + * LARGER is 150% + * LARGEST is 200% + */ + public enum TextSize { + SMALLEST(50), + SMALLER(75), + NORMAL(100), + LARGER(150), + LARGEST(200); + TextSize(int size) { + value = size; + } + int value; + } + + /** + * Default cache usage pattern Use with {@link #setCacheMode}. + */ + public static final int LOAD_DEFAULT = -1; + + /** + * Normal cache usage pattern Use with {@link #setCacheMode}. + */ + public static final int LOAD_NORMAL = 0; + + /** + * Use cache if content is there, even if expired (eg, history nav) + * If it is not in the cache, load from network. + * Use with {@link #setCacheMode}. + */ + public static final int LOAD_CACHE_ELSE_NETWORK = 1; + + /** + * Don't use the cache, load from network + * Use with {@link #setCacheMode}. + */ + public static final int LOAD_NO_CACHE = 2; + + /** + * Don't use the network, load from cache only. + * Use with {@link #setCacheMode}. + */ + public static final int LOAD_CACHE_ONLY = 3; + + public enum RenderPriority { + NORMAL, + HIGH, + LOW + } + + // BrowserFrame used to access the native frame pointer. + private BrowserFrame mBrowserFrame; + // Flag to prevent multiple SYNC messages at one time. + private boolean mSyncPending = false; + // Custom handler that queues messages until the WebCore thread is active. + private final EventHandler mEventHandler; + // Private settings so we don't have to go into native code to + // retrieve the values. After setXXX, postSync() needs to be called. + // XXX: The default values need to match those in WebSettings.cpp + private LayoutAlgorithm mLayoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS; + private Context mContext; + private TextSize mTextSize = TextSize.NORMAL; + private String mStandardFontFamily = "sans-serif"; + private String mFixedFontFamily = "monospace"; + private String mSansSerifFontFamily = "sans-serif"; + private String mSerifFontFamily = "serif"; + private String mCursiveFontFamily = "cursive"; + private String mFantasyFontFamily = "fantasy"; + private String mDefaultTextEncoding = "Latin-1"; + private String mUserAgent; + private boolean mUseDefaultUserAgent; + private String mAcceptLanguage; + private String mPluginsPath = ""; + private int mMinimumFontSize = 8; + private int mMinimumLogicalFontSize = 8; + private int mDefaultFontSize = 16; + private int mDefaultFixedFontSize = 13; + private boolean mLoadsImagesAutomatically = true; + private boolean mBlockNetworkImage = false; + private boolean mBlockNetworkLoads = false; + private boolean mJavaScriptEnabled = false; + private boolean mPluginsEnabled = false; + private boolean mJavaScriptCanOpenWindowsAutomatically = false; + private boolean mUseDoubleTree = false; + private boolean mUseWideViewport = false; + private boolean mSupportMultipleWindows = false; + private boolean mShrinksStandaloneImagesToFit = false; + // Don't need to synchronize the get/set methods as they + // are basic types, also none of these values are used in + // native WebCore code. + private RenderPriority mRenderPriority = RenderPriority.NORMAL; + private int mOverrideCacheMode = LOAD_DEFAULT; + private boolean mSaveFormData = true; + private boolean mSavePassword = true; + private boolean mLightTouchEnabled = false; + private boolean mNeedInitialFocus = true; + private boolean mNavDump = false; + private boolean mSupportZoom = true; + private boolean mAllowFileAccess = true; + + // Class to handle messages before WebCore is ready. + private class EventHandler { + // Message id for syncing + static final int SYNC = 0; + // Message id for setting priority + static final int PRIORITY = 1; + // Actual WebCore thread handler + private Handler mHandler; + + private synchronized void createHandler() { + // as mRenderPriority can be set before thread is running, sync up + setRenderPriority(); + + // create a new handler + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SYNC: + synchronized (WebSettings.this) { + if (mBrowserFrame.mNativeFrame != 0) { + nativeSync(mBrowserFrame.mNativeFrame); + } + mSyncPending = false; + } + break; + + case PRIORITY: { + setRenderPriority(); + break; + } + } + } + }; + } + + private void setRenderPriority() { + synchronized (WebSettings.this) { + if (mRenderPriority == RenderPriority.NORMAL) { + android.os.Process.setThreadPriority( + android.os.Process.THREAD_PRIORITY_DEFAULT); + } else if (mRenderPriority == RenderPriority.HIGH) { + android.os.Process.setThreadPriority( + android.os.Process.THREAD_PRIORITY_FOREGROUND + + android.os.Process.THREAD_PRIORITY_LESS_FAVORABLE); + } else if (mRenderPriority == RenderPriority.LOW) { + android.os.Process.setThreadPriority( + android.os.Process.THREAD_PRIORITY_BACKGROUND); + } + } + } + + /** + * Send a message to the private queue or handler. + */ + private synchronized boolean sendMessage(Message msg) { + if (mHandler != null) { + mHandler.sendMessage(msg); + return true; + } else { + return false; + } + } + } + + // User agent strings. + private static final String DESKTOP_USERAGENT = + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en)" + + " AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2" + + " Safari/525.20.1"; + private static final String IPHONE_USERAGENT = + "Mozilla/5.0 (iPhone; U; CPU iPhone 2_1 like Mac OS X; en)" + + " AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2" + + " Mobile/5F136 Safari/525.20.1"; + private static Locale sLocale; + private static Object sLockForLocaleSettings; + + /** + * Package constructor to prevent clients from creating a new settings + * instance. + */ + WebSettings(Context context) { + mEventHandler = new EventHandler(); + mContext = context; + + if (sLockForLocaleSettings == null) { + sLockForLocaleSettings = new Object(); + sLocale = Locale.getDefault(); + } + mAcceptLanguage = getCurrentAcceptLanguage(); + mUserAgent = getCurrentUserAgent(); + mUseDefaultUserAgent = true; + + verifyNetworkAccess(); + } + + /** + * Looks at sLocale and returns current AcceptLanguage String. + * @return Current AcceptLanguage String. + */ + private String getCurrentAcceptLanguage() { + Locale locale; + synchronized(sLockForLocaleSettings) { + locale = sLocale; + } + StringBuffer buffer = new StringBuffer(); + final String language = locale.getLanguage(); + if (language != null) { + buffer.append(language); + final String country = locale.getCountry(); + if (country != null) { + buffer.append("-"); + buffer.append(country); + } + } + if (!locale.equals(Locale.US)) { + buffer.append(", "); + java.util.Locale us = Locale.US; + if (us.getLanguage() != null) { + buffer.append(us.getLanguage()); + final String country = us.getCountry(); + if (country != null) { + buffer.append("-"); + buffer.append(country); + } + } + } + + return buffer.toString(); + } + + /** + * Looks at sLocale and mContext and returns current UserAgent String. + * @return Current UserAgent String. + */ + private synchronized String getCurrentUserAgent() { + Locale locale; + synchronized(sLockForLocaleSettings) { + locale = sLocale; + } + StringBuffer buffer = new StringBuffer(); + // Add version + final String version = Build.VERSION.RELEASE; + if (version.length() > 0) { + buffer.append(version); + } else { + // default to "1.0" + buffer.append("1.0"); + } + buffer.append("; "); + final String language = locale.getLanguage(); + if (language != null) { + buffer.append(language.toLowerCase()); + final String country = locale.getCountry(); + if (country != null) { + buffer.append("-"); + buffer.append(country.toLowerCase()); + } + } else { + // default to "en" + buffer.append("en"); + } + + final String model = Build.MODEL; + if (model.length() > 0) { + buffer.append("; "); + buffer.append(model); + } + final String id = Build.ID; + if (id.length() > 0) { + buffer.append(" Build/"); + buffer.append(id); + } + final String base = mContext.getResources().getText( + com.android.internal.R.string.web_user_agent).toString(); + return String.format(base, buffer); + } + + /** + * Enables dumping the pages navigation cache to a text file. + */ + public void setNavDump(boolean enabled) { + mNavDump = enabled; + } + + /** + * Returns true if dumping the navigation cache is enabled. + */ + public boolean getNavDump() { + return mNavDump; + } + + /** + * Set whether the WebView supports zoom + */ + public void setSupportZoom(boolean support) { + mSupportZoom = support; + } + + /** + * Returns whether the WebView supports zoom + */ + public boolean supportZoom() { + return mSupportZoom; + } + + /** + * Enable or disable file access within WebView. File access is enabled by + * default. + */ + public void setAllowFileAccess(boolean allow) { + mAllowFileAccess = allow; + } + + /** + * Returns true if this WebView supports file access. + */ + public boolean getAllowFileAccess() { + return mAllowFileAccess; + } + + /** + * Store whether the WebView is saving form data. + */ + public void setSaveFormData(boolean save) { + mSaveFormData = save; + } + + /** + * Return whether the WebView is saving form data. + */ + public boolean getSaveFormData() { + return mSaveFormData; + } + + /** + * Store whether the WebView is saving password. + */ + public void setSavePassword(boolean save) { + mSavePassword = save; + } + + /** + * Return whether the WebView is saving password. + */ + public boolean getSavePassword() { + return mSavePassword; + } + + /** + * Set the text size of the page. + * @param t A TextSize value for increasing or decreasing the text. + * @see WebSettings.TextSize + */ + public synchronized void setTextSize(TextSize t) { + if (WebView.mLogEvent && mTextSize != t ) { + Checkin.updateStats(mContext.getContentResolver(), + Checkin.Stats.Tag.BROWSER_TEXT_SIZE_CHANGE, 1, 0.0); + } + mTextSize = t; + postSync(); + } + + /** + * Get the text size of the page. + * @return A TextSize enum value describing the text size. + * @see WebSettings.TextSize + */ + public synchronized TextSize getTextSize() { + return mTextSize; + } + + /** + * Enables using light touches to make a selection and activate mouseovers. + */ + public void setLightTouchEnabled(boolean enabled) { + mLightTouchEnabled = enabled; + } + + /** + * Returns true if light touches are enabled. + */ + public boolean getLightTouchEnabled() { + return mLightTouchEnabled; + } + + /** + * Tell the WebView to use the double tree rendering algorithm. + * @param use True if the WebView is to use double tree rendering, false + * otherwise. + */ + public synchronized void setUseDoubleTree(boolean use) { + if (mUseDoubleTree != use) { + mUseDoubleTree = use; + postSync(); + } + } + + /** + * Return true if the WebView is using the double tree rendering algorithm. + * @return True if the WebView is using the double tree rendering + * algorithm. + */ + public synchronized boolean getUseDoubleTree() { + return mUseDoubleTree; + } + + /** + * Tell the WebView about user-agent string. + * @param ua 0 if the WebView should use an Android user-agent string, + * 1 if the WebView should use a desktop user-agent string. + * + * @deprecated Please use setUserAgentString instead. + */ + @Deprecated + public synchronized void setUserAgent(int ua) { + String uaString = null; + if (ua == 1) { + if (DESKTOP_USERAGENT.equals(mUserAgent)) { + return; // do nothing + } else { + uaString = DESKTOP_USERAGENT; + } + } else if (ua == 2) { + if (IPHONE_USERAGENT.equals(mUserAgent)) { + return; // do nothing + } else { + uaString = IPHONE_USERAGENT; + } + } else if (ua != 0) { + return; // do nothing + } + setUserAgentString(uaString); + } + + /** + * Return user-agent as int + * @return int 0 if the WebView is using an Android user-agent string. + * 1 if the WebView is using a desktop user-agent string. + * -1 if the WebView is using user defined user-agent string. + * + * @deprecated Please use getUserAgentString instead. + */ + @Deprecated + public synchronized int getUserAgent() { + if (DESKTOP_USERAGENT.equals(mUserAgent)) { + return 1; + } else if (IPHONE_USERAGENT.equals(mUserAgent)) { + return 2; + } else if (mUseDefaultUserAgent) { + return 0; + } + return -1; + } + + /** + * Tell the WebView to use the wide viewport + */ + public synchronized void setUseWideViewPort(boolean use) { + if (mUseWideViewport != use) { + mUseWideViewport = use; + postSync(); + } + } + + /** + * @return True if the WebView is using a wide viewport + */ + public synchronized boolean getUseWideViewPort() { + return mUseWideViewport; + } + + /** + * Tell the WebView whether it supports multiple windows. TRUE means + * that {@link WebChromeClient#onCreateWindow(WebView, boolean, + * boolean, Message)} is implemented by the host application. + */ + public synchronized void setSupportMultipleWindows(boolean support) { + if (mSupportMultipleWindows != support) { + mSupportMultipleWindows = support; + postSync(); + } + } + + /** + * @return True if the WebView is supporting multiple windows. This means + * that {@link WebChromeClient#onCreateWindow(WebView, boolean, + * boolean, Message)} is implemented by the host application. + */ + public synchronized boolean supportMultipleWindows() { + return mSupportMultipleWindows; + } + + /** + * Set the underlying layout algorithm. This will cause a relayout of the + * WebView. + * @param l A LayoutAlgorithm enum specifying the algorithm to use. + * @see WebSettings.LayoutAlgorithm + */ + public synchronized void setLayoutAlgorithm(LayoutAlgorithm l) { + // XXX: This will only be affective if libwebcore was built with + // ANDROID_LAYOUT defined. + if (mLayoutAlgorithm != l) { + mLayoutAlgorithm = l; + postSync(); + } + } + + /** + * Return the current layout algorithm. + * @return LayoutAlgorithm enum value describing the layout algorithm + * being used. + * @see WebSettings.LayoutAlgorithm + */ + public synchronized LayoutAlgorithm getLayoutAlgorithm() { + return mLayoutAlgorithm; + } + + /** + * Set the standard font family name. + * @param font A font family name. + */ + public synchronized void setStandardFontFamily(String font) { + if (font != null && !font.equals(mStandardFontFamily)) { + mStandardFontFamily = font; + postSync(); + } + } + + /** + * Get the standard font family name. + * @return The standard font family name as a string. + */ + public synchronized String getStandardFontFamily() { + return mStandardFontFamily; + } + + /** + * Set the fixed font family name. + * @param font A font family name. + */ + public synchronized void setFixedFontFamily(String font) { + if (font != null && !font.equals(mFixedFontFamily)) { + mFixedFontFamily = font; + postSync(); + } + } + + /** + * Get the fixed font family name. + * @return The fixed font family name as a string. + */ + public synchronized String getFixedFontFamily() { + return mFixedFontFamily; + } + + /** + * Set the sans-serif font family name. + * @param font A font family name. + */ + public synchronized void setSansSerifFontFamily(String font) { + if (font != null && !font.equals(mSansSerifFontFamily)) { + mSansSerifFontFamily = font; + postSync(); + } + } + + /** + * Get the sans-serif font family name. + * @return The sans-serif font family name as a string. + */ + public synchronized String getSansSerifFontFamily() { + return mSansSerifFontFamily; + } + + /** + * Set the serif font family name. + * @param font A font family name. + */ + public synchronized void setSerifFontFamily(String font) { + if (font != null && !font.equals(mSerifFontFamily)) { + mSerifFontFamily = font; + postSync(); + } + } + + /** + * Get the serif font family name. + * @return The serif font family name as a string. + */ + public synchronized String getSerifFontFamily() { + return mSerifFontFamily; + } + + /** + * Set the cursive font family name. + * @param font A font family name. + */ + public synchronized void setCursiveFontFamily(String font) { + if (font != null && !font.equals(mCursiveFontFamily)) { + mCursiveFontFamily = font; + postSync(); + } + } + + /** + * Get the cursive font family name. + * @return The cursive font family name as a string. + */ + public synchronized String getCursiveFontFamily() { + return mCursiveFontFamily; + } + + /** + * Set the fantasy font family name. + * @param font A font family name. + */ + public synchronized void setFantasyFontFamily(String font) { + if (font != null && !font.equals(mFantasyFontFamily)) { + mFantasyFontFamily = font; + postSync(); + } + } + + /** + * Get the fantasy font family name. + * @return The fantasy font family name as a string. + */ + public synchronized String getFantasyFontFamily() { + return mFantasyFontFamily; + } + + /** + * Set the minimum font size. + * @param size A non-negative integer between 1 and 72. + * Any number outside the specified range will be pinned. + */ + public synchronized void setMinimumFontSize(int size) { + size = pin(size); + if (mMinimumFontSize != size) { + mMinimumFontSize = size; + postSync(); + } + } + + /** + * Get the minimum font size. + * @return A non-negative integer between 1 and 72. + */ + public synchronized int getMinimumFontSize() { + return mMinimumFontSize; + } + + /** + * Set the minimum logical font size. + * @param size A non-negative integer between 1 and 72. + * Any number outside the specified range will be pinned. + */ + public synchronized void setMinimumLogicalFontSize(int size) { + size = pin(size); + if (mMinimumLogicalFontSize != size) { + mMinimumLogicalFontSize = size; + postSync(); + } + } + + /** + * Get the minimum logical font size. + * @return A non-negative integer between 1 and 72. + */ + public synchronized int getMinimumLogicalFontSize() { + return mMinimumLogicalFontSize; + } + + /** + * Set the default font size. + * @param size A non-negative integer between 1 and 72. + * Any number outside the specified range will be pinned. + */ + public synchronized void setDefaultFontSize(int size) { + size = pin(size); + if (mDefaultFontSize != size) { + mDefaultFontSize = size; + postSync(); + } + } + + /** + * Get the default font size. + * @return A non-negative integer between 1 and 72. + */ + public synchronized int getDefaultFontSize() { + return mDefaultFontSize; + } + + /** + * Set the default fixed font size. + * @param size A non-negative integer between 1 and 72. + * Any number outside the specified range will be pinned. + */ + public synchronized void setDefaultFixedFontSize(int size) { + size = pin(size); + if (mDefaultFixedFontSize != size) { + mDefaultFixedFontSize = size; + postSync(); + } + } + + /** + * Get the default fixed font size. + * @return A non-negative integer between 1 and 72. + */ + public synchronized int getDefaultFixedFontSize() { + return mDefaultFixedFontSize; + } + + /** + * Tell the WebView to load image resources automatically. + * @param flag True if the WebView should load images automatically. + */ + public synchronized void setLoadsImagesAutomatically(boolean flag) { + if (mLoadsImagesAutomatically != flag) { + mLoadsImagesAutomatically = flag; + postSync(); + } + } + + /** + * Return true if the WebView will load image resources automatically. + * @return True if the WebView loads images automatically. + */ + public synchronized boolean getLoadsImagesAutomatically() { + return mLoadsImagesAutomatically; + } + + /** + * Tell the WebView to block network image. This is only checked when + * getLoadsImagesAutomatically() is true. + * @param flag True if the WebView should block network image + */ + public synchronized void setBlockNetworkImage(boolean flag) { + if (mBlockNetworkImage != flag) { + mBlockNetworkImage = flag; + postSync(); + } + } + + /** + * Return true if the WebView will block network image. + * @return True if the WebView blocks network image. + */ + public synchronized boolean getBlockNetworkImage() { + return mBlockNetworkImage; + } + + /** + * @hide + * Tell the WebView to block all network load requests. + * @param flag True if the WebView should block all network loads + */ + public synchronized void setBlockNetworkLoads(boolean flag) { + if (mBlockNetworkLoads != flag) { + mBlockNetworkLoads = flag; + verifyNetworkAccess(); + } + } + + /** + * @hide + * Return true if the WebView will block all network loads. + * @return True if the WebView blocks all network loads. + */ + public synchronized boolean getBlockNetworkLoads() { + return mBlockNetworkLoads; + } + + + private void verifyNetworkAccess() { + if (!mBlockNetworkLoads) { + if (mContext.checkPermission("android.permission.INTERNET", + android.os.Process.myPid(), 0) != + PackageManager.PERMISSION_GRANTED) { + throw new SecurityException + ("Permission denied - " + + "application missing INTERNET permission"); + } + } + } + + /** + * Tell the WebView to enable javascript execution. + * @param flag True if the WebView should execute javascript. + */ + public synchronized void setJavaScriptEnabled(boolean flag) { + if (mJavaScriptEnabled != flag) { + mJavaScriptEnabled = flag; + postSync(); + } + } + + /** + * Tell the WebView to enable plugins. + * @param flag True if the WebView should load plugins. + */ + public synchronized void setPluginsEnabled(boolean flag) { + if (mPluginsEnabled != flag) { + mPluginsEnabled = flag; + postSync(); + } + } + + /** + * Set a custom path to plugins used by the WebView. The client + * must ensure it exists before this call. + * @param pluginsPath String path to the directory containing plugins. + */ + public synchronized void setPluginsPath(String pluginsPath) { + if (pluginsPath != null && !pluginsPath.equals(mPluginsPath)) { + mPluginsPath = pluginsPath; + postSync(); + } + } + + /** + * Return true if javascript is enabled. + * @return True if javascript is enabled. + */ + public synchronized boolean getJavaScriptEnabled() { + return mJavaScriptEnabled; + } + + /** + * Return true if plugins are enabled. + * @return True if plugins are enabled. + */ + public synchronized boolean getPluginsEnabled() { + return mPluginsEnabled; + } + + /** + * Return the current path used for plugins in the WebView. + * @return The string path to the WebView plugins. + */ + public synchronized String getPluginsPath() { + return mPluginsPath; + } + + /** + * Tell javascript to open windows automatically. This applies to the + * javascript function window.open(). + * @param flag True if javascript can open windows automatically. + */ + public synchronized void setJavaScriptCanOpenWindowsAutomatically( + boolean flag) { + if (mJavaScriptCanOpenWindowsAutomatically != flag) { + mJavaScriptCanOpenWindowsAutomatically = flag; + postSync(); + } + } + + /** + * Return true if javascript can open windows automatically. + * @return True if javascript can open windows automatically during + * window.open(). + */ + public synchronized boolean getJavaScriptCanOpenWindowsAutomatically() { + return mJavaScriptCanOpenWindowsAutomatically; + } + + /** + * Set the default text encoding name to use when decoding html pages. + * @param encoding The text encoding name. + */ + public synchronized void setDefaultTextEncodingName(String encoding) { + if (encoding != null && !encoding.equals(mDefaultTextEncoding)) { + mDefaultTextEncoding = encoding; + postSync(); + } + } + + /** + * Get the default text encoding name. + * @return The default text encoding name as a string. + */ + public synchronized String getDefaultTextEncodingName() { + return mDefaultTextEncoding; + } + + /** + * Set the WebView's user-agent string. If the string "ua" is null or empty, + * it will use the system default user-agent string. + */ + public synchronized void setUserAgentString(String ua) { + if (ua == null || ua.length() == 0) { + synchronized(sLockForLocaleSettings) { + Locale currentLocale = Locale.getDefault(); + if (!sLocale.equals(currentLocale)) { + sLocale = currentLocale; + mAcceptLanguage = getCurrentAcceptLanguage(); + } + } + ua = getCurrentUserAgent(); + mUseDefaultUserAgent = true; + } else { + mUseDefaultUserAgent = false; + } + + if (!ua.equals(mUserAgent)) { + mUserAgent = ua; + postSync(); + } + } + + /** + * Return the WebView's user-agent string. + */ + public synchronized String getUserAgentString() { + if (DESKTOP_USERAGENT.equals(mUserAgent) || + IPHONE_USERAGENT.equals(mUserAgent) || + !mUseDefaultUserAgent) { + return mUserAgent; + } + + boolean doPostSync = false; + synchronized(sLockForLocaleSettings) { + Locale currentLocale = Locale.getDefault(); + if (!sLocale.equals(currentLocale)) { + sLocale = currentLocale; + mUserAgent = getCurrentUserAgent(); + mAcceptLanguage = getCurrentAcceptLanguage(); + doPostSync = true; + } + } + if (doPostSync) { + postSync(); + } + return mUserAgent; + } + + /* package api to grab the Accept Language string. */ + /*package*/ synchronized String getAcceptLanguage() { + synchronized(sLockForLocaleSettings) { + Locale currentLocale = Locale.getDefault(); + if (!sLocale.equals(currentLocale)) { + sLocale = currentLocale; + mAcceptLanguage = getCurrentAcceptLanguage(); + } + } + return mAcceptLanguage; + } + + /** + * Tell the WebView whether it needs to set a node to have focus when + * {@link WebView#requestFocus(int, android.graphics.Rect)} is called. + * + * @param flag + */ + public void setNeedInitialFocus(boolean flag) { + if (mNeedInitialFocus != flag) { + mNeedInitialFocus = flag; + } + } + + /* Package api to get the choice whether it needs to set initial focus. */ + /* package */ boolean getNeedInitialFocus() { + return mNeedInitialFocus; + } + + /** + * Set the priority of the Render thread. Unlike the other settings, this + * one only needs to be called once per process. + * + * @param priority RenderPriority, can be normal, high or low. + */ + public synchronized void setRenderPriority(RenderPriority priority) { + if (mRenderPriority != priority) { + mRenderPriority = priority; + mEventHandler.sendMessage(Message.obtain(null, + EventHandler.PRIORITY)); + } + } + + /** + * Override the way the cache is used. The way the cache is used is based + * on the navigation option. For a normal page load, the cache is checked + * and content is re-validated as needed. When navigating back, content is + * not revalidated, instead the content is just pulled from the cache. + * This function allows the client to override this behavior. + * @param mode One of the LOAD_ values. + */ + public void setCacheMode(int mode) { + if (mode != mOverrideCacheMode) { + mOverrideCacheMode = mode; + } + } + + /** + * Return the current setting for overriding the cache mode. For a full + * description, see the {@link #setCacheMode(int)} function. + */ + public int getCacheMode() { + return mOverrideCacheMode; + } + + /** + * If set, webkit alternately shrinks and expands images viewed outside + * of an HTML page to fit the screen. This conflicts with attempts by + * the UI to zoom in and out of an image, so it is set false by default. + * @param shrink Set true to let webkit shrink the standalone image to fit. + * {@hide} + */ + public void setShrinksStandaloneImagesToFit(boolean shrink) { + if (mShrinksStandaloneImagesToFit != shrink) { + mShrinksStandaloneImagesToFit = shrink; + postSync(); + } + } + + /** + * Transfer messages from the queue to the new WebCoreThread. Called from + * WebCore thread. + */ + /*package*/ + synchronized void syncSettingsAndCreateHandler(BrowserFrame frame) { + mBrowserFrame = frame; + if (android.util.Config.DEBUG) { + junit.framework.Assert.assertTrue(frame.mNativeFrame != 0); + } + nativeSync(frame.mNativeFrame); + mSyncPending = false; + mEventHandler.createHandler(); + } + + private int pin(int size) { + // FIXME: 72 is just an arbitrary max text size value. + if (size < 1) { + return 1; + } else if (size > 72) { + return 72; + } + return size; + } + + /* Post a SYNC message to handle syncing the native settings. */ + private synchronized void postSync() { + // Only post if a sync is not pending + if (!mSyncPending) { + mSyncPending = mEventHandler.sendMessage( + Message.obtain(null, EventHandler.SYNC)); + } + } + + // Synchronize the native and java settings. + private native void nativeSync(int nativeFrame); +} diff --git a/core/java/android/webkit/WebSyncManager.java b/core/java/android/webkit/WebSyncManager.java new file mode 100644 index 0000000..e6e9994 --- /dev/null +++ b/core/java/android/webkit/WebSyncManager.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2007 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.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.util.Config; +import android.util.Log; + +abstract class WebSyncManager implements Runnable { + // message code for sync message + private static final int SYNC_MESSAGE = 101; + // time delay in millisec for a sync (now) message + private static int SYNC_NOW_INTERVAL = 100; // 100 millisec + // time delay in millisec for a sync (later) message + private static int SYNC_LATER_INTERVAL = 5 * 60 * 1000; // 5 minutes + // thread for syncing + private Thread mSyncThread; + // Name of thread + private String mThreadName; + // handler of the sync thread + protected Handler mHandler; + // database for the persistent storage + protected WebViewDatabase mDataBase; + // Ref count for calls to start/stop sync + private int mStartSyncRefCount; + // log tag + protected static final String LOGTAG = "websync"; + + private class SyncHandler extends Handler { + @Override + public void handleMessage(Message msg) { + if (msg.what == SYNC_MESSAGE) { + if (Config.LOGV) { + Log.v(LOGTAG, "*** WebSyncManager sync ***"); + } + syncFromRamToFlash(); + + // send a delayed message to request sync later + Message newmsg = obtainMessage(SYNC_MESSAGE); + sendMessageDelayed(newmsg, SYNC_LATER_INTERVAL); + } + } + } + + protected WebSyncManager(Context context, String name) { + mThreadName = name; + if (context != null) { + mDataBase = WebViewDatabase.getInstance(context); + mSyncThread = new Thread(this); + mSyncThread.setName(mThreadName); + mSyncThread.start(); + } else { + throw new IllegalStateException( + "WebSyncManager can't be created without context"); + } + } + + protected Object clone() throws CloneNotSupportedException { + throw new CloneNotSupportedException("doesn't implement Cloneable"); + } + + public void run() { + // prepare Looper for sync handler + Looper.prepare(); + mHandler = new SyncHandler(); + onSyncInit(); + // lower the priority after onSyncInit() is done + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + Message msg = mHandler.obtainMessage(SYNC_MESSAGE); + mHandler.sendMessageDelayed(msg, SYNC_LATER_INTERVAL); + + Looper.loop(); + } + + /** + * sync() forces sync manager to sync now + */ + public void sync() { + if (Config.LOGV) { + Log.v(LOGTAG, "*** WebSyncManager sync ***"); + } + if (mHandler == null) { + return; + } + mHandler.removeMessages(SYNC_MESSAGE); + Message msg = mHandler.obtainMessage(SYNC_MESSAGE); + mHandler.sendMessageDelayed(msg, SYNC_NOW_INTERVAL); + } + + /** + * resetSync() resets sync manager's timer + */ + public void resetSync() { + if (Config.LOGV) { + Log.v(LOGTAG, "*** WebSyncManager resetSync ***"); + } + if (mHandler == null) { + return; + } + mHandler.removeMessages(SYNC_MESSAGE); + Message msg = mHandler.obtainMessage(SYNC_MESSAGE); + mHandler.sendMessageDelayed(msg, SYNC_LATER_INTERVAL); + } + + /** + * startSync() requests sync manager to start sync + */ + public void startSync() { + if (Config.LOGV) { + Log.v(LOGTAG, "*** WebSyncManager startSync ***, Ref count:" + + mStartSyncRefCount); + } + if (mHandler == null) { + return; + } + if (++mStartSyncRefCount == 1) { + Message msg = mHandler.obtainMessage(SYNC_MESSAGE); + mHandler.sendMessageDelayed(msg, SYNC_LATER_INTERVAL); + } + } + + /** + * stopSync() requests sync manager to stop sync. remove any SYNC_MESSAGE in + * the queue to break the sync loop + */ + public void stopSync() { + if (Config.LOGV) { + Log.v(LOGTAG, "*** WebSyncManager stopSync ***, Ref count:" + + mStartSyncRefCount); + } + if (mHandler == null) { + return; + } + if (--mStartSyncRefCount == 0) { + mHandler.removeMessages(SYNC_MESSAGE); + } + } + + protected void onSyncInit() { + } + + abstract void syncFromRamToFlash(); +} diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java new file mode 100644 index 0000000..5126ef0 --- /dev/null +++ b/core/java/android/webkit/WebView.java @@ -0,0 +1,5406 @@ +/* + * 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.ZoomButtonsController; +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; + +/** + * <p>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.</p> + * <p>Note that, in order for your Activity to access the Internet and load web pages + * in a WebView, you must add the <var>INTERNET</var> permissions to your + * Android Manifest file:</p> + * <pre><uses-permission android:name="android.permission.INTERNET" /></pre> + * <p>This must be a child of the <code><manifest></code> element.</p> + */ +public class WebView extends AbsoluteLayout + implements ViewTreeObserver.OnGlobalFocusChangeListener, + ViewGroup.OnHierarchyChangeListener { + + // 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; + + // keep debugging parameters near the top of the file + static final String LOGTAG = "webview"; + static final boolean DEBUG = false; + static final boolean LOGV_ENABLED = DEBUG ? Config.LOGD : Config.LOGV; + + private 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); + mZoomControls = (ZoomControls) findViewById(com.android.internal.R.id.zoomControls); + mZoomMagnify = (ImageView) findViewById(com.android.internal.R.id.zoomMagnify); + } + + public void show(boolean showZoom, boolean canZoomOut) { + mZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE); + mZoomMagnify.setVisibility(canZoomOut ? 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 void setIsZoomMagnifyEnabled(boolean isEnabled) { + mZoomMagnify.setEnabled(isEnabled); + } + + public boolean hasFocus() { + return mZoomControls.hasFocus() || mZoomMagnify.hasFocus(); + } + + public void setOnZoomInClickListener(OnClickListener listener) { + mZoomControls.setOnZoomInClickListener(listener); + } + + public void setOnZoomOutClickListener(OnClickListener listener) { + mZoomControls.setOnZoomOutClickListener(listener); + } + + public void setOnZoomMagnifyClickListener(OnClickListener listener) { + mZoomMagnify.setOnClickListener(listener); + } + + ZoomControls mZoomControls; + ImageView mZoomMagnify; + } + + /** + * 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 TextDialog mTextEntry; + // Used to ignore changes to webkit text that arrives to the UI side after + // more key events. + private int mTextGeneration; + + // The list of loaded plugins. + private static PluginList sPluginList; + + /** + * 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 + */ + private static final int TOUCH_SENT_INTERVAL = 100; + + /** + * Helper class to get velocity for fling + */ + VelocityTracker mVelocityTracker; + + private static boolean mShowZoomRingTutorial = true; + private static final int ZOOM_RING_TUTORIAL_DURATION = 3000; + + /** + * 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_DOUBLECLICK_MODE = 6; + private static final int TOUCH_DONE_MODE = 7; + private static final int TOUCH_SELECT_MODE = 8; + // touch mode values specific to scale+scroll + private static final int FIRST_SCROLL_ZOOM = 9; + private static final int SCROLL_ZOOM_ANIMATION_IN = 9; + private static final int SCROLL_ZOOM_ANIMATION_OUT = 10; + private static final int SCROLL_ZOOM_OUT = 11; + private static final int LAST_SCROLL_ZOOM = 11; + // end of touch mode values specific to scale+scroll + + // Whether to forward the touch events to WebCore + private boolean mForwardTouchEvents = false; + + // Whether we are in the drag tap mode, which exists starting at the second + // tap's down, through its move, and includes its up. These events should be + // given to the method on the zoom controller. + private boolean mInZoomTapDragMode = false; + + // Whether to prevent drag during touch. The initial value depends on + // mForwardTouchEvents. If WebCore wants touch events, we assume it will + // take control of touch events unless it says no for touch down event. + private boolean mPreventDrag; + + // If updateTextEntry gets called while we are out of focus, use this + // variable to remember to do it next time we gain focus. + private boolean mNeedsUpdateTextEntry = false; + + // Whether or not to draw the focus ring. + private boolean mDrawFocusRing = true; + + /** + * Customizable constant + */ + // pre-computed square of ViewConfiguration.getScaledTouchSlop() + private int mTouchSlopSquare; + // pre-computed square of ViewConfiguration.getScaledDoubleTapSlop() + private int mDoubleTapSlopSquare; + // 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; + // The duration in milliseconds we will wait to see if it is a double tap. + private static final int DOUBLE_TAP_TIMEOUT = 250; + // 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; + // 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 + + static int MAX_FLOAT_CONTENT_WIDTH = 480; + // the calculated minimum content width for calculating the minimum scale. + // If it is 0, it means don't use it. + private int mMinContentWidth; + + // 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 Scroller mScroller; + + private boolean mWrapContent; + + // The View containing the zoom controls + private ExtendedZoomControls mZoomControls; + private Runnable mZoomControlRunnable; + + // true if we should call webcore to draw the content, false means we have + // requested something but it isn't ready to draw yet. + private WebViewCore.FocusData mFocusData; + /** + * 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 UPDATE_TEXT_ENTRY_ADAPTER = 6; + private static final int SWITCH_TO_ENTER = 7; + private static final int RESUME_WEBCORE_UPDATE = 8; + private static final int DISMISS_ZOOM_RING_TUTORIAL = 9; + + //! arg1=x, arg2=y + static final int SCROLL_TO_MSG_ID = 10; + static final int SCROLL_BY_MSG_ID = 11; + //! arg1=x, arg2=y + static final int SPAWN_SCROLL_TO_MSG_ID = 12; + //! arg1=x, arg2=y + static final int SYNC_SCROLL_TO_MSG_ID = 13; + static final int NEW_PICTURE_MSG_ID = 14; + static final int UPDATE_TEXT_ENTRY_MSG_ID = 15; + static final int WEBCORE_INITIALIZED_MSG_ID = 16; + static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17; + static final int DID_FIRST_LAYOUT_MSG_ID = 18; + static final int RECOMPUTE_FOCUS_MSG_ID = 19; + static final int NOTIFY_FOCUS_SET_MSG_ID = 20; + static final int MARK_NODE_INVALID_ID = 21; + static final int UPDATE_CLIPBOARD = 22; + static final int LONG_PRESS_ENTER = 23; + static final int PREVENT_TOUCH_ID = 24; + static final int WEBCORE_NEED_TOUCH_EVENTS = 25; + // obj=Rect in doc coordinates + static final int INVAL_RECT_MSG_ID = 26; + + static final String[] HandlerDebugString = { + "REMEMBER_PASSWORD", // = 1; + "NEVER_REMEMBER_PASSWORD", // = 2; + "SWITCH_TO_SHORTPRESS", // = 3; + "SWITCH_TO_LONGPRESS", // = 4; + "RELEASE_SINGLE_TAP", // = 5; + "UPDATE_TEXT_ENTRY_ADAPTER", // = 6; + "SWITCH_TO_ENTER", // = 7; + "RESUME_WEBCORE_UPDATE", // = 8; + "9", + "SCROLL_TO_MSG_ID", // = 10; + "SCROLL_BY_MSG_ID", // = 11; + "SPAWN_SCROLL_TO_MSG_ID", // = 12; + "SYNC_SCROLL_TO_MSG_ID", // = 13; + "NEW_PICTURE_MSG_ID", // = 14; + "UPDATE_TEXT_ENTRY_MSG_ID", // = 15; + "WEBCORE_INITIALIZED_MSG_ID", // = 16; + "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 17; + "DID_FIRST_LAYOUT_MSG_ID", // = 18; + "RECOMPUTE_FOCUS_MSG_ID", // = 19; + "NOTIFY_FOCUS_SET_MSG_ID", // = 20; + "MARK_NODE_INVALID_ID", // = 21; + "UPDATE_CLIPBOARD", // = 22; + "LONG_PRESS_ENTER", // = 23; + "PREVENT_TOUCH_ID", // = 24; + "WEBCORE_NEED_TOUCH_EVENTS", // = 25; + "INVAL_RECT_MSG_ID" // = 26; + }; + + // width which view is considered to be fully zoomed out + static final int ZOOM_OUT_WIDTH = 1024; + + private static final float MAX_ZOOM_RING_ANGLE = (float) (Math.PI * 2 / 3); + private static final int ZOOM_RING_STEPS = 4; + private static final float ZOOM_RING_ANGLE_UNIT = MAX_ZOOM_RING_ANGLE + / ZOOM_RING_STEPS; + + private static final float DEFAULT_MAX_ZOOM_SCALE = 2; + private static final float DEFAULT_MIN_ZOOM_SCALE = (float) 1/3; + // scale limit, which can be set through viewport meta tag in the web page + private float mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; + private float mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + + // initial scale in percent. 0 means using default. + private int mInitialScale = 0; + + // set to true temporarily while the zoom control is being dragged + private boolean mPreviewZoomOnly = false; + + // computed scale and inverse, from mZoomWidth. + private float mActualScale = 1; + private float mInvActualScale = 1; + // if this is non-zero, it is used on drawing rather than mActualScale + private float mZoomScale; + private float mInvInitialZoomScale; + private float mInvFinalZoomScale; + 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 = 1; + private static final int SNAP_X = 2; + private static final int SNAP_Y = 3; + private static final int SNAP_X_LOCK = 4; + private static final int SNAP_Y_LOCK = 5; + private boolean mSnapPositive; + + // Used to match key downs and key ups + private boolean mGotKeyDown; + + /* package */ static boolean mLogEvent = true; + private static final int EVENT_LOG_ZOOM_LEVEL_CHANGE = 70101; + private static final int EVENT_LOG_DOUBLE_TAP_DURATION = 70102; + + // 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); + } + + public 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; + } + } + + private ZoomButtonsController mZoomButtonsController; + + private ZoomRingController mZoomRingController; + private ImageView mZoomRingOverview; + private Animation mZoomRingOverviewExitAnimation; + + // These keep track of the center point of the zoom ring. They are used to + // determine the point around which we should zoom. + private float mZoomCenterX; + private float mZoomCenterY; + + private ZoomRingController.OnZoomListener mZoomListener = + new ZoomRingController.OnZoomListener() { + + private float mClockwiseBound; + private float mCounterClockwiseBound; + private float mStartScale; + + public void onCenter(int x, int y) { + // Don't translate when the control is invoked, hence we do nothing + // in this callback + } + + public void onBeginPan() { + setZoomOverviewVisible(false); + if (mLogEvent) { + Checkin.updateStats(mContext.getContentResolver(), + Checkin.Stats.Tag.BROWSER_ZOOM_RING_DRAG, 1, 0.0); + } + } + + public boolean onPan(int deltaX, int deltaY) { + return pinScrollBy(deltaX, deltaY, false, 0); + } + + public void onEndPan() { + } + + public void onVisibilityChanged(boolean visible) { + if (visible) { + switchOutDrawHistory(); + if (mMaxZoomScale - 1 > ZOOM_RING_STEPS * 0.01f) { + mClockwiseBound = (float) (2 * Math.PI - MAX_ZOOM_RING_ANGLE); + } else { + mClockwiseBound = (float) (2 * Math.PI); + } + mZoomRingController.setThumbClockwiseBound(mClockwiseBound); + if (1 - mMinZoomScale > ZOOM_RING_STEPS * 0.01f) { + mCounterClockwiseBound = MAX_ZOOM_RING_ANGLE; + } else { + mCounterClockwiseBound = 0; + } + mZoomRingController + .setThumbCounterclockwiseBound(mCounterClockwiseBound); + float angle = 0f; + if (mActualScale > 1 && mClockwiseBound < (float) (2 * Math.PI)) { + angle = -(float) Math.round(ZOOM_RING_STEPS + * (mActualScale - 1) / (mMaxZoomScale - 1)) + / ZOOM_RING_STEPS; + } else if (mActualScale < 1 && mCounterClockwiseBound > 0) { + angle = (float) Math.round(ZOOM_RING_STEPS + * (1 - mActualScale) / (1 - mMinZoomScale)) + / ZOOM_RING_STEPS; + } + mZoomRingController.setThumbAngle(angle * MAX_ZOOM_RING_ANGLE); + + // Don't show a thumb if the user cannot zoom + mZoomRingController.setThumbVisible(mMinZoomScale != mMaxZoomScale); + + // Show the zoom overview tab on the ring + setZoomOverviewVisible(true); + if (mLogEvent) { + Checkin.updateStats(mContext.getContentResolver(), + Checkin.Stats.Tag.BROWSER_ZOOM_RING, 1, 0.0); + } + } + } + + public void onBeginDrag() { + mPreviewZoomOnly = true; + mStartScale = mActualScale; + setZoomOverviewVisible(false); + } + + public void onEndDrag() { + mPreviewZoomOnly = false; + if (mLogEvent) { + EventLog.writeEvent(EVENT_LOG_ZOOM_LEVEL_CHANGE, + (int) mStartScale * 100, (int) mActualScale * 100, + System.currentTimeMillis()); + } + setNewZoomScale(mActualScale, true); + } + + public boolean onDragZoom(int deltaZoomLevel, int centerX, + int centerY, float startAngle, float curAngle) { + if (deltaZoomLevel < 0 + && Math.abs(mActualScale - mMinZoomScale) < 0.01f + || deltaZoomLevel > 0 + && Math.abs(mActualScale - mMaxZoomScale) < 0.01f + || deltaZoomLevel == 0) { + return false; + } + mZoomCenterX = (float) centerX; + mZoomCenterY = (float) centerY; + + float scale = 1.0f; + // curAngle is [0, 2 * Math.PI) + if (curAngle < (float) Math.PI) { + if (curAngle >= mCounterClockwiseBound) { + scale = mMinZoomScale; + } else { + scale = 1 - (float) Math.round(curAngle + / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS + * (1 - mMinZoomScale); + } + } else { + if (curAngle <= mClockwiseBound) { + scale = mMaxZoomScale; + } else { + scale = 1 + (float) Math.round( + ((float) 2 * Math.PI - curAngle) + / ZOOM_RING_ANGLE_UNIT) / ZOOM_RING_STEPS + * (mMaxZoomScale - 1); + } + } + zoomWithPreview(scale); + return true; + } + + public void onSimpleZoom(boolean zoomIn) { + if (zoomIn) { + zoomIn(); + } else { + zoomOut(); + } + } + + }; + + /** + * 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) { + super(context, attrs, defStyle); + init(); + + mCallbackProxy = new CallbackProxy(context, this); + mWebViewCore = new WebViewCore(context, this, mCallbackProxy); + mDatabase = WebViewDatabase.getInstance(context); + mFocusData = new WebViewCore.FocusData(); + mFocusData.mFrame = 0; + mFocusData.mNode = 0; + mFocusData.mX = 0; + mFocusData.mY = 0; + mScroller = new Scroller(context); + mZoomRingController = new ZoomRingController(context, this); + mZoomRingController.setResetThumbAutomatically(false); + mZoomRingController.setCallback(mZoomListener); + mZoomRingController.setZoomRingTrack( + com.android.internal.R.drawable.zoom_ring_track_absolute); + mZoomRingController.setPannerAcceleration(160); + mZoomRingController.setPannerStartAcceleratingDuration(700); + createZoomRingOverviewTab(); + mZoomButtonsController = new ZoomButtonsController(context, this); + mZoomButtonsController.setOverviewVisible(true); + mZoomButtonsController.setCallback(new ZoomButtonsController.OnZoomListener() { + public void onCenter(int x, int y) { + mZoomListener.onCenter(x, y); + } + + public void onOverview() { + mZoomButtonsController.setVisible(false); + zoomScrollOut(); + } + + public void onVisibilityChanged(boolean visible) { + mZoomListener.onVisibilityChanged(visible); + } + + public void onZoom(boolean zoomIn) { + mZoomListener.onSimpleZoom(zoomIn); + } + }); + } + + private void init() { + setWillNotDraw(false); + setFocusable(true); + setFocusableInTouchMode(true); + setClickable(true); + setLongClickable(true); + + final int slop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + mTouchSlopSquare = slop * slop; + mMinLockSnapReverseDistance = slop; + final int doubleTapslop = ViewConfiguration.get(getContext()) + .getScaledDoubleTapSlop(); + mDoubleTapSlopSquare = doubleTapslop * doubleTapslop; + } + + private void createZoomRingOverviewTab() { + Context context = getContext(); + + mZoomRingOverviewExitAnimation = AnimationUtils.loadAnimation(context, + com.android.internal.R.anim.fade_out); + + mZoomRingOverview = new ImageView(context); + mZoomRingOverview.setBackgroundResource( + com.android.internal.R.drawable.zoom_ring_overview_tab); + mZoomRingOverview.setImageResource(com.android.internal.R.drawable.btn_zoom_page); + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER); + // TODO: magic constant that's based on the zoom ring radius + some offset + lp.topMargin = 200; + mZoomRingOverview.setLayoutParams(lp); + mZoomRingOverview.setOnClickListener(new View.OnClickListener() { + public void onClick(View v) { + // Hide the zoom ring + mZoomRingController.setVisible(false); + if (mLogEvent) { + Checkin.updateStats(mContext.getContentResolver(), + Checkin.Stats.Tag.BROWSER_ZOOM_OVERVIEW, 1, 0.0); + } + zoomScrollOut(); + }}); + + // Measure the overview View to figure out its height + mZoomRingOverview.forceLayout(); + mZoomRingOverview.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); + + ViewGroup container = mZoomRingController.getContainer(); + // Find the index of the zoom ring in the container + View zoomRing = container.findViewById(mZoomRingController.getZoomRingId()); + int zoomRingIndex; + for (zoomRingIndex = container.getChildCount() - 1; zoomRingIndex >= 0; zoomRingIndex--) { + if (container.getChildAt(zoomRingIndex) == zoomRing) break; + } + // Add the overview tab below the zoom ring (so we don't steal its events) + container.addView(mZoomRingOverview, zoomRingIndex); + // Since we use margins to adjust the vertical placement of the tab, the widget + // ends up getting clipped off. Ensure the container is big enough for + // us. + int myHeight = mZoomRingOverview.getMeasuredHeight() + lp.topMargin / 2; + // Multiplied by 2 b/c the zoom ring needs to be centered on the screen + container.setMinimumHeight(myHeight * 2); + } + + private void setZoomOverviewVisible(boolean visible) { + int newVisibility = visible ? View.VISIBLE : View.INVISIBLE; + if (mZoomRingOverview.getVisibility() == newVisibility) return; + + if (!visible) { + mZoomRingOverview.startAnimation(mZoomRingOverviewExitAnimation); + } + mZoomRingOverview.setVisibility(newVisibility); + } + + /* 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. + */ + private int getViewWidth() { + if (!isVerticalScrollBarEnabled() || mOverlayVerticalScrollbar) { + return getWidth(); + } else { + return getWidth() - getVerticalScrollbarWidth(); + } + } + + /* + * Return the height of the view where the content of WebView should render + * to. + */ + private int getViewHeight() { + if (!isHorizontalScrollBarEnabled() || mOverlayHorizontalScrollbar) { + return getHeight(); + } else { + return getHeight() - getHorizontalScrollbarHeight(); + } + } + + /** + * @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) { + // 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); + } + + /** + * 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() { + clearTextEntry(); + 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 + 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 onPause() or onStop(). + */ + public static void disablePlatformNotifications() { + Network.disablePlatformNotifications(); + } + + /** + * 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 + * + * @hide pending API Council approval + */ + public void setNetworkAvailable(boolean networkUp) { + BrowserFrame.sJavaBridge.setNetworkOnLine(networkUp); + } + + /** + * 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<byte[]> history = new ArrayList<byte[]>(size); + for (int i = 0; i < size; i++) { + WebHistoryItem item = list.getItemAtIndex(i); + 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, File dest) { + if (dest == null || b == null) { + return false; + } + final Picture p = capturePicture(); + try { + final FileOutputStream out = new FileOutputStream(dest); + p.writeToStream(out); + out.close(); + } catch (FileNotFoundException e){ + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } + if (dest.length() > 0) { + b.putInt("scrollX", mScrollX); + b.putInt("scrollY", mScrollY); + b.putFloat("scale", mActualScale); + return true; + } + return false; + } + + /** + * 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()) { + Picture p = null; + try { + final FileInputStream in = new FileInputStream(src); + p = Picture.createFromStream(in); + in.close(); + } catch (FileNotFoundException e){ + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + if (p != null) { + 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; + invalidate(); + return true; + } + } + return false; + } + + /** + * 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<byte[]> history = + (List<byte[]>) 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. + * @param url The url of the resource to load. + */ + public void loadUrl(String url) { + switchOutDrawHistory(); + mWebViewCore.sendMessage(EventHub.LOAD_URL, url); + clearTextEntry(); + } + + /** + * 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. + * @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 for the + * history entry and to resolve any relative URLs. The failUrl is used if + * browser fails to load the data provided. If it is empty or null, and the + * load fails, then no history entry is created. + * <p> + * 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. + * <p> + * 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. + * <p><strong>IMPORTANT:</strong> + * <ul> + * <li> 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.<br> + * Do not use addJavascriptInterface() unless all of the HTML in this + * WebView was written by you.</li> + * <li> The Java object that is bound runs in another thread and not in + * the thread that it was constructed in.</li> + * </ul></p> + * @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 + (float) (mScrollX - lp.x); + float y = mLastTouchY + (float) (mScrollY - 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<String> pastEntries = mDatabase.getFormData(mUrl, mName); + if (pastEntries.size() > 0) { + AutoCompleteAdapter adapter = new + AutoCompleteAdapter(mContext, pastEntries); + ((HashMap) mUpdateMessage.obj).put("adapter", adapter); + mUpdateMessage.sendToTarget(); + } + } + } + + private void setTextEntryRect(int x, int y, int width, int height) { + x = contentToView(x); + y = contentToView(y); + width = contentToView(width); + height = contentToView(height); + mTextEntry.setRect(x, y, width, height); + } + + // This is used to determine long press with the enter key, or + // a center key. Does not affect long press with the trackball/touch. + private boolean mGotEnterDown = false; + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() + + ", " + event); + } + + if (mNativeClass == 0) { + return false; + } + + // do this hack up front, so it always works, regardless of touch-mode + if (AUTO_REDRAW_HACK && (keyCode == KeyEvent.KEYCODE_CALL)) { + mAutoRedraw = !mAutoRedraw; + if (mAutoRedraw) { + invalidate(); + } + return true; + } + + // Bubble up the key event if + // 1. it is a system key; or + // 2. the host application wants to handle it; or + // 3. webview is in scroll-zoom state; + if (event.isSystem() + || mCallbackProxy.uiOverrideKeyEvent(event) + || (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM)) { + return false; + } + + if (mShiftIsPressed == false && nativeFocusNodeWantsKeyEvents() == false + && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { + mExtendSelection = false; + mShiftIsPressed = true; + if (nativeUpdateFocusNode()) { + FocusNode node = mFocusNode; + mSelectX = contentToView(node.mBounds.left); + mSelectY = contentToView(node.mBounds.top); + } else { + mSelectX = mScrollX + (int) mLastTouchX; + mSelectY = mScrollY + (int) mLastTouchY; + } + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + nativeClearFocus(contentX, contentY); + } + + if (keyCode >= KeyEvent.KEYCODE_DPAD_UP + && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { + // always handle the navigation keys in the UI thread + switchOutDrawHistory(); + if (navHandledKey(keyCode, 1, false, event.getEventTime())) { + playSoundEffect(keyCodeToSoundsEffect(keyCode)); + return true; + } + // Bubble up the key event as WebView doesn't handle it + return false; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER) { + switchOutDrawHistory(); + if (event.getRepeatCount() == 0) { + mGotEnterDown = true; + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(LONG_PRESS_ENTER), LONG_PRESS_TIMEOUT); + // Already checked mNativeClass, so we do not need to check it + // again. + nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); + return true; + } + // Bubble up the key event as WebView doesn't handle it + return false; + } + + if (getSettings().getNavDump()) { + switch (keyCode) { + case KeyEvent.KEYCODE_4: + // "/data/data/com.android.browser/displayTree.txt" + nativeDumpDisplayTree(getUrl()); + break; + case KeyEvent.KEYCODE_5: + case KeyEvent.KEYCODE_6: + // 5: dump the dom tree to the file + // "/data/data/com.android.browser/domTree.txt" + // 6: dump the dom tree to the adb log + mWebViewCore.sendMessage(EventHub.DUMP_DOMTREE, + (keyCode == KeyEvent.KEYCODE_5) ? 1 : 0, 0); + break; + case KeyEvent.KEYCODE_7: + case KeyEvent.KEYCODE_8: + // 7: dump the render tree to the file + // "/data/data/com.android.browser/renderTree.txt" + // 8: dump the render tree to the adb log + mWebViewCore.sendMessage(EventHub.DUMP_RENDERTREE, + (keyCode == KeyEvent.KEYCODE_7) ? 1 : 0, 0); + break; + case KeyEvent.KEYCODE_9: + nativeInstrumentReport(); + return true; + } + } + + // TODO: should we pass all the keys to DOM or check the meta tag + if (nativeFocusNodeWantsKeyEvents() || true) { + // pass the key to DOM + mWebViewCore.sendMessage(EventHub.KEY_DOWN, event); + // return true as DOM handles the key + return true; + } + + // Bubble up the key event as WebView doesn't handle it + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() + + ", " + event); + } + + if (mNativeClass == 0) { + return false; + } + + // special CALL handling when focus node's href is "tel:XXX" + if (keyCode == KeyEvent.KEYCODE_CALL && nativeUpdateFocusNode()) { + FocusNode node = mFocusNode; + String text = node.mText; + if (!node.mIsTextField && !node.mIsTextArea && text != null + && text.startsWith(SCHEME_TEL)) { + Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(text)); + getContext().startActivity(intent); + return true; + } + } + + // Bubble up the key event if + // 1. it is a system key; or + // 2. the host application wants to handle it; + if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) { + return false; + } + + // special handling in scroll_zoom state + if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { + if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode + && mTouchMode != SCROLL_ZOOM_ANIMATION_IN) { + setZoomScrollIn(); + mTouchMode = SCROLL_ZOOM_ANIMATION_IN; + invalidate(); + return true; + } + return false; + } + + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + if (commitCopy()) { + return true; + } + } + + if (keyCode >= KeyEvent.KEYCODE_DPAD_UP + && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { + // always handle the navigation keys in the UI thread + // Bubble up the key event as WebView doesn't handle it + return false; + } + + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER) { + // remove the long press message first + mPrivateHandler.removeMessages(LONG_PRESS_ENTER); + mGotEnterDown = false; + + if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { + if (mShiftIsPressed) { + return false; + } + if (getSettings().supportZoom()) { + if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { + zoomScrollOut(); + } else { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "TOUCH_DOUBLECLICK_MODE"); + } + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(SWITCH_TO_ENTER), TAP_TIMEOUT); + mTouchMode = TOUCH_DOUBLECLICK_MODE; + } + return true; + } + } + + Rect visibleRect = sendOurVisibleRect(); + // Note that sendOurVisibleRect calls viewToContent, so the + // coordinates should be in content coordinates. + if (nativeUpdateFocusNode()) { + if (Rect.intersects(mFocusNode.mBounds, visibleRect)) { + nativeSetFollowedLink(true); + mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, + EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0, + new WebViewCore.FocusData(mFocusData)); + playSoundEffect(SoundEffectConstants.CLICK); + if (!mCallbackProxy.uiOverrideUrlLoading(mFocusNode.mText)) { + // use CLICK instead of KEY_DOWN/KEY_UP so that we can + // trigger mouse click events + mWebViewCore.sendMessage(EventHub.CLICK); + } + } + return true; + } + // Bubble up the key event as WebView doesn't handle it + return false; + } + + // TODO: should we pass all the keys to DOM or check the meta tag + if (nativeFocusNodeWantsKeyEvents() || true) { + // pass the key to DOM + mWebViewCore.sendMessage(EventHub.KEY_UP, event); + // return true as DOM handles the key + return true; + } + + // Bubble up the key event as WebView doesn't handle it + return false; + } + + /** + * @hide + */ + public void emulateShiftHeld() { + mExtendSelection = false; + mShiftIsPressed = true; + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + nativeClearFocus(contentX, contentY); + } + + private boolean commitCopy() { + boolean copiedSomething = false; + if (mExtendSelection) { + // copy region so core operates on copy without touching orig. + Region selection = new Region(nativeGetSelection()); + if (selection.isEmpty() == false) { + Toast.makeText(mContext + , com.android.internal.R.string.text_copied + , Toast.LENGTH_SHORT).show(); + mWebViewCore.sendMessage(EventHub.GET_SELECTION, selection); + copiedSomething = true; + } + mExtendSelection = false; + } + mShiftIsPressed = false; + if (mTouchMode == TOUCH_SELECT_MODE) { + mTouchMode = TOUCH_INIT_MODE; + } + return copiedSomething; + } + + // Set this as a hierarchy change listener so we can know when this view + // is removed and still have access to our parent. + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + ViewParent parent = getParent(); + if (parent instanceof ViewGroup) { + ViewGroup p = (ViewGroup) parent; + p.setOnHierarchyChangeListener(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + ViewParent parent = getParent(); + if (parent instanceof ViewGroup) { + ViewGroup p = (ViewGroup) parent; + p.setOnHierarchyChangeListener(null); + } + + // Clean up the zoom ring + mZoomRingController.setVisible(false); + mZoomButtonsController.setVisible(false); + } + + // Implementation for OnHierarchyChangeListener + public void onChildViewAdded(View parent, View child) {} + + public void onChildViewRemoved(View p, View child) { + if (child == this) { + if (inEditingMode()) { + clearTextEntry(); + mNeedsUpdateTextEntry = true; + } + } + } + + /** + * @deprecated WebView should not have implemented + * ViewTreeObserver.OnGlobalFocusChangeListener. This method + * does nothing now. + */ + @Deprecated + public void onGlobalFocusChanged(View oldFocus, View newFocus) { + } + + // To avoid drawing the focus ring, and remove the TextView when our window + // loses focus. + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + if (hasWindowFocus) { + if (hasFocus()) { + // If our window regained focus, and we have focus, then begin + // drawing the focus ring, and restore the TextView if + // necessary. + mDrawFocusRing = true; + if (mNeedsUpdateTextEntry) { + updateTextEntry(); + } + if (mNativeClass != 0) { + nativeRecordButtons(true, false, true); + } + } else { + // If our window gained focus, but we do not have it, do not + // draw the focus ring. + mDrawFocusRing = false; + // We do not call nativeRecordButtons here because we assume + // that when we lost focus, or window focus, it got called with + // false for the first parameter + } + } else { + if (!mZoomButtonsController.isVisible()) { + /* + * The zoom controls come in their own window, so our window + * loses focus. Our policy is to not draw the focus ring if + * our window is not focused, but this is an exception since + * the user can still navigate the web page with the zoom + * controls showing. + */ + // If our window has lost focus, stop drawing the focus ring + mDrawFocusRing = false; + } + mGotKeyDown = false; + mShiftIsPressed = false; + if (mNativeClass != 0) { + nativeRecordButtons(false, false, true); + } + } + invalidate(); + super.onWindowFocusChanged(hasWindowFocus); + } + + @Override + protected void onFocusChanged(boolean focused, int direction, + Rect previouslyFocusedRect) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "MT focusChanged " + focused + ", " + direction); + } + if (focused) { + // When we regain focus, if we have window focus, resume drawing + // the focus ring, and add the TextView if necessary. + if (hasWindowFocus()) { + mDrawFocusRing = true; + if (mNeedsUpdateTextEntry) { + updateTextEntry(); + mNeedsUpdateTextEntry = false; + } + if (mNativeClass != 0) { + nativeRecordButtons(true, false, true); + } + //} else { + // The WebView has gained focus while we do not have + // windowfocus. When our window lost focus, we should have + // called nativeRecordButtons(false...) + } + } else { + // When we lost focus, unless focus went to the TextView (which is + // true if we are in editing mode), stop drawing the focus ring. + if (!inEditingMode()) { + mDrawFocusRing = false; + if (mNativeClass != 0) { + nativeRecordButtons(false, false, true); + } + } + mGotKeyDown = false; + } + + super.onFocusChanged(focused, direction, previouslyFocusedRect); + } + + @Override + protected void onSizeChanged(int w, int h, int ow, int oh) { + super.onSizeChanged(w, h, ow, oh); + // Center zooming to the center of the screen. This is appropriate for + // this case of zooming, and it also sets us up properly if we remove + // the new zoom ring controller + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + + // update mMinZoomScale + if (mMinContentWidth > MAX_FLOAT_CONTENT_WIDTH) { + boolean atMin = Math.abs(mActualScale - mMinZoomScale) < 0.01f; + mMinZoomScale = (float) getViewWidth() / mContentWidth; + if (atMin) { + // if the WebView was at the minimum zoom scale, keep it. e,g., + // the WebView was at the minimum zoom scale at the portrait + // mode, rotate it to the landscape modifying the scale to the + // new minimum zoom scale, when rotating back, we would like to + // keep the minimum zoom scale instead of keeping the same scale + // as normally we do. + mActualScale = mMinZoomScale; + } + } + + // we always force, in case our height changed, in which case we still + // want to send the notification over to webkit + setNewZoomScale(mActualScale, true); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + sendOurVisibleRect(); + } + + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + boolean dispatch = true; + + if (!inEditingMode()) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + mGotKeyDown = true; + } else { + if (!mGotKeyDown) { + /* + * We got a key up for which we were not the recipient of + * the original key down. Don't give it to the view. + */ + dispatch = false; + } + mGotKeyDown = false; + } + } + + if (dispatch) { + return super.dispatchKeyEvent(event); + } else { + // We didn't dispatch, so let something else handle the key + return false; + } + } + + // Here are the snap align logic: + // 1. If it starts nearly horizontally or vertically, snap align; + // 2. If there is a dramitic direction change, let it go; + // 3. If there is a same direction back and forth, lock it. + + // adjustable parameters + private int mMinLockSnapReverseDistance; + private static final float MAX_SLOPE_FOR_DIAG = 1.5f; + private static final int MIN_BREAK_SNAP_CROSS_DISTANCE = 80; + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mNativeClass == 0 || !isClickable() || !isLongClickable()) { + return false; + } + + if (mShowZoomRingTutorial && getSettings().supportZoom() + && (mMaxZoomScale - mMinZoomScale) > ZOOM_RING_STEPS * 0.01f) { + ZoomRingController.showZoomTutorialOnce(mContext); + mShowZoomRingTutorial = false; + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(DISMISS_ZOOM_RING_TUTORIAL), + ZOOM_RING_TUTORIAL_DURATION); + } + + if (LOGV_ENABLED) { + Log.v(LOGTAG, ev + " at " + ev.getEventTime() + " mTouchMode=" + + mTouchMode); + } + + if ((mZoomRingController.isVisible() || mZoomButtonsController.isVisible()) + && mInZoomTapDragMode) { + if (ev.getAction() == MotionEvent.ACTION_UP) { + // Just released the second tap, no longer in tap-drag mode + mInZoomTapDragMode = false; + } + return mZoomRingController.handleDoubleTapEvent(ev); + } + + int action = ev.getAction(); + float x = ev.getX(); + float y = ev.getY(); + long eventTime = ev.getEventTime(); + + // Due to the touch screen edge effect, a touch closer to the edge + // always snapped to the edge. As getViewWidth() can be different from + // getWidth() due to the scrollbar, adjusting the point to match + // getViewWidth(). Same applied to the height. + if (x > getViewWidth() - 1) { + x = getViewWidth() - 1; + } + if (y > getViewHeight() - 1) { + y = getViewHeight() - 1; + } + + // pass the touch events from UI thread to WebCore thread + if (mForwardTouchEvents && mTouchMode != SCROLL_ZOOM_OUT + && mTouchMode != SCROLL_ZOOM_ANIMATION_IN + && mTouchMode != SCROLL_ZOOM_ANIMATION_OUT + && (action != MotionEvent.ACTION_MOVE || + eventTime - mLastSentTouchTime > TOUCH_SENT_INTERVAL)) { + WebViewCore.TouchEventData ted = new WebViewCore.TouchEventData(); + ted.mAction = action; + ted.mX = viewToContent((int) x + mScrollX); + ted.mY = viewToContent((int) y + mScrollY);; + mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); + mLastSentTouchTime = eventTime; + } + + int deltaX = (int) (mLastTouchX - x); + int deltaY = (int) (mLastTouchY - y); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN + || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { + // no interaction while animation is in progress + break; + } else if (mTouchMode == SCROLL_ZOOM_OUT) { + mLastScrollX = mZoomScrollX; + mLastScrollY = mZoomScrollY; + // If two taps are close, ignore the first tap + } else if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + mTouchMode = TOUCH_DRAG_START_MODE; + mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE); + } else if (mShiftIsPressed) { + mSelectX = mScrollX + (int) x; + mSelectY = mScrollY + (int) y; + mTouchMode = TOUCH_SELECT_MODE; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY); + } + nativeMoveSelection(viewToContent(mSelectX) + , viewToContent(mSelectY), false); + mTouchSelection = mExtendSelection = true; + } else if (!ZoomRingController.useOldZoom(mContext) && + mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP) && + (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare)) { + // Found doubletap, invoke the zoom controller + mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + if (inEditingMode()) { + mTextEntry.updateCachedTextfield(); + } + nativeClearFocus(contentX, contentY); + mInZoomTapDragMode = true; + if (mLogEvent) { + EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION, + (eventTime - mLastTouchUpTime), eventTime); + } + return mZoomRingController.handleDoubleTapEvent(ev) || + mZoomButtonsController.handleDoubleTapEvent(ev); + } else { + mTouchMode = TOUCH_INIT_MODE; + mPreventDrag = mForwardTouchEvents; + if (mLogEvent && eventTime - mLastTouchUpTime < 1000) { + EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION, + (eventTime - mLastTouchUpTime), eventTime); + } + } + // don't trigger the link if zoom ring is visible + if (mTouchMode == TOUCH_INIT_MODE + && !mZoomRingController.isVisible()) { + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(SWITCH_TO_SHORTPRESS), TAP_TIMEOUT); + } + // Remember where the motion event started + mLastTouchX = x; + mLastTouchY = y; + mLastTouchTime = eventTime; + mVelocityTracker = VelocityTracker.obtain(); + mSnapScrollMode = SNAP_NONE; + break; + } + case MotionEvent.ACTION_MOVE: { + if (mTouchMode == TOUCH_DONE_MODE + || mTouchMode == SCROLL_ZOOM_ANIMATION_IN + || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { + // no dragging during scroll zoom animation + break; + } + if (mTouchMode == SCROLL_ZOOM_OUT) { + // while fully zoomed out, move the virtual window + moveZoomScrollWindow(x, y); + break; + } + mVelocityTracker.addMovement(ev); + + if (mTouchMode != TOUCH_DRAG_MODE) { + if (mTouchMode == TOUCH_SELECT_MODE) { + mSelectX = mScrollX + (int) x; + mSelectY = mScrollY + (int) y; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY); + } + nativeMoveSelection(viewToContent(mSelectX) + , viewToContent(mSelectY), true); + invalidate(); + break; + } + if (mPreventDrag || (deltaX * deltaX + deltaY * deltaY) + < mTouchSlopSquare) { + break; + } + + if (mTouchMode == TOUCH_SHORTPRESS_MODE + || mTouchMode == TOUCH_SHORTPRESS_START_MODE) { + mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); + } else if (mTouchMode == TOUCH_INIT_MODE) { + mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); + } + + // if it starts nearly horizontal or vertical, enforce it + int ax = Math.abs(deltaX); + int ay = Math.abs(deltaY); + if (ax > MAX_SLOPE_FOR_DIAG * ay) { + mSnapScrollMode = SNAP_X; + mSnapPositive = deltaX > 0; + } else if (ay > MAX_SLOPE_FOR_DIAG * ax) { + mSnapScrollMode = SNAP_Y; + mSnapPositive = deltaY > 0; + } + + mTouchMode = TOUCH_DRAG_MODE; + WebViewCore.pauseUpdate(mWebViewCore); + int contentX = viewToContent((int) x + mScrollX); + int contentY = viewToContent((int) y + mScrollY); + if (inEditingMode()) { + mTextEntry.updateCachedTextfield(); + } + nativeClearFocus(contentX, contentY); + // remove the zoom anchor if there is any + if (mZoomScale != 0) { + mWebViewCore + .sendMessage(EventHub.SET_SNAP_ANCHOR, 0, 0); + } + } + + // do pan + int newScrollX = pinLocX(mScrollX + deltaX); + deltaX = newScrollX - mScrollX; + int newScrollY = pinLocY(mScrollY + deltaY); + deltaY = newScrollY - mScrollY; + boolean done = false; + if (deltaX == 0 && deltaY == 0) { + done = true; + } else { + if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_Y) { + int ax = Math.abs(deltaX); + int ay = Math.abs(deltaY); + if (mSnapScrollMode == SNAP_X) { + // radical change means getting out of snap mode + if (ay > MAX_SLOPE_FOR_DIAG * ax + && ay > MIN_BREAK_SNAP_CROSS_DISTANCE) { + mSnapScrollMode = SNAP_NONE; + } + // reverse direction means lock in the snap mode + if ((ax > MAX_SLOPE_FOR_DIAG * ay) && + ((mSnapPositive && + deltaX < -mMinLockSnapReverseDistance) + || (!mSnapPositive && + deltaX > mMinLockSnapReverseDistance))) { + mSnapScrollMode = SNAP_X_LOCK; + } + } else { + // radical change means getting out of snap mode + if ((ax > MAX_SLOPE_FOR_DIAG * ay) + && ax > MIN_BREAK_SNAP_CROSS_DISTANCE) { + mSnapScrollMode = SNAP_NONE; + } + // reverse direction means lock in the snap mode + if ((ay > MAX_SLOPE_FOR_DIAG * ax) && + ((mSnapPositive && + deltaY < -mMinLockSnapReverseDistance) + || (!mSnapPositive && + deltaY > mMinLockSnapReverseDistance))) { + mSnapScrollMode = SNAP_Y_LOCK; + } + } + } + + if (mSnapScrollMode == SNAP_X + || mSnapScrollMode == SNAP_X_LOCK) { + scrollBy(deltaX, 0); + mLastTouchX = x; + } else if (mSnapScrollMode == SNAP_Y + || mSnapScrollMode == SNAP_Y_LOCK) { + scrollBy(0, deltaY); + mLastTouchY = y; + } else { + scrollBy(deltaX, deltaY); + mLastTouchX = x; + mLastTouchY = y; + } + mLastTouchTime = eventTime; + mUserScroll = true; + } + + if (ZoomRingController.useOldZoom(mContext)) { + boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; + boolean showMagnify = canZoomScrollOut(); + if (mZoomControls != null && (showPlusMinus || showMagnify)) { + if (mZoomControls.getVisibility() == View.VISIBLE) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + } else { + mZoomControls.show(showPlusMinus, showMagnify); + } + mPrivateHandler.postDelayed(mZoomControlRunnable, + ZOOM_CONTROLS_TIMEOUT); + } + } + if (done) { + // return false to indicate that we can't pan out of the + // view space + return false; + } + break; + } + case MotionEvent.ACTION_UP: { + mLastTouchUpTime = eventTime; + switch (mTouchMode) { + case TOUCH_INIT_MODE: // tap + if (mZoomRingController.isVisible()) { + // don't trigger the link if zoom ring is visible, + // but still allow the double tap + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(RELEASE_SINGLE_TAP, + new Boolean(false)), + DOUBLE_TAP_TIMEOUT); + break; + } + mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); + if (getSettings().supportZoom()) { + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(RELEASE_SINGLE_TAP, + new Boolean(true)), + DOUBLE_TAP_TIMEOUT); + } else { + // do short press now + mTouchMode = TOUCH_DONE_MODE; + doShortPress(); + } + break; + case TOUCH_SELECT_MODE: + commitCopy(); + mTouchSelection = false; + break; + case SCROLL_ZOOM_ANIMATION_IN: + case SCROLL_ZOOM_ANIMATION_OUT: + // no action during scroll animation + break; + case SCROLL_ZOOM_OUT: + if (LOGV_ENABLED) { + Log.v(LOGTAG, "ACTION_UP SCROLL_ZOOM_OUT" + + " eventTime - mLastTouchTime=" + + (eventTime - mLastTouchTime)); + } + // for now, always zoom back when the drag completes + if (true || eventTime - mLastTouchTime < TAP_TIMEOUT) { + // but if we tap, zoom in where we tap + if (eventTime - mLastTouchTime < TAP_TIMEOUT) { + zoomScrollTap(x, y); + } + // start zooming in back to the original view + setZoomScrollIn(); + mTouchMode = SCROLL_ZOOM_ANIMATION_IN; + invalidate(); + } + break; + case TOUCH_SHORTPRESS_START_MODE: + case TOUCH_SHORTPRESS_MODE: { + mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); + if (eventTime - mLastTouchTime < TAP_TIMEOUT + && getSettings().supportZoom()) { + // Note: window manager will not release ACTION_UP + // until all the previous action events are + // returned. If GC happens, it can cause + // SWITCH_TO_SHORTPRESS message fired before + // ACTION_UP sent even time stamp of ACTION_UP is + // less than the tap time out. We need to treat this + // as tap instead of short press. + mTouchMode = TOUCH_INIT_MODE; + mPrivateHandler.sendMessageDelayed(mPrivateHandler + .obtainMessage(RELEASE_SINGLE_TAP, + new Boolean(true)), + DOUBLE_TAP_TIMEOUT); + } else { + mTouchMode = TOUCH_DONE_MODE; + doShortPress(); + } + break; + } + case TOUCH_DRAG_MODE: + // if the user waits a while w/o moving before the + // up, we don't want to do a fling + if (eventTime - mLastTouchTime <= MIN_FLING_TIME) { + mVelocityTracker.addMovement(ev); + doFling(); + break; + } + WebViewCore.resumeUpdate(mWebViewCore); + break; + case TOUCH_DRAG_START_MODE: + case TOUCH_DONE_MODE: + // do nothing + break; + } + // we also use mVelocityTracker == null to tell us that we are + // not "moving around", so we can take the slower/prettier + // mode in the drawing code + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + } + case MotionEvent.ACTION_CANCEL: { + // we also use mVelocityTracker == null to tell us that we are + // not "moving around", so we can take the slower/prettier + // mode in the drawing code + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + if (mTouchMode == SCROLL_ZOOM_OUT || + mTouchMode == SCROLL_ZOOM_ANIMATION_IN) { + scrollTo(mZoomScrollX, mZoomScrollY); + } else if (mTouchMode == TOUCH_DRAG_MODE) { + WebViewCore.resumeUpdate(mWebViewCore); + } + mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); + mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); + mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); + mTouchMode = TOUCH_DONE_MODE; + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + if (inEditingMode()) { + mTextEntry.updateCachedTextfield(); + } + nativeClearFocus(contentX, contentY); + break; + } + } + return true; + } + + private long mTrackballFirstTime = 0; + private long mTrackballLastTime = 0; + private float mTrackballRemainsX = 0.0f; + private float mTrackballRemainsY = 0.0f; + private int mTrackballXMove = 0; + private int mTrackballYMove = 0; + private boolean mExtendSelection = false; + private boolean mTouchSelection = false; + private static final int TRACKBALL_KEY_TIMEOUT = 1000; + private static final int TRACKBALL_TIMEOUT = 200; + private static final int TRACKBALL_WAIT = 100; + private static final int TRACKBALL_SCALE = 400; + private static final int TRACKBALL_SCROLL_COUNT = 5; + private static final int TRACKBALL_MOVE_COUNT = 10; + private static final int TRACKBALL_MULTIPLIER = 3; + private static final int SELECT_CURSOR_OFFSET = 16; + private int mSelectX = 0; + private int mSelectY = 0; + private boolean mShiftIsPressed = false; + private boolean mTrackballDown = false; + private long mTrackballUpTime = 0; + private long mLastFocusTime = 0; + private Rect mLastFocusBounds; + + // Set by default; BrowserActivity clears to interpret trackball data + // directly for movement. Currently, the framework only passes + // arrow key events, not trackball events, from one child to the next + private boolean mMapTrackballToArrowKeys = true; + + public void setMapTrackballToArrowKeys(boolean setMap) { + mMapTrackballToArrowKeys = setMap; + } + + void resetTrackballTime() { + mTrackballLastTime = 0; + } + + @Override + public boolean onTrackballEvent(MotionEvent ev) { + long time = ev.getEventTime(); + if ((ev.getMetaState() & KeyEvent.META_ALT_ON) != 0) { + if (ev.getY() > 0) pageDown(true); + if (ev.getY() < 0) pageUp(true); + return true; + } + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mPrivateHandler.removeMessages(SWITCH_TO_ENTER); + mTrackballDown = true; + if (mNativeClass != 0) { + nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); + } + if (time - mLastFocusTime <= TRACKBALL_TIMEOUT + && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) { + nativeSelectBestAt(mLastFocusBounds); + } + if (LOGV_ENABLED) { + Log.v(LOGTAG, "onTrackballEvent down ev=" + ev + + " time=" + time + + " mLastFocusTime=" + mLastFocusTime); + } + if (isInTouchMode()) requestFocusFromTouch(); + return false; // let common code in onKeyDown at it + } + if (ev.getAction() == MotionEvent.ACTION_UP) { + // LONG_PRESS_ENTER is set in common onKeyDown + mPrivateHandler.removeMessages(LONG_PRESS_ENTER); + mTrackballDown = false; + mTrackballUpTime = time; + if (mShiftIsPressed) { + if (mExtendSelection) { + commitCopy(); + } else { + mExtendSelection = true; + } + } + if (LOGV_ENABLED) { + Log.v(LOGTAG, "onTrackballEvent up ev=" + ev + + " time=" + time + ); + } + return false; // let common code in onKeyUp at it + } + if (mMapTrackballToArrowKeys && mShiftIsPressed == false) { + if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent gmail quit"); + return false; + } + // no move if we're still waiting on SWITCH_TO_ENTER timeout + if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { + if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent 2 click quit"); + return true; + } + if (mTrackballDown) { + if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent down quit"); + return true; // discard move if trackball is down + } + if (time - mTrackballUpTime < TRACKBALL_TIMEOUT) { + if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); + return true; + } + // TODO: alternatively we can do panning as touch does + switchOutDrawHistory(); + if (time - mTrackballLastTime > TRACKBALL_TIMEOUT) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "onTrackballEvent time=" + + time + " last=" + mTrackballLastTime); + } + mTrackballFirstTime = time; + mTrackballXMove = mTrackballYMove = 0; + } + mTrackballLastTime = time; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "onTrackballEvent ev=" + ev + " time=" + time); + } + mTrackballRemainsX += ev.getX(); + mTrackballRemainsY += ev.getY(); + doTrackball(time); + return true; + } + + void moveSelection(float xRate, float yRate) { + if (mNativeClass == 0) + return; + int width = getViewWidth(); + int height = getViewHeight(); + mSelectX += scaleTrackballX(xRate, width); + mSelectY += scaleTrackballY(yRate, height); + int maxX = width + mScrollX; + int maxY = height + mScrollY; + mSelectX = Math.min(maxX, Math.max(mScrollX - SELECT_CURSOR_OFFSET + , mSelectX)); + mSelectY = Math.min(maxY, Math.max(mScrollY - SELECT_CURSOR_OFFSET + , mSelectY)); + if (LOGV_ENABLED) { + Log.v(LOGTAG, "moveSelection" + + " mSelectX=" + mSelectX + + " mSelectY=" + mSelectY + + " mScrollX=" + mScrollX + + " mScrollY=" + mScrollY + + " xRate=" + xRate + + " yRate=" + yRate + ); + } + nativeMoveSelection(viewToContent(mSelectX) + , viewToContent(mSelectY), mExtendSelection); + int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET + : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : 0; + int scrollY = mSelectY < mScrollY ? -SELECT_CURSOR_OFFSET + : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : 0; + pinScrollBy(scrollX, scrollY, true, 0); + Rect select = new Rect(mSelectX, mSelectY, mSelectX + 1, mSelectY + 1); + requestRectangleOnScreen(select); + invalidate(); + } + + private int scaleTrackballX(float xRate, int width) { + int xMove = (int) (xRate / TRACKBALL_SCALE * width); + int nextXMove = xMove; + if (xMove > 0) { + if (xMove > mTrackballXMove) { + xMove -= mTrackballXMove; + } + } else if (xMove < mTrackballXMove) { + xMove -= mTrackballXMove; + } + mTrackballXMove = nextXMove; + return xMove; + } + + private int scaleTrackballY(float yRate, int height) { + int yMove = (int) (yRate / TRACKBALL_SCALE * height); + int nextYMove = yMove; + if (yMove > 0) { + if (yMove > mTrackballYMove) { + yMove -= mTrackballYMove; + } + } else if (yMove < mTrackballYMove) { + yMove -= mTrackballYMove; + } + mTrackballYMove = nextYMove; + return yMove; + } + + private int keyCodeToSoundsEffect(int keyCode) { + switch(keyCode) { + case KeyEvent.KEYCODE_DPAD_UP: + return SoundEffectConstants.NAVIGATION_UP; + case KeyEvent.KEYCODE_DPAD_RIGHT: + return SoundEffectConstants.NAVIGATION_RIGHT; + case KeyEvent.KEYCODE_DPAD_DOWN: + return SoundEffectConstants.NAVIGATION_DOWN; + case KeyEvent.KEYCODE_DPAD_LEFT: + return SoundEffectConstants.NAVIGATION_LEFT; + } + throw new IllegalArgumentException("keyCode must be one of " + + "{KEYCODE_DPAD_UP, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_DOWN, " + + "KEYCODE_DPAD_LEFT}."); + } + + private void doTrackball(long time) { + int elapsed = (int) (mTrackballLastTime - mTrackballFirstTime); + if (elapsed == 0) { + elapsed = TRACKBALL_TIMEOUT; + } + float xRate = mTrackballRemainsX * 1000 / elapsed; + float yRate = mTrackballRemainsY * 1000 / elapsed; + if (mShiftIsPressed) { + moveSelection(xRate, yRate); + mTrackballRemainsX = mTrackballRemainsY = 0; + return; + } + float ax = Math.abs(xRate); + float ay = Math.abs(yRate); + float maxA = Math.max(ax, ay); + if (LOGV_ENABLED) { + Log.v(LOGTAG, "doTrackball elapsed=" + elapsed + + " xRate=" + xRate + + " yRate=" + yRate + + " mTrackballRemainsX=" + mTrackballRemainsX + + " mTrackballRemainsY=" + mTrackballRemainsY); + } + int width = mContentWidth - getViewWidth(); + int height = mContentHeight - getViewHeight(); + if (width < 0) width = 0; + if (height < 0) height = 0; + if (mTouchMode == SCROLL_ZOOM_OUT) { + int oldX = mZoomScrollX; + int oldY = mZoomScrollY; + int maxWH = Math.max(width, height); + mZoomScrollX += scaleTrackballX(xRate, maxWH); + mZoomScrollY += scaleTrackballY(yRate, maxWH); + if (LOGV_ENABLED) { + Log.v(LOGTAG, "doTrackball SCROLL_ZOOM_OUT" + + " mZoomScrollX=" + mZoomScrollX + + " mZoomScrollY=" + mZoomScrollY); + } + mZoomScrollX = Math.min(width, Math.max(0, mZoomScrollX)); + mZoomScrollY = Math.min(height, Math.max(0, mZoomScrollY)); + if (oldX != mZoomScrollX || oldY != mZoomScrollY) { + invalidate(); + } + mTrackballRemainsX = mTrackballRemainsY = 0; + return; + } + ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER); + ay = Math.abs(mTrackballRemainsY * TRACKBALL_MULTIPLIER); + maxA = Math.max(ax, ay); + int count = Math.max(0, (int) maxA); + int oldScrollX = mScrollX; + int oldScrollY = mScrollY; + if (count > 0) { + int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? + KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : + mTrackballRemainsX < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : + KeyEvent.KEYCODE_DPAD_RIGHT; + count = Math.min(count, TRACKBALL_MOVE_COUNT); + if (LOGV_ENABLED) { + Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + + " count=" + count + + " mTrackballRemainsX=" + mTrackballRemainsX + + " mTrackballRemainsY=" + mTrackballRemainsY); + } + if (navHandledKey(selectKeyCode, count, false, time)) { + playSoundEffect(keyCodeToSoundsEffect(selectKeyCode)); + } + mTrackballRemainsX = mTrackballRemainsY = 0; + } + if (count >= TRACKBALL_SCROLL_COUNT) { + int xMove = scaleTrackballX(xRate, width); + int yMove = scaleTrackballY(yRate, height); + if (LOGV_ENABLED) { + Log.v(LOGTAG, "doTrackball pinScrollBy" + + " count=" + count + + " xMove=" + xMove + " yMove=" + yMove + + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX) + + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY) + ); + } + if (Math.abs(mScrollX - oldScrollX) > Math.abs(xMove)) { + xMove = 0; + } + if (Math.abs(mScrollY - oldScrollY) > Math.abs(yMove)) { + yMove = 0; + } + if (xMove != 0 || yMove != 0) { + pinScrollBy(xMove, yMove, true, 0); + } + mUserScroll = true; + } + mWebViewCore.sendMessage(EventHub.UNBLOCK_FOCUS); + } + + public void flingScroll(int vx, int vy) { + int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); + int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); + + mScroller.fling(mScrollX, mScrollY, vx, vy, 0, maxX, 0, maxY); + invalidate(); + } + + private void doFling() { + if (mVelocityTracker == null) { + return; + } + int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); + int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); + + mVelocityTracker.computeCurrentVelocity(1000); + int vx = (int) mVelocityTracker.getXVelocity(); + int vy = (int) mVelocityTracker.getYVelocity(); + + if (mSnapScrollMode != SNAP_NONE) { + if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_X_LOCK) { + vy = 0; + } else { + vx = 0; + } + } + + if (true /* EMG release: make our fling more like Maps' */) { + // maps cuts their velocity in half + vx = vx * 3 / 4; + vy = vy * 3 / 4; + } + + mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY); + // TODO: duration is calculated based on velocity, if the range is + // small, the animation will stop before duration is up. We may + // want to calculate how long the animation is going to run to precisely + // resume the webcore update. + final int time = mScroller.getDuration(); + mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_UPDATE, time); + invalidate(); + } + + private boolean zoomWithPreview(float scale) { + float oldScale = mActualScale; + + // snap to 100% if it is close + if (scale > 0.95f && scale < 1.05f) { + scale = 1.0f; + } + + setNewZoomScale(scale, false); + + if (oldScale != mActualScale) { + // use mZoomPickerScale to see zoom preview first + mZoomStart = SystemClock.uptimeMillis(); + mInvInitialZoomScale = 1.0f / oldScale; + mInvFinalZoomScale = 1.0f / mActualScale; + mZoomScale = mActualScale; + invalidate(); + return true; + } else { + return false; + } + } + + /** + * Returns a view containing zoom controls i.e. +/- buttons. The caller is + * in charge of installing this view to the view hierarchy. This view will + * become visible when the user starts scrolling via touch and fade away if + * the user does not interact with it. + */ + public View getZoomControls() { + if (!getSettings().supportZoom()) { + Log.w(LOGTAG, "This WebView doesn't support zoom."); + return null; + } + if (mZoomControls == null) { + mZoomControls = createZoomControls(); + + /* + * need to be set to VISIBLE first so that getMeasuredHeight() in + * {@link #onSizeChanged()} can return the measured value for proper + * layout. + */ + mZoomControls.setVisibility(View.VISIBLE); + mZoomControlRunnable = new Runnable() { + public void run() { + + /* Don't dismiss the controls if the user has + * focus on them. Wait and check again later. + */ + if (!mZoomControls.hasFocus()) { + mZoomControls.hide(); + } else { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, + ZOOM_CONTROLS_TIMEOUT); + } + } + }; + } + return mZoomControls; + } + + /** + * @hide pending API council? Assuming we make ZoomRingController itself + * public, which I think we will. + */ + public ZoomRingController getZoomRingController() { + return mZoomRingController; + } + + /** + * Perform zoom in in the webview + * @return TRUE if zoom in succeeds. FALSE if no zoom changes. + */ + public boolean zoomIn() { + // TODO: alternatively we can disallow this during draw history mode + switchOutDrawHistory(); + return zoomWithPreview(mActualScale * 1.25f); + } + + /** + * Perform zoom out in the webview + * @return TRUE if zoom out succeeds. FALSE if no zoom changes. + */ + public boolean zoomOut() { + // TODO: alternatively we can disallow this during draw history mode + switchOutDrawHistory(); + return zoomWithPreview(mActualScale * 0.8f); + } + + private ExtendedZoomControls createZoomControls() { + ExtendedZoomControls zoomControls = new ExtendedZoomControls(mContext + , null); + zoomControls.setOnZoomInClickListener(new OnClickListener() { + public void onClick(View v) { + // reset time out + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, + ZOOM_CONTROLS_TIMEOUT); + zoomIn(); + } + }); + zoomControls.setOnZoomOutClickListener(new OnClickListener() { + public void onClick(View v) { + // reset time out + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, + ZOOM_CONTROLS_TIMEOUT); + zoomOut(); + } + }); + zoomControls.setOnZoomMagnifyClickListener(new OnClickListener() { + public void onClick(View v) { + // Hide the zoom ring + mZoomRingController.setVisible(false); + + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + mPrivateHandler.postDelayed(mZoomControlRunnable, + ZOOM_CONTROLS_TIMEOUT); + zoomScrollOut(); + } + }); + return zoomControls; + } + + private void updateSelection() { + if (mNativeClass == 0) { + return; + } + // mLastTouchX and mLastTouchY are the point in the current viewport + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + int contentSize = ViewConfiguration.getTouchSlop(); + Rect rect = new Rect(contentX - contentSize, contentY - contentSize, + contentX + contentSize, contentY + contentSize); + // If we were already focused on a textfield, update its cache. + if (inEditingMode()) { + mTextEntry.updateCachedTextfield(); + } + nativeSelectBestAt(rect); + } + + /*package*/ void shortPressOnTextField() { + if (inEditingMode()) { + View v = mTextEntry; + int x = viewToContent((v.getLeft() + v.getRight()) >> 1); + int y = viewToContent((v.getTop() + v.getBottom()) >> 1); + int contentSize = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + nativeMotionUp(x, y, contentSize, true); + } + } + + private void doShortPress() { + if (mNativeClass == 0) { + return; + } + switchOutDrawHistory(); + // mLastTouchX and mLastTouchY are the point in the current viewport + int contentX = viewToContent((int) mLastTouchX + mScrollX); + int contentY = viewToContent((int) mLastTouchY + mScrollY); + int contentSize = ViewConfiguration.get(getContext()).getScaledTouchSlop(); + if (nativeMotionUp(contentX, contentY, contentSize, true)) { + if (mLogEvent) { + Checkin.updateStats(mContext.getContentResolver(), + Checkin.Stats.Tag.BROWSER_SNAP_CENTER, 1, 0.0); + } + } + if (nativeUpdateFocusNode() && !mFocusNode.mIsTextField + && !mFocusNode.mIsTextArea) { + playSoundEffect(SoundEffectConstants.CLICK); + } + } + + // Called by JNI to handle a touch on a node representing an email address, + // address, or phone number + private void overrideLoading(String url) { + mCallbackProxy.uiOverrideUrlLoading(url); + } + + @Override + public boolean requestFocus(int direction, Rect previouslyFocusedRect) { + boolean result = false; + if (inEditingMode()) { + result = mTextEntry.requestFocus(direction, previouslyFocusedRect); + } else { + result = super.requestFocus(direction, previouslyFocusedRect); + if (mWebViewCore.getSettings().getNeedInitialFocus()) { + // For cases such as GMail, where we gain focus from a direction, + // we want to move to the first available link. + // FIXME: If there are no visible links, we may not want to + int fakeKeyDirection = 0; + switch(direction) { + case View.FOCUS_UP: + fakeKeyDirection = KeyEvent.KEYCODE_DPAD_UP; + break; + case View.FOCUS_DOWN: + fakeKeyDirection = KeyEvent.KEYCODE_DPAD_DOWN; + break; + case View.FOCUS_LEFT: + fakeKeyDirection = KeyEvent.KEYCODE_DPAD_LEFT; + break; + case View.FOCUS_RIGHT: + fakeKeyDirection = KeyEvent.KEYCODE_DPAD_RIGHT; + break; + default: + return result; + } + if (mNativeClass != 0 && !nativeUpdateFocusNode()) { + navHandledKey(fakeKeyDirection, 1, true, 0); + } + } + } + return result; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + + int measuredHeight = heightSize; + int measuredWidth = widthSize; + + // Grab the content size from WebViewCore. + int contentHeight = mContentHeight; + int contentWidth = mContentWidth; + +// Log.d(LOGTAG, "------- measure " + heightMode); + + if (heightMode != MeasureSpec.EXACTLY) { + mHeightCanMeasure = true; + measuredHeight = contentHeight; + if (heightMode == MeasureSpec.AT_MOST) { + // If we are larger than the AT_MOST height, then our height can + // no longer be measured and we should scroll internally. + if (measuredHeight > heightSize) { + measuredHeight = heightSize; + mHeightCanMeasure = false; + } + } + } else { + mHeightCanMeasure = false; + } + if (mNativeClass != 0) { + nativeSetHeightCanMeasure(mHeightCanMeasure); + } + // For the width, always use the given size unless unspecified. + if (widthMode == MeasureSpec.UNSPECIFIED) { + mWidthCanMeasure = true; + measuredWidth = contentWidth; + } else { + mWidthCanMeasure = false; + } + + synchronized (this) { + setMeasuredDimension(measuredWidth, measuredHeight); + } + } + + @Override + public boolean requestChildRectangleOnScreen(View child, + Rect rect, + boolean immediate) { + rect.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + int height = getHeight() - getHorizontalScrollbarHeight(); + int screenTop = mScrollY; + int screenBottom = screenTop + height; + + int scrollYDelta = 0; + + if (rect.bottom > screenBottom && rect.top > screenTop) { + if (rect.height() > height) { + scrollYDelta += (rect.top - screenTop); + } else { + scrollYDelta += (rect.bottom - screenBottom); + } + } else if (rect.top < screenTop) { + scrollYDelta -= (screenTop - rect.top); + } + + int width = getWidth() - getVerticalScrollbarWidth(); + int screenLeft = mScrollX; + int screenRight = screenLeft + width; + + int scrollXDelta = 0; + + if (rect.right > screenRight && rect.left > screenLeft) { + if (rect.width() > width) { + scrollXDelta += (rect.left - screenLeft); + } else { + scrollXDelta += (rect.right - screenRight); + } + } else if (rect.left < screenLeft) { + scrollXDelta -= (screenLeft - rect.left); + } + + if ((scrollYDelta | scrollXDelta) != 0) { + return pinScrollBy(scrollXDelta, scrollYDelta, !immediate, 0); + } + + return false; + } + + /* package */ void replaceTextfieldText(int oldStart, int oldEnd, + String replace, int newStart, int newEnd) { + HashMap arg = new HashMap(); + arg.put("focusData", new WebViewCore.FocusData(mFocusData)); + arg.put("replace", replace); + arg.put("start", new Integer(newStart)); + arg.put("end", new Integer(newEnd)); + mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg); + } + + /* package */ void passToJavaScript(String currentText, KeyEvent event) { + HashMap arg = new HashMap(); + arg.put("focusData", new WebViewCore.FocusData(mFocusData)); + arg.put("event", event); + arg.put("currentText", currentText); + // Increase our text generation number, and pass it to webcore thread + mTextGeneration++; + mWebViewCore.sendMessage(EventHub.PASS_TO_JS, mTextGeneration, 0, arg); + // WebKit's document state is not saved until about to leave the page. + // To make sure the host application, like Browser, has the up to date + // document state when it goes to background, we force to save the + // document state. + mWebViewCore.removeMessages(EventHub.SAVE_DOCUMENT_STATE); + mWebViewCore.sendMessageDelayed(EventHub.SAVE_DOCUMENT_STATE, + new WebViewCore.FocusData(mFocusData), 1000); + } + + /* package */ WebViewCore getWebViewCore() { + return mWebViewCore; + } + + //------------------------------------------------------------------------- + // Methods can be called from a separate thread, like WebViewCore + // If it needs to call the View system, it has to send message. + //------------------------------------------------------------------------- + + /** + * General handler to receive message coming from webkit thread + */ + class PrivateHandler extends Handler { + @Override + public void handleMessage(Message msg) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what + > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) + : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); + } + switch (msg.what) { + case REMEMBER_PASSWORD: { + mDatabase.setUsernamePassword( + msg.getData().getString("host"), + msg.getData().getString("username"), + msg.getData().getString("password")); + ((Message) msg.obj).sendToTarget(); + break; + } + case NEVER_REMEMBER_PASSWORD: { + mDatabase.setUsernamePassword( + msg.getData().getString("host"), null, null); + ((Message) msg.obj).sendToTarget(); + break; + } + case SWITCH_TO_SHORTPRESS: { + if (mTouchMode == TOUCH_INIT_MODE) { + mTouchMode = TOUCH_SHORTPRESS_START_MODE; + updateSelection(); + } + break; + } + case SWITCH_TO_LONGPRESS: { + mTouchMode = TOUCH_DONE_MODE; + performLongClick(); + updateTextEntry(); + break; + } + case RELEASE_SINGLE_TAP: { + mTouchMode = TOUCH_DONE_MODE; + if ((Boolean)msg.obj) { + doShortPress(); + } + break; + } + case SWITCH_TO_ENTER: + if (LOGV_ENABLED) Log.v(LOGTAG, "SWITCH_TO_ENTER"); + mTouchMode = TOUCH_DONE_MODE; + onKeyUp(KeyEvent.KEYCODE_ENTER + , new KeyEvent(KeyEvent.ACTION_UP + , KeyEvent.KEYCODE_ENTER)); + break; + case SCROLL_BY_MSG_ID: + setContentScrollBy(msg.arg1, msg.arg2, (Boolean) msg.obj); + break; + case SYNC_SCROLL_TO_MSG_ID: + if (mUserScroll) { + // if user has scrolled explicitly, don't sync the + // scroll position any more + mUserScroll = false; + break; + } + // fall through + case SCROLL_TO_MSG_ID: + if (setContentScrollTo(msg.arg1, msg.arg2)) { + // if we can't scroll to the exact position due to pin, + // send a message to WebCore to re-scroll when we get a + // new picture + mUserScroll = false; + mWebViewCore.sendMessage(EventHub.SYNC_SCROLL, + msg.arg1, msg.arg2); + } + break; + case SPAWN_SCROLL_TO_MSG_ID: + spawnContentScrollTo(msg.arg1, msg.arg2); + break; + case NEW_PICTURE_MSG_ID: + // called for new content + final WebViewCore.DrawData draw = + (WebViewCore.DrawData) msg.obj; + final Point viewSize = draw.mViewPoint; + if (mZoomScale > 0) { + // use the same logic in sendViewSizeZoom() to make sure + // the mZoomScale has matched the viewSize so that we + // can clear mZoomScale + if (Math.round(getViewWidth() / mZoomScale) == viewSize.x) { + mZoomScale = 0; + mWebViewCore.sendMessage(EventHub.SET_SNAP_ANCHOR, + 0, 0); + } + } + mMinContentWidth = msg.arg1; + if (mMinContentWidth > MAX_FLOAT_CONTENT_WIDTH) { + mMinZoomScale = (float) getViewWidth() + / draw.mWidthHeight.x; + } + // We update the layout (i.e. request a layout from the + // view system) if the last view size that we sent to + // WebCore matches the view size of the picture we just + // received in the fixed dimension. + final boolean updateLayout = viewSize.x == mLastWidthSent + && viewSize.y == mLastHeightSent; + recordNewContentSize(draw.mWidthHeight.x, + draw.mWidthHeight.y, updateLayout); + if (LOGV_ENABLED) { + Rect b = draw.mInvalRegion.getBounds(); + Log.v(LOGTAG, "NEW_PICTURE_MSG_ID {" + + b.left+","+b.top+","+b.right+","+b.bottom+"}"); + } + invalidate(contentToView(draw.mInvalRegion.getBounds())); + if (mPictureListener != null) { + mPictureListener.onNewPicture(WebView.this, capturePicture()); + } + break; + case WEBCORE_INITIALIZED_MSG_ID: + // nativeCreate sets mNativeClass to a non-zero value + nativeCreate(msg.arg1); + break; + case UPDATE_TEXTFIELD_TEXT_MSG_ID: + // Make sure that the textfield is currently focused + // and representing the same node as the pointer. + if (inEditingMode() && + mTextEntry.isSameTextField(msg.arg1)) { + if (msg.getData().getBoolean("password")) { + Spannable text = (Spannable) mTextEntry.getText(); + int start = Selection.getSelectionStart(text); + int end = Selection.getSelectionEnd(text); + mTextEntry.setInPassword(true); + // Restore the selection, which may have been + // ruined by setInPassword. + Spannable pword = (Spannable) mTextEntry.getText(); + Selection.setSelection(pword, start, end); + // If the text entry has created more events, ignore + // this one. + } else if (msg.arg2 == mTextGeneration) { + mTextEntry.setTextAndKeepSelection( + (String) msg.obj); + } + } + break; + case DID_FIRST_LAYOUT_MSG_ID: + if (mNativeClass == 0) { + break; + } +// Do not reset the focus or clear the text; the user may have already +// navigated or entered text at this point. The focus should have gotten +// reset, if need be, when the focus cache was built. Similarly, the text +// view should already be torn down and rebuilt if needed. +// nativeResetFocus(); +// clearTextEntry(); + HashMap scaleLimit = (HashMap) msg.obj; + int minScale = (Integer) scaleLimit.get("minScale"); + if (minScale == 0) { + mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + } else { + mMinZoomScale = (float) (minScale / 100.0); + } + int maxScale = (Integer) scaleLimit.get("maxScale"); + if (maxScale == 0) { + mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; + } else { + mMaxZoomScale = (float) (maxScale / 100.0); + } + // If history Picture is drawn, don't update zoomWidth + if (mDrawHistory) { + break; + } + int width = getViewWidth(); + if (width == 0) { + break; + } + int initialScale = msg.arg1; + int viewportWidth = msg.arg2; + // by default starting a new page with 100% zoom scale. + float scale = 1.0f; + if (mInitialScale > 0) { + scale = mInitialScale / 100.0f; + } else { + if (mWebViewCore.getSettings().getUseWideViewPort()) { + // force viewSizeChanged by setting mLastWidthSent + // to 0 + mLastWidthSent = 0; + } + if (initialScale == 0) { + // if viewportWidth is defined and it is smaller + // than the view width, zoom in to fill the view + if (viewportWidth > 0 && viewportWidth < width) { + scale = (float) width / viewportWidth; + } + } else { + scale = initialScale / 100.0f; + } + } + setNewZoomScale(scale, false); + break; + case MARK_NODE_INVALID_ID: + nativeMarkNodeInvalid(msg.arg1); + break; + case NOTIFY_FOCUS_SET_MSG_ID: + if (mNativeClass != 0) { + nativeNotifyFocusSet(inEditingMode()); + } + break; + case UPDATE_TEXT_ENTRY_MSG_ID: + // this is sent after finishing resize in WebViewCore. Make + // sure the text edit box is still on the screen. + boolean alreadyThere = inEditingMode(); + if (alreadyThere && nativeUpdateFocusNode()) { + FocusNode node = mFocusNode; + if (node.mIsTextField || node.mIsTextArea) { + mTextEntry.bringIntoView(); + } + } + updateTextEntry(); + break; + case RECOMPUTE_FOCUS_MSG_ID: + if (mNativeClass != 0) { + nativeRecomputeFocus(); + } + break; + case INVAL_RECT_MSG_ID: { + Rect r = (Rect)msg.obj; + if (r == null) { + invalidate(); + } else { + // we need to scale r from content into view coords, + // which viewInvalidate() does for us + viewInvalidate(r.left, r.top, r.right, r.bottom); + } + break; + } + case UPDATE_TEXT_ENTRY_ADAPTER: + HashMap data = (HashMap) msg.obj; + if (mTextEntry.isSameTextField(msg.arg1)) { + AutoCompleteAdapter adapter = + (AutoCompleteAdapter) data.get("adapter"); + mTextEntry.setAdapterCustom(adapter); + } + break; + case UPDATE_CLIPBOARD: + String str = (String) msg.obj; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "UPDATE_CLIPBOARD " + str); + } + try { + IClipboard clip = IClipboard.Stub.asInterface( + ServiceManager.getService("clipboard")); + clip.setClipboardText(str); + } catch (android.os.RemoteException e) { + Log.e(LOGTAG, "Clipboard failed", e); + } + break; + case RESUME_WEBCORE_UPDATE: + WebViewCore.resumeUpdate(mWebViewCore); + break; + + case LONG_PRESS_ENTER: + // as this is shared by keydown and trackballdown, reset all + // the states + mGotEnterDown = false; + mTrackballDown = false; + // LONG_PRESS_ENTER is sent as 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) { + performLongClick(); + } + break; + + case WEBCORE_NEED_TOUCH_EVENTS: + mForwardTouchEvents = (msg.arg1 != 0); + break; + + case PREVENT_TOUCH_ID: + if (msg.arg1 == MotionEvent.ACTION_DOWN) { + mPreventDrag = msg.arg2 == 1; + if (mPreventDrag) { + mTouchMode = TOUCH_DONE_MODE; + } + } + break; + + case DISMISS_ZOOM_RING_TUTORIAL: + mZoomRingController.finishZoomTutorial(); + break; + + default: + super.handleMessage(msg); + break; + } + } + } + + // Class used to use a dropdown for a <select> element + private class InvokeListBox implements Runnable { + // Strings for the labels in the listbox. + private String[] mArray; + // Array representing whether each item is enabled. + private boolean[] mEnableArray; + // Whether the listbox allows multiple selection. + private boolean mMultiple; + // Passed in to a list with multiple selection to tell + // which items are selected. + private int[] mSelectedArray; + // Passed in to a list with single selection to tell + // where the initial selection is. + private int mSelection; + + private Container[] mContainers; + + // Need these to provide stable ids to my ArrayAdapter, + // which normally does not have stable ids. (Bug 1250098) + private class Container extends Object { + String mString; + boolean mEnabled; + int mId; + + public String toString() { + return mString; + } + } + + /** + * Subclass ArrayAdapter so we can disable OptionGroupLabels, + * and allow filtering. + */ + private class MyArrayListAdapter extends ArrayAdapter<Container> { + public MyArrayListAdapter(Context context, Container[] objects, boolean multiple) { + super(context, + multiple ? com.android.internal.R.layout.select_dialog_multichoice : + com.android.internal.R.layout.select_dialog_singlechoice, + objects); + } + + @Override + public boolean hasStableIds() { + return true; + } + + private Container item(int position) { + if (position < 0 || position >= getCount()) { + return null; + } + return (Container) getItem(position); + } + + @Override + public long getItemId(int position) { + Container item = item(position); + if (item == null) { + return -1; + } + return item.mId; + } + + @Override + public boolean areAllItemsEnabled() { + return false; + } + + @Override + public boolean isEnabled(int position) { + Container item = item(position); + if (item == null) { + return false; + } + return item.mEnabled; + } + } + + private InvokeListBox(String[] array, + boolean[] enabled, int[] selected) { + mMultiple = true; + mSelectedArray = selected; + + int length = array.length; + mContainers = new Container[length]; + for (int i = 0; i < length; i++) { + mContainers[i] = new Container(); + mContainers[i].mString = array[i]; + mContainers[i].mEnabled = enabled[i]; + mContainers[i].mId = i; + } + } + + private InvokeListBox(String[] array, boolean[] enabled, int + selection) { + mSelection = selection; + mMultiple = false; + + int length = array.length; + mContainers = new Container[length]; + for (int i = 0; i < length; i++) { + mContainers[i] = new Container(); + mContainers[i].mString = array[i]; + mContainers[i].mEnabled = enabled[i]; + mContainers[i].mId = i; + } + } + + public void run() { + final ListView listView = (ListView) LayoutInflater.from(mContext) + .inflate(com.android.internal.R.layout.select_dialog, null); + final MyArrayListAdapter adapter = new + MyArrayListAdapter(mContext, mContainers, mMultiple); + AlertDialog.Builder b = new AlertDialog.Builder(mContext) + .setView(listView).setCancelable(true) + .setInverseBackgroundForced(true); + + if (mMultiple) { + b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mWebViewCore.sendMessage( + EventHub.LISTBOX_CHOICES, + adapter.getCount(), 0, + listView.getCheckedItemPositions()); + }}); + b.setNegativeButton(android.R.string.cancel, null); + } + final AlertDialog dialog = b.create(); + listView.setAdapter(adapter); + listView.setFocusableInTouchMode(true); + // There is a bug (1250103) where the checks in a ListView with + // multiple items selected are associated with the positions, not + // the ids, so the items do not properly retain their checks when + // filtered. Do not allow filtering on multiple lists until + // that bug is fixed. + + // Disable filter altogether + // listView.setTextFilterEnabled(!mMultiple); + if (mMultiple) { + listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + int length = mSelectedArray.length; + for (int i = 0; i < length; i++) { + listView.setItemChecked(mSelectedArray[i], true); + } + } else { + listView.setOnItemClickListener(new OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, + int position, long id) { + mWebViewCore.sendMessage( + EventHub.SINGLE_LISTBOX_CHOICE, (int)id, 0); + dialog.dismiss(); + } + }); + if (mSelection != -1) { + listView.setSelection(mSelection); + listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); + listView.setItemChecked(mSelection, true); + } + } + dialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + public void onCancel(DialogInterface dialog) { + mWebViewCore.sendMessage( + EventHub.SINGLE_LISTBOX_CHOICE, -2, 0); + } + }); + dialog.show(); + } + } + + /* + * Request a dropdown menu for a listbox with multiple selection. + * + * @param array Labels for the listbox. + * @param enabledArray Which positions are enabled. + * @param selectedArray Which positions are initally selected. + */ + void requestListBox(String[] array, boolean[]enabledArray, int[] + selectedArray) { + mPrivateHandler.post( + new InvokeListBox(array, enabledArray, selectedArray)); + } + + /* + * Request a dropdown menu for a listbox with single selection or a single + * <select> element. + * + * @param array Labels for the listbox. + * @param enabledArray Which positions are enabled. + * @param selection Which position is initally selected. + */ + void requestListBox(String[] array, boolean[]enabledArray, int selection) { + mPrivateHandler.post( + new InvokeListBox(array, enabledArray, selection)); + } + + // called by JNI + private void sendFinalFocus(int frame, int node, int x, int y) { + WebViewCore.FocusData focusData = new WebViewCore.FocusData(); + focusData.mFrame = frame; + focusData.mNode = node; + focusData.mX = x; + focusData.mY = y; + mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, + EventHub.NO_FOCUS_CHANGE_BLOCK, 0, focusData); + } + + // called by JNI + private void setFocusData(int moveGeneration, int buildGeneration, + int frame, int node, int x, int y, boolean ignoreNullFocus) { + mFocusData.mMoveGeneration = moveGeneration; + mFocusData.mBuildGeneration = buildGeneration; + mFocusData.mFrame = frame; + mFocusData.mNode = node; + mFocusData.mX = x; + mFocusData.mY = y; + mFocusData.mIgnoreNullFocus = ignoreNullFocus; + } + + // called by JNI + private void sendKitFocus() { + WebViewCore.FocusData focusData = new WebViewCore.FocusData(mFocusData); + mWebViewCore.sendMessage(EventHub.SET_KIT_FOCUS, focusData); + } + + // called by JNI + private void sendMotionUp(int touchGeneration, int buildGeneration, + int frame, int node, int x, int y, int size, boolean isClick, + boolean retry) { + WebViewCore.TouchUpData touchUpData = new WebViewCore.TouchUpData(); + touchUpData.mMoveGeneration = touchGeneration; + touchUpData.mBuildGeneration = buildGeneration; + touchUpData.mSize = size; + touchUpData.mIsClick = isClick; + touchUpData.mRetry = retry; + mFocusData.mFrame = touchUpData.mFrame = frame; + mFocusData.mNode = touchUpData.mNode = node; + mFocusData.mX = touchUpData.mX = x; + mFocusData.mY = touchUpData.mY = y; + mWebViewCore.sendMessage(EventHub.TOUCH_UP, touchUpData); + } + + + private int getScaledMaxXScroll() { + int width; + if (mHeightCanMeasure == false) { + width = getViewWidth() / 4; + } else { + Rect visRect = new Rect(); + calcOurVisibleRect(visRect); + width = visRect.width() / 2; + } + // FIXME the divisor should be retrieved from somewhere + return viewToContent(width); + } + + private int getScaledMaxYScroll() { + int height; + if (mHeightCanMeasure == false) { + height = getViewHeight() / 4; + } else { + Rect visRect = new Rect(); + calcOurVisibleRect(visRect); + height = visRect.height() / 2; + } + // FIXME the divisor should be retrieved from somewhere + // the closest thing today is hard-coded into ScrollView.java + // (from ScrollView.java, line 363) int maxJump = height/2; + return viewToContent(height); + } + + /** + * Called by JNI to invalidate view + */ + private void viewInvalidate() { + invalidate(); + } + + // return true if the key was handled + private boolean navHandledKey(int keyCode, int count, boolean noScroll + , long time) { + if (mNativeClass == 0) { + return false; + } + mLastFocusTime = time; + mLastFocusBounds = nativeGetFocusRingBounds(); + boolean keyHandled = nativeMoveFocus(keyCode, count, noScroll) == false; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "navHandledKey mLastFocusBounds=" + mLastFocusBounds + + " mLastFocusTime=" + mLastFocusTime + + " handled=" + keyHandled); + } + if (keyHandled == false || mHeightCanMeasure == false) { + return keyHandled; + } + Rect contentFocus = nativeGetFocusRingBounds(); + if (contentFocus.isEmpty()) return keyHandled; + Rect viewFocus = contentToView(contentFocus); + Rect visRect = new Rect(); + calcOurVisibleRect(visRect); + Rect outset = new Rect(visRect); + int maxXScroll = visRect.width() / 2; + int maxYScroll = visRect.height() / 2; + outset.inset(-maxXScroll, -maxYScroll); + if (Rect.intersects(outset, viewFocus) == false) { + return keyHandled; + } + // FIXME: Necessary because ScrollView/ListView do not scroll left/right + int maxH = Math.min(viewFocus.right - visRect.right, maxXScroll); + if (maxH > 0) { + pinScrollBy(maxH, 0, true, 0); + } else { + maxH = Math.max(viewFocus.left - visRect.left, -maxXScroll); + if (maxH < 0) { + pinScrollBy(maxH, 0, true, 0); + } + } + if (mLastFocusBounds.isEmpty()) return keyHandled; + if (mLastFocusBounds.equals(contentFocus)) return keyHandled; + if (LOGV_ENABLED) { + Log.v(LOGTAG, "navHandledKey contentFocus=" + contentFocus); + } + requestRectangleOnScreen(viewFocus); + mUserScroll = true; + return keyHandled; + } + + /** + * Set the background color. It's white by default. Pass + * zero to make the view transparent. + * @param color the ARGB color described by Color.java + */ + public void setBackgroundColor(int color) { + mBackgroundColor = color; + mWebViewCore.sendMessage(EventHub.SET_BACKGROUND_COLOR, color); + } + + public void debugDump() { + nativeDebugDump(); + mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); + } + + /** + * Update our cache with updatedText. + * @param updatedText The new text to put in our cache. + */ + /* package */ void updateCachedTextfield(String updatedText) { + // Also place our generation number so that when we look at the cache + // we recognize that it is up to date. + nativeUpdateCachedTextfield(updatedText, mTextGeneration); + } + + // Never call this version except by updateCachedTextfield(String) - + // we always want to pass in our generation number. + private native void nativeUpdateCachedTextfield(String updatedText, + int generation); + private native void nativeClearFocus(int x, int y); + private native void nativeCreate(int ptr); + private native void nativeDebugDump(); + private native void nativeDestroy(); + private native void nativeDrawFocusRing(Canvas content); + private native void nativeDrawSelection(Canvas content + , int x, int y, boolean extendSelection); + private native void nativeDrawSelectionRegion(Canvas content); + private native boolean nativeUpdateFocusNode(); + private native Rect nativeGetFocusRingBounds(); + private native Rect nativeGetNavBounds(); + private native void nativeInstrumentReport(); + private native void nativeMarkNodeInvalid(int node); + // return true if the page has been scrolled + private native boolean nativeMotionUp(int x, int y, int slop, boolean isClick); + // returns false if it handled the key + private native boolean nativeMoveFocus(int keyCode, int count, + boolean noScroll); + private native void nativeNotifyFocusSet(boolean inEditingMode); + private native void nativeRecomputeFocus(); + // Like many other of our native methods, you must make sure that + // mNativeClass is not null before calling this method. + private native void nativeRecordButtons(boolean focused, + boolean pressed, boolean invalidate); + private native void nativeResetFocus(); + private native void nativeResetNavClipBounds(); + private native void nativeSelectBestAt(Rect rect); + private native void nativeSetFindIsDown(); + private native void nativeSetFollowedLink(boolean followed); + private native void nativeSetHeightCanMeasure(boolean measure); + private native void nativeSetNavBounds(Rect rect); + private native void nativeSetNavClipBounds(Rect rect); + private native String nativeImageURI(int x, int y); + /** + * Returns true if the native focus nodes says it wants to handle key events + * (ala plugins). This can only be called if mNativeClass is non-zero! + */ + private native boolean nativeFocusNodeWantsKeyEvents(); + private native void nativeMoveSelection(int x, int y + , boolean extendSelection); + private native Region nativeGetSelection(); + + private native void nativeDumpDisplayTree(String urlOrNull); +} diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java new file mode 100644 index 0000000..a185779 --- /dev/null +++ b/core/java/android/webkit/WebViewClient.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2008 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.graphics.Bitmap; +import android.net.http.SslError; +import android.os.Message; +import android.view.KeyEvent; + +public class WebViewClient { + + /** + * Give the host application a chance to take over the control when a new + * url is about to be loaded in the current WebView. If WebViewClient is not + * provided, by default WebView will ask Activity Manager to choose the + * proper handler for the url. If WebViewClient is provided, return true + * means the host application handles the url, while return false means the + * current WebView handles the url. + * + * @param view The WebView that is initiating the callback. + * @param url The url to be loaded. + * @return True if the host application wants to leave the current WebView + * and handle the url itself, otherwise return false. + */ + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return false; + } + + /** + * Notify the host application that a page has started loading. This method + * is called once for each main frame load so a page with iframes or + * framesets will call onPageStarted one time for the main frame. This also + * means that onPageStarted will not be called when the contents of an + * embedded frame changes, i.e. clicking a link whose target is an iframe. + * + * @param view The WebView that is initiating the callback. + * @param url The url to be loaded. + * @param favicon The favicon for this page if it already exists in the + * database. + */ + public void onPageStarted(WebView view, String url, Bitmap favicon) { + } + + /** + * Notify the host application that a page has finished loading. This method + * is called only for main frame. When onPageFinished() is called, the + * rendering picture may not be updated yet. To get the notification for the + * new Picture, use {@link WebView.PictureListener#onNewPicture}. + * + * @param view The WebView that is initiating the callback. + * @param url The url of the page. + */ + public void onPageFinished(WebView view, String url) { + } + + /** + * Notify the host application that the WebView will load the resource + * specified by the given url. + * + * @param view The WebView that is initiating the callback. + * @param url The url of the resource the WebView will load. + */ + public void onLoadResource(WebView view, String url) { + } + + /** + * Notify the host application that there have been an excessive number of + * HTTP redirects. As the host application if it would like to continue + * trying to load the resource. The default behavior is to send the cancel + * message. + * + * @param view The WebView that is initiating the callback. + * @param cancelMsg The message to send if the host wants to cancel + * @param continueMsg The message to send if the host wants to continue + */ + public void onTooManyRedirects(WebView view, Message cancelMsg, + Message continueMsg) { + cancelMsg.sendToTarget(); + } + + /** + * Report an error to an activity. These errors come up from WebCore, and + * are network errors. + * + * @param view The WebView that is initiating the callback. + * @param errorCode The HTTP error code. + * @param description A String description. + * @param failingUrl The url that failed. + */ + public void onReceivedError(WebView view, int errorCode, + String description, String failingUrl) { + } + + /** + * As the host application if the browser should resend data as the + * requested page was a result of a POST. The default is to not resend the + * data. + * + * @param view The WebView that is initiating the callback. + * @param dontResend The message to send if the browser should not resend + * @param resend The message to send if the browser should resend data + */ + public void onFormResubmission(WebView view, Message dontResend, + Message resend) { + dontResend.sendToTarget(); + } + + /** + * Notify the host application to update its visited links database. + * + * @param view The WebView that is initiating the callback. + * @param url The url being visited. + * @param isReload True if this url is being reloaded. + */ + public void doUpdateVisitedHistory(WebView view, String url, + boolean isReload) { + } + + /** + * Notify the host application to handle a ssl certificate error request + * (display the error to the user and ask whether to proceed or not). The + * host application has to call either handler.cancel() or handler.proceed() + * as the connection is suspended and waiting for the response. The default + * behavior is to cancel the load. + * + * @param view The WebView that is initiating the callback. + * @param handler An SslErrorHandler object that will handle the user's + * response. + * @param error The SSL error object. + * @hide - hide this because it contains a parameter of type SslError, + * which is located in a hidden package. + */ + public void onReceivedSslError(WebView view, SslErrorHandler handler, + SslError error) { + handler.cancel(); + } + + /** + * Notify the host application to handle an authentication request. The + * default behavior is to cancel the request. + * + * @param view The WebView that is initiating the callback. + * @param handler The HttpAuthHandler that will handle the user's response. + * @param host The host requiring authentication. + * @param realm A description to help store user credentials for future + * visits. + */ + public void onReceivedHttpAuthRequest(WebView view, + HttpAuthHandler handler, String host, String realm) { + handler.cancel(); + } + + /** + * Give the host application a chance to handle the key event synchronously. + * e.g. menu shortcut key events need to be filtered this way. If return + * true, WebView will not handle the key event. If return false, WebView + * will always handle the key event, so none of the super in the view chain + * will see the key event. The default behavior returns false. + * + * @param view The WebView that is initiating the callback. + * @param event The key event. + * @return True if the host application wants to handle the key event + * itself, otherwise return false + */ + public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { + return false; + } + + /** + * Notify the host application that a key was not handled by the WebView. + * Except system keys, WebView always consumes the keys in the normal flow + * or if shouldOverrideKeyEvent returns true. This is called asynchronously + * from where the key is dispatched. It gives the host application an chance + * to handle the unhandled key events. + * + * @param view The WebView that is initiating the callback. + * @param event The key event. + */ + public void onUnhandledKeyEvent(WebView view, KeyEvent event) { + } + + /** + * Notify the host application that the scale applied to the WebView has + * changed. + * + * @param view he WebView that is initiating the callback. + * @param oldScale The old scale factor + * @param newScale The new scale factor + */ + public void onScaleChanged(WebView view, float oldScale, float newScale) { + } +} diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java new file mode 100644 index 0000000..6ab088d --- /dev/null +++ b/core/java/android/webkit/WebViewCore.java @@ -0,0 +1,1674 @@ +/* + * Copyright (C) 2007 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.content.Context; +import android.graphics.Canvas; +import android.graphics.DrawFilter; +import android.graphics.Paint; +import android.graphics.PaintFlagsDrawFilter; +import android.graphics.Picture; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.Region; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Process; +import android.util.Config; +import android.util.Log; +import android.util.SparseBooleanArray; +import android.view.KeyEvent; + +import java.util.ArrayList; +import java.util.HashMap; + +import junit.framework.Assert; + +final class WebViewCore { + + private static final String LOGTAG = "webcore"; + static final boolean DEBUG = false; + static final boolean LOGV_ENABLED = DEBUG ? Config.LOGD : Config.LOGV; + + static { + // Load libwebcore during static initialization. This happens in the + // zygote process so it will be shared read-only across all app + // processes. + System.loadLibrary("webcore"); + } + + /* + * WebViewCore always executes in the same thread as the native webkit. + */ + + // The WebView that corresponds to this WebViewCore. + private WebView mWebView; + // Proxy for handling callbacks from native code + private final CallbackProxy mCallbackProxy; + // Settings object for maintaining all settings + private final WebSettings mSettings; + // Context for initializing the BrowserFrame with the proper assets. + private final Context mContext; + // The pointer to a native view object. + private int mNativeClass; + // The BrowserFrame is an interface to the native Frame component. + private BrowserFrame mBrowserFrame; + + /* + * range is from 200 to 10,000. 0 is a special value means device-width. -1 + * means undefined. + */ + private int mViewportWidth = -1; + + /* + * range is from 200 to 10,000. 0 is a special value means device-height. -1 + * means undefined. + */ + private int mViewportHeight = -1; + + /* + * scale in percent, range is from 1 to 1000. 0 means undefined. + */ + private int mViewportInitialScale = 0; + + /* + * scale in percent, range is from 1 to 1000. 0 means undefined. + */ + private int mViewportMinimumScale = 0; + + /* + * scale in percent, range is from 1 to 1000. 0 means undefined. + */ + private int mViewportMaximumScale = 0; + + private boolean mViewportUserScalable = true; + + private int mRestoredScale = 100; + private int mRestoredX = 0; + private int mRestoredY = 0; + + private int mWebkitScrollX = 0; + private int mWebkitScrollY = 0; + + // The thread name used to identify the WebCore thread and for use in + // debugging other classes that require operation within the WebCore thread. + /* package */ static final String THREAD_NAME = "WebViewCoreThread"; + + public WebViewCore(Context context, WebView w, CallbackProxy proxy) { + // No need to assign this in the WebCore thread. + mCallbackProxy = proxy; + mWebView = w; + // This context object is used to initialize the WebViewCore during + // subwindow creation. + mContext = context; + + // We need to wait for the initial thread creation before sending + // a message to the WebCore thread. + // XXX: This is the only time the UI thread will wait for the WebCore + // thread! + synchronized (WebViewCore.class) { + if (sWebCoreHandler == null) { + // Create a global thread and start it. + Thread t = new Thread(new WebCoreThread()); + t.setName(THREAD_NAME); + t.start(); + try { + WebViewCore.class.wait(); + } catch (InterruptedException e) { + Log.e(LOGTAG, "Caught exception while waiting for thread " + + "creation."); + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + } + } + // Create an EventHub to handle messages before and after the thread is + // ready. + mEventHub = new EventHub(); + // Create a WebSettings object for maintaining all settings + mSettings = new WebSettings(mContext); + // The WebIconDatabase needs to be initialized within the UI thread so + // just request the instance here. + WebIconDatabase.getInstance(); + // Send a message to initialize the WebViewCore. + Message init = sWebCoreHandler.obtainMessage( + WebCoreThread.INITIALIZE, this); + sWebCoreHandler.sendMessage(init); + } + + /* Initialize private data within the WebCore thread. + */ + private void initialize() { + /* Initialize our private BrowserFrame class to handle all + * frame-related functions. We need to create a new view which + * in turn creates a C level FrameView and attaches it to the frame. + */ + mBrowserFrame = new BrowserFrame(mContext, this, mCallbackProxy, + mSettings); + // Sync the native settings and also create the WebCore thread handler. + mSettings.syncSettingsAndCreateHandler(mBrowserFrame); + // Create the handler and transfer messages for the IconDatabase + WebIconDatabase.getInstance().createHandler(); + // The transferMessages call will transfer all pending messages to the + // WebCore thread handler. + mEventHub.transferMessages(); + + // Send a message back to WebView to tell it that we have set up the + // WebCore thread. + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.WEBCORE_INITIALIZED_MSG_ID, + mNativeClass, 0).sendToTarget(); + } + + } + + /* Handle the initialization of WebViewCore during subwindow creation. This + * method is called from the WebCore thread but it is called before the + * INITIALIZE message can be handled. + */ + /* package */ void initializeSubwindow() { + // Go ahead and initialize the core components. + initialize(); + // Remove the INITIALIZE method so we don't try to initialize twice. + sWebCoreHandler.removeMessages(WebCoreThread.INITIALIZE, this); + } + + /* Get the BrowserFrame component. This is used for subwindow creation and + * is called only from BrowserFrame in the WebCore thread. */ + /* package */ BrowserFrame getBrowserFrame() { + return mBrowserFrame; + } + + //------------------------------------------------------------------------- + // Common methods + //------------------------------------------------------------------------- + + /** + * Causes all timers to pause. This applies to all WebViews in the current + * app process. + */ + public static void pauseTimers() { + if (BrowserFrame.sJavaBridge == null) { + throw new IllegalStateException( + "No WebView has been created in this process!"); + } + BrowserFrame.sJavaBridge.pause(); + } + + /** + * Resume all timers. This applies to all WebViews in the current process. + */ + public static void resumeTimers() { + if (BrowserFrame.sJavaBridge == null) { + throw new IllegalStateException( + "No WebView has been created in this process!"); + } + BrowserFrame.sJavaBridge.resume(); + } + + public WebSettings getSettings() { + return mSettings; + } + + /** + * Invoke a javascript alert. + * @param message The message displayed in the alert. + */ + protected void jsAlert(String url, String message) { + mCallbackProxy.onJsAlert(url, message); + } + + /** + * Invoke a javascript confirm dialog. + * @param message The message displayed in the dialog. + * @return True if the user confirmed or false if the user cancelled. + */ + protected boolean jsConfirm(String url, String message) { + return mCallbackProxy.onJsConfirm(url, message); + } + + /** + * Invoke a javascript prompt dialog. + * @param message The message to be displayed in the dialog. + * @param defaultValue The default value in the prompt input. + * @return The input from the user or null to indicate the user cancelled + * the dialog. + */ + protected String jsPrompt(String url, String message, String defaultValue) { + return mCallbackProxy.onJsPrompt(url, message, defaultValue); + } + + /** + * Invoke a javascript before unload dialog. + * @param url The url that is requesting the dialog. + * @param message The message displayed in the dialog. + * @return True if the user confirmed or false if the user cancelled. False + * will cancel the navigation. + */ + protected boolean jsUnload(String url, String message) { + return mCallbackProxy.onJsBeforeUnload(url, message); + } + + //------------------------------------------------------------------------- + // JNI methods + //------------------------------------------------------------------------- + + static native String nativeFindAddress(String addr); + + /** + * Rebuild the nav cache if the dom changed. + */ + private native void nativeCheckNavCache(); + + /** + * Empty the picture set. + */ + private native void nativeClearContent(); + + /** + * Create a flat picture from the set of pictures. + */ + private native void nativeCopyContentToPicture(Picture picture); + + /** + * Draw the picture set with a background color. Returns true + * if some individual picture took too long to draw and can be + * split into parts. Called from the UI thread. + */ + private native boolean nativeDrawContent(Canvas canvas, int color); + + /** + * Redraw a portion of the picture set. The Point wh returns the + * width and height of the overall picture. + */ + private native boolean nativeRecordContent(Region invalRegion, Point wh); + + /** + * Splits slow parts of the picture set. Called from the webkit + * thread after nativeDrawContent returns true. + */ + private native void nativeSplitContent(); + + private native boolean nativeKey(int keyCode, int unichar, + int repeatCount, boolean isShift, boolean isAlt, boolean isDown); + + private native boolean nativeClick(); + + private native void nativeSendListBoxChoices(boolean[] choices, int size); + + private native void nativeSendListBoxChoice(int choice); + + /* Tell webkit what its width and height are, for the purposes + of layout/line-breaking. These coordinates are in document space, + which is the same as View coords unless we have zoomed the document + (see nativeSetZoom). + screenWidth is used by layout to wrap column around. If viewport uses + fixed size, screenWidth can be different from width with zooming. + should this be called nativeSetViewPortSize? + */ + private native void nativeSetSize(int width, int height, int screenWidth, + float scale, int realScreenWidth, int screenHeight); + + private native int nativeGetContentMinPrefWidth(); + + // Start: functions that deal with text editing + private native void nativeReplaceTextfieldText(int frame, int node, int x, + int y, int oldStart, int oldEnd, String replace, int newStart, + int newEnd); + + private native void passToJs(int frame, int node, int x, int y, int gen, + String currentText, int keyCode, int keyValue, boolean down, + boolean cap, boolean fn, boolean sym); + + private native void nativeSaveDocumentState(int frame); + + private native void nativeSetFinalFocus(int framePtr, int nodePtr, int x, + int y, boolean block); + + private native void nativeSetKitFocus(int moveGeneration, + int buildGeneration, int framePtr, int nodePtr, int x, int y, + boolean ignoreNullFocus); + + private native String nativeRetrieveHref(int framePtr, int nodePtr); + + private native void nativeTouchUp(int touchGeneration, + int buildGeneration, int framePtr, int nodePtr, int x, int y, + int size, boolean isClick, boolean retry); + + private native boolean nativeHandleTouchEvent(int action, int x, int y); + + private native void nativeUnblockFocus(); + + private native void nativeUpdateFrameCache(); + + private native void nativeSetSnapAnchor(int x, int y); + + private native void nativeSnapToAnchor(); + + private native void nativeSetBackgroundColor(int color); + + private native void nativeDumpDomTree(boolean useFile); + + private native void nativeDumpRenderTree(boolean useFile); + + private native void nativeDumpNavTree(); + + private native void nativeRefreshPlugins(boolean reloadOpenPages); + + /** + * 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. + */ + private native void nativeDeleteSelection(int frame, int node, int x, int y, + int start, int end); + + /** + * 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. + */ + private native void nativeSetSelection(int frame, int node, int x, int y, + int start, int end); + + private native String nativeGetSelection(Region sel); + + // Register a scheme to be treated as local scheme so that it can access + // local asset files for resources + private native void nativeRegisterURLSchemeAsLocal(String scheme); + + // EventHub for processing messages + private final EventHub mEventHub; + // WebCore thread handler + private static Handler sWebCoreHandler; + // Class for providing Handler creation inside the WebCore thread. + private static class WebCoreThread implements Runnable { + // Message id for initializing a new WebViewCore. + private static final int INITIALIZE = 0; + private static final int REDUCE_PRIORITY = 1; + private static final int RESUME_PRIORITY = 2; + private static final int CACHE_TICKER = 3; + private static final int BLOCK_CACHE_TICKER = 4; + private static final int RESUME_CACHE_TICKER = 5; + + private static final int CACHE_TICKER_INTERVAL = 60 * 1000; // 1 minute + + private static boolean mCacheTickersBlocked = true; + + public void run() { + Looper.prepare(); + Assert.assertNull(sWebCoreHandler); + synchronized (WebViewCore.class) { + sWebCoreHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case INITIALIZE: + WebViewCore core = (WebViewCore) msg.obj; + core.initialize(); + break; + + case REDUCE_PRIORITY: + // 3 is an adjustable number. + Process.setThreadPriority( + Process.THREAD_PRIORITY_DEFAULT + 3 * + Process.THREAD_PRIORITY_LESS_FAVORABLE); + break; + + case RESUME_PRIORITY: + Process.setThreadPriority( + Process.THREAD_PRIORITY_DEFAULT); + break; + + case CACHE_TICKER: + if (!mCacheTickersBlocked) { + CacheManager.endCacheTransaction(); + CacheManager.startCacheTransaction(); + sendMessageDelayed( + obtainMessage(CACHE_TICKER), + CACHE_TICKER_INTERVAL); + } + break; + + case BLOCK_CACHE_TICKER: + if (CacheManager.endCacheTransaction()) { + mCacheTickersBlocked = true; + } + break; + + case RESUME_CACHE_TICKER: + if (CacheManager.startCacheTransaction()) { + mCacheTickersBlocked = false; + } + break; + } + } + }; + WebViewCore.class.notify(); + } + Looper.loop(); + } + } + + static class FocusData { + FocusData() {} + FocusData(FocusData d) { + mMoveGeneration = d.mMoveGeneration; + mBuildGeneration = d.mBuildGeneration; + mFrame = d.mFrame; + mNode = d.mNode; + mX = d.mX; + mY = d.mY; + mIgnoreNullFocus = d.mIgnoreNullFocus; + } + int mMoveGeneration; + int mBuildGeneration; + int mFrame; + int mNode; + int mX; + int mY; + boolean mIgnoreNullFocus; + } + + static class TouchUpData { + int mMoveGeneration; + int mBuildGeneration; + int mFrame; + int mNode; + int mX; + int mY; + int mSize; + boolean mIsClick; + boolean mRetry; + } + + static class TouchEventData { + int mAction; // MotionEvent.getAction() + int mX; + int mY; + } + + static final String[] HandlerDebugString = { + "LOAD_URL", // = 100; + "STOP_LOADING", // = 101; + "RELOAD", // = 102; + "KEY_DOWN", // = 103; + "KEY_UP", // = 104; + "VIEW_SIZE_CHANGED", // = 105; + "GO_BACK_FORWARD", // = 106; + "SET_SCROLL_OFFSET", // = 107; + "RESTORE_STATE", // = 108; + "PAUSE_TIMERS", // = 109; + "RESUME_TIMERS", // = 110; + "CLEAR_CACHE", // = 111; + "CLEAR_HISTORY", // = 112; + "SET_SELECTION", // = 113; + "REPLACE_TEXT", // = 114; + "PASS_TO_JS", // = 115; + "SET_GLOBAL_BOUNDS", // = 116; + "UPDATE_CACHE_AND_TEXT_ENTRY", // = 117; + "CLICK", // = 118; + "119", + "DOC_HAS_IMAGES", // = 120; + "SET_SNAP_ANCHOR", // = 121; + "DELETE_SELECTION", // = 122; + "LISTBOX_CHOICES", // = 123; + "SINGLE_LISTBOX_CHOICE", // = 124; + "125", + "SET_BACKGROUND_COLOR", // = 126; + "UNBLOCK_FOCUS", // = 127; + "SAVE_DOCUMENT_STATE", // = 128; + "GET_SELECTION", // = 129; + "WEBKIT_DRAW", // = 130; + "SYNC_SCROLL", // = 131; + "REFRESH_PLUGINS", // = 132; + "SPLIT_PICTURE_SET", // = 133; + "CLEAR_CONTENT", // = 134; + "SET_FINAL_FOCUS", // = 135; + "SET_KIT_FOCUS", // = 136; + "REQUEST_FOCUS_HREF", // = 137; + "ADD_JS_INTERFACE", // = 138; + "LOAD_DATA", // = 139; + "TOUCH_UP", // = 140; + "TOUCH_EVENT", // = 141; + }; + + class EventHub { + // Message Ids + static final int LOAD_URL = 100; + static final int STOP_LOADING = 101; + static final int RELOAD = 102; + static final int KEY_DOWN = 103; + static final int KEY_UP = 104; + static final int VIEW_SIZE_CHANGED = 105; + static final int GO_BACK_FORWARD = 106; + static final int SET_SCROLL_OFFSET = 107; + static final int RESTORE_STATE = 108; + static final int PAUSE_TIMERS = 109; + static final int RESUME_TIMERS = 110; + static final int CLEAR_CACHE = 111; + static final int CLEAR_HISTORY = 112; + static final int SET_SELECTION = 113; + static final int REPLACE_TEXT = 114; + static final int PASS_TO_JS = 115; + static final int SET_GLOBAL_BOUNDS = 116; + static final int UPDATE_CACHE_AND_TEXT_ENTRY = 117; + static final int CLICK = 118; + static final int DOC_HAS_IMAGES = 120; + static final int SET_SNAP_ANCHOR = 121; + static final int DELETE_SELECTION = 122; + static final int LISTBOX_CHOICES = 123; + static final int SINGLE_LISTBOX_CHOICE = 124; + static final int SET_BACKGROUND_COLOR = 126; + static final int UNBLOCK_FOCUS = 127; + static final int SAVE_DOCUMENT_STATE = 128; + static final int GET_SELECTION = 129; + static final int WEBKIT_DRAW = 130; + static final int SYNC_SCROLL = 131; + static final int REFRESH_PLUGINS = 132; + static final int SPLIT_PICTURE_SET = 133; + static final int CLEAR_CONTENT = 134; + + // UI nav messages + static final int SET_FINAL_FOCUS = 135; + static final int SET_KIT_FOCUS = 136; + static final int REQUEST_FOCUS_HREF = 137; + static final int ADD_JS_INTERFACE = 138; + static final int LOAD_DATA = 139; + + // motion + static final int TOUCH_UP = 140; + // message used to pass UI touch events to WebCore + static final int TOUCH_EVENT = 141; + + // Network-based messaging + static final int CLEAR_SSL_PREF_TABLE = 150; + + // Test harness messages + static final int REQUEST_EXT_REPRESENTATION = 160; + static final int REQUEST_DOC_AS_TEXT = 161; + + // debugging + static final int DUMP_DOMTREE = 170; + static final int DUMP_RENDERTREE = 171; + static final int DUMP_NAVTREE = 172; + + // private message ids + private static final int DESTROY = 200; + + // flag values passed to message SET_FINAL_FOCUS + static final int NO_FOCUS_CHANGE_BLOCK = 0; + static final int BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP = 1; + + // Private handler for WebCore messages. + private Handler mHandler; + // Message queue for containing messages before the WebCore thread is + // ready. + private ArrayList<Message> mMessages = new ArrayList<Message>(); + // Flag for blocking messages. This is used during DESTROY to avoid + // posting more messages to the EventHub or to WebView's event handler. + private boolean mBlockMessages; + + private int mTid; + private int mSavedPriority; + + /** + * Prevent other classes from creating an EventHub. + */ + private EventHub() {} + + /** + * Transfer all messages to the newly created webcore thread handler. + */ + private void transferMessages() { + mTid = Process.myTid(); + mSavedPriority = Process.getThreadPriority(mTid); + + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, msg.what < LOAD_URL || msg.what + > TOUCH_EVENT ? Integer.toString(msg.what) + : HandlerDebugString[msg.what - LOAD_URL]); + } + switch (msg.what) { + case WEBKIT_DRAW: + webkitDraw(); + break; + + case DESTROY: + // Time to take down the world. Cancel all pending + // loads and destroy the native view and frame. + mBrowserFrame.destroy(); + mBrowserFrame = null; + mNativeClass = 0; + break; + + case LOAD_URL: + loadUrl((String) msg.obj); + break; + + case LOAD_DATA: + HashMap loadParams = (HashMap) msg.obj; + String baseUrl = (String) loadParams.get("baseUrl"); + if (baseUrl != null) { + int i = baseUrl.indexOf(':'); + if (i > 0) { + /* + * In 1.0, {@link + * WebView#loadDataWithBaseURL} can access + * local asset files as long as the data is + * valid. In the new WebKit, the restriction + * is tightened. To be compatible with 1.0, + * we automatically add the scheme of the + * baseUrl for local access as long as it is + * not http(s)/ftp(s)/about/javascript + */ + String scheme = baseUrl.substring(0, i); + if (!scheme.startsWith("http") && + !scheme.startsWith("ftp") && + !scheme.startsWith("about") && + !scheme.startsWith("javascript")) { + nativeRegisterURLSchemeAsLocal(scheme); + } + } + } + mBrowserFrame.loadData(baseUrl, + (String) loadParams.get("data"), + (String) loadParams.get("mimeType"), + (String) loadParams.get("encoding"), + (String) loadParams.get("failUrl")); + break; + + case STOP_LOADING: + // If the WebCore has committed the load, but not + // finished the first layout yet, we need to set + // first layout done to trigger the interpreted side sync + // up with native side + if (mBrowserFrame.committed() + && !mBrowserFrame.firstLayoutDone()) { + mBrowserFrame.didFirstLayout(); + } + // Do this after syncing up the layout state. + stopLoading(); + break; + + case RELOAD: + mBrowserFrame.reload(false); + break; + + case KEY_DOWN: + key((KeyEvent) msg.obj, true); + break; + + case KEY_UP: + key((KeyEvent) msg.obj, false); + break; + + case CLICK: + nativeClick(); + break; + + case VIEW_SIZE_CHANGED: + viewSizeChanged(msg.arg1, msg.arg2, + ((Integer) msg.obj).intValue()); + break; + + case SET_SCROLL_OFFSET: + // note: these are in document coordinates + // (inv-zoom) + nativeSetScrollOffset(msg.arg1, msg.arg2); + break; + + case SET_GLOBAL_BOUNDS: + Rect r = (Rect) msg.obj; + nativeSetGlobalBounds(r.left, r.top, r.width(), + r.height()); + break; + + case GO_BACK_FORWARD: + // If it is a standard load and the load is not + // committed yet, we interpret BACK as RELOAD + if (!mBrowserFrame.committed() && msg.arg1 == -1 && + (mBrowserFrame.loadType() == + BrowserFrame.FRAME_LOADTYPE_STANDARD)) { + mBrowserFrame.reload(true); + } else { + mBrowserFrame.goBackOrForward(msg.arg1); + } + break; + + case RESTORE_STATE: + stopLoading(); + restoreState(msg.arg1); + break; + + case PAUSE_TIMERS: + mSavedPriority = Process.getThreadPriority(mTid); + Process.setThreadPriority(mTid, + Process.THREAD_PRIORITY_BACKGROUND); + pauseTimers(); + if (CacheManager.disableTransaction()) { + WebCoreThread.mCacheTickersBlocked = true; + sWebCoreHandler.removeMessages( + WebCoreThread.CACHE_TICKER); + } + break; + + case RESUME_TIMERS: + Process.setThreadPriority(mTid, mSavedPriority); + resumeTimers(); + if (CacheManager.enableTransaction()) { + WebCoreThread.mCacheTickersBlocked = false; + sWebCoreHandler.sendMessageDelayed( + sWebCoreHandler.obtainMessage( + WebCoreThread.CACHE_TICKER), + WebCoreThread.CACHE_TICKER_INTERVAL); + } + break; + + case CLEAR_CACHE: + mBrowserFrame.clearCache(); + if (msg.arg1 == 1) { + CacheManager.removeAllCacheFiles(); + } + break; + + case CLEAR_HISTORY: + mCallbackProxy.getBackForwardList(). + close(mBrowserFrame.mNativeFrame); + break; + + case REPLACE_TEXT: + HashMap jMap = (HashMap) msg.obj; + FocusData fData = (FocusData) jMap.get("focusData"); + String replace = (String) jMap.get("replace"); + int newStart = + ((Integer) jMap.get("start")).intValue(); + int newEnd = + ((Integer) jMap.get("end")).intValue(); + nativeReplaceTextfieldText(fData.mFrame, + fData.mNode, fData.mX, fData.mY, msg.arg1, + msg.arg2, replace, newStart, newEnd); + break; + + case PASS_TO_JS: { + HashMap jsMap = (HashMap) msg.obj; + FocusData fDat = (FocusData) jsMap.get("focusData"); + KeyEvent evt = (KeyEvent) jsMap.get("event"); + int keyCode = evt.getKeyCode(); + int keyValue = evt.getUnicodeChar(); + int generation = msg.arg1; + passToJs(fDat.mFrame, fDat.mNode, fDat.mX, fDat.mY, + generation, + (String) jsMap.get("currentText"), + keyCode, + keyValue, + evt.isDown(), + evt.isShiftPressed(), evt.isAltPressed(), + evt.isSymPressed()); + break; + } + + case SAVE_DOCUMENT_STATE: { + FocusData fDat = (FocusData) msg.obj; + nativeSaveDocumentState(fDat.mFrame); + break; + } + + case CLEAR_SSL_PREF_TABLE: + Network.getInstance(mContext) + .clearUserSslPrefTable(); + break; + + case TOUCH_UP: + TouchUpData touchUpData = (TouchUpData) msg.obj; + nativeTouchUp(touchUpData.mMoveGeneration, + touchUpData.mBuildGeneration, + touchUpData.mFrame, touchUpData.mNode, + touchUpData.mX, touchUpData.mY, + touchUpData.mSize, touchUpData.mIsClick, + touchUpData.mRetry); + break; + + case TOUCH_EVENT: { + TouchEventData ted = (TouchEventData) msg.obj; + Message.obtain( + mWebView.mPrivateHandler, + WebView.PREVENT_TOUCH_ID, ted.mAction, + nativeHandleTouchEvent(ted.mAction, ted.mX, + ted.mY) ? 1 : 0).sendToTarget(); + break; + } + + case ADD_JS_INTERFACE: + HashMap map = (HashMap) msg.obj; + Object obj = map.get("object"); + String interfaceName = (String) + map.get("interfaceName"); + mBrowserFrame.addJavascriptInterface(obj, + interfaceName); + break; + + case REQUEST_EXT_REPRESENTATION: + mBrowserFrame.externalRepresentation( + (Message) msg.obj); + break; + + case REQUEST_DOC_AS_TEXT: + mBrowserFrame.documentAsText((Message) msg.obj); + break; + + case SET_FINAL_FOCUS: + FocusData finalData = (FocusData) msg.obj; + nativeSetFinalFocus(finalData.mFrame, + finalData.mNode, finalData.mX, + finalData.mY, msg.arg1 + != EventHub.NO_FOCUS_CHANGE_BLOCK); + break; + + case UNBLOCK_FOCUS: + nativeUnblockFocus(); + break; + + case SET_KIT_FOCUS: + FocusData focusData = (FocusData) msg.obj; + nativeSetKitFocus(focusData.mMoveGeneration, + focusData.mBuildGeneration, + focusData.mFrame, focusData.mNode, + focusData.mX, focusData.mY, + focusData.mIgnoreNullFocus); + break; + + case REQUEST_FOCUS_HREF: { + Message hrefMsg = (Message) msg.obj; + String res = nativeRetrieveHref(msg.arg1, msg.arg2); + hrefMsg.getData().putString("url", res); + hrefMsg.sendToTarget(); + break; + } + + case UPDATE_CACHE_AND_TEXT_ENTRY: + nativeUpdateFrameCache(); + // FIXME: this should provide a minimal rectangle + if (mWebView != null) { + mWebView.postInvalidate(); + } + sendUpdateTextEntry(); + break; + + case DOC_HAS_IMAGES: + Message imageResult = (Message) msg.obj; + imageResult.arg1 = + mBrowserFrame.documentHasImages() ? 1 : 0; + imageResult.sendToTarget(); + break; + + case SET_SNAP_ANCHOR: + nativeSetSnapAnchor(msg.arg1, msg.arg2); + break; + + case DELETE_SELECTION: + FocusData delData = (FocusData) msg.obj; + nativeDeleteSelection(delData.mFrame, + delData.mNode, delData.mX, + delData.mY, msg.arg1, msg.arg2); + break; + + case SET_SELECTION: + FocusData selData = (FocusData) msg.obj; + nativeSetSelection(selData.mFrame, + selData.mNode, selData.mX, + selData.mY, msg.arg1, msg.arg2); + break; + + case LISTBOX_CHOICES: + SparseBooleanArray choices = (SparseBooleanArray) + msg.obj; + int choicesSize = msg.arg1; + boolean[] choicesArray = new boolean[choicesSize]; + for (int c = 0; c < choicesSize; c++) { + choicesArray[c] = choices.get(c); + } + nativeSendListBoxChoices(choicesArray, + choicesSize); + break; + + case SINGLE_LISTBOX_CHOICE: + nativeSendListBoxChoice(msg.arg1); + break; + + case SET_BACKGROUND_COLOR: + nativeSetBackgroundColor(msg.arg1); + break; + + case GET_SELECTION: + String str = nativeGetSelection((Region) msg.obj); + Message.obtain(mWebView.mPrivateHandler + , WebView.UPDATE_CLIPBOARD, str) + .sendToTarget(); + break; + + case DUMP_DOMTREE: + nativeDumpDomTree(msg.arg1 == 1); + break; + + case DUMP_RENDERTREE: + nativeDumpRenderTree(msg.arg1 == 1); + break; + + case DUMP_NAVTREE: + nativeDumpNavTree(); + break; + + case SYNC_SCROLL: + mWebkitScrollX = msg.arg1; + mWebkitScrollY = msg.arg2; + break; + + case REFRESH_PLUGINS: + nativeRefreshPlugins(msg.arg1 != 0); + break; + + case SPLIT_PICTURE_SET: + nativeSplitContent(); + mSplitPictureIsScheduled = false; + break; + + case CLEAR_CONTENT: + // Clear the view so that onDraw() will draw nothing + // but white background + // (See public method WebView.clearView) + nativeClearContent(); + break; + } + } + }; + // Take all queued messages and resend them to the new handler. + synchronized (this) { + int size = mMessages.size(); + for (int i = 0; i < size; i++) { + mHandler.sendMessage(mMessages.get(i)); + } + mMessages = null; + } + } + + /** + * Send a message internally to the queue or to the handler + */ + private synchronized void sendMessage(Message msg) { + if (mBlockMessages) { + return; + } + if (mMessages != null) { + mMessages.add(msg); + } else { + mHandler.sendMessage(msg); + } + } + + private synchronized void removeMessages(int what) { + if (mBlockMessages) { + return; + } + if (what == EventHub.WEBKIT_DRAW) { + mDrawIsScheduled = false; + } + if (mMessages != null) { + Log.w(LOGTAG, "Not supported in this case."); + } else { + mHandler.removeMessages(what); + } + } + + private synchronized void sendMessageDelayed(Message msg, long delay) { + if (mBlockMessages) { + return; + } + mHandler.sendMessageDelayed(msg, delay); + } + + /** + * Send a message internally to the front of the queue. + */ + private synchronized void sendMessageAtFrontOfQueue(Message msg) { + if (mBlockMessages) { + return; + } + if (mMessages != null) { + mMessages.add(0, msg); + } else { + mHandler.sendMessageAtFrontOfQueue(msg); + } + } + + /** + * Remove all the messages. + */ + private synchronized void removeMessages() { + // reset mDrawIsScheduled flag as WEBKIT_DRAW may be removed + mDrawIsScheduled = false; + mSplitPictureIsScheduled = false; + if (mMessages != null) { + mMessages.clear(); + } else { + mHandler.removeCallbacksAndMessages(null); + } + } + + /** + * Block sending messages to the EventHub. + */ + private synchronized void blockMessages() { + mBlockMessages = true; + } + } + + //------------------------------------------------------------------------- + // Methods called by host activity (in the same thread) + //------------------------------------------------------------------------- + + void stopLoading() { + if (LOGV_ENABLED) Log.v(LOGTAG, "CORE stopLoading"); + if (mBrowserFrame != null) { + mBrowserFrame.stopLoading(); + } + } + + //------------------------------------------------------------------------- + // Methods called by WebView + // If it refers to local variable, it needs synchronized(). + // If it needs WebCore, it has to send message. + //------------------------------------------------------------------------- + + void sendMessage(Message msg) { + mEventHub.sendMessage(msg); + } + + void sendMessage(int what) { + mEventHub.sendMessage(Message.obtain(null, what)); + } + + void sendMessage(int what, Object obj) { + mEventHub.sendMessage(Message.obtain(null, what, obj)); + } + + void sendMessage(int what, int arg1) { + // just ignore the second argument (make it 0) + mEventHub.sendMessage(Message.obtain(null, what, arg1, 0)); + } + + void sendMessage(int what, int arg1, int arg2) { + mEventHub.sendMessage(Message.obtain(null, what, arg1, arg2)); + } + + void sendMessage(int what, int arg1, Object obj) { + // just ignore the second argument (make it 0) + mEventHub.sendMessage(Message.obtain(null, what, arg1, 0, obj)); + } + + void sendMessage(int what, int arg1, int arg2, Object obj) { + mEventHub.sendMessage(Message.obtain(null, what, arg1, arg2, obj)); + } + + void sendMessageDelayed(int what, Object obj, long delay) { + mEventHub.sendMessageDelayed(Message.obtain(null, what, obj), delay); + } + + void removeMessages(int what) { + mEventHub.removeMessages(what); + } + + void removeMessages() { + mEventHub.removeMessages(); + } + + /** + * Removes pending messages and trigger a DESTROY message to send to + * WebCore. + * Called from UI thread. + */ + void destroy() { + // We don't want anyone to post a message between removing pending + // messages and sending the destroy message. + synchronized (mEventHub) { + mEventHub.removeMessages(); + mEventHub.sendMessageAtFrontOfQueue( + Message.obtain(null, EventHub.DESTROY)); + mEventHub.blockMessages(); + mWebView = null; + } + } + + //------------------------------------------------------------------------- + // WebViewCore private methods + //------------------------------------------------------------------------- + + private void loadUrl(String url) { + if (LOGV_ENABLED) Log.v(LOGTAG, " CORE loadUrl " + url); + mBrowserFrame.loadUrl(url); + } + + private void key(KeyEvent evt, boolean isDown) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "CORE key at " + System.currentTimeMillis() + ", " + + evt); + } + if (!nativeKey(evt.getKeyCode(), evt.getUnicodeChar(), + evt.getRepeatCount(), evt.isShiftPressed(), evt.isAltPressed(), + isDown)) { + // bubble up the event handling + mCallbackProxy.onUnhandledKeyEvent(evt); + } + } + + // These values are used to avoid requesting a layout based on old values + private int mCurrentViewWidth = 0; + private int mCurrentViewHeight = 0; + + // Define a minimum screen width so that we won't wrap the paragraph to one + // word per line during zoom-in. + private static final int MIN_SCREEN_WIDTH = 160; + + // notify webkit that our virtual view size changed size (after inv-zoom) + private void viewSizeChanged(int w, int h, int viewWidth) { + if (LOGV_ENABLED) Log.v(LOGTAG, "CORE onSizeChanged"); + if (w == 0) { + Log.w(LOGTAG, "skip viewSizeChanged as w is 0"); + return; + } + // negative scale indicate that WebCore should reuse the current scale + float scale = (float) viewWidth / w; + if (mSettings.getUseWideViewPort() + && (w < mViewportWidth || mViewportWidth == -1)) { + int width = mViewportWidth; + int screenWidth = Math.max(w, MIN_SCREEN_WIDTH); + if (mViewportWidth == -1) { + if (mSettings.getLayoutAlgorithm() == + WebSettings.LayoutAlgorithm.NORMAL) { + width = WebView.ZOOM_OUT_WIDTH; + } else { + /* + * if a page's minimum preferred width is wider than the + * given "w", use it instead to get better layout result. If + * we start a page with MAX_ZOOM_WIDTH, "w" will be always + * wider. If we start a page with screen width, due to the + * delay between {@link #didFirstLayout} and + * {@link #viewSizeChanged}, + * {@link #nativeGetContentMinPrefWidth} will return a more + * accurate value than initial 0 to result a better layout. + * In the worse case, the native width will be adjusted when + * next zoom or screen orientation change happens. + */ + int minContentWidth = nativeGetContentMinPrefWidth(); + if (minContentWidth > WebView.MAX_FLOAT_CONTENT_WIDTH) { + // keep the same width and screen width so that there is + // no reflow when zoom-out + width = minContentWidth; + screenWidth = Math.min(screenWidth, Math.abs(viewWidth)); + } else { + width = Math.max(w, minContentWidth); + } + } + } + nativeSetSize(width, Math.round((float) width * h / w), + screenWidth, scale, w, h); + } else { + nativeSetSize(w, h, w, scale, w, h); + } + // Remember the current width and height + boolean needInvalidate = (mCurrentViewWidth == 0); + mCurrentViewWidth = w; + mCurrentViewHeight = h; + if (needInvalidate) { + // ensure {@link #webkitDraw} is called as we were blocking in + // {@link #contentDraw} when mCurrentViewWidth is 0 + if (LOGV_ENABLED) Log.v(LOGTAG, "viewSizeChanged"); + contentDraw(); + } + mEventHub.sendMessage(Message.obtain(null, + EventHub.UPDATE_CACHE_AND_TEXT_ENTRY)); + } + + private void sendUpdateTextEntry() { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.UPDATE_TEXT_ENTRY_MSG_ID).sendToTarget(); + } + } + + // Used to avoid posting more than one draw message. + private boolean mDrawIsScheduled; + + // Used to avoid posting more than one split picture message. + private boolean mSplitPictureIsScheduled; + + // Used to suspend drawing. + private boolean mDrawIsPaused; + + // Used to end scale+scroll mode, accessed by both threads + boolean mEndScaleZoom = false; + + public class DrawData { + public DrawData() { + mInvalRegion = new Region(); + mWidthHeight = new Point(); + } + public Region mInvalRegion; + public Point mViewPoint; + public Point mWidthHeight; + } + + private void webkitDraw() { + mDrawIsScheduled = false; + DrawData draw = new DrawData(); + if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw start"); + if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight) + == false) { + if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw abort"); + return; + } + if (mWebView != null) { + // Send the native view size that was used during the most recent + // layout. + draw.mViewPoint = new Point(mCurrentViewWidth, mCurrentViewHeight); + if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID"); + Message.obtain(mWebView.mPrivateHandler, + WebView.NEW_PICTURE_MSG_ID, + mViewportMinimumScale == 0 ? nativeGetContentMinPrefWidth() + : 0, + 0, draw).sendToTarget(); + nativeCheckNavCache(); + if (mWebkitScrollX != 0 || mWebkitScrollY != 0) { + // as we have the new picture, try to sync the scroll position + Message.obtain(mWebView.mPrivateHandler, + WebView.SYNC_SCROLL_TO_MSG_ID, mWebkitScrollX, + mWebkitScrollY).sendToTarget(); + mWebkitScrollX = mWebkitScrollY = 0; + } + // nativeSnapToAnchor() needs to be called after NEW_PICTURE_MSG_ID + // is sent, so that scroll will be based on the new content size. + nativeSnapToAnchor(); + } + } + + /////////////////////////////////////////////////////////////////////////// + // These are called from the UI thread, not our thread + + static final int ZOOM_BITS = Paint.FILTER_BITMAP_FLAG | + Paint.DITHER_FLAG | + Paint.SUBPIXEL_TEXT_FLAG; + static final int SCROLL_BITS = Paint.FILTER_BITMAP_FLAG | + Paint.DITHER_FLAG; + + final DrawFilter mZoomFilter = + new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); + final DrawFilter mScrollFilter = + new PaintFlagsDrawFilter(SCROLL_BITS, 0); + + /* package */ void drawContentPicture(Canvas canvas, int color, + boolean animatingZoom, + boolean animatingScroll) { + DrawFilter df = null; + if (animatingZoom) { + df = mZoomFilter; + } else if (animatingScroll) { + df = mScrollFilter; + } + canvas.setDrawFilter(df); + boolean tookTooLong = nativeDrawContent(canvas, color); + canvas.setDrawFilter(null); + if (tookTooLong && mSplitPictureIsScheduled == false) { + mSplitPictureIsScheduled = true; + sendMessage(EventHub.SPLIT_PICTURE_SET); + } + } + + /*package*/ Picture copyContentPicture() { + Picture result = new Picture(); + nativeCopyContentToPicture(result); + return result; + } + + static void pauseUpdate(WebViewCore core) { + // remove the pending REDUCE_PRIORITY and RESUME_PRIORITY messages + sWebCoreHandler.removeMessages(WebCoreThread.REDUCE_PRIORITY); + sWebCoreHandler.removeMessages(WebCoreThread.RESUME_PRIORITY); + sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler + .obtainMessage(WebCoreThread.REDUCE_PRIORITY)); + // Note: there is one possible failure mode. If pauseUpdate() is called + // from UI thread while in webcore thread WEBKIT_DRAW is just pulled out + // of the queue and about to be executed. mDrawIsScheduled may be set to + // false in webkitDraw(). So update won't be blocked. But at least the + // webcore thread priority is still lowered. + if (core != null) { + synchronized (core) { + core.mDrawIsPaused = true; + core.mEventHub.removeMessages(EventHub.WEBKIT_DRAW); + } + } + } + + static void resumeUpdate(WebViewCore core) { + // remove the pending REDUCE_PRIORITY and RESUME_PRIORITY messages + sWebCoreHandler.removeMessages(WebCoreThread.REDUCE_PRIORITY); + sWebCoreHandler.removeMessages(WebCoreThread.RESUME_PRIORITY); + sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler + .obtainMessage(WebCoreThread.RESUME_PRIORITY)); + if (core != null) { + synchronized (core) { + core.mDrawIsScheduled = false; + core.mDrawIsPaused = false; + if (LOGV_ENABLED) Log.v(LOGTAG, "resumeUpdate"); + core.contentDraw(); + } + } + } + + static void startCacheTransaction() { + sWebCoreHandler.sendMessage(sWebCoreHandler + .obtainMessage(WebCoreThread.RESUME_CACHE_TICKER)); + } + + static void endCacheTransaction() { + sWebCoreHandler.sendMessage(sWebCoreHandler + .obtainMessage(WebCoreThread.BLOCK_CACHE_TICKER)); + } + + ////////////////////////////////////////////////////////////////////////// + + private void restoreState(int index) { + WebBackForwardList list = mCallbackProxy.getBackForwardList(); + int size = list.getSize(); + for (int i = 0; i < size; i++) { + list.getItemAtIndex(i).inflate(mBrowserFrame.mNativeFrame); + } + mBrowserFrame.mLoadInitFromJava = true; + list.restoreIndex(mBrowserFrame.mNativeFrame, index); + mBrowserFrame.mLoadInitFromJava = false; + } + + //------------------------------------------------------------------------- + // Implement abstract methods in WebViewCore, native WebKit callback part + //------------------------------------------------------------------------- + + // called from JNI or WebView thread + /* package */ void contentDraw() { + // don't update the Picture until we have an initial width and finish + // the first layout + if (mCurrentViewWidth == 0 || !mBrowserFrame.firstLayoutDone()) { + return; + } + // only fire an event if this is our first request + synchronized (this) { + if (mDrawIsPaused || mDrawIsScheduled) { + return; + } + mDrawIsScheduled = true; + mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW)); + } + } + + // called by JNI + private void contentScrollBy(int dx, int dy, boolean animate) { + if (!mBrowserFrame.firstLayoutDone()) { + // Will this happen? If yes, we need to do something here. + return; + } + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.SCROLL_BY_MSG_ID, dx, dy, + new Boolean(animate)).sendToTarget(); + } + } + + // called by JNI + private void contentScrollTo(int x, int y) { + if (!mBrowserFrame.firstLayoutDone()) { + /* + * WebKit restore state will be called before didFirstLayout(), + * remember the position as it has to be applied after restoring + * zoom factor which is controlled by screenWidth. + */ + mRestoredX = x; + mRestoredY = y; + return; + } + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.SCROLL_TO_MSG_ID, x, y).sendToTarget(); + } + } + + // called by JNI + private void contentSpawnScrollTo(int x, int y) { + if (!mBrowserFrame.firstLayoutDone()) { + /* + * WebKit restore state will be called before didFirstLayout(), + * remember the position as it has to be applied after restoring + * zoom factor which is controlled by screenWidth. + */ + mRestoredX = x; + mRestoredY = y; + return; + } + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.SPAWN_SCROLL_TO_MSG_ID, x, y).sendToTarget(); + } + } + + // called by JNI + private void sendMarkNodeInvalid(int node) { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.MARK_NODE_INVALID_ID, node, 0).sendToTarget(); + } + } + + // called by JNI + private void sendNotifyFocusSet() { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.NOTIFY_FOCUS_SET_MSG_ID).sendToTarget(); + } + } + + // called by JNI + private void sendNotifyProgressFinished() { + sendUpdateTextEntry(); + // as CacheManager can behave based on database transaction, we need to + // call tick() to trigger endTransaction + sWebCoreHandler.removeMessages(WebCoreThread.CACHE_TICKER); + sWebCoreHandler.sendMessage(sWebCoreHandler + .obtainMessage(WebCoreThread.CACHE_TICKER)); + contentDraw(); + } + + // called by JNI + private void sendRecomputeFocus() { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.RECOMPUTE_FOCUS_MSG_ID).sendToTarget(); + } + } + + /* Called by JNI. The coordinates are in doc coordinates, so they need to + be scaled before they can be used by the view system, which happens + in WebView since it (and its thread) know the current scale factor. + */ + private void sendViewInvalidate(int left, int top, int right, int bottom) { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.INVAL_RECT_MSG_ID, + new Rect(left, top, right, bottom)).sendToTarget(); + } + } + + /* package */ WebView getWebView() { + return mWebView; + } + + private native void setViewportSettingsFromNative(); + + // called by JNI + private void didFirstLayout() { + // Trick to ensure that the Picture has the exact height for the content + // by forcing to layout with 0 height after the page is ready, which is + // indicated by didFirstLayout. This is essential to get rid of the + // white space in the GMail which uses WebView for message view. + if (mWebView != null && mWebView.mHeightCanMeasure) { + mWebView.mLastHeightSent = 0; + // Send a negative screen width to indicate that WebCore should + // reuse the current scale + mEventHub.sendMessage(Message.obtain(null, + EventHub.VIEW_SIZE_CHANGED, mWebView.mLastWidthSent, + mWebView.mLastHeightSent, -mWebView.mLastWidthSent)); + } + + mBrowserFrame.didFirstLayout(); + + // reset the scroll position as it is a new page now + mWebkitScrollX = mWebkitScrollY = 0; + + // set the viewport settings from WebKit + setViewportSettingsFromNative(); + + // infer the values if they are not defined. + if (mViewportWidth == 0) { + if (mViewportInitialScale == 0) { + mViewportInitialScale = 100; + } + if (mViewportMinimumScale == 0) { + mViewportMinimumScale = 100; + } + } + if (mViewportUserScalable == false) { + mViewportInitialScale = 100; + mViewportMinimumScale = 100; + mViewportMaximumScale = 100; + } + if (mViewportMinimumScale > mViewportInitialScale) { + if (mViewportInitialScale == 0) { + mViewportInitialScale = mViewportMinimumScale; + } else { + mViewportMinimumScale = mViewportInitialScale; + } + } + if (mViewportMaximumScale > 0) { + if (mViewportMaximumScale < mViewportInitialScale) { + mViewportMaximumScale = mViewportInitialScale; + } else if (mViewportInitialScale == 0) { + mViewportInitialScale = mViewportMaximumScale; + } + } + if (mViewportWidth < 0 && mViewportInitialScale == 100) { + mViewportWidth = 0; + } + + // now notify webview + if (mWebView != null) { + HashMap scaleLimit = new HashMap(); + scaleLimit.put("minScale", mViewportMinimumScale); + scaleLimit.put("maxScale", mViewportMaximumScale); + + if (mRestoredScale > 0) { + Message.obtain(mWebView.mPrivateHandler, + WebView.DID_FIRST_LAYOUT_MSG_ID, mRestoredScale, 0, + scaleLimit).sendToTarget(); + mRestoredScale = 0; + } else { + Message.obtain(mWebView.mPrivateHandler, + WebView.DID_FIRST_LAYOUT_MSG_ID, mViewportInitialScale, + mViewportWidth, scaleLimit).sendToTarget(); + } + + // if no restored offset, move the new page to (0, 0) + Message.obtain(mWebView.mPrivateHandler, WebView.SCROLL_TO_MSG_ID, + mRestoredX, mRestoredY).sendToTarget(); + mRestoredX = mRestoredY = 0; + + // force an early draw for quick feedback after the first layout + if (mCurrentViewWidth != 0) { + synchronized (this) { + if (mDrawIsScheduled) { + mEventHub.removeMessages(EventHub.WEBKIT_DRAW); + } + mDrawIsScheduled = true; + mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null, + EventHub.WEBKIT_DRAW)); + } + } + } + } + + // called by JNI + private void restoreScale(int scale) { + if (mBrowserFrame.firstLayoutDone() == false) { + mRestoredScale = scale; + } + } + + // called by JNI + private void needTouchEvents(boolean need) { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.WEBCORE_NEED_TOUCH_EVENTS, need ? 1 : 0, 0) + .sendToTarget(); + } + } + + // called by JNI + private void updateTextfield(int ptr, boolean changeToPassword, + String text, int textGeneration) { + if (mWebView != null) { + Message msg = Message.obtain(mWebView.mPrivateHandler, + WebView.UPDATE_TEXTFIELD_TEXT_MSG_ID, ptr, + textGeneration, text); + msg.getData().putBoolean("password", changeToPassword); + msg.sendToTarget(); + } + } + + // these must be in document space (i.e. not scaled/zoomed). + private native void nativeSetScrollOffset(int dx, int dy); + + private native void nativeSetGlobalBounds(int x, int y, int w, int h); + + // called by JNI + private void requestListBox(String[] array, boolean[] enabledArray, + int[] selectedArray) { + if (mWebView != null) { + mWebView.requestListBox(array, enabledArray, selectedArray); + } + } + + // called by JNI + private void requestListBox(String[] array, boolean[] enabledArray, + int selection) { + if (mWebView != null) { + mWebView.requestListBox(array, enabledArray, selection); + } + + } +} diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java new file mode 100644 index 0000000..1004e30 --- /dev/null +++ b/core/java/android/webkit/WebViewDatabase.java @@ -0,0 +1,967 @@ +/* + * Copyright (C) 2007 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Set; +import java.util.Map.Entry; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.util.Log; +import android.webkit.CookieManager.Cookie; +import android.webkit.CacheManager.CacheResult; + +public class WebViewDatabase { + private static final String DATABASE_FILE = "webview.db"; + private static final String CACHE_DATABASE_FILE = "webviewCache.db"; + + // log tag + protected static final String LOGTAG = "webviewdatabase"; + + private static final int DATABASE_VERSION = 9; + // 2 -> 3 Modified Cache table to allow cache of redirects + // 3 -> 4 Added Oma-Downloads table + // 4 -> 5 Modified Cache table to support persistent contentLength + // 5 -> 4 Removed Oma-Downoads table + // 5 -> 6 Add INDEX for cache table + // 6 -> 7 Change cache localPath from int to String + // 7 -> 8 Move cache to its own db + // 8 -> 9 Store both scheme and host when storing passwords + private static final int CACHE_DATABASE_VERSION = 1; + + private static WebViewDatabase mInstance = null; + + private static SQLiteDatabase mDatabase = null; + private static SQLiteDatabase mCacheDatabase = null; + + // synchronize locks + private final Object mCookieLock = new Object(); + private final Object mPasswordLock = new Object(); + private final Object mFormLock = new Object(); + private final Object mHttpAuthLock = new Object(); + + private static final String mTableNames[] = { + "cookies", "password", "formurl", "formdata", "httpauth" + }; + + // Table ids (they are index to mTableNames) + private static final int TABLE_COOKIES_ID = 0; + + private static final int TABLE_PASSWORD_ID = 1; + + private static final int TABLE_FORMURL_ID = 2; + + private static final int TABLE_FORMDATA_ID = 3; + + private static final int TABLE_HTTPAUTH_ID = 4; + + // column id strings for "_id" which can be used by any table + private static final String ID_COL = "_id"; + + private static final String[] ID_PROJECTION = new String[] { + "_id" + }; + + // column id strings for "cookies" table + private static final String COOKIES_NAME_COL = "name"; + + private static final String COOKIES_VALUE_COL = "value"; + + private static final String COOKIES_DOMAIN_COL = "domain"; + + private static final String COOKIES_PATH_COL = "path"; + + private static final String COOKIES_EXPIRES_COL = "expires"; + + private static final String COOKIES_SECURE_COL = "secure"; + + // column id strings for "cache" table + private static final String CACHE_URL_COL = "url"; + + private static final String CACHE_FILE_PATH_COL = "filepath"; + + private static final String CACHE_LAST_MODIFY_COL = "lastmodify"; + + private static final String CACHE_ETAG_COL = "etag"; + + private static final String CACHE_EXPIRES_COL = "expires"; + + private static final String CACHE_MIMETYPE_COL = "mimetype"; + + private static final String CACHE_ENCODING_COL = "encoding"; + + private static final String CACHE_HTTP_STATUS_COL = "httpstatus"; + + private static final String CACHE_LOCATION_COL = "location"; + + private static final String CACHE_CONTENTLENGTH_COL = "contentlength"; + + // column id strings for "password" table + private static final String PASSWORD_HOST_COL = "host"; + + private static final String PASSWORD_USERNAME_COL = "username"; + + private static final String PASSWORD_PASSWORD_COL = "password"; + + // column id strings for "formurl" table + private static final String FORMURL_URL_COL = "url"; + + // column id strings for "formdata" table + private static final String FORMDATA_URLID_COL = "urlid"; + + private static final String FORMDATA_NAME_COL = "name"; + + private static final String FORMDATA_VALUE_COL = "value"; + + // column id strings for "httpauth" table + private static final String HTTPAUTH_HOST_COL = "host"; + + private static final String HTTPAUTH_REALM_COL = "realm"; + + private static final String HTTPAUTH_USERNAME_COL = "username"; + + private static final String HTTPAUTH_PASSWORD_COL = "password"; + + // use InsertHelper to improve insert performance by 40% + private static DatabaseUtils.InsertHelper mCacheInserter; + private static int mCacheUrlColIndex; + private static int mCacheFilePathColIndex; + private static int mCacheLastModifyColIndex; + private static int mCacheETagColIndex; + private static int mCacheExpiresColIndex; + private static int mCacheMimeTypeColIndex; + private static int mCacheEncodingColIndex; + private static int mCacheHttpStatusColIndex; + private static int mCacheLocationColIndex; + private static int mCacheContentLengthColIndex; + + private static int mCacheTransactionRefcount; + + private WebViewDatabase() { + // Singleton only, use getInstance() + } + + public static synchronized WebViewDatabase getInstance(Context context) { + if (mInstance == null) { + mInstance = new WebViewDatabase(); + mDatabase = context.openOrCreateDatabase(DATABASE_FILE, 0, null); + + // mDatabase should not be null, + // the only case is RequestAPI test has problem to create db + if (mDatabase != null && mDatabase.getVersion() != DATABASE_VERSION) { + mDatabase.beginTransaction(); + try { + upgradeDatabase(); + mDatabase.setTransactionSuccessful(); + } finally { + mDatabase.endTransaction(); + } + } + + if (mDatabase != null) { + // use per table Mutex lock, turn off database lock, this + // improves performance as database's ReentrantLock is expansive + mDatabase.setLockingEnabled(false); + } + + mCacheDatabase = context.openOrCreateDatabase(CACHE_DATABASE_FILE, + 0, null); + + // mCacheDatabase should not be null, + // the only case is RequestAPI test has problem to create db + if (mCacheDatabase != null + && mCacheDatabase.getVersion() != CACHE_DATABASE_VERSION) { + mCacheDatabase.beginTransaction(); + try { + upgradeCacheDatabase(); + bootstrapCacheDatabase(); + mCacheDatabase.setTransactionSuccessful(); + } finally { + mCacheDatabase.endTransaction(); + } + // Erase the files from the file system in the + // case that the database was updated and the + // there were existing cache content + CacheManager.removeAllCacheFiles(); + } + + if (mCacheDatabase != null) { + // use InsertHelper for faster insertion + mCacheInserter = new DatabaseUtils.InsertHelper(mCacheDatabase, + "cache"); + mCacheUrlColIndex = mCacheInserter + .getColumnIndex(CACHE_URL_COL); + mCacheFilePathColIndex = mCacheInserter + .getColumnIndex(CACHE_FILE_PATH_COL); + mCacheLastModifyColIndex = mCacheInserter + .getColumnIndex(CACHE_LAST_MODIFY_COL); + mCacheETagColIndex = mCacheInserter + .getColumnIndex(CACHE_ETAG_COL); + mCacheExpiresColIndex = mCacheInserter + .getColumnIndex(CACHE_EXPIRES_COL); + mCacheMimeTypeColIndex = mCacheInserter + .getColumnIndex(CACHE_MIMETYPE_COL); + mCacheEncodingColIndex = mCacheInserter + .getColumnIndex(CACHE_ENCODING_COL); + mCacheHttpStatusColIndex = mCacheInserter + .getColumnIndex(CACHE_HTTP_STATUS_COL); + mCacheLocationColIndex = mCacheInserter + .getColumnIndex(CACHE_LOCATION_COL); + mCacheContentLengthColIndex = mCacheInserter + .getColumnIndex(CACHE_CONTENTLENGTH_COL); + } + } + + return mInstance; + } + + private static void upgradeDatabase() { + int oldVersion = mDatabase.getVersion(); + if (oldVersion != 0) { + Log.i(LOGTAG, "Upgrading database from version " + + oldVersion + " to " + + DATABASE_VERSION + ", which will destroy old data"); + } + boolean justPasswords = 8 == oldVersion && 9 == DATABASE_VERSION; + if (!justPasswords) { + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_COOKIES_ID]); + mDatabase.execSQL("DROP TABLE IF EXISTS cache"); + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_FORMURL_ID]); + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_FORMDATA_ID]); + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_HTTPAUTH_ID]); + } + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_PASSWORD_ID]); + + mDatabase.setVersion(DATABASE_VERSION); + + if (!justPasswords) { + // cookies + mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_COOKIES_ID] + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + + COOKIES_NAME_COL + " TEXT, " + COOKIES_VALUE_COL + + " TEXT, " + COOKIES_DOMAIN_COL + " TEXT, " + + COOKIES_PATH_COL + " TEXT, " + COOKIES_EXPIRES_COL + + " INTEGER, " + COOKIES_SECURE_COL + " INTEGER" + ");"); + mDatabase.execSQL("CREATE INDEX cookiesIndex ON " + + mTableNames[TABLE_COOKIES_ID] + " (path)"); + + // formurl + mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID] + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL + + " TEXT" + ");"); + + // formdata + mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMDATA_ID] + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + + FORMDATA_URLID_COL + " INTEGER, " + FORMDATA_NAME_COL + + " TEXT, " + FORMDATA_VALUE_COL + " TEXT," + " UNIQUE (" + + FORMDATA_URLID_COL + ", " + FORMDATA_NAME_COL + ", " + + FORMDATA_VALUE_COL + ") ON CONFLICT IGNORE);"); + + // httpauth + mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL + + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " + + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" + + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL + ", " + + HTTPAUTH_USERNAME_COL + ") ON CONFLICT REPLACE);"); + } + // passwords + mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID] + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + + PASSWORD_HOST_COL + " TEXT, " + PASSWORD_USERNAME_COL + + " TEXT, " + PASSWORD_PASSWORD_COL + " TEXT," + " UNIQUE (" + + PASSWORD_HOST_COL + ", " + PASSWORD_USERNAME_COL + + ") ON CONFLICT REPLACE);"); + } + + private static void upgradeCacheDatabase() { + int oldVersion = mCacheDatabase.getVersion(); + if (oldVersion != 0) { + Log.i(LOGTAG, "Upgrading cache database from version " + + oldVersion + " to " + + DATABASE_VERSION + ", which will destroy all old data"); + } + mCacheDatabase.execSQL("DROP TABLE IF EXISTS cache"); + mCacheDatabase.setVersion(CACHE_DATABASE_VERSION); + } + + private static void bootstrapCacheDatabase() { + if (mCacheDatabase != null) { + mCacheDatabase.execSQL("CREATE TABLE cache" + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + CACHE_URL_COL + + " TEXT, " + CACHE_FILE_PATH_COL + " TEXT, " + + CACHE_LAST_MODIFY_COL + " TEXT, " + CACHE_ETAG_COL + + " TEXT, " + CACHE_EXPIRES_COL + " INTEGER, " + + CACHE_MIMETYPE_COL + " TEXT, " + CACHE_ENCODING_COL + + " TEXT," + CACHE_HTTP_STATUS_COL + " INTEGER, " + + CACHE_LOCATION_COL + " TEXT, " + CACHE_CONTENTLENGTH_COL + + " INTEGER, " + " UNIQUE (" + CACHE_URL_COL + + ") ON CONFLICT REPLACE);"); + mCacheDatabase.execSQL("CREATE INDEX cacheUrlIndex ON cache (" + + CACHE_URL_COL + ")"); + } + } + + private boolean hasEntries(int tableId) { + if (mDatabase == null) { + return false; + } + + Cursor cursor = mDatabase.query(mTableNames[tableId], ID_PROJECTION, + null, null, null, null, null); + boolean ret = cursor.moveToFirst() == true; + cursor.close(); + return ret; + } + + // + // cookies functions + // + + /** + * Get cookies in the format of CookieManager.Cookie inside an ArrayList for + * a given domain + * + * @return ArrayList<Cookie> If nothing is found, return an empty list. + */ + ArrayList<Cookie> getCookiesForDomain(String domain) { + ArrayList<Cookie> list = new ArrayList<Cookie>(); + if (domain == null || mDatabase == null) { + return list; + } + + synchronized (mCookieLock) { + final String[] columns = new String[] { + ID_COL, COOKIES_DOMAIN_COL, COOKIES_PATH_COL, + COOKIES_NAME_COL, COOKIES_VALUE_COL, COOKIES_EXPIRES_COL, + COOKIES_SECURE_COL + }; + final String selection = "(" + COOKIES_DOMAIN_COL + + " GLOB '*' || ?)"; + Cursor cursor = mDatabase.query(mTableNames[TABLE_COOKIES_ID], + columns, selection, new String[] { domain }, null, null, + null); + if (cursor.moveToFirst()) { + int domainCol = cursor.getColumnIndex(COOKIES_DOMAIN_COL); + int pathCol = cursor.getColumnIndex(COOKIES_PATH_COL); + int nameCol = cursor.getColumnIndex(COOKIES_NAME_COL); + int valueCol = cursor.getColumnIndex(COOKIES_VALUE_COL); + int expiresCol = cursor.getColumnIndex(COOKIES_EXPIRES_COL); + int secureCol = cursor.getColumnIndex(COOKIES_SECURE_COL); + do { + Cookie cookie = new Cookie(); + cookie.domain = cursor.getString(domainCol); + cookie.path = cursor.getString(pathCol); + cookie.name = cursor.getString(nameCol); + cookie.value = cursor.getString(valueCol); + if (cursor.isNull(expiresCol)) { + cookie.expires = -1; + } else { + cookie.expires = cursor.getLong(expiresCol); + } + cookie.secure = cursor.getShort(secureCol) != 0; + cookie.mode = Cookie.MODE_NORMAL; + list.add(cookie); + } while (cursor.moveToNext()); + } + cursor.close(); + return list; + } + } + + /** + * Delete cookies which matches (domain, path, name). + * + * @param domain If it is null, nothing happens. + * @param path If it is null, all the cookies match (domain) will be + * deleted. + * @param name If it is null, all the cookies match (domain, path) will be + * deleted. + */ + void deleteCookies(String domain, String path, String name) { + if (domain == null || mDatabase == null) { + return; + } + + synchronized (mCookieLock) { + final String where = "(" + COOKIES_DOMAIN_COL + " == ?) AND (" + + COOKIES_PATH_COL + " == ?) AND (" + COOKIES_NAME_COL + + " == ?)"; + mDatabase.delete(mTableNames[TABLE_COOKIES_ID], where, + new String[] { domain, path, name }); + } + } + + /** + * Add a cookie to the database + * + * @param cookie + */ + void addCookie(Cookie cookie) { + if (cookie.domain == null || cookie.path == null || cookie.name == null + || mDatabase == null) { + return; + } + + synchronized (mCookieLock) { + ContentValues cookieVal = new ContentValues(); + cookieVal.put(COOKIES_DOMAIN_COL, cookie.domain); + cookieVal.put(COOKIES_PATH_COL, cookie.path); + cookieVal.put(COOKIES_NAME_COL, cookie.name); + cookieVal.put(COOKIES_VALUE_COL, cookie.value); + if (cookie.expires != -1) { + cookieVal.put(COOKIES_EXPIRES_COL, cookie.expires); + } + cookieVal.put(COOKIES_SECURE_COL, cookie.secure); + mDatabase.insert(mTableNames[TABLE_COOKIES_ID], null, cookieVal); + } + } + + /** + * Whether there is any cookies in the database + * + * @return TRUE if there is cookie. + */ + boolean hasCookies() { + synchronized (mCookieLock) { + return hasEntries(TABLE_COOKIES_ID); + } + } + + /** + * Clear cookie database + */ + void clearCookies() { + if (mDatabase == null) { + return; + } + + synchronized (mCookieLock) { + mDatabase.delete(mTableNames[TABLE_COOKIES_ID], null, null); + } + } + + /** + * Clear session cookies, which means cookie doesn't have EXPIRES. + */ + void clearSessionCookies() { + if (mDatabase == null) { + return; + } + + final String sessionExpired = COOKIES_EXPIRES_COL + " ISNULL"; + synchronized (mCookieLock) { + mDatabase.delete(mTableNames[TABLE_COOKIES_ID], sessionExpired, + null); + } + } + + /** + * Clear expired cookies + * + * @param now Time for now + */ + void clearExpiredCookies(long now) { + if (mDatabase == null) { + return; + } + + final String expires = COOKIES_EXPIRES_COL + " <= ?"; + synchronized (mCookieLock) { + mDatabase.delete(mTableNames[TABLE_COOKIES_ID], expires, + new String[] { Long.toString(now) }); + } + } + + // + // cache functions, can only be called from WebCoreThread + // + + boolean startCacheTransaction() { + if (++mCacheTransactionRefcount == 1) { + mCacheDatabase.beginTransaction(); + return true; + } + return false; + } + + boolean endCacheTransaction() { + if (--mCacheTransactionRefcount == 0) { + try { + mCacheDatabase.setTransactionSuccessful(); + } finally { + mCacheDatabase.endTransaction(); + } + return true; + } + return false; + } + + /** + * Get a cache item. + * + * @param url The url + * @return CacheResult The CacheManager.CacheResult + */ + CacheResult getCache(String url) { + if (url == null || mCacheDatabase == null) { + return null; + } + + Cursor cursor = mCacheDatabase.rawQuery("SELECT filepath, lastmodify, etag, expires, " + + "mimetype, encoding, httpstatus, location, contentlength " + + "FROM cache WHERE url = ?", + new String[] { url }); + + try { + if (cursor.moveToFirst()) { + CacheResult ret = new CacheResult(); + ret.localPath = cursor.getString(0); + ret.lastModified = cursor.getString(1); + ret.etag = cursor.getString(2); + ret.expires = cursor.getLong(3); + ret.mimeType = cursor.getString(4); + ret.encoding = cursor.getString(5); + ret.httpStatusCode = cursor.getInt(6); + ret.location = cursor.getString(7); + ret.contentLength = cursor.getLong(8); + return ret; + } + } finally { + if (cursor != null) cursor.close(); + } + return null; + } + + /** + * Remove a cache item. + * + * @param url The url + */ + void removeCache(String url) { + if (url == null || mCacheDatabase == null) { + return; + } + + mCacheDatabase.execSQL("DELETE FROM cache WHERE url = ?", new String[] { url }); + } + + /** + * Add or update a cache. CACHE_URL_COL is unique in the table. + * + * @param url The url + * @param c The CacheManager.CacheResult + */ + void addCache(String url, CacheResult c) { + if (url == null || mCacheDatabase == null) { + return; + } + + mCacheInserter.prepareForInsert(); + mCacheInserter.bind(mCacheUrlColIndex, url); + mCacheInserter.bind(mCacheFilePathColIndex, c.localPath); + mCacheInserter.bind(mCacheLastModifyColIndex, c.lastModified); + mCacheInserter.bind(mCacheETagColIndex, c.etag); + mCacheInserter.bind(mCacheExpiresColIndex, c.expires); + mCacheInserter.bind(mCacheMimeTypeColIndex, c.mimeType); + mCacheInserter.bind(mCacheEncodingColIndex, c.encoding); + mCacheInserter.bind(mCacheHttpStatusColIndex, c.httpStatusCode); + mCacheInserter.bind(mCacheLocationColIndex, c.location); + mCacheInserter.bind(mCacheContentLengthColIndex, c.contentLength); + mCacheInserter.execute(); + } + + /** + * Clear cache database + */ + void clearCache() { + if (mCacheDatabase == null) { + return; + } + + mCacheDatabase.delete("cache", null, null); + } + + boolean hasCache() { + if (mCacheDatabase == null) { + return false; + } + + Cursor cursor = mCacheDatabase.query("cache", ID_PROJECTION, + null, null, null, null, null); + boolean ret = cursor.moveToFirst() == true; + cursor.close(); + return ret; + } + + long getCacheTotalSize() { + long size = 0; + Cursor cursor = mCacheDatabase.rawQuery( + "SELECT SUM(contentlength) as sum FROM cache", null); + if (cursor.moveToFirst()) { + size = cursor.getLong(0); + } + cursor.close(); + return size; + } + + ArrayList<String> trimCache(long amount) { + ArrayList<String> pathList = new ArrayList<String>(100); + Cursor cursor = mCacheDatabase.rawQuery( + "SELECT contentlength, filepath FROM cache ORDER BY expires ASC", + null); + if (cursor.moveToFirst()) { + int batchSize = 100; + StringBuilder pathStr = new StringBuilder(20 + 16 * batchSize); + pathStr.append("DELETE FROM cache WHERE filepath IN (?"); + for (int i = 1; i < batchSize; i++) { + pathStr.append(", ?"); + } + pathStr.append(")"); + SQLiteStatement statement = mCacheDatabase.compileStatement(pathStr + .toString()); + // as bindString() uses 1-based index, initialize index to 1 + int index = 1; + do { + long length = cursor.getLong(0); + if (length == 0) { + continue; + } + amount -= length; + String filePath = cursor.getString(1); + statement.bindString(index, filePath); + pathList.add(filePath); + if (index++ == batchSize) { + statement.execute(); + statement.clearBindings(); + index = 1; + } + } while (cursor.moveToNext() && amount > 0); + if (index > 1) { + // there may be old bindings from the previous statement if + // index is less than batchSize, which is Ok. + statement.execute(); + } + statement.close(); + } + cursor.close(); + return pathList; + } + + // + // password functions + // + + /** + * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique. + * + * @param schemePlusHost The scheme and host for the password + * @param username The username for the password. If it is null, it means + * password can't be saved. + * @param password The password + */ + void setUsernamePassword(String schemePlusHost, String username, + String password) { + if (schemePlusHost == null || mDatabase == null) { + return; + } + + synchronized (mPasswordLock) { + final ContentValues c = new ContentValues(); + c.put(PASSWORD_HOST_COL, schemePlusHost); + c.put(PASSWORD_USERNAME_COL, username); + c.put(PASSWORD_PASSWORD_COL, password); + mDatabase.insert(mTableNames[TABLE_PASSWORD_ID], PASSWORD_HOST_COL, + c); + } + } + + /** + * Retrieve the username and password for a given host + * + * @param schemePlusHost The scheme and host which passwords applies to + * @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. + */ + String[] getUsernamePassword(String schemePlusHost) { + if (schemePlusHost == null || mDatabase == null) { + return null; + } + + final String[] columns = new String[] { + PASSWORD_USERNAME_COL, PASSWORD_PASSWORD_COL + }; + final String selection = "(" + PASSWORD_HOST_COL + " == ?)"; + synchronized (mPasswordLock) { + String[] ret = null; + Cursor cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID], + columns, selection, new String[] { schemePlusHost }, null, + null, null); + if (cursor.moveToFirst()) { + ret = new String[2]; + ret[0] = cursor.getString( + cursor.getColumnIndex(PASSWORD_USERNAME_COL)); + ret[1] = cursor.getString( + cursor.getColumnIndex(PASSWORD_PASSWORD_COL)); + } + cursor.close(); + return ret; + } + } + + /** + * Find out if there are any passwords saved. + * + * @return TRUE if there is passwords saved + */ + public boolean hasUsernamePassword() { + synchronized (mPasswordLock) { + return hasEntries(TABLE_PASSWORD_ID); + } + } + + /** + * Clear password database + */ + public void clearUsernamePassword() { + if (mDatabase == null) { + return; + } + + synchronized (mPasswordLock) { + mDatabase.delete(mTableNames[TABLE_PASSWORD_ID], null, null); + } + } + + // + // http authentication password functions + // + + /** + * Set HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, + * HTTPAUTH_REALM_COL, HTTPAUTH_USERNAME_COL) is unique. + * + * @param host The host for the password + * @param realm The realm for the password + * @param username The username for the password. If it is null, it means + * password can't be saved. + * @param password The password + */ + void setHttpAuthUsernamePassword(String host, String realm, String username, + String password) { + if (host == null || realm == null || mDatabase == null) { + return; + } + + synchronized (mHttpAuthLock) { + final ContentValues c = new ContentValues(); + c.put(HTTPAUTH_HOST_COL, host); + c.put(HTTPAUTH_REALM_COL, realm); + c.put(HTTPAUTH_USERNAME_COL, username); + c.put(HTTPAUTH_PASSWORD_COL, password); + mDatabase.insert(mTableNames[TABLE_HTTPAUTH_ID], HTTPAUTH_HOST_COL, + c); + } + } + + /** + * Retrieve the HTTP authentication username and password for a given + * host+realm pair + * + * @param host The host the password applies to + * @param realm The realm the password applies to + * @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. + */ + String[] getHttpAuthUsernamePassword(String host, String realm) { + if (host == null || realm == null || mDatabase == null){ + return null; + } + + final String[] columns = new String[] { + HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL + }; + final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND (" + + HTTPAUTH_REALM_COL + " == ?)"; + synchronized (mHttpAuthLock) { + String[] ret = null; + Cursor cursor = mDatabase.query(mTableNames[TABLE_HTTPAUTH_ID], + columns, selection, new String[] { host, realm }, null, + null, null); + if (cursor.moveToFirst()) { + ret = new String[2]; + ret[0] = cursor.getString( + cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)); + ret[1] = cursor.getString( + cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)); + } + cursor.close(); + return ret; + } + } + + /** + * Find out if there are any HTTP authentication passwords saved. + * + * @return TRUE if there are passwords saved + */ + public boolean hasHttpAuthUsernamePassword() { + synchronized (mHttpAuthLock) { + return hasEntries(TABLE_HTTPAUTH_ID); + } + } + + /** + * Clear HTTP authentication password database + */ + public void clearHttpAuthUsernamePassword() { + if (mDatabase == null) { + return; + } + + synchronized (mHttpAuthLock) { + mDatabase.delete(mTableNames[TABLE_HTTPAUTH_ID], null, null); + } + } + + // + // form data functions + // + + /** + * Set form data for a site. Tuple (FORMDATA_URLID_COL, FORMDATA_NAME_COL, + * FORMDATA_VALUE_COL) is unique + * + * @param url The url of the site + * @param formdata The form data in HashMap + */ + void setFormData(String url, HashMap<String, String> formdata) { + if (url == null || formdata == null || mDatabase == null) { + return; + } + + final String selection = "(" + FORMURL_URL_COL + " == ?)"; + synchronized (mFormLock) { + long urlid = -1; + Cursor cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], + ID_PROJECTION, selection, new String[] { url }, null, null, + null); + if (cursor.moveToFirst()) { + urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); + } else { + ContentValues c = new ContentValues(); + c.put(FORMURL_URL_COL, url); + urlid = mDatabase.insert( + mTableNames[TABLE_FORMURL_ID], null, c); + } + cursor.close(); + if (urlid >= 0) { + Set<Entry<String, String>> set = formdata.entrySet(); + Iterator<Entry<String, String>> iter = set.iterator(); + ContentValues map = new ContentValues(); + map.put(FORMDATA_URLID_COL, urlid); + while (iter.hasNext()) { + Entry<String, String> entry = iter.next(); + map.put(FORMDATA_NAME_COL, entry.getKey()); + map.put(FORMDATA_VALUE_COL, entry.getValue()); + mDatabase.insert(mTableNames[TABLE_FORMDATA_ID], null, map); + } + } + } + } + + /** + * Get all the values for a form entry with "name" in a given site + * + * @param url The url of the site + * @param name The name of the form entry + * @return A list of values. Return empty list if nothing is found. + */ + ArrayList<String> getFormData(String url, String name) { + ArrayList<String> values = new ArrayList<String>(); + if (url == null || name == null || mDatabase == null) { + return values; + } + + final String urlSelection = "(" + FORMURL_URL_COL + " == ?)"; + final String dataSelection = "(" + FORMDATA_URLID_COL + " == ?) AND (" + + FORMDATA_NAME_COL + " == ?)"; + synchronized (mFormLock) { + Cursor cursor = mDatabase.query(mTableNames[TABLE_FORMURL_ID], + ID_PROJECTION, urlSelection, new String[] { url }, null, + null, null); + if (cursor.moveToFirst()) { + long urlid = cursor.getLong(cursor.getColumnIndex(ID_COL)); + Cursor dataCursor = mDatabase.query( + mTableNames[TABLE_FORMDATA_ID], + new String[] { ID_COL, FORMDATA_VALUE_COL }, + dataSelection, + new String[] { Long.toString(urlid), name }, null, + null, null); + if (dataCursor.moveToFirst()) { + int valueCol = + dataCursor.getColumnIndex(FORMDATA_VALUE_COL); + do { + values.add(dataCursor.getString(valueCol)); + } while (dataCursor.moveToNext()); + } + dataCursor.close(); + } + cursor.close(); + return values; + } + } + + /** + * Find out if there is form data saved. + * + * @return TRUE if there is form data in the database + */ + public boolean hasFormData() { + synchronized (mFormLock) { + return hasEntries(TABLE_FORMURL_ID); + } + } + + /** + * Clear form database + */ + public void clearFormData() { + if (mDatabase == null) { + return; + } + + synchronized (mFormLock) { + mDatabase.delete(mTableNames[TABLE_FORMURL_ID], null, null); + mDatabase.delete(mTableNames[TABLE_FORMDATA_ID], null, null); + } + } +} diff --git a/core/java/android/webkit/gears/AndroidGpsLocationProvider.java b/core/java/android/webkit/gears/AndroidGpsLocationProvider.java new file mode 100644 index 0000000..3646042 --- /dev/null +++ b/core/java/android/webkit/gears/AndroidGpsLocationProvider.java @@ -0,0 +1,156 @@ +// Copyright 2008, The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebView; + +/** + * GPS provider implementation for Android. + */ +public final class AndroidGpsLocationProvider implements LocationListener { + /** + * Logging tag + */ + private static final String TAG = "Gears-J-GpsProvider"; + /** + * Our location manager instance. + */ + private LocationManager locationManager; + /** + * The native object ID. + */ + private long nativeObject; + + public AndroidGpsLocationProvider(WebView webview, long object) { + nativeObject = object; + locationManager = (LocationManager) webview.getContext().getSystemService( + Context.LOCATION_SERVICE); + if (locationManager == null) { + Log.e(TAG, + "AndroidGpsLocationProvider: could not get location manager."); + throw new NullPointerException( + "AndroidGpsLocationProvider: locationManager is null."); + } + // Register for location updates. + try { + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, + this); + } catch (IllegalArgumentException ex) { + Log.e(TAG, + "AndroidLocationGpsProvider: could not register for updates: " + ex); + throw ex; + } catch (SecurityException ex) { + Log.e(TAG, + "AndroidGpsLocationProvider: not allowed to register for update: " + + ex); + throw ex; + } + } + + /** + * Called when the provider is no longer needed. + */ + public void shutdown() { + locationManager.removeUpdates(this); + Log.i(TAG, "GPS provider closed."); + } + + /** + * Called when the location has changed. + * @param location The new location, as a Location object. + */ + public void onLocationChanged(Location location) { + Log.i(TAG, "Location changed: " + location); + nativeLocationChanged(location, nativeObject); + } + + /** + * Called when the provider status changes. + * + * @param provider the name of the location provider associated with this + * update. + * @param status {@link LocationProvider#OUT_OF_SERVICE} if the + * provider is out of service, and this is not expected to change in the + * near future; {@link LocationProvider#TEMPORARILY_UNAVAILABLE} if + * the provider is temporarily unavailable but is expected to be available + * shortly; and {@link LocationProvider#AVAILABLE} if the + * provider is currently available. + * @param extras an optional Bundle which will contain provider specific + * status variables (such as number of satellites). + */ + public void onStatusChanged(String provider, int status, Bundle extras) { + Log.i(TAG, "Provider " + provider + " status changed to " + status); + if (status == LocationProvider.OUT_OF_SERVICE || + status == LocationProvider.TEMPORARILY_UNAVAILABLE) { + nativeProviderError(false, nativeObject); + } + } + + /** + * Called when the provider is enabled. + * + * @param provider the name of the location provider that is now enabled. + */ + public void onProviderEnabled(String provider) { + Log.i(TAG, "Provider " + provider + " enabled."); + // No need to notify the native side. It's enough to start sending + // valid position fixes again. + } + + /** + * Called when the provider is disabled. + * + * @param provider the name of the location provider that is now disabled. + */ + public void onProviderDisabled(String provider) { + Log.i(TAG, "Provider " + provider + " disabled."); + nativeProviderError(true, nativeObject); + } + + /** + * The native method called when a new location is available. + * @param location is the new Location instance to pass to the native side. + * @param nativeObject is a pointer to the corresponding + * AndroidGpsLocationProvider C++ instance. + */ + private native void nativeLocationChanged(Location location, long object); + + /** + * The native method called when there is a GPS provder error. + * @param isDisabled is true when the error signifies the fact that the GPS + * HW is disabled. For other errors, this param is always false. + * @param nativeObject is a pointer to the corresponding + * AndroidGpsLocationProvider C++ instance. + */ + private native void nativeProviderError(boolean isDisabled, long object); +} diff --git a/core/java/android/webkit/gears/AndroidRadioDataProvider.java b/core/java/android/webkit/gears/AndroidRadioDataProvider.java new file mode 100644 index 0000000..c920d45 --- /dev/null +++ b/core/java/android/webkit/gears/AndroidRadioDataProvider.java @@ -0,0 +1,244 @@ +// Copyright 2008, The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.Context; +import android.telephony.CellLocation; +import android.telephony.ServiceState; +import android.telephony.gsm.GsmCellLocation; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.webkit.WebView; + +/** + * Radio data provider implementation for Android. + */ +public final class AndroidRadioDataProvider extends PhoneStateListener { + + /** Logging tag */ + private static final String TAG = "Gears-J-RadioProvider"; + + /** Network types */ + private static final int RADIO_TYPE_UNKNOWN = 0; + private static final int RADIO_TYPE_GSM = 1; + private static final int RADIO_TYPE_WCDMA = 2; + + /** Simple container for radio data */ + public static final class RadioData { + public int cellId = -1; + public int locationAreaCode = -1; + public int signalStrength = -1; + public int mobileCountryCode = -1; + public int mobileNetworkCode = -1; + public int homeMobileCountryCode = -1; + public int homeMobileNetworkCode = -1; + public int radioType = RADIO_TYPE_UNKNOWN; + public String carrierName; + + /** + * Constructs radioData object from the given telephony data. + * @param telephonyManager contains the TelephonyManager instance. + * @param cellLocation contains information about the current GSM cell. + * @param signalStrength is the strength of the network signal. + * @param serviceState contains information about the network service. + * @return a new RadioData object populated with the currently + * available network information or null if there isn't + * enough information. + */ + public static RadioData getInstance(TelephonyManager telephonyManager, + CellLocation cellLocation, int signalStrength, + ServiceState serviceState) { + + if (!(cellLocation instanceof GsmCellLocation)) { + // This also covers the case when cellLocation is null. + // When that happens, we do not bother creating a + // RadioData instance. + return null; + } + + RadioData radioData = new RadioData(); + GsmCellLocation gsmCellLocation = (GsmCellLocation) cellLocation; + + // Extract the cell id, LAC, and signal strength. + radioData.cellId = gsmCellLocation.getCid(); + radioData.locationAreaCode = gsmCellLocation.getLac(); + radioData.signalStrength = signalStrength; + + // Extract the home MCC and home MNC. + String operator = telephonyManager.getSimOperator(); + radioData.setMobileCodes(operator, true); + + if (serviceState != null) { + // Extract the carrier name. + radioData.carrierName = serviceState.getOperatorAlphaLong(); + + // Extract the MCC and MNC. + operator = serviceState.getOperatorNumeric(); + radioData.setMobileCodes(operator, false); + } + + // Finally get the radio type. + int type = telephonyManager.getNetworkType(); + if (type == TelephonyManager.NETWORK_TYPE_UMTS) { + radioData.radioType = RADIO_TYPE_WCDMA; + } else if (type == TelephonyManager.NETWORK_TYPE_GPRS + || type == TelephonyManager.NETWORK_TYPE_EDGE) { + radioData.radioType = RADIO_TYPE_GSM; + } + + // Print out what we got. + Log.i(TAG, "Got the following data:"); + Log.i(TAG, "CellId: " + radioData.cellId); + Log.i(TAG, "LAC: " + radioData.locationAreaCode); + Log.i(TAG, "MNC: " + radioData.mobileNetworkCode); + Log.i(TAG, "MCC: " + radioData.mobileCountryCode); + Log.i(TAG, "home MNC: " + radioData.homeMobileNetworkCode); + Log.i(TAG, "home MCC: " + radioData.homeMobileCountryCode); + Log.i(TAG, "Signal strength: " + radioData.signalStrength); + Log.i(TAG, "Carrier: " + radioData.carrierName); + Log.i(TAG, "Network type: " + radioData.radioType); + + return radioData; + } + + private RadioData() {} + + /** + * Parses a string containing a mobile country code and a mobile + * network code and sets the corresponding member variables. + * @param codes is the string to parse. + * @param homeValues flags whether the codes are for the home operator. + */ + private void setMobileCodes(String codes, boolean homeValues) { + if (codes != null) { + try { + // The operator numeric format is 3 digit country code plus 2 or + // 3 digit network code. + int mcc = Integer.parseInt(codes.substring(0, 3)); + int mnc = Integer.parseInt(codes.substring(3)); + if (homeValues) { + homeMobileCountryCode = mcc; + homeMobileNetworkCode = mnc; + } else { + mobileCountryCode = mcc; + mobileNetworkCode = mnc; + } + } catch (IndexOutOfBoundsException ex) { + Log.e( + TAG, + "AndroidRadioDataProvider: Invalid operator numeric data: " + ex); + } catch (NumberFormatException ex) { + Log.e( + TAG, + "AndroidRadioDataProvider: Operator numeric format error: " + ex); + } + } + } + }; + + /** The native object ID */ + private long nativeObject; + + /** The last known cellLocation */ + private CellLocation cellLocation = null; + + /** The last known signal strength */ + private int signalStrength = -1; + + /** The last known serviceState */ + private ServiceState serviceState = null; + + /** + * Our TelephonyManager instance. + */ + private TelephonyManager telephonyManager; + + /** + * Public constructor. Uses the webview to get the Context object. + */ + public AndroidRadioDataProvider(WebView webview, long object) { + super(); + nativeObject = object; + telephonyManager = (TelephonyManager) webview.getContext().getSystemService( + Context.TELEPHONY_SERVICE); + if (telephonyManager == null) { + Log.e(TAG, + "AndroidRadioDataProvider: could not get tepephony manager."); + throw new NullPointerException( + "AndroidRadioDataProvider: telephonyManager is null."); + } + + // Register for cell id, signal strength and service state changed + // notifications. + telephonyManager.listen(this, PhoneStateListener.LISTEN_CELL_LOCATION + | PhoneStateListener.LISTEN_SIGNAL_STRENGTH + | PhoneStateListener.LISTEN_SERVICE_STATE); + } + + /** + * Should be called when the provider is no longer needed. + */ + public void shutdown() { + telephonyManager.listen(this, PhoneStateListener.LISTEN_NONE); + Log.i(TAG, "AndroidRadioDataProvider shutdown."); + } + + @Override + public void onServiceStateChanged(ServiceState state) { + serviceState = state; + notifyListeners(); + } + + @Override + public void onSignalStrengthChanged(int asu) { + signalStrength = asu; + notifyListeners(); + } + + @Override + public void onCellLocationChanged(CellLocation location) { + cellLocation = location; + notifyListeners(); + } + + private void notifyListeners() { + RadioData radioData = RadioData.getInstance(telephonyManager, cellLocation, + signalStrength, serviceState); + if (radioData != null) { + onUpdateAvailable(radioData, nativeObject); + } + } + + /** + * The native method called when new radio data is available. + * @param radioData is the RadioData instance to pass to the native side. + * @param nativeObject is a pointer to the corresponding + * AndroidRadioDataProvider C++ instance. + */ + private static native void onUpdateAvailable( + RadioData radioData, long nativeObject); +} diff --git a/core/java/android/webkit/gears/AndroidWifiDataProvider.java b/core/java/android/webkit/gears/AndroidWifiDataProvider.java new file mode 100644 index 0000000..7379f59 --- /dev/null +++ b/core/java/android/webkit/gears/AndroidWifiDataProvider.java @@ -0,0 +1,136 @@ +// Copyright 2008, Google Inc. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.Looper; +import android.util.Config; +import android.util.Log; +import android.webkit.WebView; +import java.util.List; + +/** + * WiFi data provider implementation for Android. + * {@hide} + */ +public final class AndroidWifiDataProvider extends BroadcastReceiver { + /** + * Logging tag + */ + private static final String TAG = "Gears-J-WifiProvider"; + /** + * Our Wifi manager instance. + */ + private WifiManager mWifiManager; + /** + * The native object ID. + */ + private long mNativeObject; + /** + * The Context instance. + */ + private Context mContext; + + /** + * Constructs a instance of this class and registers for wifi scan + * updates. Note that this constructor must be called on a Looper + * thread. Suitable threads can be created on the native side using + * the AndroidLooperThread C++ class. + */ + public AndroidWifiDataProvider(WebView webview, long object) { + mNativeObject = object; + mContext = webview.getContext(); + mWifiManager = + (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE); + if (mWifiManager == null) { + Log.e(TAG, + "AndroidWifiDataProvider: could not get location manager."); + throw new NullPointerException( + "AndroidWifiDataProvider: locationManager is null."); + } + + // Create a Handler that identifies the message loop associated + // with the current thread. Note that it is not necessary to + // override handleMessage() at all since the Intent + // ReceiverDispatcher (see the ActivityThread class) only uses + // this handler to post a Runnable to this thread's loop. + Handler handler = new Handler(Looper.myLooper()); + + IntentFilter filter = new IntentFilter(); + filter.addAction(mWifiManager.SCAN_RESULTS_AVAILABLE_ACTION); + mContext.registerReceiver(this, filter, null, handler); + + // Get the last scan results and pass them to the native side. + // We can't just invoke the callback here, so we queue a message + // to this thread's loop. + handler.post(new Runnable() { + public void run() { + onUpdateAvailable(mWifiManager.getScanResults(), mNativeObject); + } + }); + } + + /** + * Called when the provider is no longer needed. + */ + public void shutdown() { + mContext.unregisterReceiver(this); + if (Config.LOGV) { + Log.v(TAG, "Wifi provider closed."); + } + } + + /** + * This method is called when the AndroidWifiDataProvider is receiving an + * Intent broadcast. + * @param context The Context in which the receiver is running. + * @param intent The Intent being received. + */ + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals( + mWifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + if (Config.LOGV) { + Log.v(TAG, "Wifi scan resulst available"); + } + onUpdateAvailable(mWifiManager.getScanResults(), mNativeObject); + } + } + + /** + * The native method called when new wifi data is available. + * @param scanResults is a list of ScanResults to pass to the native side. + * @param nativeObject is a pointer to the corresponding + * AndroidWifiDataProvider C++ instance. + */ + private static native void onUpdateAvailable( + List<ScanResult> scanResults, long nativeObject); +} diff --git a/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java new file mode 100644 index 0000000..0569255 --- /dev/null +++ b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java @@ -0,0 +1,1122 @@ +// Copyright 2008, The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.net.http.Headers; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Config; +import android.util.Log; +import android.webkit.CacheManager; +import android.webkit.CacheManager.CacheResult; +import android.webkit.CookieManager; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import java.lang.StringBuilder; +import java.util.Date; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.HttpResponse; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.client.*; +import org.apache.http.client.methods.*; +import org.apache.http.impl.client.AbstractHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.conn.ssl.StrictHostnameVerifier; +import org.apache.http.impl.cookie.DateUtils; +import org.apache.http.util.CharArrayBuffer; + +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Performs the underlying HTTP/HTTPS GET, POST, HEAD, PUT, DELETE requests. + * <p> These are performed synchronously (blocking). The caller should + * ensure that it is in a background thread if asynchronous behavior + * is required. All data is pushed, so there is no need for JNI native + * callbacks. + * <p> This uses Apache's HttpClient framework to perform most + * of the underlying network activity. The Android brower's cache, + * android.webkit.CacheManager, is also used when caching is enabled, + * and updated with new data. The android.webkit.CookieManager is also + * queried and updated as necessary. + * <p> The public interface is designed to be called by native code + * through JNI, and to simplify coding none of the public methods will + * surface a checked exception. Unchecked exceptions may still be + * raised but only if the system is in an ill state, such as out of + * memory. + * <p> TODO: This isn't plumbed into LocalServer yet. Mutually + * dependent on LocalServer - will attach the two together once both + * are submitted. + */ +public final class ApacheHttpRequestAndroid { + /** Debug logging tag. */ + private static final String LOG_TAG = "Gears-J"; + /** HTTP response header line endings are CR-LF style. */ + private static final String HTTP_LINE_ENDING = "\r\n"; + /** Safe MIME type to use whenever it isn't specified. */ + private static final String DEFAULT_MIME_TYPE = "text/plain"; + /** Case-sensitive header keys */ + public static final String KEY_CONTENT_LENGTH = "Content-Length"; + public static final String KEY_EXPIRES = "Expires"; + public static final String KEY_LAST_MODIFIED = "Last-Modified"; + public static final String KEY_ETAG = "ETag"; + public static final String KEY_LOCATION = "Location"; + public static final String KEY_CONTENT_TYPE = "Content-Type"; + /** Number of bytes to send and receive on the HTTP connection in + * one go. */ + private static final int BUFFER_SIZE = 4096; + + /** The first element of the String[] value in a headers map is the + * unmodified (case-sensitive) key. */ + public static final int HEADERS_MAP_INDEX_KEY = 0; + /** The second element of the String[] value in a headers map is the + * associated value. */ + public static final int HEADERS_MAP_INDEX_VALUE = 1; + + /** Request headers, as key -> value map. */ + // TODO: replace this design by a simpler one (the C++ side has to + // be modified too), where we do not store both the original header + // and the lowercase one. + private Map<String, String[]> mRequestHeaders = + new HashMap<String, String[]>(); + /** Response headers, as a lowercase key -> value map. */ + private Map<String, String[]> mResponseHeaders = + new HashMap<String, String[]>(); + /** The URL used for createCacheResult() */ + private String mCacheResultUrl; + /** CacheResult being saved into, if inserting a new cache entry. */ + private CacheResult mCacheResult; + /** Initialized by initChildThread(). Used to target abort(). */ + private Thread mBridgeThread; + + /** Our HttpClient */ + private AbstractHttpClient mClient; + /** The HttpMethod associated with this request */ + private HttpRequestBase mMethod; + /** The complete response line e.g "HTTP/1.0 200 OK" */ + private String mResponseLine; + /** HTTP body stream, setup after connection. */ + private InputStream mBodyInputStream; + + /** HTTP Response Entity */ + private HttpResponse mResponse; + + /** Post Entity, used to stream the request to the server */ + private StreamEntity mPostEntity = null; + /** Content lenght, mandatory when using POST */ + private long mContentLength; + + /** The request executes in a parallel thread */ + private Thread mHttpThread = null; + /** protect mHttpThread, if interrupt() is called concurrently */ + private Lock mHttpThreadLock = new ReentrantLock(); + /** Flag set to true when the request thread is joined */ + private boolean mConnectionFinished = false; + /** Flag set to true by interrupt() and/or connection errors */ + private boolean mConnectionFailed = false; + /** Lock protecting the access to mConnectionFailed */ + private Lock mConnectionFailedLock = new ReentrantLock(); + + /** Lock on the loop in StreamEntity */ + private Lock mStreamingReadyLock = new ReentrantLock(); + /** Condition variable used to signal the loop is ready... */ + private Condition mStreamingReady = mStreamingReadyLock.newCondition(); + + /** Used to pass around the block of data POSTed */ + private Buffer mBuffer = new Buffer(); + /** Used to signal that the block of data has been written */ + private SignalConsumed mSignal = new SignalConsumed(); + + // inner classes + + /** + * Implements the http request + */ + class Connection implements Runnable { + public void run() { + boolean problem = false; + try { + if (Config.LOGV) { + Log.i(LOG_TAG, "REQUEST : " + mMethod.getRequestLine()); + } + mResponse = mClient.execute(mMethod); + if (mResponse != null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "response (status line): " + + mResponse.getStatusLine()); + } + mResponseLine = "" + mResponse.getStatusLine(); + } else { + if (Config.LOGV) { + Log.i(LOG_TAG, "problem, response == null"); + } + problem = true; + } + } catch (IOException e) { + Log.e(LOG_TAG, "Connection IO exception ", e); + problem = true; + } catch (RuntimeException e) { + Log.e(LOG_TAG, "Connection runtime exception ", e); + problem = true; + } + + if (!problem) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Request complete (" + + mMethod.getRequestLine() + ")"); + } + } else { + mConnectionFailedLock.lock(); + mConnectionFailed = true; + mConnectionFailedLock.unlock(); + if (Config.LOGV) { + Log.i(LOG_TAG, "Request FAILED (" + + mMethod.getRequestLine() + ")"); + } + // We abort the execution in order to shutdown and release + // the underlying connection + mMethod.abort(); + if (mPostEntity != null) { + // If there is a post entity, we need to wake it up from + // a potential deadlock + mPostEntity.signalOutputStream(); + } + } + } + } + + /** + * simple buffer class implementing a producer/consumer model + */ + class Buffer { + private DataPacket mPacket; + private boolean mEmpty = true; + public synchronized void put(DataPacket packet) { + while (!mEmpty) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while putting " + + "a DataPacket in the Buffer: " + e); + } + } + } + mPacket = packet; + mEmpty = false; + notify(); + } + public synchronized DataPacket get() { + while (mEmpty) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while getting " + + "a DataPacket in the Buffer: " + e); + } + } + } + mEmpty = true; + notify(); + return mPacket; + } + } + + /** + * utility class used to block until the packet is signaled as being + * consumed + */ + class SignalConsumed { + private boolean mConsumed = false; + public synchronized void waitUntilPacketConsumed() { + while (!mConsumed) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while waiting " + + "until a DataPacket is consumed: " + e); + } + } + } + mConsumed = false; + notify(); + } + public synchronized void packetConsumed() { + while (mConsumed) { + try { + wait(); + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException while indicating " + + "that the DataPacket has been consumed: " + e); + } + } + } + mConsumed = true; + notify(); + } + } + + /** + * Utility class encapsulating a packet of data + */ + class DataPacket { + private byte[] mContent; + private int mLength; + public DataPacket(byte[] content, int length) { + mContent = content; + mLength = length; + } + public byte[] getBytes() { + return mContent; + } + public int getLength() { + return mLength; + } + } + + /** + * HttpEntity class to write the bytes received by the C++ thread + * on the connection outputstream, in a streaming way. + * This entity is executed in the request thread. + * The writeTo() method is automatically called by the + * HttpPost execution; upon reception, we loop while receiving + * the data packets from the main thread, until completion + * or error. When done, we flush the outputstream. + * The main thread (sendPostData()) also blocks until the + * outputstream is made available (or an error happens) + */ + class StreamEntity implements HttpEntity { + private OutputStream mOutputStream; + + // HttpEntity interface methods + + public boolean isRepeatable() { + return false; + } + + public boolean isChunked() { + return false; + } + + public long getContentLength() { + return mContentLength; + } + + public Header getContentType() { + return null; + } + + public Header getContentEncoding() { + return null; + } + + public InputStream getContent() throws IOException { + return null; + } + + public void writeTo(final OutputStream out) throws IOException { + // We signal that the outputstream is available + mStreamingReadyLock.lock(); + mOutputStream = out; + mStreamingReady.signal(); + mStreamingReadyLock.unlock(); + + // We then loop waiting on messages to process. + boolean finished = false; + while (!finished) { + DataPacket packet = mBuffer.get(); + if (packet == null) { + finished = true; + } else { + write(packet); + } + mSignal.packetConsumed(); + mConnectionFailedLock.lock(); + if (mConnectionFailed) { + if (Config.LOGV) { + Log.i(LOG_TAG, "stopping loop on error"); + } + finished = true; + } + mConnectionFailedLock.unlock(); + } + if (Config.LOGV) { + Log.i(LOG_TAG, "flushing the outputstream..."); + } + mOutputStream.flush(); + } + + public boolean isStreaming() { + return true; + } + + public void consumeContent() throws IOException { + // Nothing to release + } + + // local methods + + private void write(DataPacket packet) { + try { + if (mOutputStream == null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "NO OUTPUT STREAM !!!"); + } + return; + } + mOutputStream.write(packet.getBytes(), 0, packet.getLength()); + mOutputStream.flush(); + } catch (IOException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "exc: " + e); + } + mConnectionFailedLock.lock(); + mConnectionFailed = true; + mConnectionFailedLock.unlock(); + } + } + + public boolean isReady() { + mStreamingReadyLock.lock(); + try { + if (mOutputStream == null) { + mStreamingReady.await(); + } + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "InterruptedException in " + + "StreamEntity::isReady() : ", e); + } + } finally { + mStreamingReadyLock.unlock(); + } + if (mOutputStream == null) { + return false; + } + return true; + } + + public void signalOutputStream() { + mStreamingReadyLock.lock(); + mStreamingReady.signal(); + mStreamingReadyLock.unlock(); + } + } + + /** + * Initialize mBridgeThread using the TLS value of + * Thread.currentThread(). Called on start up of the native child + * thread. + */ + public synchronized void initChildThread() { + mBridgeThread = Thread.currentThread(); + } + + public void setContentLength(long length) { + mContentLength = length; + } + + /** + * Analagous to the native-side HttpRequest::open() function. This + * initializes an underlying HttpClient method, but does + * not go to the wire. On success, this enables a call to send() to + * initiate the transaction. + * + * @param method The HTTP method, e.g GET or POST. + * @param url The URL to open. + * @return True on success with a complete HTTP response. + * False on failure. + */ + public synchronized boolean open(String method, String url) { + if (Config.LOGV) { + Log.i(LOG_TAG, "open " + method + " " + url); + } + // Create the client + if (mConnectionFailed) { + // interrupt() could have been called even before open() + return false; + } + mClient = new DefaultHttpClient(); + mClient.setHttpRequestRetryHandler( + new DefaultHttpRequestRetryHandler(0, false)); + mBodyInputStream = null; + mResponseLine = null; + mResponseHeaders = null; + mPostEntity = null; + mHttpThread = null; + mConnectionFailed = false; + mConnectionFinished = false; + + // Create the method. We support everything that + // Apache HttpClient supports, apart from TRACE. + if ("GET".equalsIgnoreCase(method)) { + mMethod = new HttpGet(url); + } else if ("POST".equalsIgnoreCase(method)) { + mMethod = new HttpPost(url); + mPostEntity = new StreamEntity(); + ((HttpPost)mMethod).setEntity(mPostEntity); + } else if ("HEAD".equalsIgnoreCase(method)) { + mMethod = new HttpHead(url); + } else if ("PUT".equalsIgnoreCase(method)) { + mMethod = new HttpPut(url); + } else if ("DELETE".equalsIgnoreCase(method)) { + mMethod = new HttpDelete(url); + } else { + if (Config.LOGV) { + Log.i(LOG_TAG, "Method " + method + " not supported"); + } + return false; + } + HttpParams params = mClient.getParams(); + // We handle the redirections C++-side + HttpClientParams.setRedirecting(params, false); + HttpProtocolParams.setUseExpectContinue(params, false); + return true; + } + + /** + * We use this to start the connection thread (doing the method execute). + * We usually always return true here, as the connection will run its + * course in the thread. + * We only return false if interrupted beforehand -- if a connection + * problem happens, we will thus fail in either sendPostData() or + * parseHeaders(). + */ + public synchronized boolean connectToRemote() { + boolean ret = false; + applyRequestHeaders(); + mConnectionFailedLock.lock(); + if (!mConnectionFailed) { + mHttpThread = new Thread(new Connection()); + mHttpThread.start(); + } + ret = mConnectionFailed; + mConnectionFailedLock.unlock(); + return !ret; + } + + /** + * Get the complete response line of the HTTP request. Only valid on + * completion of the transaction. + * @return The complete HTTP response line, e.g "HTTP/1.0 200 OK". + */ + public synchronized String getResponseLine() { + return mResponseLine; + } + + /** + * Wait for the request thread completion + * (unless already finished) + */ + private void waitUntilConnectionFinished() { + if (Config.LOGV) { + Log.i(LOG_TAG, "waitUntilConnectionFinished(" + + mConnectionFinished + ")"); + } + if (!mConnectionFinished) { + if (mHttpThread != null) { + try { + mHttpThread.join(); + mConnectionFinished = true; + if (Config.LOGV) { + Log.i(LOG_TAG, "http thread joined"); + } + } catch (InterruptedException e) { + if (Config.LOGV) { + Log.i(LOG_TAG, "interrupted: " + e); + } + } + } else { + Log.e(LOG_TAG, ">>> Trying to join on mHttpThread " + + "when it does not exist!"); + } + } + } + + // Headers handling + + /** + * Receive all headers from the server and populate + * mResponseHeaders. + * @return True if headers are successfully received, False on + * connection error. + */ + public synchronized boolean parseHeaders() { + mConnectionFailedLock.lock(); + if (mConnectionFailed) { + mConnectionFailedLock.unlock(); + return false; + } + mConnectionFailedLock.unlock(); + waitUntilConnectionFinished(); + mResponseHeaders = new HashMap<String, String[]>(); + if (mResponse == null) + return false; + + Header[] headers = mResponse.getAllHeaders(); + for (int i = 0; i < headers.length; i++) { + Header header = headers[i]; + if (Config.LOGV) { + Log.i(LOG_TAG, "header " + header.getName() + + " -> " + header.getValue()); + } + setResponseHeader(header.getName(), header.getValue()); + } + + return true; + } + + /** + * Set a header to send with the HTTP request. Will not take effect + * on a transaction already in progress. The key is associated + * case-insensitive, but stored case-sensitive. + * @param name The name of the header, e.g "Set-Cookie". + * @param value The value for this header, e.g "text/html". + */ + public synchronized void setRequestHeader(String name, String value) { + String[] mapValue = { name, value }; + if (Config.LOGV) { + Log.i(LOG_TAG, "setRequestHeader: " + name + " => " + value); + } + if (name.equalsIgnoreCase(KEY_CONTENT_LENGTH)) { + setContentLength(Long.parseLong(value)); + } else { + mRequestHeaders.put(name.toLowerCase(), mapValue); + } + } + + /** + * Returns the value associated with the given request header. + * @param name The name of the request header, non-null, case-insensitive. + * @return The value associated with the request header, or null if + * not set, or error. + */ + public synchronized String getRequestHeader(String name) { + String[] value = mRequestHeaders.get(name.toLowerCase()); + if (value != null) { + return value[HEADERS_MAP_INDEX_VALUE]; + } else { + return null; + } + } + + private void applyRequestHeaders() { + if (mMethod == null) + return; + Iterator<String[]> it = mRequestHeaders.values().iterator(); + while (it.hasNext()) { + // Set the key case-sensitive. + String[] entry = it.next(); + if (Config.LOGV) { + Log.i(LOG_TAG, "apply header " + entry[HEADERS_MAP_INDEX_KEY] + + " => " + entry[HEADERS_MAP_INDEX_VALUE]); + } + mMethod.setHeader(entry[HEADERS_MAP_INDEX_KEY], + entry[HEADERS_MAP_INDEX_VALUE]); + } + } + + /** + * Returns the value associated with the given response header. + * @param name The name of the response header, non-null, case-insensitive. + * @return The value associated with the response header, or null if + * not set or error. + */ + public synchronized String getResponseHeader(String name) { + if (mResponseHeaders != null) { + String[] value = mResponseHeaders.get(name.toLowerCase()); + if (value != null) { + return value[HEADERS_MAP_INDEX_VALUE]; + } else { + return null; + } + } else { + if (Config.LOGV) { + Log.i(LOG_TAG, "getResponseHeader() called but " + + "response not received"); + } + return null; + } + } + + /** + * Return all response headers, separated by CR-LF line endings, and + * ending with a trailing blank line. This mimics the format of the + * raw response header up to but not including the body. + * @return A string containing the entire response header. + */ + public synchronized String getAllResponseHeaders() { + if (mResponseHeaders == null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "getAllResponseHeaders() called but " + + "response not received"); + } + return null; + } + StringBuilder result = new StringBuilder(); + Iterator<String[]> it = mResponseHeaders.values().iterator(); + while (it.hasNext()) { + String[] entry = it.next(); + // Output the "key: value" lines. + result.append(entry[HEADERS_MAP_INDEX_KEY]); + result.append(": "); + result.append(entry[HEADERS_MAP_INDEX_VALUE]); + result.append(HTTP_LINE_ENDING); + } + result.append(HTTP_LINE_ENDING); + return result.toString(); + } + + + /** + * Set a response header and associated value. The key is associated + * case-insensitively, but stored case-sensitively. + * @param name Case sensitive request header key. + * @param value The associated value. + */ + private void setResponseHeader(String name, String value) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Set response header " + name + ": " + value); + } + String mapValue[] = { name, value }; + mResponseHeaders.put(name.toLowerCase(), mapValue); + } + + // Cookie handling + + /** + * Get the cookie for the given URL. + * @param url The fully qualified URL. + * @return A string containing the cookie for the URL if it exists, + * or null if not. + */ + public static String getCookieForUrl(String url) { + // Get the cookie for this URL, set as a header + return CookieManager.getInstance().getCookie(url); + } + + /** + * Set the cookie for the given URL. + * @param url The fully qualified URL. + * @param cookie The new cookie value. + * @return A string containing the cookie for the URL if it exists, + * or null if not. + */ + public static void setCookieForUrl(String url, String cookie) { + // Get the cookie for this URL, set as a header + CookieManager.getInstance().setCookie(url, cookie); + } + + // Cache handling + + /** + * Perform a request using LocalServer if possible. Initializes + * class members so that receive() will obtain data from the stream + * provided by the response. + * @param url The fully qualified URL to try in LocalServer. + * @return True if the url was found and is now setup to receive. + * False if not found, with no side-effect. + */ + public synchronized boolean useLocalServerResult(String url) { + UrlInterceptHandlerGears handler = + UrlInterceptHandlerGears.getInstance(); + if (handler == null) { + return false; + } + UrlInterceptHandlerGears.ServiceResponse serviceResponse = + handler.getServiceResponse(url, mRequestHeaders); + if (serviceResponse == null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "No response in LocalServer"); + } + return false; + } + // LocalServer will handle this URL. Initialize stream and + // response. + mBodyInputStream = serviceResponse.getInputStream(); + mResponseLine = serviceResponse.getStatusLine(); + mResponseHeaders = serviceResponse.getResponseHeaders(); + if (Config.LOGV) { + Log.i(LOG_TAG, "Got response from LocalServer: " + mResponseLine); + } + return true; + } + + /** + * Perform a request using the cache result if present. Initializes + * class members so that receive() will obtain data from the cache. + * @param url The fully qualified URL to try in the cache. + * @return True is the url was found and is now setup to receive + * from cache. False if not found, with no side-effect. + */ + public synchronized boolean useCacheResult(String url) { + // Try the browser's cache. CacheManager wants a Map<String, String>. + Map<String, String> cacheRequestHeaders = new HashMap<String, String>(); + Iterator<Map.Entry<String, String[]>> it = + mRequestHeaders.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<String, String[]> entry = it.next(); + cacheRequestHeaders.put( + entry.getKey(), + entry.getValue()[HEADERS_MAP_INDEX_VALUE]); + } + CacheResult mCacheResult = + CacheManager.getCacheFile(url, cacheRequestHeaders); + if (mCacheResult == null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "No CacheResult for " + url); + } + return false; + } + if (Config.LOGV) { + Log.i(LOG_TAG, "Got CacheResult from browser cache"); + } + // Check for expiry. -1 is "never", otherwise milliseconds since 1970. + // Can be compared to System.currentTimeMillis(). + long expires = mCacheResult.getExpires(); + if (expires >= 0 && System.currentTimeMillis() >= expires) { + if (Config.LOGV) { + Log.i(LOG_TAG, "CacheResult expired " + + (System.currentTimeMillis() - expires) + + " milliseconds ago"); + } + // Cache hit has expired. Do not return it. + return false; + } + // Setup the mBodyInputStream to come from the cache. + mBodyInputStream = mCacheResult.getInputStream(); + if (mBodyInputStream == null) { + // Cache result may have gone away. + if (Config.LOGV) { + Log.i(LOG_TAG, "No mBodyInputStream for CacheResult " + url); + } + return false; + } + // Cache hit. Parse headers. + synthesizeHeadersFromCacheResult(mCacheResult); + return true; + } + + /** + * Take the limited set of headers in a CacheResult and synthesize + * response headers. + * @param cacheResult A CacheResult to populate mResponseHeaders with. + */ + private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) { + int statusCode = cacheResult.getHttpStatusCode(); + // The status message is informal, so we can greatly simplify it. + String statusMessage; + if (statusCode >= 200 && statusCode < 300) { + statusMessage = "OK"; + } else if (statusCode >= 300 && statusCode < 400) { + statusMessage = "MOVED"; + } else { + statusMessage = "UNAVAILABLE"; + } + // Synthesize the response line. + mResponseLine = "HTTP/1.1 " + statusCode + " " + statusMessage; + if (Config.LOGV) { + Log.i(LOG_TAG, "Synthesized " + mResponseLine); + } + // Synthesize the returned headers from cache. + mResponseHeaders = new HashMap<String, String[]>(); + String contentLength = Long.toString(cacheResult.getContentLength()); + setResponseHeader(KEY_CONTENT_LENGTH, contentLength); + long expires = cacheResult.getExpires(); + if (expires >= 0) { + // "Expires" header is valid and finite. Milliseconds since 1970 + // epoch, formatted as RFC-1123. + String expiresString = DateUtils.formatDate(new Date(expires)); + setResponseHeader(KEY_EXPIRES, expiresString); + } + String lastModified = cacheResult.getLastModified(); + if (lastModified != null) { + // Last modification time of the page. Passed end-to-end, but + // not used by us. + setResponseHeader(KEY_LAST_MODIFIED, lastModified); + } + String eTag = cacheResult.getETag(); + if (eTag != null) { + // Entity tag. A kind of GUID to identify identical resources. + setResponseHeader(KEY_ETAG, eTag); + } + String location = cacheResult.getLocation(); + if (location != null) { + // If valid, refers to the location of a redirect. + setResponseHeader(KEY_LOCATION, location); + } + String mimeType = cacheResult.getMimeType(); + if (mimeType == null) { + // Use a safe default MIME type when none is + // specified. "text/plain" is safe to render in the browser + // window (even if large) and won't be intepreted as anything + // that would cause execution. + mimeType = DEFAULT_MIME_TYPE; + } + String encoding = cacheResult.getEncoding(); + // Encoding may not be specified. No default. + String contentType = mimeType; + if (encoding != null) { + if (encoding.length() > 0) { + contentType += "; charset=" + encoding; + } + } + setResponseHeader(KEY_CONTENT_TYPE, contentType); + } + + /** + * Create a CacheResult for this URL. This enables the repsonse body + * to be sent in calls to appendCacheResult(). + * @param url The fully qualified URL to add to the cache. + * @param responseCode The response code returned for the request, e.g 200. + * @param mimeType The MIME type of the body, e.g "text/plain". + * @param encoding The encoding, e.g "utf-8". Use "" for unknown. + */ + public synchronized boolean createCacheResult( + String url, int responseCode, String mimeType, String encoding) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Making cache entry for " + url); + } + // Take the headers and parse them into a format needed by + // CacheManager. + Headers cacheHeaders = new Headers(); + Iterator<Map.Entry<String, String[]>> it = + mResponseHeaders.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry<String, String[]> entry = it.next(); + // Headers.parseHeader() expects lowercase keys. + String keyValue = entry.getKey() + ": " + + entry.getValue()[HEADERS_MAP_INDEX_VALUE]; + CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length()); + buffer.append(keyValue); + // Parse it into the header container. + cacheHeaders.parseHeader(buffer); + } + mCacheResult = CacheManager.createCacheFile( + url, responseCode, cacheHeaders, mimeType, true); + if (mCacheResult != null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Saving into cache"); + } + mCacheResult.setEncoding(encoding); + mCacheResultUrl = url; + return true; + } else { + if (Config.LOGV) { + Log.i(LOG_TAG, "Couldn't create mCacheResult"); + } + return false; + } + } + + /** + * Add data from the response body to the CacheResult created with + * createCacheResult(). + * @param data A byte array of the next sequential bytes in the + * response body. + * @param bytes The number of bytes to write from the start of + * the array. + * @return True if all bytes successfully written, false on failure. + */ + public synchronized boolean appendCacheResult(byte[] data, int bytes) { + if (mCacheResult == null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "appendCacheResult() called without a " + + "CacheResult initialized"); + } + return false; + } + try { + mCacheResult.getOutputStream().write(data, 0, bytes); + } catch (IOException ex) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Got IOException writing cache data: " + ex); + } + return false; + } + return true; + } + + /** + * Save the completed CacheResult into the CacheManager. This must + * have been created first with createCacheResult(). + * @return Returns true if the entry has been successfully saved. + */ + public synchronized boolean saveCacheResult() { + if (mCacheResult == null || mCacheResultUrl == null) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Tried to save cache result but " + + "createCacheResult not called"); + } + return false; + } + + if (Config.LOGV) { + Log.i(LOG_TAG, "Saving cache result"); + } + CacheManager.saveCacheFile(mCacheResultUrl, mCacheResult); + mCacheResult = null; + mCacheResultUrl = null; + return true; + } + + + /** + * Interrupt a blocking IO operation. This will cause the child + * thread to expediently return from an operation if it was stuck at + * the time. Note that this inherently races, and unfortunately + * requires the caller to loop. + */ + public synchronized void interrupt() { + if (Config.LOGV) { + Log.i(LOG_TAG, "INTERRUPT CALLED"); + } + mConnectionFailedLock.lock(); + mConnectionFailed = true; + mConnectionFailedLock.unlock(); + if (mMethod != null) { + mMethod.abort(); + } + if (mHttpThread != null) { + waitUntilConnectionFinished(); + } + } + + /** + * Receive the next sequential bytes of the response body after + * successful connection. This will receive up to the size of the + * provided byte array. If there is no body, this will return 0 + * bytes on the first call after connection. + * @param buf A pre-allocated byte array to receive data into. + * @return The number of bytes from the start of the array which + * have been filled, 0 on EOF, or negative on error. + */ + public synchronized int receive(byte[] buf) { + if (mBodyInputStream == null) { + // If this is the first call, setup the InputStream. This may + // fail if there were headers, but no body returned by the + // server. + try { + if (mResponse != null) { + HttpEntity entity = mResponse.getEntity(); + mBodyInputStream = entity.getContent(); + } + } catch (IOException inputException) { + if (Config.LOGV) { + Log.i(LOG_TAG, "Failed to connect InputStream: " + + inputException); + } + // Not unexpected. For example, 404 response return headers, + // and sometimes a body with a detailed error. + } + if (mBodyInputStream == null) { + // No error stream either. Treat as a 0 byte response. + if (Config.LOGV) { + Log.i(LOG_TAG, "No InputStream"); + } + return 0; // EOF. + } + } + int ret; + try { + int got = mBodyInputStream.read(buf); + if (got > 0) { + // Got some bytes, not EOF. + ret = got; + } else { + // EOF. + mBodyInputStream.close(); + ret = 0; + } + } catch (IOException e) { + // An abort() interrupts us by calling close() on our stream. + if (Config.LOGV) { + Log.i(LOG_TAG, "Got IOException in mBodyInputStream.read(): ", e); + } + ret = -1; + } + return ret; + } + + /** + * For POST method requests, send a stream of data provided by the + * native side in repeated callbacks. + * We put the data in mBuffer, and wait until it is consumed + * by the StreamEntity in the request thread. + * @param data A byte array containing the data to sent, or null + * if indicating EOF. + * @param bytes The number of bytes from the start of the array to + * send, or 0 if indicating EOF. + * @return True if all bytes were successfully sent, false on error. + */ + public boolean sendPostData(byte[] data, int bytes) { + mConnectionFailedLock.lock(); + if (mConnectionFailed) { + mConnectionFailedLock.unlock(); + return false; + } + mConnectionFailedLock.unlock(); + if (mPostEntity == null) return false; + + // We block until the outputstream is available + // (or in case of connection error) + if (!mPostEntity.isReady()) return false; + + if (data == null && bytes == 0) { + mBuffer.put(null); + } else { + mBuffer.put(new DataPacket(data, bytes)); + } + mSignal.waitUntilPacketConsumed(); + + mConnectionFailedLock.lock(); + if (mConnectionFailed) { + Log.e(LOG_TAG, "failure"); + mConnectionFailedLock.unlock(); + return false; + } + mConnectionFailedLock.unlock(); + return true; + } + +} diff --git a/core/java/android/webkit/gears/DesktopAndroid.java b/core/java/android/webkit/gears/DesktopAndroid.java new file mode 100644 index 0000000..ee8ca49 --- /dev/null +++ b/core/java/android/webkit/gears/DesktopAndroid.java @@ -0,0 +1,109 @@ +// Copyright 2008 The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.Context; +import android.content.ComponentName; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.util.Log; +import android.webkit.WebView; + +/** + * Utility class to create a shortcut on Android + */ +public class DesktopAndroid { + + private static final String TAG = "Gears-J-Desktop"; + private static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; + private static final String ACTION_INSTALL_SHORTCUT = + "com.android.launcher.action.INSTALL_SHORTCUT"; + + // Android now enforces a 64x64 limit for the icon + private static int MAX_WIDTH = 64; + private static int MAX_HEIGHT = 64; + + /** + * Small utility function returning a Bitmap object. + * + * @param path the icon path + */ + private static Bitmap getBitmap(String path) { + return BitmapFactory.decodeFile(path); + } + + /** + * Create a shortcut for a webpage. + * + * <p>To set a shortcut on Android, we use the ACTION_INSTALL_SHORTCUT + * from the default Home application. We only have to create an Intent + * containing extra parameters specifying the shortcut. + * <p>Note: the shortcut mechanism is not system wide and depends on the + * Home application; if phone carriers decide to rewrite a Home application + * that does not accept this Intent, no shortcut will be added. + * + * @param webview the webview we are called from + * @param title the shortcut's title + * @param url the shortcut's url + * @param imagePath the local path of the shortcut's icon + */ + public static void setShortcut(WebView webview, String title, + String url, String imagePath) { + Context context = webview.getContext(); + + Intent viewWebPage = new Intent(Intent.ACTION_VIEW); + viewWebPage.setData(Uri.parse(url)); + viewWebPage.addCategory(Intent.CATEGORY_BROWSABLE); + + Intent intent = new Intent(ACTION_INSTALL_SHORTCUT); + intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, viewWebPage); + intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); + + // We disallow the creation of duplicate shortcuts (i.e. same + // url, same title, but different screen position). + intent.putExtra(EXTRA_SHORTCUT_DUPLICATE, false); + + Bitmap bmp = getBitmap(imagePath); + if (bmp != null) { + if ((bmp.getWidth() > MAX_WIDTH) || + (bmp.getHeight() > MAX_HEIGHT)) { + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bmp, + MAX_WIDTH, MAX_HEIGHT, true); + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaledBitmap); + } else { + intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bmp); + } + } else { + // This should not happen as we just downloaded the icon + Log.e(TAG, "icon file <" + imagePath + "> not found"); + } + + context.sendBroadcast(intent); + } + +} diff --git a/core/java/android/webkit/gears/NativeDialog.java b/core/java/android/webkit/gears/NativeDialog.java new file mode 100644 index 0000000..9e2b375 --- /dev/null +++ b/core/java/android/webkit/gears/NativeDialog.java @@ -0,0 +1,142 @@ +// Copyright 2008 The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +import java.io.File; +import java.lang.InterruptedException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class to call a modal native dialog on Android + * The dialog itself is an Activity defined in the Browser. + * @hide + */ +public class NativeDialog { + + private static final String TAG = "Gears-J-NativeDialog"; + + private final String DIALOG_PACKAGE = "com.android.browser"; + private final String DIALOG_CLASS = DIALOG_PACKAGE + ".GearsNativeDialog"; + + private static Lock mLock = new ReentrantLock(); + private static Condition mDialogFinished = mLock.newCondition(); + private static String mResults = null; + + private static boolean mAsynchronousDialog; + + /** + * Utility function to build the intent calling the + * dialog activity + */ + private Intent createIntent(String type, String arguments) { + Intent intent = new Intent(); + intent.setClassName(DIALOG_PACKAGE, DIALOG_CLASS); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("dialogArguments", arguments); + intent.putExtra("dialogType", type); + return intent; + } + + /** + * Opens a native dialog synchronously and waits for its completion. + * + * The dialog is an activity (GearsNativeDialog) provided by the Browser + * that we call via startActivity(). Contrary to a normal activity though, + * we need to block until it returns. To do so, we define a static lock + * object in this class, which GearsNativeDialog can unlock once done + */ + public String showDialog(Context context, String file, + String arguments) { + + try { + mAsynchronousDialog = false; + mLock.lock(); + File path = new File(file); + String fileName = path.getName(); + String type = fileName.substring(0, fileName.indexOf(".html")); + Intent intent = createIntent(type, arguments); + + mResults = null; + context.startActivity(intent); + mDialogFinished.await(); + } catch (InterruptedException e) { + Log.e(TAG, "exception e: " + e); + } catch (ActivityNotFoundException e) { + Log.e(TAG, "exception e: " + e); + } finally { + mLock.unlock(); + } + + return mResults; + } + + /** + * Opens a native dialog asynchronously + * + * The dialog is an activity (GearsNativeDialog) provided by the + * Browser. + */ + public void showAsyncDialog(Context context, String type, + String arguments) { + mAsynchronousDialog = true; + Intent intent = createIntent(type, arguments); + context.startActivity(intent); + } + + /** + * Static method that GearsNativeDialog calls to unlock us + */ + public static void signalFinishedDialog() { + if (!mAsynchronousDialog) { + mLock.lock(); + mDialogFinished.signal(); + mLock.unlock(); + } else { + // we call the native callback + closeAsynchronousDialog(mResults); + } + } + + /** + * Static method that GearsNativeDialog calls to set the + * dialog's result + */ + public static void closeDialog(String res) { + mResults = res; + } + + /** + * Native callback method + */ + private native static void closeAsynchronousDialog(String res); +} diff --git a/core/java/android/webkit/gears/PluginSettings.java b/core/java/android/webkit/gears/PluginSettings.java new file mode 100644 index 0000000..2d0cc13 --- /dev/null +++ b/core/java/android/webkit/gears/PluginSettings.java @@ -0,0 +1,79 @@ +// Copyright 2008 The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.content.Context; +import android.util.Log; +import android.webkit.Plugin; +import android.webkit.Plugin.PreferencesClickHandler; + +/** + * Simple bridge class intercepting the click in the + * browser plugin list and calling the Gears settings + * dialog. + */ +public class PluginSettings { + + private static final String TAG = "Gears-J-PluginSettings"; + private Context mContext; + + public PluginSettings(Plugin plugin) { + plugin.setClickHandler(new ClickHandler()); + } + + /** + * We do not call the dialog synchronously here as the main + * message loop would be blocked, so we call it via a secondary thread. + */ + private class ClickHandler implements PreferencesClickHandler { + public void handleClickEvent(Context context) { + mContext = context.getApplicationContext(); + Thread startDialog = new Thread(new StartDialog(context)); + startDialog.start(); + } + } + + /** + * Simple wrapper class to call the gears native method in + * a separate thread (the native code will then instanciate a NativeDialog + * object which will start the GearsNativeDialog activity defined in + * the Browser). + */ + private class StartDialog implements Runnable { + Context mContext; + + public StartDialog(Context context) { + mContext = context; + } + + public void run() { + runSettingsDialog(mContext); + } + } + + private static native void runSettingsDialog(Context c); + +} diff --git a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java new file mode 100644 index 0000000..2a5cbe9 --- /dev/null +++ b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java @@ -0,0 +1,501 @@ +// Copyright 2008, The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.net.http.Headers; +import android.util.Log; +import android.webkit.CacheManager; +import android.webkit.CacheManager.CacheResult; +import android.webkit.Plugin; +import android.webkit.UrlInterceptRegistry; +import android.webkit.UrlInterceptHandler; +import android.webkit.WebView; + +import org.apache.http.impl.cookie.DateUtils; +import org.apache.http.util.CharArrayBuffer; + +import java.io.*; +import java.util.*; + +/** + * Services requests to handle URLs coming from the browser or + * HttpRequestAndroid. This registers itself with the + * UrlInterceptRegister in Android so we get a chance to service all + * URLs passing through the browser before anything else. + */ +public class UrlInterceptHandlerGears implements UrlInterceptHandler { + /** Singleton instance. */ + private static UrlInterceptHandlerGears instance; + /** Debug logging tag. */ + private static final String LOG_TAG = "Gears-J"; + /** Buffer size for reading/writing streams. */ + private static final int BUFFER_SIZE = 4096; + /** + * Number of milliseconds to expire LocalServer temporary entries in + * the browser's cache. Somewhat arbitrarily chosen as a compromise + * between being a) long enough not to expire during page load and + * b) short enough to evict entries during a session. */ + private static final int CACHE_EXPIRY_MS = 60000; // 1 minute. + /** Enable/disable all logging in this class. */ + private static boolean logEnabled = false; + /** The unmodified (case-sensitive) key in the headers map is the + * same index as used by HttpRequestAndroid. */ + public static final int HEADERS_MAP_INDEX_KEY = + ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_KEY; + /** The associated value in the headers map is the same index as + * used by HttpRequestAndroid. */ + public static final int HEADERS_MAP_INDEX_VALUE = + ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_VALUE; + + /** + * Object passed to the native side, containing information about + * the URL to service. + */ + public static class ServiceRequest { + // The URL being requested. + private String url; + // Request headers. Map of lowercase key to [ unmodified key, value ]. + private Map<String, String[]> requestHeaders; + + /** + * Initialize members on construction. + * @param url The URL being requested. + * @param requestHeaders Headers associated with the request, + * or null if none. + * Map of lowercase key to [ unmodified key, value ]. + */ + public ServiceRequest(String url, Map<String, String[]> requestHeaders) { + this.url = url; + this.requestHeaders = requestHeaders; + } + + /** + * Returns the URL being requested. + * @return The URL being requested. + */ + public String getUrl() { + return url; + } + + /** + * Get the value associated with a request header key, if any. + * @param header The key to find, case insensitive. + * @return The value associated with this header, or null if not found. + */ + public String getRequestHeader(String header) { + if (requestHeaders != null) { + String[] value = requestHeaders.get(header.toLowerCase()); + if (value != null) { + return value[HEADERS_MAP_INDEX_VALUE]; + } else { + return null; + } + } else { + return null; + } + } + } + + /** + * Object returned by the native side, containing information needed + * to pass the entire response back to the browser or + * HttpRequestAndroid. Works from either an in-memory array or a + * file on disk. + */ + public class ServiceResponse { + // The response status code, e.g 200. + private int statusCode; + // The full status line, e.g "HTTP/1.1 200 OK". + private String statusLine; + // All headers associated with the response. Map of lowercase key + // to [ unmodified key, value ]. + private Map<String, String[]> responseHeaders = + new HashMap<String, String[]>(); + // The MIME type, e.g "text/html". + private String mimeType; + // The encoding, e.g "utf-8", or null if none. + private String encoding; + // The stream which contains the body when read(). + private InputStream inputStream; + + /** + * Initialize members using an in-memory array to return the body. + * @param statusCode The response status code, e.g 200. + * @param statusLine The full status line, e.g "HTTP/1.1 200 OK". + * @param mimeType The MIME type, e.g "text/html". + * @param encoding Encoding, e.g "utf-8" or null if none. + * @param body The response body as a byte array, non-empty. + */ + void setResultArray( + int statusCode, + String statusLine, + String mimeType, + String encoding, + byte[] body) { + this.statusCode = statusCode; + this.statusLine = statusLine; + this.mimeType = mimeType; + this.encoding = encoding; + // Setup a stream to read out of the byte array. + this.inputStream = new ByteArrayInputStream(body); + } + + /** + * Initialize members using a file on disk to return the body. + * @param statusCode The response status code, e.g 200. + * @param statusLine The full status line, e.g "HTTP/1.1 200 OK". + * @param mimeType The MIME type, e.g "text/html". + * @param encoding Encoding, e.g "utf-8" or null if none. + * @param path Full path to the file containing the body. + * @return True if the file is successfully setup to stream, + * false on error such as file not found. + */ + boolean setResultFile( + int statusCode, + String statusLine, + String mimeType, + String encoding, + String path) { + this.statusCode = statusCode; + this.statusLine = statusLine; + this.mimeType = mimeType; + this.encoding = encoding; + try { + // Setup a stream to read out of a file on disk. + this.inputStream = new FileInputStream(new File(path)); + return true; + } catch (java.io.FileNotFoundException ex) { + log("File not found: " + path); + return false; + } + } + + /** + * Set a response header, adding its settings to the header members. + * @param key The case sensitive key for the response header, + * e.g "Set-Cookie". + * @param value The value associated with this key, e.g "cookie1234". + */ + public void setResponseHeader(String key, String value) { + // The map value contains the unmodified key (not lowercase). + String[] mapValue = { key, value }; + responseHeaders.put(key.toLowerCase(), mapValue); + } + + /** + * Return the "Content-Type" header possibly supplied by a + * previous setResponseHeader(). + * @return The "Content-Type" value, or null if not present. + */ + public String getContentType() { + // The map keys are lowercase. + String[] value = responseHeaders.get("content-type"); + if (value != null) { + return value[HEADERS_MAP_INDEX_VALUE]; + } else { + return null; + } + } + + /** + * Returns the HTTP status code for the response, supplied in + * setResultArray() or setResultFile(). + * @return The HTTP statue code, e.g 200. + */ + public int getStatusCode() { + return statusCode; + } + + /** + * Returns the full HTTP status line for the response, supplied in + * setResultArray() or setResultFile(). + * @return The HTTP statue line, e.g "HTTP/1.1 200 OK". + */ + public String getStatusLine() { + return statusLine; + } + + /** + * Get all response headers supplied in calls in + * setResponseHeader(). + * @return A Map<String, String[]> containing all headers. + */ + public Map<String, String[]> getResponseHeaders() { + return responseHeaders; + } + + /** + * Returns the MIME type for the response, supplied in + * setResultArray() or setResultFile(). + * @return The MIME type, e.g "text/html". + */ + public String getMimeType() { + return mimeType; + } + + /** + * Returns the encoding for the response, supplied in + * setResultArray() or setResultFile(), or null if none. + * @return The encoding, e.g "utf-8", or null if none. + */ + public String getEncoding() { + return encoding; + } + + /** + * Returns the InputStream setup by setResultArray() or + * setResultFile() to allow reading data either from memory or + * disk. + * @return The InputStream containing the response body. + */ + public InputStream getInputStream() { + return inputStream; + } + } + + /** + * Construct and initialize the singleton instance. + */ + public UrlInterceptHandlerGears() { + if (instance != null) { + Log.e(LOG_TAG, "UrlInterceptHandlerGears singleton already constructed"); + throw new RuntimeException(); + } + instance = this; + } + + /** + * Turn on/off logging in this class. + * @param on Logging enable state. + */ + public static void enableLogging(boolean on) { + logEnabled = on; + } + + /** + * Get the singleton instance. + * @return The singleton instance. + */ + public static UrlInterceptHandlerGears getInstance() { + return instance; + } + + /** + * Register the singleton instance with the browser's interception + * mechanism. + */ + public synchronized void register() { + UrlInterceptRegistry.registerHandler(this); + } + + /** + * Unregister the singleton instance from the browser's interception + * mechanism. + */ + public synchronized void unregister() { + UrlInterceptRegistry.unregisterHandler(this); + } + + /** + * Copy the entire InputStream to OutputStream. + * @param inputStream The stream to read from. + * @param outputStream The stream to write to. + * @return True if the entire stream copied successfully, false on error. + */ + private boolean copyStream(InputStream inputStream, + OutputStream outputStream) { + try { + // Temporary buffer to copy stream through. + byte[] buf = new byte[BUFFER_SIZE]; + for (;;) { + // Read up to BUFFER_SIZE bytes. + int bytes = inputStream.read(buf); + if (bytes < 0) { + break; + } + // Write the number of bytes we just read. + outputStream.write(buf, 0, bytes); + } + } catch (IOException ex) { + log("Got IOException copying stream: " + ex); + return false; + } + return true; + } + + /** + * Given an URL, returns a CacheResult which contains the response + * for the request. This implements the UrlInterceptHandler interface. + * + * @param url The fully qualified URL being requested. + * @param requestHeaders The request headers for this URL. + * @return If a response can be crafted, a CacheResult initialized + * to return the surrogate response. If this URL cannot + * be serviced, returns null. + */ + public CacheResult service(String url, Map<String, String> requestHeaders) { + // Thankfully the browser does call us with case-sensitive + // headers. We just need to map it case-insensitive. + Map<String, String[]> lowercaseRequestHeaders = + new HashMap<String, String[]>(); + Iterator<Map.Entry<String, String>> requestHeadersIt = + requestHeaders.entrySet().iterator(); + while (requestHeadersIt.hasNext()) { + Map.Entry<String, String> entry = requestHeadersIt.next(); + String key = entry.getKey(); + String mapValue[] = { key, entry.getValue() }; + lowercaseRequestHeaders.put(key.toLowerCase(), mapValue); + } + ServiceResponse response = getServiceResponse(url, lowercaseRequestHeaders); + if (response == null) { + // No result for this URL. + return null; + } + // Translate the ServiceResponse to a CacheResult. + // Translate http -> gears, https -> gearss, so we don't overwrite + // existing entries. + String gearsUrl = "gears" + url.substring("http".length()); + // Set the result to expire, so that entries don't pollute the + // browser's cache for too long. + long now_ms = System.currentTimeMillis(); + String expires = DateUtils.formatDate(new Date(now_ms + CACHE_EXPIRY_MS)); + response.setResponseHeader(ApacheHttpRequestAndroid.KEY_EXPIRES, expires); + // The browser is only interested in a small subset of headers, + // contained in a Headers object. Iterate the map of all headers + // and add them to Headers. + Headers headers = new Headers(); + Iterator<Map.Entry<String, String[]>> responseHeadersIt = + response.getResponseHeaders().entrySet().iterator(); + while (responseHeadersIt.hasNext()) { + Map.Entry<String, String[]> entry = responseHeadersIt.next(); + // Headers.parseHeader() expects lowercase keys. + String keyValue = entry.getKey() + ": " + + entry.getValue()[HEADERS_MAP_INDEX_VALUE]; + CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length()); + buffer.append(keyValue); + // Parse it into the header container. + headers.parseHeader(buffer); + } + CacheResult cacheResult = CacheManager.createCacheFile( + gearsUrl, + response.getStatusCode(), + headers, + response.getMimeType(), + true); // forceCache + + if (cacheResult == null) { + // With the no-cache policy we could end up + // with a null result + return null; + } + + // Set encoding if specified. + String encoding = response.getEncoding(); + if (encoding != null) { + cacheResult.setEncoding(encoding); + } + // Copy the response body to the CacheResult. This handles all + // combinations of memory vs on-disk on both sides. + InputStream inputStream = response.getInputStream(); + OutputStream outputStream = cacheResult.getOutputStream(); + boolean copied = copyStream(inputStream, outputStream); + // Close the input and output streams to relinquish their + // resources earlier. + try { + inputStream.close(); + } catch (IOException ex) { + log("IOException closing InputStream: " + ex); + copied = false; + } + try { + outputStream.close(); + } catch (IOException ex) { + log("IOException closing OutputStream: " + ex); + copied = false; + } + if (!copied) { + log("copyStream of local result failed"); + return null; + } + // Save the entry into the browser's cache. + CacheManager.saveCacheFile(gearsUrl, cacheResult); + // Get it back from the cache, this time properly initialized to + // be used for input. + cacheResult = CacheManager.getCacheFile(gearsUrl, null); + if (cacheResult != null) { + log("Returning surrogate result"); + return cacheResult; + } else { + // Not an expected condition, but handle gracefully. Perhaps out + // of memory or disk? + Log.e(LOG_TAG, "Lost CacheResult between save and get. Can't serve.\n"); + return null; + } + } + + /** + * Given an URL, returns a CacheResult and headers which contain the + * response for the request. + * + * @param url The fully qualified URL being requested. + * @param requestHeaders The request headers for this URL. + * @return If a response can be crafted, a ServiceResponse is + * created which contains all response headers and an InputStream + * attached to the body. If there is no response, null is returned. + */ + public ServiceResponse getServiceResponse(String url, + Map<String, String[]> requestHeaders) { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + // Don't know how to service non-HTTP URLs + return null; + } + // Call the native handler to craft a response for this URL. + return nativeService(new ServiceRequest(url, requestHeaders)); + } + + /** + * Convenience debug function. Calls the Android logging + * mechanism. logEnabled is not a constant, so if the string + * evaluation is potentially expensive, the caller also needs to + * check it. + * @param str String to log to the Android console. + */ + private void log(String str) { + if (logEnabled) { + Log.i(LOG_TAG, str); + } + } + + /** + * Native method which handles the bulk of the request in LocalServer. + * @param request A ServiceRequest object containing information about + * the request. + * @return If serviced, a ServiceResponse object containing all the + * information to provide a response for the URL, or null + * if no response available for this URL. + */ + private native static ServiceResponse nativeService(ServiceRequest request); +} diff --git a/core/java/android/webkit/gears/VersionExtractor.java b/core/java/android/webkit/gears/VersionExtractor.java new file mode 100644 index 0000000..172dacb --- /dev/null +++ b/core/java/android/webkit/gears/VersionExtractor.java @@ -0,0 +1,147 @@ +// Copyright 2008, The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.util.Log; +import java.io.IOException; +import java.io.StringReader; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.w3c.dom.Document; +import org.w3c.dom.DOMException; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +/** + * A class that can extract the Gears version and upgrade URL from an + * xml document. + */ +public final class VersionExtractor { + + /** + * Logging tag + */ + private static final String TAG = "Gears-J-VersionExtractor"; + /** + * XML element names. + */ + private static final String VERSION = "em:version"; + private static final String URL = "em:updateLink"; + + /** + * Parses the input xml string and invokes the native + * setVersionAndUrl method. + * @param xml is the XML string to parse. + * @return true if the extraction is successful and false otherwise. + */ + public static boolean extract(String xml, long nativeObject) { + try { + // Build the builders. + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + + // Create the document. + Document doc = builder.parse(new InputSource(new StringReader(xml))); + + // Look for the version and url elements and get their text + // contents. + String version = extractText(doc, VERSION); + String url = extractText(doc, URL); + + // If we have both, let the native side know. + if (version != null && url != null) { + setVersionAndUrl(version, url, nativeObject); + return true; + } + + return false; + + } catch (FactoryConfigurationError ex) { + Log.e(TAG, "Could not create the DocumentBuilderFactory " + ex); + } catch (ParserConfigurationException ex) { + Log.e(TAG, "Could not create the DocumentBuilder " + ex); + } catch (SAXException ex) { + Log.e(TAG, "Could not parse the xml " + ex); + } catch (IOException ex) { + Log.e(TAG, "Could not read the xml " + ex); + } + + return false; + } + + /** + * Extracts the text content of the first element with the given name. + * @param doc is the Document where the element is searched for. + * @param elementName is name of the element to searched for. + * @return the text content of the element or null if no such + * element is found. + */ + private static String extractText(Document doc, String elementName) { + String text = null; + NodeList node_list = doc.getElementsByTagName(elementName); + + if (node_list.getLength() > 0) { + // We are only interested in the first node. Normally there + // should not be more than one anyway. + Node node = node_list.item(0); + + // Iterate through the text children. + NodeList child_list = node.getChildNodes(); + + try { + for (int i = 0; i < child_list.getLength(); ++i) { + Node child = child_list.item(i); + if (child.getNodeType() == Node.TEXT_NODE) { + if (text == null) { + text = new String(); + } + text += child.getNodeValue(); + } + } + } catch (DOMException ex) { + Log.e(TAG, "getNodeValue() failed " + ex); + } + } + + if (text != null) { + text = text.trim(); + } + + return text; + } + + /** + * Native method used to send the version and url back to the C++ + * side. + */ + private static native void setVersionAndUrl( + String version, String url, long nativeObject); +} diff --git a/core/java/android/webkit/gears/ZipInflater.java b/core/java/android/webkit/gears/ZipInflater.java new file mode 100644 index 0000000..f6b6be5 --- /dev/null +++ b/core/java/android/webkit/gears/ZipInflater.java @@ -0,0 +1,200 @@ +// Copyright 2008, The Android Open Source Project +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// 2. Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// 3. Neither the name of Google Inc. nor the names of its contributors may be +// used to endorse or promote products derived from this software without +// specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED +// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package android.webkit.gears; + +import android.os.StatFs; +import android.util.Log; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Enumeration; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; + + +/** + * A class that can inflate a zip archive. + */ +public final class ZipInflater { + /** + * Logging tag + */ + private static final String TAG = "Gears-J-ZipInflater"; + + /** + * The size of the buffer used to read from the archive. + */ + private static final int BUFFER_SIZE_BYTES = 32 * 1024; // 32 KB. + /** + * The path navigation component (i.e. "../"). + */ + private static final String PATH_NAVIGATION_COMPONENT = ".." + File.separator; + /** + * The root of the data partition. + */ + private static final String DATA_PARTITION_ROOT = "/data"; + + /** + * We need two be able to store two versions of gears in parallel: + * - the zipped version + * - the unzipped version, which will be loaded next time the browser is started. + * We are conservative and do not attempt to unpack unless there enough free + * space on the device to store 4 times the new Gears size. + */ + private static final long SIZE_MULTIPLIER = 4; + + /** + * Unzips the archive with the given name. + * @param filename is the name of the zip to inflate. + * @param path is the path where the zip should be unpacked. It must contain + * a trailing separator, or the extraction will fail. + * @return true if the extraction is successful and false otherwise. + */ + public static boolean inflate(String filename, String path) { + Log.i(TAG, "Extracting " + filename + " to " + path); + + // Check that the path ends with a separator. + if (!path.endsWith(File.separator)) { + Log.e(TAG, "Path missing trailing separator: " + path); + return false; + } + + boolean result = false; + + // Use a ZipFile to get an enumeration of the entries and + // calculate the overall uncompressed size of the archive. Also + // check for existing files or directories that have the same + // name as the entries in the archive. Also check for invalid + // entry names (e.g names that attempt to navigate to the + // parent directory). + ZipInputStream zipStream = null; + long uncompressedSize = 0; + try { + ZipFile zipFile = new ZipFile(filename); + try { + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry entry = (ZipEntry) entries.nextElement(); + uncompressedSize += entry.getSize(); + // Check against entry names that may attempt to navigate + // out of the destination directory. + if (entry.getName().indexOf(PATH_NAVIGATION_COMPONENT) >= 0) { + throw new IOException("Illegal entry name: " + entry.getName()); + } + + // Check against entries with the same name as pre-existing files or + // directories. + File file = new File(path + entry.getName()); + if (file.exists()) { + // A file or directory with the same name already exist. + // This must not happen, so we treat this as an error. + throw new IOException( + "A file or directory with the same name already exists."); + } + } + } finally { + zipFile.close(); + } + + Log.i(TAG, "Determined uncompressed size: " + uncompressedSize); + // Check we have enough space to unpack this archive. + if (freeSpace() <= uncompressedSize * SIZE_MULTIPLIER) { + throw new IOException("Not enough space to unpack this archive."); + } + + zipStream = new ZipInputStream( + new BufferedInputStream(new FileInputStream(filename))); + ZipEntry entry; + int counter; + byte buffer[] = new byte[BUFFER_SIZE_BYTES]; + + // Iterate through the entries and write each of them to a file. + while ((entry = zipStream.getNextEntry()) != null) { + File file = new File(path + entry.getName()); + if (entry.isDirectory()) { + // If the entry denotes a directory, we need to create a + // directory with the same name. + file.mkdirs(); + } else { + CRC32 checksum = new CRC32(); + BufferedOutputStream output = new BufferedOutputStream( + new FileOutputStream(file), + BUFFER_SIZE_BYTES); + try { + // Read the entry and write it to the file. + while ((counter = zipStream.read(buffer, 0, BUFFER_SIZE_BYTES)) != + -1) { + output.write(buffer, 0, counter); + checksum.update(buffer, 0, counter); + } + output.flush(); + } finally { + output.close(); + } + + if (checksum.getValue() != entry.getCrc()) { + throw new IOException( + "Integrity check failed for: " + entry.getName()); + } + } + zipStream.closeEntry(); + } + + result = true; + + } catch (FileNotFoundException ex) { + Log.e(TAG, "The zip file could not be found. " + ex); + } catch (IOException ex) { + Log.e(TAG, "Could not read or write an entry. " + ex); + } catch(IllegalArgumentException ex) { + Log.e(TAG, "Could not create the BufferedOutputStream. " + ex); + } finally { + if (zipStream != null) { + try { + zipStream.close(); + } catch (IOException ex) { + // Ignored. + } + } + // Discard any exceptions and return the result to the native side. + return result; + } + } + + private static final long freeSpace() { + StatFs data_partition = new StatFs(DATA_PARTITION_ROOT); + long freeSpace = data_partition.getAvailableBlocks() * + data_partition.getBlockSize(); + Log.i(TAG, "Free space on the data partition: " + freeSpace); + return freeSpace; + } +} diff --git a/core/java/android/webkit/gears/package.html b/core/java/android/webkit/gears/package.html new file mode 100644 index 0000000..db6f78b --- /dev/null +++ b/core/java/android/webkit/gears/package.html @@ -0,0 +1,3 @@ +<body> +{@hide} +</body>
\ No newline at end of file diff --git a/core/java/android/webkit/package.html b/core/java/android/webkit/package.html new file mode 100644 index 0000000..4ed08da --- /dev/null +++ b/core/java/android/webkit/package.html @@ -0,0 +1,7 @@ +<html> +<body> +Provides tools for browsing the web. +<p>The only classes or interfaces in this package intended for use by SDK +developers are WebView, BroswerCallbackAdapter, BrowserCallback, and CookieManager. +</body> +</html>
\ No newline at end of file |