summaryrefslogtreecommitdiffstats
path: root/core/java/android/webkit
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 19:31:44 -0800
commit9066cfe9886ac131c34d59ed0e2d287b0e3c0087 (patch)
treed88beb88001f2482911e3d28e43833b50e4b4e97 /core/java/android/webkit
parentd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (diff)
downloadframeworks_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')
-rw-r--r--core/java/android/webkit/BrowserFrame.java786
-rw-r--r--core/java/android/webkit/ByteArrayBuilder.java142
-rw-r--r--core/java/android/webkit/CacheLoader.java65
-rw-r--r--core/java/android/webkit/CacheManager.java703
-rw-r--r--core/java/android/webkit/CallbackProxy.java1020
-rw-r--r--core/java/android/webkit/ContentLoader.java123
-rw-r--r--core/java/android/webkit/CookieManager.java934
-rw-r--r--core/java/android/webkit/CookieSyncManager.java205
-rw-r--r--core/java/android/webkit/DataLoader.java80
-rw-r--r--core/java/android/webkit/DateSorter.java124
-rw-r--r--core/java/android/webkit/DownloadListener.java33
-rw-r--r--core/java/android/webkit/FileLoader.java136
-rw-r--r--core/java/android/webkit/FrameLoader.java370
-rw-r--r--core/java/android/webkit/HttpAuthHandler.java186
-rw-r--r--core/java/android/webkit/HttpDateTime.java195
-rw-r--r--core/java/android/webkit/JWebCoreJavaBridge.java195
-rw-r--r--core/java/android/webkit/JsPromptResult.java52
-rw-r--r--core/java/android/webkit/JsResult.java82
-rw-r--r--core/java/android/webkit/LoadListener.java1499
-rw-r--r--core/java/android/webkit/MimeTypeMap.java503
-rw-r--r--core/java/android/webkit/Network.java349
-rw-r--r--core/java/android/webkit/PerfChecker.java49
-rw-r--r--core/java/android/webkit/Plugin.java126
-rw-r--r--core/java/android/webkit/PluginList.java83
-rw-r--r--core/java/android/webkit/SslErrorHandler.java255
-rw-r--r--core/java/android/webkit/StreamLoader.java199
-rw-r--r--core/java/android/webkit/TextDialog.java610
-rw-r--r--core/java/android/webkit/URLUtil.java363
-rw-r--r--core/java/android/webkit/UrlInterceptHandler.java34
-rw-r--r--core/java/android/webkit/UrlInterceptRegistry.java106
-rw-r--r--core/java/android/webkit/WebBackForwardList.java188
-rw-r--r--core/java/android/webkit/WebChromeClient.java160
-rw-r--r--core/java/android/webkit/WebHistoryItem.java179
-rw-r--r--core/java/android/webkit/WebIconDatabase.java251
-rw-r--r--core/java/android/webkit/WebSettings.java1112
-rw-r--r--core/java/android/webkit/WebSyncManager.java162
-rw-r--r--core/java/android/webkit/WebView.java5406
-rw-r--r--core/java/android/webkit/WebViewClient.java206
-rw-r--r--core/java/android/webkit/WebViewCore.java1674
-rw-r--r--core/java/android/webkit/WebViewDatabase.java967
-rw-r--r--core/java/android/webkit/gears/AndroidGpsLocationProvider.java156
-rw-r--r--core/java/android/webkit/gears/AndroidRadioDataProvider.java244
-rw-r--r--core/java/android/webkit/gears/AndroidWifiDataProvider.java136
-rw-r--r--core/java/android/webkit/gears/ApacheHttpRequestAndroid.java1122
-rw-r--r--core/java/android/webkit/gears/DesktopAndroid.java109
-rw-r--r--core/java/android/webkit/gears/NativeDialog.java142
-rw-r--r--core/java/android/webkit/gears/PluginSettings.java79
-rw-r--r--core/java/android/webkit/gears/UrlInterceptHandlerGears.java501
-rw-r--r--core/java/android/webkit/gears/VersionExtractor.java147
-rw-r--r--core/java/android/webkit/gears/ZipInflater.java200
-rw-r--r--core/java/android/webkit/gears/package.html3
-rw-r--r--core/java/android/webkit/package.html7
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>&lt;uses-permission android:name="android.permission.INTERNET" /></pre>
+ * <p>This must be a child of the <code>&lt;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