diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:05:43 -0800 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-12-17 18:05:43 -0800 |
commit | f013e1afd1e68af5e3b868c26a653bbfb39538f8 (patch) | |
tree | 7ad6c8fd9c7b55f4b4017171dec1cb760bbd26bf /core/java/android/webkit | |
parent | e70cfafe580c6f2994c4827cd8a534aabf3eb05c (diff) | |
download | frameworks_base-f013e1afd1e68af5e3b868c26a653bbfb39538f8.zip frameworks_base-f013e1afd1e68af5e3b868c26a653bbfb39538f8.tar.gz frameworks_base-f013e1afd1e68af5e3b868c26a653bbfb39538f8.tar.bz2 |
Code drop from //branches/cupcake/...@124589
Diffstat (limited to 'core/java/android/webkit')
26 files changed, 1979 insertions, 945 deletions
diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index e99c444..1dd37be 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -26,10 +26,10 @@ 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.URLDecoder; import java.net.URLEncoder; import java.util.HashMap; import java.util.Iterator; @@ -52,9 +52,7 @@ class BrowserFrame extends Handler { private final WebViewDatabase mDatabase; private final WebViewCore mWebViewCore; private boolean mLoadInitFromJava; - private String mCurrentUrl; private int mLoadType; - private String mCompletedUrl; private boolean mFirstLayoutDone = true; private boolean mCommitted = true; @@ -114,9 +112,7 @@ class BrowserFrame extends Handler { CookieSyncManager.createInstance(context); } AssetManager am = context.getAssets(); - nativeCreateFrame(am, proxy.getBackForwardList()); - // Create a native FrameView and attach it to the native frame. - nativeCreateView(w); + nativeCreateFrame(w, am, proxy.getBackForwardList()); mSettings = settings; mContext = context; @@ -142,11 +138,7 @@ class BrowserFrame extends Handler { stringByEvaluatingJavaScriptFromString( url.substring("javascript:".length())); } else { - if (!nativeLoadUrl(url)) { - reportError(android.net.http.EventHandler.ERROR_BAD_URL, - mContext.getString(com.android.internal.R.string.httpErrorBadUrl), - url); - } + nativeLoadUrl(url); } mLoadInitFromJava = false; } @@ -186,6 +178,17 @@ class BrowserFrame extends Handler { } /** + * 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. @@ -216,34 +219,12 @@ class BrowserFrame extends Handler { return mLoadType; } - /* package */String currentUrl() { - return mCurrentUrl; - } - - /* package */void didFirstLayout(String url) { - // this is common case - if (url.equals(mCurrentUrl)) { - if (!mFirstLayoutDone) { - mFirstLayoutDone = true; - // ensure {@link WebViewCore#webkitDraw} is called as we were - // blocking the update in {@link #loadStarted} - mWebViewCore.contentInvalidate(); - } - } else if (url.equals(mCompletedUrl)) { - /* - * FIXME: when loading http://www.google.com/m, - * mCurrentUrl will be http://www.google.com/m, - * mCompletedUrl will be http://www.google.com/m#search - * and url will be http://www.google.com/m#search. - * This is probably a bug in WebKit. If url matches mCompletedUrl, - * also set mFirstLayoutDone to be true and update. - */ - if (!mFirstLayoutDone) { - mFirstLayoutDone = true; - // ensure {@link WebViewCore#webkitDraw} is called as we were - // blocking the update in {@link #loadStarted} - mWebViewCore.contentInvalidate(); - } + /* 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; } @@ -258,8 +239,6 @@ class BrowserFrame extends Handler { mIsMainFrame = isMainFrame; if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { - mCurrentUrl = url; - mCompletedUrl = null; mLoadType = loadType; if (isMainFrame) { @@ -310,7 +289,6 @@ class BrowserFrame extends Handler { // mIsMainFrame and isMainFrame are better be equal!!! if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { - mCompletedUrl = url; if (isMainFrame) { mCallbackProxy.switchOutDrawHistory(); mCallbackProxy.onPageFinished(url); @@ -355,8 +333,8 @@ class BrowserFrame extends Handler { WebAddress uri = new WebAddress( mCallbackProxy.getBackForwardList().getCurrentItem() .getUrl()); - String host = uri.mHost; - String[] up = mDatabase.getUsernamePassword(host); + String schemePlusHost = uri.mScheme + uri.mHost; + String[] up = mDatabase.getUsernamePassword(schemePlusHost); if (up != null && up[0] != null) { setUsernamePassword(up[0], up[1]); } @@ -441,7 +419,13 @@ class BrowserFrame extends Handler { if (mLoadInitFromJava == true) { return false; } - return mCallbackProxy.shouldOverrideUrlLoading(url); + 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) { @@ -462,7 +446,7 @@ class BrowserFrame extends Handler { * @param method The http method. * @param headers The http headers. * @param postData If the method is "POST" postData is sent as the request - * body. + * 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. @@ -473,7 +457,7 @@ class BrowserFrame extends Handler { String url, String method, HashMap headers, - String postData, + byte[] postData, int cacheMode, boolean isHighPriority, boolean synchronous) { @@ -497,41 +481,47 @@ class BrowserFrame extends Handler { } WebAddress uri = new WebAddress(mCallbackProxy .getBackForwardList().getCurrentItem().getUrl()); - String host = uri.mHost; + String schemePlusHost = uri.mScheme + uri.mHost; String[] ret = getUsernamePassword(); - if (ret != null && postData != null && ret[0].length() > 0 - && ret[1].length() > 0 - && postData.contains(URLEncoder.encode(ret[0])) - && postData.contains(URLEncoder.encode(ret[1]))) { - String[] saved = mDatabase.getUsernamePassword(host); - 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(host, ret[0], - ret[1]); + // 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); } - } else { - // CallbackProxy will handle creating the resume - // message - mCallbackProxy.onSavePassword(host, ret[0], ret[1], - null); } } } catch (ParseException ex) { // if it is bad uri, don't save its password } - } - if (postData == null) { - postData = ""; + } } // is this resource the main-frame top-level page? - boolean isMainFramePage = mIsMainFrame && url.equals(mCurrentUrl); + boolean isMainFramePage = mIsMainFrame; if (Config.LOGV) { Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method=" @@ -561,8 +551,8 @@ class BrowserFrame extends Handler { CacheManager.endCacheTransaction(); } - FrameLoader loader = new FrameLoader(loadListener, - mSettings.getUserAgentString(), method, isHighPriority); + 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 @@ -666,36 +656,51 @@ class BrowserFrame extends Handler { 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. + * 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(AssetManager am, + private native void nativeCreateFrame(WebViewCore w, AssetManager am, WebBackForwardList list); /** - * Create a native view attached to a WebView. - * @param w A WebView that the frame draws into. - */ - private native void nativeCreateView(WebViewCore w); - - private native void nativeCallPolicyFunction(int policyFunction, - int decision); - /** * Destroy the native frame. */ public native void nativeDestroyFrame(); - /** - * Detach the view from the frame. - */ - private native void nativeDetachView(); + private native void nativeCallPolicyFunction(int policyFunction, + int decision); /** * Reload the current main frame. @@ -707,7 +712,7 @@ class BrowserFrame extends Handler { * @param steps A negative or positive number indicating the direction * and number of steps to move. */ - public native void goBackOrForward(int steps); + private native void nativeGoBackOrForward(int steps); /** * stringByEvaluatingJavaScriptFromString will execute the @@ -738,7 +743,7 @@ class BrowserFrame extends Handler { /** * Returns false if the url is bad. */ - private native boolean nativeLoadUrl(String url); + private native void nativeLoadUrl(String url); private native void nativeLoadData(String baseUrl, String data, String mimeType, String encoding, String failUrl); diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index f5a09b8..d12940d 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -62,8 +62,18 @@ public final class CacheManager { // 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 @@ -145,16 +155,37 @@ public final class CacheManager { 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; + 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; } /** @@ -224,7 +255,12 @@ public final class CacheManager { // only called from WebCore thread // make sure to call startCacheTransaction/endCacheTransaction in pair public static boolean endCacheTransaction() { - return mDataBase.endCacheTransaction(); + boolean ret = mDataBase.endCacheTransaction(); + if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) { + mTrimCacheCount = 0; + trimCacheIfNeeded(); + } + return ret; } /** @@ -319,7 +355,20 @@ public final class CacheManager { try { ret.outStream = new FileOutputStream(ret.outFile); } catch (FileNotFoundException e) { - return null; + // 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; } @@ -371,14 +420,25 @@ public final class CacheManager { */ // 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(); - for (int i = 0; i < files.length; i++) { - new File(mBaseDir, files[i]).delete(); + // 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. diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 7c296cc..cae94c9 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -354,12 +354,12 @@ class CallbackProxy extends Handler { case SAVE_PASSWORD: Bundle bundle = msg.getData(); - String host = bundle.getString("host"); + 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(host, username, password, + if (!mWebView.onSavePassword(schemePlusHost, username, password, (Message) msg.obj)) { synchronized (this) { notify(); @@ -700,8 +700,8 @@ class CallbackProxy extends Handler { // functions just need to operate within the UI thread. //-------------------------------------------------------------------------- - public boolean onSavePassword(String host, String username, String password, - Message resumeMsg) { + 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) { @@ -711,7 +711,7 @@ class CallbackProxy extends Handler { Message msg = obtainMessage(SAVE_PASSWORD, resumeMsg); Bundle bundle = msg.getData(); - bundle.putString("host", host); + bundle.putString("host", schemePlusHost); bundle.putString("username", username); bundle.putString("password", password); synchronized (this) { diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index 176471f..00b17d2 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -151,13 +151,34 @@ public final class CookieManager { } boolean domainMatch(String urlHost) { - return urlHost.equals(domain) || - (domain.startsWith(".") && - urlHost.endsWith(domain.substring(1))); + 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) { - return urlPath.startsWith (path); + if (urlPath.startsWith(path)) { + int len = path.length(); + int urlLen = urlPath.length(); + if (urlLen > len) { + // make sure /wee doesn't match /we + return urlPath.charAt(len) == PATH_DELIM; + } + return true; + } + return false; } public String toString() { @@ -232,7 +253,7 @@ public final class CookieManager { * a system private class. */ public synchronized void setCookie(WebAddress uri, String value) { - if (value != null && value.length() > 4096) { + if (value != null && value.length() > MAX_COOKIE_LENGTH) { return; } if (!mAcceptCookie || uri == null) { @@ -246,25 +267,19 @@ public final class CookieManager { 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 { - /* Google is setting cookies like the following to detect whether - * a browser supports cookie. We need to skip the leading "www" for - * the default host. Otherwise the second cookie will make the first - * cookie expired. - * - * url: https://www.google.com/accounts/ServiceLoginAuth - * value: LSID=xxxxxxxxxxxxx;Path=/accounts; - * Expires=Tue, 13-Mar-2018 01:41:39 GMT - * - * url: https://www.google.com/accounts/ServiceLoginAuth - * value:LSID=EXPIRED;Domain=www.google.com;Path=/accounts; - * Expires=Mon, 01-Jan-1990 00:00:00 GMT - */ - if (hostAndPath[0].startsWith("www.")) { - hostAndPath[0] = hostAndPath[0].substring(3); - } cookies = parseCookie(hostAndPath[0], hostAndPath[1], value); } catch (RuntimeException ex) { Log.e(LOGTAG, "parse cookie failed for: " + value); @@ -622,26 +637,17 @@ public final class CookieManager { /* * 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/" - * for http://www.google.com/lab?hl=en, the path is "/lab/" - * for http://www.google.com/lab.asp?hl=en, 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); - if (ret[1].charAt(ret[1].length() - 1) != PATH_DELIM) { - index = ret[1].lastIndexOf(PATH_DELIM); - if (ret[1].lastIndexOf('.') > index) { - ret[1] = ret[1].substring(0, index + 1); - } else { - ret[1] += PATH_DELIM; - } - } - } else if (ret[1].charAt(ret[1].length() - 1) != PATH_DELIM) { - ret[1] = ret[1].substring(0, - ret[1].lastIndexOf(PATH_DELIM) + 1); } return ret; } else @@ -687,10 +693,6 @@ public final class CookieManager { String cookieString) { ArrayList<Cookie> ret = new ArrayList<Cookie>(); - // domain needs at least two PERIOD, - if (host.indexOf(PERIOD) == host.lastIndexOf(PERIOD)) { - host = PERIOD + host; - } int index = 0; int length = cookieString.length(); while (true) { @@ -841,15 +843,14 @@ public final class CookieManager { "illegal format for max-age: " + value); } } else if (name.equals(PATH)) { - // make sure path ends with PATH_DELIM - if (value.length() > 1 && - value.charAt(value.length() - 1) != PATH_DELIM) { - cookie.path = value + PATH_DELIM; - } else { - cookie.path = value; - } + 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 @@ -862,15 +863,22 @@ public final class CookieManager { // ignore the exception, value is a host name } value = value.toLowerCase(); - if (value.endsWith(host) || host.endsWith(value)) { - // domain needs at least two PERIOD - if (value.indexOf(PERIOD) == lastPeriod) { - value = PERIOD + value; + 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] - int len = value.length(); - if ((value.charAt(0) == PERIOD) - && (len == lastPeriod + 3) + if ((len == lastPeriod + 3) && (len >= 6 && len <= 8)) { String s = value.substring(1, lastPeriod); if (Arrays.binarySearch(BAD_COUNTRY_2LDS, s) >= 0) { @@ -880,7 +888,7 @@ public final class CookieManager { } cookie.domain = value; } else { - // no cross-site cookie + // no cross-site or more specific sub-domain cookie cookie.domain = null; } } diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index 3dc15c1..750403b 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -17,7 +17,7 @@ package android.webkit; import android.content.Context; -import android.util.Log; +import android.content.res.Resources; import java.util.Calendar; import java.util.Date; @@ -40,6 +40,8 @@ public class DateSorter { 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(); @@ -48,6 +50,7 @@ public class DateSorter { * @param context Application context */ public DateSorter(Context context) { + Resources resources = context.getResources(); Calendar c = Calendar.getInstance(); beginningOfDay(c); @@ -56,9 +59,9 @@ public class DateSorter { mBins[0] = c.getTimeInMillis(); // Today c.roll(Calendar.DAY_OF_YEAR, -1); mBins[1] = c.getTimeInMillis(); // Yesterday - c.roll(Calendar.DAY_OF_YEAR, -4); + c.roll(Calendar.DAY_OF_YEAR, -(NUM_DAYS_AGO - 1)); mBins[2] = c.getTimeInMillis(); // Five days ago - c.roll(Calendar.DAY_OF_YEAR, 5); // move back to today + 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); @@ -67,14 +70,14 @@ public class DateSorter { // build labels mLabels[0] = context.getText(com.android.internal.R.string.today).toString(); mLabels[1] = context.getText(com.android.internal.R.string.yesterday).toString(); - mLabels[2] = context.getString(com.android.internal.R.string.daysDurationPastPlural, 5); - mLabels[3] = context.getText(com.android.internal.R.string.oneMonthDurationPast).toString(); - StringBuilder sb = new StringBuilder(); - sb.append(context.getText(com.android.internal.R.string.before)).append(" "); - sb.append(context.getText(com.android.internal.R.string.oneMonthDurationPast)); - mLabels[4] = sb.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(); } /** diff --git a/core/java/android/webkit/FileLoader.java b/core/java/android/webkit/FileLoader.java index 6696bae..10343b2 100644 --- a/core/java/android/webkit/FileLoader.java +++ b/core/java/android/webkit/FileLoader.java @@ -16,6 +16,8 @@ package android.webkit; +import com.android.internal.R; + import android.content.Context; import android.content.res.AssetManager; import android.net.http.EventHandler; @@ -35,6 +37,7 @@ 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 @@ -44,12 +47,15 @@ class FileLoader extends StreamLoader { * @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 asset, boolean allowFileAccess) { super(loadListener); mIsAsset = asset; mContext = context; + mAllowFileAccess = allowFileAccess; // clean the Url int index = url.indexOf('?'); @@ -73,35 +79,27 @@ class FileLoader extends StreamLoader { mDataStream = mContext.getAssets().open(mPath, AssetManager.ACCESS_STREAMING); } else { - mHandler.error(EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound)); - return false; -/* - if (!mPath.startsWith( - Environment.getExternalStorageDirectory().getPath())) { + if (!mAllowFileAccess) { mHandler.error(EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound)); + 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(com.android.internal.R.string.httpErrorFileNotFound) + + mContext.getString(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) + + mContext.getString(R.string.httpErrorFileNotFound) + " " + ex.getMessage()); return false; } @@ -121,10 +119,13 @@ class FileLoader extends StreamLoader { * @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) { - FileLoader loader = new FileLoader(url, loadListener, context, asset); + 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 index ebfebd0..7a3bbe6 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -28,16 +28,16 @@ import java.util.Map; class FrameLoader { - protected LoadListener mListener; - protected Map<String, String> mHeaders; - protected String mMethod; - protected String mPostData; - protected boolean mIsHighPriority; - protected Network mNetwork; - protected int mCacheMode; - protected String mReferrer; - protected String mUserAgent; - protected String mContentType; + 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; @@ -53,43 +53,14 @@ class FrameLoader { private static final String LOGTAG = "webkit"; - /* - * Construct the Accept_Language once. If the user changes language, then - * the phone will be rebooted. - */ - private static String ACCEPT_LANGUAGE; - static { - // Set the accept-language to the current locale plus US if we are in a - // different locale than US. - java.util.Locale l = java.util.Locale.getDefault(); - ACCEPT_LANGUAGE = ""; - if (l.getLanguage() != null) { - ACCEPT_LANGUAGE += l.getLanguage(); - if (l.getCountry() != null) { - ACCEPT_LANGUAGE += "-" + l.getCountry(); - } - } - if (!l.equals(java.util.Locale.US)) { - ACCEPT_LANGUAGE += ", "; - java.util.Locale us = java.util.Locale.US; - if (us.getLanguage() != null) { - ACCEPT_LANGUAGE += us.getLanguage(); - if (us.getCountry() != null) { - ACCEPT_LANGUAGE += "-" + us.getCountry(); - } - } - } - } - - - FrameLoader(LoadListener listener, String userAgent, + FrameLoader(LoadListener listener, WebSettings settings, String method, boolean highPriority) { mListener = listener; mHeaders = null; mMethod = method; mIsHighPriority = highPriority; mCacheMode = WebSettings.LOAD_NORMAL; - mUserAgent = userAgent; + mSettings = settings; } public void setReferrer(String ref) { @@ -97,7 +68,7 @@ class FrameLoader { if (URLUtil.isNetworkUrl(ref)) mReferrer = ref; } - public void setPostData(String postData) { + public void setPostData(byte[] postData) { mPostData = postData; } @@ -140,12 +111,15 @@ class FrameLoader { } 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(false); - } else if (URLUtil.isCookielessProxyUrl(url)) { - mNetwork = Network.getInstance(mListener.getContext()); - return handleHTTPLoad(true); - } else if (handleLocalFile(url, mListener)) { + return handleHTTPLoad(); + } else if (handleLocalFile(url, mListener, mSettings)) { return true; } if (Config.LOGV) { @@ -160,14 +134,15 @@ class FrameLoader { } /* package */ - static boolean handleLocalFile(String url, LoadListener loadListener) { + static boolean handleLocalFile(String url, LoadListener loadListener, + WebSettings settings) { if (URLUtil.isAssetUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), - true); + true, settings.getAllowFileAccess()); return true; } else if (URLUtil.isFileUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), - false); + false, settings.getAllowFileAccess()); return true; } else if (URLUtil.isContentUrl(url)) { // Send the raw url to the ContentLoader because it will do a @@ -186,21 +161,12 @@ class FrameLoader { return false; } - protected boolean handleHTTPLoad(boolean proxyUrl) { + private boolean handleHTTPLoad() { if (mHeaders == null) { mHeaders = new HashMap<String, String>(); } populateStaticHeaders(); - - if (!proxyUrl) { - // Don't add private information if this is a proxy load, ie don't - // add cookies and authentication - populateHeaders(); - } else { - // If this is a proxy URL, fix it to be a network load - mListener.setUrl("http://" - + mListener.url().substring(URLUtil.PROXY_BASE.length())); - } + populateHeaders(); // response was handled by UrlIntercept, don't issue HTTP request if (handleUrlIntercept()) return true; @@ -246,7 +212,7 @@ class FrameLoader { * This function is used by handleUrlInterecpt and handleCache to * setup a load from the byte stream in a CacheResult. */ - protected void startCacheLoad(CacheResult result) { + private void startCacheLoad(CacheResult result) { if (Config.LOGV) { Log.v(LOGTAG, "FrameLoader: loading from cache: " + mListener.url()); @@ -264,7 +230,7 @@ class FrameLoader { * * Returns true if the response was handled by UrlIntercept. */ - protected boolean handleUrlIntercept() { + 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( @@ -284,7 +250,7 @@ class FrameLoader { * correctly. * Returns true if the response was handled from the cache */ - protected boolean handleCache() { + private boolean handleCache() { switch (mCacheMode) { // This mode is normally used for a reload, it instructs the http // loader to not use the cached content. @@ -357,11 +323,12 @@ class FrameLoader { } mHeaders.put("Accept-Charset", "utf-8, iso-8859-1, utf-16, *;q=0.7"); - if (ACCEPT_LANGUAGE.length() > 0) { - mHeaders.put("Accept-Language", ACCEPT_LANGUAGE); + String acceptLanguage = mSettings.getAcceptLanguage(); + if (acceptLanguage.length() > 0) { + mHeaders.put("Accept-Language", acceptLanguage); } - - mHeaders.put("User-Agent", mUserAgent); + + mHeaders.put("User-Agent", mSettings.getUserAgentString()); } /** diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java index b22f2ba..c6ec2d2 100644 --- a/core/java/android/webkit/HttpDateTime.java +++ b/core/java/android/webkit/HttpDateTime.java @@ -16,7 +16,7 @@ package android.webkit; -import android.pim.Time; +import android.text.format.Time; import java.util.Calendar; import java.util.regex.Matcher; diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index 1cfea99..a0049ac 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -191,4 +191,5 @@ final class JWebCoreJavaBridge extends Handler { 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/LoadListener.java b/core/java/android/webkit/LoadListener.java index 86947a2..c45ab29 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -99,7 +99,7 @@ class LoadListener extends Handler implements EventHandler { // cache. It is needed if the cache returns a redirect private String mMethod; private Map<String, String> mRequestHeaders; - private String mPostData; + private byte[] mPostData; private boolean mIsHighPriority; // Flag to indicate that this load is synchronous. private boolean mSynchronous; @@ -220,7 +220,8 @@ class LoadListener extends Handler implements EventHandler { */ Message contMsg = obtainMessage(MSG_LOCATION_CHANGED); Message stopMsg = obtainMessage(MSG_CONTENT_FINISHED); - //TODO, need to call mCallbackProxy and request UI. + mBrowserFrame.getCallbackProxy().onFormResubmission( + stopMsg, contMsg); break; } @@ -286,15 +287,16 @@ class LoadListener extends Handler implements EventHandler { if (newMimeType != null) { mMimeType = newMimeType; } - } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml") || - mMimeType. - equalsIgnoreCase("application/vnd.wap.xhtml+xml")) { + } 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. - if (mMimeType.equalsIgnoreCase("application/xhtml+xml")) { + // 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"; } } @@ -527,23 +529,21 @@ class LoadListener extends Handler implements EventHandler { } else { sendMessageInternal(obtainMessage(MSG_LOCATION_CHANGED)); } - - break; + 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); - } else { - final int stringId = - com.android.internal.R.string.httpErrorUnsupportedAuthScheme; - error(EventHandler.ERROR_UNSUPPORTED_AUTH_SCHEME, - getContext().getText(stringId).toString()); + return; } - break; - + 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 @@ -554,16 +554,18 @@ class LoadListener extends Handler implements EventHandler { if (Config.LOGV) { Log.v(LOGTAG, "LoadListener cache load url=" + url()); } - break; - } // Fall through to default if there is no CacheLoader + return; + } + break; // use default case HTTP_NOT_FOUND: // Not an error, the server can send back content. default: - sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); - detachRequestHandle(); break; } + + sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); + detachRequestHandle(); } /** @@ -725,7 +727,7 @@ class LoadListener extends Handler implements EventHandler { * @param isHighPriority */ void setRequestData(String method, Map<String, String> headers, - String postData, boolean isHighPriority) { + byte[] postData, boolean isHighPriority) { mMethod = method; mRequestHeaders = headers; mPostData = postData; @@ -878,37 +880,16 @@ class LoadListener extends Handler implements EventHandler { int statusCode = mStatusCode == HTTP_NOT_MODIFIED ? HTTP_OK : mStatusCode; // pass content-type content-length and content-encoding - int nativeResponse = nativeCreateResponse(mUrl, statusCode, mStatusText, + final int nativeResponse = nativeCreateResponse( + mUrl, statusCode, mStatusText, mMimeType, mContentLength, mEncoding, mCacheResult == null ? 0 : mCacheResult.expires / 1000); if (mHeaders != null) { - // "content-disposition", - String value = mHeaders.getContentDisposition(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.CONTENT_DISPOSITION, value); - } - - // location - value = mHeaders.getLocation(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.LOCATION, value); - } - - // refresh (paypal.com are using this) - value = mHeaders.getRefresh(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.REFRESH, value); - } - - // Content-Type - value = mHeaders.getContentType(); - if (value != null) { - nativeSetResponseHeader(nativeResponse, - Headers.CONTENT_TYPE, value); - } + mHeaders.getHeaders(new Headers.HeaderCallback() { + public void header(String name, String value) { + nativeSetResponseHeader(nativeResponse, name, value); + } + }); } return nativeResponse; } @@ -1048,7 +1029,6 @@ class LoadListener extends Handler implements EventHandler { cancel(); return; } else if (!URLUtil.isNetworkUrl(redirectTo)) { - cancel(); final String text = mContext .getString(com.android.internal.R.string.open_permission_deny) + "\n" + redirectTo; @@ -1250,7 +1230,12 @@ class LoadListener extends Handler implements EventHandler { */ void setUrl(String url) { if (url != null) { - mUrl = URLUtil.stripAnchor(url); + 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 { diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index 2700aa5..85cb8c0 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -65,7 +65,7 @@ public /* package */ class MimeTypeMap { // 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)) { + Pattern.matches("[a-zA-Z_0-9\\.\\-\\(\\)]+", filename)) { int dotPos = filename.lastIndexOf('.'); if (0 <= dotPos) { return filename.substring(dotPos + 1); diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index ea42e58..74622b3 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -155,7 +155,7 @@ class Network { */ public boolean requestURL(String method, Map<String, String> headers, - String postData, + byte [] postData, LoadListener loader, boolean isHighPriority) { @@ -178,9 +178,8 @@ class Network { InputStream bodyProvider = null; int bodyLength = 0; if (postData != null) { - byte[] data = postData.getBytes(); - bodyLength = data.length; - bodyProvider = new ByteArrayInputStream(data); + bodyLength = postData.length; + bodyProvider = new ByteArrayInputStream(postData); } RequestQueue q = mRequestQueue; diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/TextDialog.java index 95209c7..4e9370c 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/TextDialog.java @@ -124,6 +124,10 @@ import android.widget.AutoCompleteTextView; 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 @@ -395,6 +399,7 @@ import android.widget.AutoCompleteTextView; * focus to the host. */ /* package */ void remove() { + mHandler.removeMessages(LONGPRESS); mWebView.removeView(this); mWebView.requestFocus(); } @@ -532,6 +537,7 @@ import android.widget.AutoCompleteTextView; mPreChange = text.toString(); Editable edit = (Editable) getText(); edit.replace(0, edit.length(), text); + updateCachedTextfield(); } /** diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index 43666c1..0e8144e 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -145,6 +145,7 @@ public final class URLUtil { /** * @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); diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java index c86b21d..9dea5ec 100644 --- a/core/java/android/webkit/WebBackForwardList.java +++ b/core/java/android/webkit/WebBackForwardList.java @@ -133,7 +133,7 @@ public class WebBackForwardList implements Cloneable, Serializable { } /* Remove the item at the given index. Called by JNI only. */ - private void removeHistoryItem(int index) { + 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 diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java index 5570af8..a408e06 100644 --- a/core/java/android/webkit/WebHistoryItem.java +++ b/core/java/android/webkit/WebHistoryItem.java @@ -33,6 +33,8 @@ public class WebHistoryItem implements Cloneable { 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. @@ -95,6 +97,18 @@ public class WebHistoryItem implements Cloneable { } /** + * 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 @@ -154,8 +168,10 @@ public class WebHistoryItem implements Cloneable { private native void inflate(int nativeFrame, byte[] data); /* Called by jni when the item is updated */ - private void update(String url, String title, Bitmap favicon, byte[] data) { + 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/WebSettings.java b/core/java/android/webkit/WebSettings.java index de64b30..1a7c4ff 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -20,6 +20,8 @@ import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.Message; +import java.lang.SecurityException; +import android.content.pm.PackageManager; import java.util.Locale; @@ -111,6 +113,7 @@ public class WebSettings { // 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"; @@ -119,7 +122,9 @@ public class WebSettings { private String mCursiveFontFamily = "cursive"; private String mFantasyFontFamily = "fantasy"; private String mDefaultTextEncoding = "Latin-1"; - private String mUserAgent = ANDROID_USERAGENT; + private String mUserAgent; + private boolean mUseDefaultUserAgent; + private String mAcceptLanguage; private String mPluginsPath = ""; private int mMinimumFontSize = 8; private int mMinimumLogicalFontSize = 8; @@ -127,12 +132,14 @@ public class WebSettings { 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. @@ -144,6 +151,7 @@ public class WebSettings { 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 { @@ -212,57 +220,112 @@ public class WebSettings { // User agent strings. private static final String DESKTOP_USERAGENT = - "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/522+ " + - "(KHTML, like Gecko) Safari/419.3"; - private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " + - "CPU like Mac OS X; en) AppleWebKit/420+ (KHTML, like Gecko) " + - "Version/3.0 Mobile/1A543 Safari/419.3"; - private static String ANDROID_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) { - if (ANDROID_USERAGENT == null) { - StringBuffer arg = new StringBuffer(); - // Add version - final String version = Build.VERSION.RELEASE; - if (version.length() > 0) { - arg.append(version); - } else { - // default to "1.0" - arg.append("1.0"); + 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); } - arg.append("; "); - // Initialize the mobile user agent with the default locale. - final Locale l = Locale.getDefault(); - final String language = l.getLanguage(); - if (language != null) { - arg.append(language.toLowerCase()); - final String country = l.getCountry(); + } + 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) { - arg.append("-"); - arg.append(country.toLowerCase()); + buffer.append("-"); + buffer.append(country); } - } else { - // default to "en" - arg.append("en"); } - // Add device name - final String device = Build.DEVICE; - if (device.length() > 0) { - arg.append("; "); - arg.append(device); + } + + 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()); } - final String base = context.getResources().getText( - com.android.internal.R.string.web_user_agent).toString(); - ANDROID_USERAGENT = String.format(base, arg); - mUserAgent = ANDROID_USERAGENT; + } else { + // default to "en" + buffer.append("en"); } - mEventHandler = new EventHandler(); + + final String device = Build.DEVICE; + if (device.length() > 0) { + buffer.append("; "); + buffer.append(device); + } + 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. */ @@ -292,6 +355,21 @@ public class WebSettings { } /** + * 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) { @@ -377,34 +455,48 @@ public class WebSettings { * 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. - * 2 if the WebView should use an iPhone user-agent string. + * + * @deprecated Please use setUserAgentString instead. */ + @Deprecated public synchronized void setUserAgent(int ua) { - if (ua == 0 && !ANDROID_USERAGENT.equals(mUserAgent)) { - mUserAgent = ANDROID_USERAGENT; - postSync(); - } else if (ua == 1 && !DESKTOP_USERAGENT.equals(mUserAgent)) { - mUserAgent = DESKTOP_USERAGENT; - postSync(); - } else if (ua == 2 && !IPHONE_USERAGENT.equals(mUserAgent)) { - mUserAgent = IPHONE_USERAGENT; - postSync(); + 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. - * 2 if the WebView is using an iPhone 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 0; + return -1; } /** @@ -706,6 +798,40 @@ public class WebSettings { 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. @@ -806,11 +932,69 @@ public class WebSettings { return mDefaultTextEncoding; } - /* Package api to grab the user agent string. */ - /*package*/ synchronized String getUserAgentString() { + /** + * 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. @@ -863,6 +1047,20 @@ public class WebSettings { 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 diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 6623257..7467b83 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -56,6 +56,7 @@ import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; import android.webkit.WebViewCore.EventHub; import android.widget.AbsoluteLayout; import android.widget.AdapterView; @@ -94,6 +95,12 @@ 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; @@ -112,7 +119,8 @@ public class WebView extends AbsoluteLayout (com.android.internal.R.id.zoomMagnify); } - public void show(boolean canZoomOut) { + 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); } @@ -211,6 +219,17 @@ public class WebView extends AbsoluteLayout 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; @@ -226,6 +245,7 @@ public class WebView extends AbsoluteLayout 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; @@ -234,6 +254,9 @@ public class WebView extends AbsoluteLayout 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; + // 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; @@ -337,7 +360,9 @@ public class WebView extends AbsoluteLayout 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_TRACKBALL = 24; + static final int LONG_PRESS_ENTER = 23; + static final int PREVENT_TOUCH_ID = 24; + static final int WEBCORE_NEED_TOUCH_EVENTS = 25; // width which view is considered to be fully zoomed out static final int ZOOM_OUT_WIDTH = 1024; @@ -524,23 +549,23 @@ public class WebView extends AbsoluteLayout setVerticalScrollBarEnabled(true); } - /* package */ boolean onSavePassword(String host, String username, + /* 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(host, username, password); + mDatabase.setUsernamePassword(schemePlusHost, username, password); } else { final Message remember = mPrivateHandler.obtainMessage( REMEMBER_PASSWORD); - remember.getData().putString("host", host); + 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", host); + neverRemember.getData().putString("host", schemePlusHost); neverRemember.getData().putString("username", username); neverRemember.getData().putString("password", password); neverRemember.obj = resumeMsg; @@ -749,14 +774,36 @@ public class WebView extends AbsoluteLayout 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 Activity.onSaveInstanceState. + * 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(); @@ -782,29 +829,6 @@ public class WebView extends AbsoluteLayout return null; } history.add(data); - if (i == currentIndex) { - Picture p = capturePicture(); - String path = mContext.getDir("thumbnails", 0).getPath() - + File.separatorChar + hashCode() + "_pic.save"; - File f = new File(path); - try { - final FileOutputStream out = new FileOutputStream(f); - p.writeToStream(out); - out.close(); - } catch (FileNotFoundException e){ - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } - if (f.length() > 0) { - outState.putString("picture", path); - outState.putInt("scrollX", mScrollX); - outState.putInt("scrollY", mScrollY); - outState.putFloat("scale", mActualScale); - } - } } outState.putSerializable("history", history); if (mCertificate != null) { @@ -815,16 +839,103 @@ public class WebView extends AbsoluteLayout } /** + * 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 - * Activity.onThaw. 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. + * {@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")); @@ -853,42 +964,6 @@ public class WebView extends AbsoluteLayout WebHistoryItem item = new WebHistoryItem(data); list.addHistoryItem(item); } - if (inState.containsKey("picture")) { - String path = inState.getString("picture"); - File f = new File(path); - if (f.exists()) { - Picture p = null; - try { - final FileInputStream in = new FileInputStream(f); - p = Picture.createFromStream(in); - in.close(); - f.delete(); - } catch (FileNotFoundException e){ - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - if (p != null) { - int sx = inState.getInt("scrollX", 0); - int sy = inState.getInt("scrollY", 0); - float scale = inState.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(); - } - } - } // Grab the most recent copy to return to the caller. returnList = copyBackForwardList(); // Update the copy to have the correct index. @@ -929,14 +1004,22 @@ public class WebView extends AbsoluteLayout * 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. + * 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" + * "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" + * 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. */ @@ -1133,7 +1216,7 @@ public class WebView extends AbsoluteLayout public void clearView() { mContentWidth = 0; mContentHeight = 0; - mWebViewCore.clearContentPicture(); + mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } /** @@ -1197,7 +1280,7 @@ public class WebView extends AbsoluteLayout clearTextEntry(); ExtendedZoomControls zoomControls = (ExtendedZoomControls) getZoomControls(); - zoomControls.show(canZoomScrollOut()); + zoomControls.show(true, canZoomScrollOut()); zoomControls.requestFocus(); mPrivateHandler.removeCallbacks(mZoomControlRunnable); mPrivateHandler.postDelayed(mZoomControlRunnable, @@ -1206,11 +1289,15 @@ public class WebView extends AbsoluteLayout /** * Return a HitTestResult based on the current focus node. If a HTML::a tag - * is found, the HitTestResult type is set to ANCHOR_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 has to be retrieved through {@link #requestFocusNodeHref} - * asynchronously. If a phone number is found, the HitTestResult type is set + * 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. @@ -1227,42 +1314,38 @@ public class WebView extends AbsoluteLayout if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; - if (node.mIsAnchor && node.mText == null) { - result.setType(HitTestResult.ANCHOR_TYPE); - } else if (node.mIsTextField || node.mIsTextArea) { + if (node.mIsTextField || node.mIsTextArea) { result.setType(HitTestResult.EDIT_TEXT_TYPE); - } else { + } else if (node.mText != null) { String text = node.mText; - if (text != null) { - if (text.startsWith(SCHEME_TEL)) { - result.setType(HitTestResult.PHONE_TYPE); - result.setExtra(text.substring(SCHEME_TEL.length())); - } else if (text.startsWith(SCHEME_MAILTO)) { - result.setType(HitTestResult.EMAIL_TYPE); - result.setExtra(text.substring(SCHEME_MAILTO.length())); - } else if (text.startsWith(SCHEME_GEO)) { - result.setType(HitTestResult.GEO_TYPE); - result.setExtra(URLDecoder.decode(text - .substring(SCHEME_GEO.length()))); - } + 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.ANCHOR_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); - if (nativeIsImage(contentX, contentY)) { + String text = nativeImageURI(contentX, contentY); + if (text != null) { result.setType(type == HitTestResult.UNKNOWN_TYPE ? HitTestResult.IMAGE_TYPE : - HitTestResult.IMAGE_ANCHOR_TYPE); - } - if (nativeHasSrcUrl()) { - result.setType(result.getType() == HitTestResult.ANCHOR_TYPE ? - HitTestResult.SRC_ANCHOR_TYPE : HitTestResult.SRC_IMAGE_ANCHOR_TYPE); + result.setExtra(text); } } return result; @@ -1284,12 +1367,15 @@ public class WebView extends AbsoluteLayout 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. @@ -1298,13 +1384,13 @@ public class WebView extends AbsoluteLayout * as the data member with "url" as key. The result can be null. */ public void requestImageRef(Message msg) { - if (msg == null || mNativeClass == 0) { - return; - } int contentX = viewToContent((int) mLastTouchX + mScrollX); int contentY = viewToContent((int) mLastTouchY + mScrollY); - mWebViewCore.sendMessage(EventHub.REQUEST_IMAGE_HREF, contentX, - contentY, msg); + 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) { @@ -1340,6 +1426,25 @@ public class WebView extends AbsoluteLayout 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) { @@ -1420,15 +1525,29 @@ public class WebView extends AbsoluteLayout // 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_VISIBLE_RECT, rect); + 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; } @@ -1488,12 +1607,19 @@ public class WebView extends AbsoluteLayout } } + // 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 { - return contentToView(mContentHeight); + int height = contentToView(mContentHeight); + if (mFindIsUp) { + height += FIND_HEIGHT; + } + return height; } } @@ -1507,6 +1633,21 @@ public class WebView extends AbsoluteLayout 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 @@ -1612,82 +1753,35 @@ public class WebView extends AbsoluteLayout } /* - * Making the find methods private since we are disabling for 1.0 - * - * Find and highlight the next occurance of the String find, beginning with - * the current selection. Wraps the page infinitely, and scrolls. When - * WebCore determines whether it has found it, the response message is sent - * to its target with true(1) or false(0) as arg1 depending on whether the - * text was found. - * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. A result of 1 means the search - * succeeded. - */ - private void findNext(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND, 1, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); - } - - /* - * Making the find methods private since we are disabling for 1.0 - * - * Find and highlight the previous occurance of the String find, beginning - * with the current selection. - * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. A result of 1 means the search - * succeeded. - */ - private void findPrevious(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND, -1, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); - } - - /* - * Making the find methods private since we are disabling for 1.0 + * Highlight and scroll to the next occurance of String in findAll. + * Wraps the page infinitely, and scrolls. Must be called after + * calling findAll. * - * Find and highlight the first occurance of find, beginning with the start - * of the page. - * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. A result of 1 means the search - * succeeded. + * @param forward Direction to search. */ - private void findFirst(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND, 0, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); + public void findNext(boolean forward) { + nativeFindNext(forward); } /* - * Making the find methods private since we are disabling for 1.0 - * * Find all instances of find on the page and highlight them. * @param find String to find. - * @param response A Message object that will be dispatched with the result - * as the arg1 member. The result will be the number of - * matches to the String find. + * @return int The number of occurances of the String "find" + * that were found. */ - private void findAll(String find, Message response) { - if (response == null) { - return; - } - Message m = Message.obtain(null, EventHub.FIND_ALL, 0, 0, response); - m.getData().putString("find", find); - mWebViewCore.sendMessage(m); + 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 @@ -1714,12 +1808,15 @@ public class WebView extends AbsoluteLayout } /* - * Making the find methods private since we are disabling for 1.0 - * * Clear the highlighting surrounding text matches created by findAll. */ - private void clearMatches() { - mWebViewCore.sendMessage(EventHub.CLEAR_MATCHES); + 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); + invalidate(); } /** @@ -2023,10 +2120,21 @@ public class WebView extends AbsoluteLayout nativeRecomputeFocus(); // Update the buttons in the picture, so when we draw the picture // to the screen, they are in the correct state. - nativeRecordButtons(); + // 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(mTouchMode == TOUCH_SHORTPRESS_START_MODE + || mTrackballDown || mGotEnterDown, false); drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing); } canvas.restoreToCount(sc); + + if (AUTO_REDRAW_HACK && mAutoRedraw) { + invalidate(); + } } @Override @@ -2097,7 +2205,12 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) return; if (mShiftIsPressed) { - nativeDrawSelection(canvas, mSelectX, mSelectY, mExtendSelection); + if (mTouchSelection) { + nativeDrawSelectionRegion(canvas); + } else { + nativeDrawSelection(canvas, mSelectX, mSelectY, + mExtendSelection); + } } else if (drawFocus) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; @@ -2111,7 +2224,14 @@ public class WebView extends AbsoluteLayout } 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) @@ -2163,19 +2283,40 @@ public class WebView extends AbsoluteLayout Rect scrollFrame = new Rect(); scrollFrame.set(mZoomScrollX, mZoomScrollY, mZoomScrollX + width, mZoomScrollY + height); - if (mContentWidth == width) { - float offsetX = (width * halfScale - width) / 2; + 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 == height) { - float offsetY = (height * halfScale - height) / 2; + 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; } @@ -2252,8 +2393,14 @@ public class WebView extends AbsoluteLayout } int sc = canvas.save(); canvas.clipRect(scrollFrame); - float halfX = maxX > 0 ? (float) mZoomScrollX / maxX : 0.5f; - float halfY = maxY > 0 ? (float) mZoomScrollY / maxY : 0.5f; + 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) { @@ -2350,7 +2497,10 @@ public class WebView extends AbsoluteLayout } private void zoomScrollOut() { - if (canZoomScrollOut() == false) return; + if (canZoomScrollOut() == false) { + mTouchMode = TOUCH_DONE_MODE; + return; + } startZoomScrollOut(); mTouchMode = SCROLL_ZOOM_ANIMATION_OUT; invalidate(); @@ -2377,8 +2527,8 @@ public class WebView extends AbsoluteLayout + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x); } x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX; - x = Math.max(0, Math.min(maxScreenX, x)); - mZoomScrollX = (int) (x * maxZoomX / maxScreenX); + x *= Math.max(maxZoomX / maxScreenX, mZoomScrollInvLimit); + mZoomScrollX = Math.max(0, Math.min(maxZoomX, (int) x)); } int maxZoomY = mContentHeight - height; if (maxZoomY > 0) { @@ -2390,8 +2540,8 @@ public class WebView extends AbsoluteLayout + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y); } y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY; - y = Math.max(0, Math.min(maxScreenY, y)); - mZoomScrollY = (int) (y * maxZoomY / maxScreenY); + y *= Math.max(maxZoomY / maxScreenY, mZoomScrollInvLimit); + mZoomScrollY = Math.max(0, Math.min(maxZoomY, (int) y)); } if (oldX != mZoomScrollX || oldY != mZoomScrollY) { invalidate(); @@ -2540,6 +2690,13 @@ public class WebView extends AbsoluteLayout 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); + } + // Used to register the global focus change listener one time to avoid // multiple references to WebView private boolean mGlobalFocusChangeListenerAdded; @@ -2663,7 +2820,8 @@ public class WebView extends AbsoluteLayout ArrayList<String> pastEntries = mDatabase.getFormData(mUrl, mName); if (pastEntries.size() > 0) { ArrayAdapter<String> adapter = new ArrayAdapter<String>( - mContext, com.android.internal.R.layout.simple_list_item_1, + mContext, com.android.internal.R.layout + .search_dropdown_item_1line, pastEntries); ((HashMap) mUpdateMessage.obj).put("adapter", adapter); mUpdateMessage.sendToTarget(); @@ -2679,152 +2837,166 @@ public class WebView extends AbsoluteLayout mTextEntry.setRect(x, y, width, height); } - // These variables are used to determine long press with the enter key, or + // 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 long mDownTime = 0; - private boolean mGotDown = false; + private boolean mGotEnterDown = false; // Enable copy/paste with trackball here. // This should be left disabled until the framework can guarantee // delivering matching key-up and key-down events for the shift key - private static final boolean ENABLE_COPY_PASTE = false; + private static final boolean ENABLE_COPY_PASTE = true; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { + if (LOGV_ENABLED) { + Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() + + ", " + event); + } + + if (mNativeClass == 0) { return false; } - if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER && - keyCode != KeyEvent.KEYCODE_ENTER) { - mGotDown = 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; } - if (mAltIsPressed == false && (keyCode == KeyEvent.KEYCODE_ALT_LEFT - || keyCode == KeyEvent.KEYCODE_ALT_RIGHT)) { - mAltIsPressed = 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 (ENABLE_COPY_PASTE && mShiftIsPressed == false && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { mExtendSelection = false; mShiftIsPressed = true; - if (mNativeClass != 0 && nativeUpdateFocusNode()) { + if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; - mSelectX = node.mBounds.left; - mSelectY = node.mBounds.top; + mSelectX = contentToView(node.mBounds.left); + mSelectY = contentToView(node.mBounds.top); } else { - mSelectX = mScrollX + SELECT_CURSOR_OFFSET; - mSelectY = mScrollY + SELECT_CURSOR_OFFSET; + mSelectX = mScrollX + (int) mLastTouchX; + mSelectY = mScrollY + (int) mLastTouchY; } } - if (keyCode == KeyEvent.KEYCODE_CALL) { - if (mNativeClass != 0 && 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; - } + + 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 (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) { - return false; - } - if (LOGV_ENABLED) { - Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() - + ", " + event); - } - boolean isArrowKey = keyCode == KeyEvent.KEYCODE_DPAD_UP || - keyCode == KeyEvent.KEYCODE_DPAD_DOWN || - keyCode == KeyEvent.KEYCODE_DPAD_LEFT || - keyCode == KeyEvent.KEYCODE_DPAD_RIGHT; - if (isArrowKey && event.getEventTime() - mTrackballLastTime - <= TRACKBALL_KEY_TIMEOUT) { - if (LOGV_ENABLED) Log.v(LOGTAG, "ignore arrow"); + 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); + nativeRecordButtons(true, true); + // FIXME, currently in webcore keydown it doesn't do anything. + // In keyup, it calls both keydown and keyup, we should fix it. + mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + return true; + } + // Bubble up the key event as WebView doesn't handle it return false; } - boolean weHandledTheKey = false; - if (event.getMetaState() == 0) { + if (getSettings().getNavDump()) { switch (keyCode) { - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_LEFT: - // TODO: alternatively we can do panning as touch does - switchOutDrawHistory(); - weHandledTheKey = navHandledKey(keyCode, 1, false - , event.getEventTime()); - if (weHandledTheKey) { - playSoundEffect(keyCodeToSoundsEffect(keyCode)); - } + case KeyEvent.KEYCODE_4: + // "/data/data/com.android.browser/displayTree.txt" + nativeDumpDisplayTree(getUrl()); break; - case KeyEvent.KEYCODE_DPAD_CENTER: - case KeyEvent.KEYCODE_ENTER: - if (event.getRepeatCount() == 0) { - switchOutDrawHistory(); - mDownTime = event.getEventTime(); - mGotDown = true; - return true; - } - if (mGotDown && event.getEventTime() - mDownTime > - ViewConfiguration.getLongPressTimeout()) { - performLongClick(); - mGotDown = false; - return true; - } + 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_9: - if (mNativeClass != 0 && getSettings().getNavDump()) { - debugDump(); - } + 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; - default: + case KeyEvent.KEYCODE_9: + debugDump(); break; } } - // suppress sending arrow keys to webkit - if (!weHandledTheKey && !isArrowKey) { - mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, 0, event); + if (nativeFocusNodeWantsKeyEvents()) { + mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, + EventHub.KEYEVENT_FOCUS_NODE_TYPE, event); + // return true as DOM handles the key + return true; + } else if (false) { // reserved to check the meta tag + // pass the key to DOM + mWebViewCore.sendMessage(EventHub.KEY_DOWN, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + // return true as DOM handles the key + return true; } - return weHandledTheKey; + // Bubble up the key event as WebView doesn't handle it + return false; } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT - || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { - 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); - } - } - mShiftIsPressed = false; - return true; - } if (LOGV_ENABLED) { - Log.v(LOGTAG, "MT keyUp at" + System.currentTimeMillis() + Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() + ", " + event); } - if (keyCode == KeyEvent.KEYCODE_ALT_LEFT - || keyCode == KeyEvent.KEYCODE_ALT_RIGHT) { - mAltIsPressed = false; + + 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) { @@ -2835,64 +3007,112 @@ public class WebView extends AbsoluteLayout } return false; } - Rect visibleRect = sendOurVisibleRect(); - // Note that sendOurVisibleRect calls viewToContent, so the coordinates - // should be in content coordinates. - boolean nodeOnScreen = false; - boolean isTextField = false; - boolean isTextArea = false; - FocusNode node = null; - if (mNativeClass != 0 && nativeUpdateFocusNode()) { - node = mFocusNode; - isTextField = node.mIsTextField; - isTextArea = node.mIsTextArea; - nodeOnScreen = Rect.intersects(node.mBounds, visibleRect); - } - - 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; - } + + if (ENABLE_COPY_PASTE && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { + if (commitCopy()) { return true; - } else { - keyCode = KeyEvent.KEYCODE_ENTER; } } - if (KeyEvent.KEYCODE_ENTER == keyCode) { - if (LOGV_ENABLED) Log.v(LOGTAG, "KEYCODE_ENTER == keyCode"); - if (!nodeOnScreen) { - return false; + + 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. + boolean nodeOnScreen = false; + boolean isTextField = false; + boolean isTextArea = false; + FocusNode node = null; + if (nativeUpdateFocusNode()) { + node = mFocusNode; + isTextField = node.mIsTextField; + isTextArea = node.mIsTextArea; + nodeOnScreen = Rect.intersects(node.mBounds, visibleRect); } - if (node != null && !isTextField && !isTextArea) { + if (nodeOnScreen && !isTextField && !isTextArea) { nativeSetFollowedLink(true); - mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, + mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0, new WebViewCore.FocusData(mFocusData)); - if (mCallbackProxy.uiOverrideUrlLoading(node.mText)) { - return true; - } playSoundEffect(SoundEffectConstants.CLICK); + if (!mCallbackProxy.uiOverrideUrlLoading(node.mText)) { + mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + } + return true; } - } - if (!nodeOnScreen) { - // FIXME: Want to give Callback a chance to handle it, and - // possibly pass down to javascript. + // Bubble up the key event as WebView doesn't handle it return false; } - if (LOGV_ENABLED) Log.v(LOGTAG, "onKeyUp send EventHub.KEY_UP"); - mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, 0, event); - return true; + + if (nativeFocusNodeWantsKeyEvents()) { + mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, + EventHub.KEYEVENT_FOCUS_NODE_TYPE, event); + // return true as DOM handles the key + return true; + } else if (false) { // reserved to check the meta tag + // pass the key to DOM + mWebViewCore.sendMessage(EventHub.KEY_UP, keyCode, + EventHub.KEYEVENT_UNHANDLED_TYPE, event); + // return true as DOM handles the key + return true; + } + + // Bubble up the key event as WebView doesn't handle it + return false; } - + + 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; + } + } + 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 @@ -2962,6 +3182,7 @@ public class WebView extends AbsoluteLayout // we regain focus. mDrawFocusRing = false; mGotKeyDown = false; + mShiftIsPressed = false; if (inEditingMode()) { clearTextEntry(); mNeedsUpdateTextEntry = true; @@ -3055,8 +3276,7 @@ public class WebView extends AbsoluteLayout @Override public boolean onTouchEvent(MotionEvent ev) { - if (mNativeClass == 0 || !isClickable() || !isLongClickable() || - !hasFocus()) { + if (mNativeClass == 0 || !isClickable() || !isLongClickable()) { return false; } @@ -3069,6 +3289,32 @@ public class WebView extends AbsoluteLayout 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; + } + switch (action) { case MotionEvent.ACTION_DOWN: { if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN @@ -3083,6 +3329,16 @@ public class WebView extends AbsoluteLayout 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 { mTouchMode = TOUCH_INIT_MODE; } @@ -3116,6 +3372,17 @@ public class WebView extends AbsoluteLayout int deltaY = (int) (mLastTouchY - y); 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 ((deltaX * deltaX + deltaY * deltaY) < TOUCH_SLOP_SQUARE) { break; @@ -3214,11 +3481,13 @@ public class WebView extends AbsoluteLayout mUserScroll = true; } - if (mZoomControls != null && mMinZoomScale < mMaxZoomScale) { + boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; + boolean showMagnify = canZoomScrollOut(); + if (mZoomControls != null && (showPlusMinus || showMagnify)) { if (mZoomControls.getVisibility() == View.VISIBLE) { mPrivateHandler.removeCallbacks(mZoomControlRunnable); } else { - mZoomControls.show(canZoomScrollOut()); + mZoomControls.show(showPlusMinus, showMagnify); } mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); @@ -3244,6 +3513,10 @@ public class WebView extends AbsoluteLayout doShortPress(); } break; + case TOUCH_SELECT_MODE: + commitCopy(); + mTouchSelection = mExtendSelection = false; + break; case SCROLL_ZOOM_ANIMATION_IN: case SCROLL_ZOOM_ANIMATION_OUT: // no action during scroll animation @@ -3349,6 +3622,7 @@ public class WebView extends AbsoluteLayout 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; @@ -3359,7 +3633,6 @@ public class WebView extends AbsoluteLayout private int mSelectX = 0; private int mSelectY = 0; private boolean mShiftIsPressed = false; - private boolean mAltIsPressed = false; private boolean mTrackballDown = false; private long mTrackballUpTime = 0; private long mLastFocusTime = 0; @@ -3386,17 +3659,18 @@ public class WebView extends AbsoluteLayout @Override public boolean onTrackballEvent(MotionEvent ev) { long time = ev.getEventTime(); - if (mAltIsPressed) { + 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); - mPrivateHandler.sendMessageDelayed( - mPrivateHandler.obtainMessage(LONG_PRESS_TRACKBALL), 1000); mTrackTrackball = true; mTrackballDown = true; + if (mNativeClass != 0) { + nativeRecordButtons(true, true); + } if (time - mLastFocusTime <= TRACKBALL_TIMEOUT && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) { nativeSelectBestAt(mLastFocusBounds); @@ -3408,7 +3682,8 @@ public class WebView extends AbsoluteLayout } return false; // let common code in onKeyDown at it } else if (mTrackTrackball) { - mPrivateHandler.removeMessages(LONG_PRESS_TRACKBALL); + // LONG_PRESS_ENTER is set in common onKeyDown + mPrivateHandler.removeMessages(LONG_PRESS_ENTER); mTrackTrackball = false; } if (ev.getAction() == MotionEvent.ACTION_UP) { @@ -3823,13 +4098,7 @@ public class WebView extends AbsoluteLayout 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.getTouchSlop(); - nativeMotionUp(contentX, contentY, contentSize, true); - - // call uiOverride next to check whether it is a special node, + // call uiOverride to check whether it is a special node, // phone/email/address, which are not handled by WebKit if (nativeUpdateFocusNode()) { FocusNode node = mFocusNode; @@ -3840,6 +4109,11 @@ public class WebView extends AbsoluteLayout } playSoundEffect(SoundEffectConstants.CLICK); } + // 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(); + nativeMotionUp(contentX, contentY, contentSize, true); } @Override @@ -3907,6 +4181,8 @@ public class WebView extends AbsoluteLayout mHeightCanMeasure = false; } } + } else { + mHeightCanMeasure = false; } if (mNativeClass != 0) { nativeSetHeightCanMeasure(mHeightCanMeasure); @@ -3915,6 +4191,8 @@ public class WebView extends AbsoluteLayout if (widthMode == MeasureSpec.UNSPECIFIED) { mWidthCanMeasure = true; measuredWidth = contentWidth; + } else { + mWidthCanMeasure = false; } synchronized (this) { @@ -4076,10 +4354,14 @@ public class WebView extends AbsoluteLayout break; case NEW_PICTURE_MSG_ID: // called for new content - final Point viewSize = (Point) msg.obj; + final WebViewCore.DrawData draw = + (WebViewCore.DrawData) msg.obj; + final Point viewSize = draw.mViewPoint; if (mZoomScale > 0) { - if (Math.abs(mZoomScale * viewSize.x - - getViewWidth()) < 1) { + // 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); @@ -4091,8 +4373,14 @@ public class WebView extends AbsoluteLayout // received in the fixed dimension. final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; - recordNewContentSize(msg.arg1, msg.arg2, updateLayout); - invalidate(); + 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()); } @@ -4156,6 +4444,7 @@ public class WebView extends AbsoluteLayout } 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; @@ -4165,10 +4454,15 @@ public class WebView extends AbsoluteLayout // to 0 mLastWidthSent = 0; } - // by default starting a new page with 100% zoom scale. - scale = initialScale == 0 ? (viewportWidth > 0 ? - ((float) width / viewportWidth) : 1.0f) - : initialScale / 100.0f; + 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; @@ -4213,10 +4507,32 @@ public class WebView extends AbsoluteLayout WebViewCore.resumeUpdate(mWebViewCore); break; - case LONG_PRESS_TRACKBALL: + case LONG_PRESS_ENTER: + // as this is shared by keydown and trackballdown, reset all + // the states + mGotEnterDown = false; mTrackTrackball = false; mTrackballDown = false; - performLongClick(); + // 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: + // update may have already been paused by touch; restore since + // this effectively aborts touch and skips logic in touch up + if (mTouchMode == TOUCH_DRAG_MODE) { + WebViewCore.resumeUpdate(mWebViewCore); + } + mTouchMode = TOUCH_DONE_MODE; break; default: @@ -4514,10 +4830,7 @@ public class WebView extends AbsoluteLayout } Rect contentFocus = nativeGetFocusRingBounds(); if (contentFocus.isEmpty()) return keyHandled; - Rect viewFocus = new Rect(contentToView(contentFocus.left) - , contentToView(contentFocus.top) - , contentToView(contentFocus.right) - , contentToView(contentFocus.bottom)); + Rect viewFocus = contentToView(contentFocus); Rect visRect = new Rect(); calcOurVisibleRect(visRect); Rect outset = new Rect(visRect); @@ -4559,7 +4872,7 @@ public class WebView extends AbsoluteLayout public void debugDump() { nativeDebugDump(); - mWebViewCore.sendMessage(EventHub.DUMP_WEBKIT); + mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); } /** @@ -4583,6 +4896,7 @@ public class WebView extends AbsoluteLayout 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(); @@ -4593,17 +4907,27 @@ public class WebView extends AbsoluteLayout boolean noScroll); private native void nativeNotifyFocusSet(boolean inEditingMode); private native void nativeRecomputeFocus(); - private native void nativeRecordButtons(); + // 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 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 boolean nativeHasSrcUrl(); - private native boolean nativeIsImage(int x, int y); + 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/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 263346e..9e413f9 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -70,13 +70,6 @@ final class WebViewCore { // The BrowserFrame is an interface to the native Frame component. private BrowserFrame mBrowserFrame; - - /* This is a ring of pictures for content. After B is built, it is swapped - with A. - */ - private Picture mContentPictureA = new Picture(); // draw() - private Picture mContentPictureB = new Picture(); // nativeDraw() - /* * range is from 200 to 10,000. 0 is a special value means device-width. -1 * means undefined. @@ -196,7 +189,8 @@ final class WebViewCore { sWebCoreHandler.removeMessages(WebCoreThread.INITIALIZE, this); } - /* Get the BrowserFrame component. This is used for subwindow creation. */ + /* 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; } @@ -278,30 +272,40 @@ final class WebViewCore { static native String nativeFindAddress(String addr); /** - * Find and highlight an occurance of text matching find - * @param find The text to find. - * @param forward If true, search forward. Else, search backwards. - * @param fromSelection Whether to start from the current selection or from - * the beginning of the viewable page. - * @return boolean Whether the text was found. + * Empty the picture set. */ - private native boolean nativeFind(String find, - boolean forward, - boolean fromSelection); - + 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); + /** - * Find all occurances of text matching find and highlight them. - * @param find The text to find. - * @return int The number of occurances of find found. + * Redraw a portion of the picture set. The Point wh returns the + * width and height of the overall picture. */ - private native int nativeFindAll(String find); + private native boolean nativeRecordContent(Region invalRegion, Point wh); /** - * Clear highlights on text created by nativeFindAll. + * Splits slow parts of the picture set. Called from the webkit + * thread after nativeDrawContent returns true. */ - private native void nativeClearMatches(); + private native void nativeSplitContent(); + + // these must be kept lock-step with the KeyState enum in WebViewCore.h + static private final int KEY_ACTION_DOWN = 0; + static private final int KEY_ACTION_UP = 1; - private native void nativeDraw(Picture content); + private native boolean nativeSendKeyToFocusNode(int keyCode, int unichar, + int repeatCount, boolean isShift, boolean isAlt, int keyAction); private native boolean nativeKeyUp(int keycode, int keyvalue); @@ -343,21 +347,12 @@ final class WebViewCore { private native String nativeRetrieveHref(int framePtr, int nodePtr); - /** - * Return the url of the image located at (x,y) in content coordinates, or - * null if there is no image at that point. - * - * @param x x content ordinate - * @param y y content ordinate - * @return String url of the image located at (x,y), or null if there is - * no image there. - */ - private native String nativeRetrieveImageRef(int x, int y); - 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(); @@ -368,7 +363,11 @@ final class WebViewCore { private native void nativeSetBackgroundColor(int color); - private native void nativeDump(); + private native void nativeDumpDomTree(boolean useFile); + + private native void nativeDumpRenderTree(boolean useFile); + + private native void nativeDumpNavTree(); private native void nativeRefreshPlugins(boolean reloadOpenPages); @@ -393,6 +392,10 @@ final class WebViewCore { 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 @@ -421,10 +424,7 @@ final class WebViewCore { switch (msg.what) { case INITIALIZE: WebViewCore core = (WebViewCore) msg.obj; - synchronized (core) { - core.initialize(); - core.notify(); - } + core.initialize(); break; case REDUCE_PRIORITY: @@ -501,6 +501,12 @@ final class WebViewCore { boolean mRetry; } + static class TouchEventData { + int mAction; // MotionEvent.getAction() + int mX; + int mY; + } + class EventHub { // Message Ids static final int LOAD_URL = 100; @@ -510,7 +516,7 @@ final class WebViewCore { static final int KEY_UP = 104; static final int VIEW_SIZE_CHANGED = 105; static final int GO_BACK_FORWARD = 106; - static final int SET_VISIBLE_RECT = 107; + 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; @@ -519,16 +525,13 @@ final class WebViewCore { static final int SET_SELECTION = 113; static final int REPLACE_TEXT = 114; static final int PASS_TO_JS = 115; - static final int FIND = 116; + static final int SET_GLOBAL_BOUNDS = 116; static final int UPDATE_CACHE_AND_TEXT_ENTRY = 117; - static final int FIND_ALL = 118; - static final int CLEAR_MATCHES = 119; 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 DUMP_WEBKIT = 125; static final int SET_BACKGROUND_COLOR = 126; static final int UNBLOCK_FOCUS = 127; static final int SAVE_DOCUMENT_STATE = 128; @@ -536,9 +539,10 @@ final class WebViewCore { 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 REQUEST_IMAGE_HREF = 134; static final int SET_FINAL_FOCUS = 135; static final int SET_KIT_FOCUS = 136; static final int REQUEST_FOCUS_HREF = 137; @@ -547,6 +551,8 @@ final class WebViewCore { // 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; @@ -554,6 +560,12 @@ final class WebViewCore { // 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; @@ -561,6 +573,19 @@ final class WebViewCore { static final int NO_FOCUS_CHANGE_BLOCK = 0; static final int BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP = 1; + /* The KEY_DOWN and KEY_UP messages pass the keyCode in arg1, and a + "type" in arg2. These are the types, and they describe what the + circumstances were that prompted the UI thread to send the keyevent + to webkit. + + FOCUS_NODE - the currently focused node says it wants key events + (e.g. plugins) + UNHANDLED - the UI side did not handle the key, so we give webkit + a shot at it. + */ + static final int KEYEVENT_FOCUS_NODE_TYPE = 0; + static final int KEYEVENT_UNHANDLED_TYPE = 1; + // Private handler for WebCore messages. private Handler mHandler; // Message queue for containing messages before the WebCore thread is @@ -607,8 +632,30 @@ final class WebViewCore { case LOAD_DATA: HashMap loadParams = (HashMap) msg.obj; - mBrowserFrame.loadData( - (String) loadParams.get("baseUrl"), + 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"), @@ -622,8 +669,7 @@ final class WebViewCore { // up with native side if (mBrowserFrame.committed() && !mBrowserFrame.firstLayoutDone()) { - mBrowserFrame.didFirstLayout(mBrowserFrame - .currentUrl()); + mBrowserFrame.didFirstLayout(); } // Do this after syncing up the layout state. stopLoading(); @@ -634,11 +680,11 @@ final class WebViewCore { break; case KEY_DOWN: - keyDown(msg.arg1, (KeyEvent) msg.obj); + keyDown(msg.arg1, msg.arg2, (KeyEvent) msg.obj); break; case KEY_UP: - keyUp(msg.arg1, (KeyEvent) msg.obj); + keyUp(msg.arg1, msg.arg2, (KeyEvent) msg.obj); break; case VIEW_SIZE_CHANGED: @@ -646,12 +692,16 @@ final class WebViewCore { ((Float) msg.obj).floatValue()); break; - case SET_VISIBLE_RECT: - Rect r = (Rect) msg.obj; + case SET_SCROLL_OFFSET: // note: these are in document coordinates // (inv-zoom) - nativeSetVisibleRect(r.left, r.top, r.width(), - r.height()); + 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: @@ -745,30 +795,6 @@ final class WebViewCore { break; } - case FIND: - /* arg1: - * 1 - Find next - * -1 - Find previous - * 0 - Find first - */ - Message response = (Message) msg.obj; - boolean find = nativeFind(msg.getData().getString("find"), - msg.arg1 != -1, msg.arg1 != 0); - response.arg1 = find ? 1 : 0; - response.sendToTarget(); - break; - - case FIND_ALL: - int found = nativeFindAll(msg.getData().getString("find")); - Message resAll = (Message) msg.obj; - resAll.arg1 = found; - resAll.sendToTarget(); - break; - - case CLEAR_MATCHES: - nativeClearMatches(); - break; - case CLEAR_SSL_PREF_TABLE: Network.getInstance(mContext) .clearUserSslPrefTable(); @@ -784,6 +810,17 @@ final class WebViewCore { touchUpData.mRetry); break; + case TOUCH_EVENT: { + TouchEventData ted = (TouchEventData) msg.obj; + if (nativeHandleTouchEvent(ted.mAction, ted.mX, + ted.mY)) { + Message.obtain(mWebView.mPrivateHandler, + WebView.PREVENT_TOUCH_ID) + .sendToTarget(); + } + break; + } + case ADD_JS_INTERFACE: HashMap map = (HashMap) msg.obj; Object obj = map.get("object"); @@ -826,27 +863,17 @@ final class WebViewCore { case REQUEST_FOCUS_HREF: { Message hrefMsg = (Message) msg.obj; String res = nativeRetrieveHref(msg.arg1, msg.arg2); - Bundle data = hrefMsg.getData(); - data.putString("url", res); - hrefMsg.setData(data); + hrefMsg.getData().putString("url", res); hrefMsg.sendToTarget(); break; } - case REQUEST_IMAGE_HREF: { - Message refMsg = (Message) msg.obj; - String ref = - nativeRetrieveImageRef(msg.arg1, msg.arg2); - Bundle data = refMsg.getData(); - data.putString("url", ref); - refMsg.setData(data); - refMsg.sendToTarget(); - break; - } - case UPDATE_CACHE_AND_TEXT_ENTRY: nativeUpdateFrameCache(); - sendViewInvalidate(); + // FIXME: this should provide a minimal rectangle + if (mWebView != null) { + mWebView.postInvalidate(); + } sendUpdateTextEntry(); break; @@ -901,9 +928,17 @@ final class WebViewCore { , WebView.UPDATE_CLIPBOARD, str) .sendToTarget(); break; - - case DUMP_WEBKIT: - nativeDump(); + + case DUMP_DOMTREE: + nativeDumpDomTree(msg.arg1 == 1); + break; + + case DUMP_RENDERTREE: + nativeDumpRenderTree(msg.arg1 == 1); + break; + + case DUMP_NAVTREE: + nativeDumpNavTree(); break; case SYNC_SCROLL: @@ -914,6 +949,18 @@ final class WebViewCore { 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; } } }; @@ -982,6 +1029,7 @@ final class WebViewCore { private synchronized void removeMessages() { // reset mDrawIsScheduled flag as WEBKIT_DRAW may be removed mDrawIsScheduled = false; + mSplitPictureIsScheduled = false; if (mMessages != null) { mMessages.clear(); } else { @@ -1001,7 +1049,7 @@ final class WebViewCore { // Methods called by host activity (in the same thread) //------------------------------------------------------------------------- - public void stopLoading() { + void stopLoading() { if (LOGV_ENABLED) Log.v(LOGTAG, "CORE stopLoading"); if (mBrowserFrame != null) { mBrowserFrame.stopLoading(); @@ -1082,23 +1130,48 @@ final class WebViewCore { mBrowserFrame.loadUrl(url); } - private void keyDown(int code, KeyEvent event) { + private void keyDown(int code, int target, KeyEvent event) { if (LOGV_ENABLED) { Log.v(LOGTAG, "CORE keyDown at " + System.currentTimeMillis() + ", " + event); } + switch (target) { + case EventHub.KEYEVENT_UNHANDLED_TYPE: + break; + case EventHub.KEYEVENT_FOCUS_NODE_TYPE: + if (nativeSendKeyToFocusNode(code, event.getUnicodeChar(), + event.getRepeatCount(), + event.isShiftPressed(), + event.isAltPressed(), + KEY_ACTION_DOWN)) { + return; + } + break; + } + // If we get here, no one handled it, so call our proxy mCallbackProxy.onUnhandledKeyEvent(event); } - private void keyUp(int code, KeyEvent event) { + private void keyUp(int code, int target, KeyEvent event) { if (LOGV_ENABLED) { Log.v(LOGTAG, "CORE keyUp at " + System.currentTimeMillis() + ", " + event); } - if (!nativeKeyUp(code, event.getUnicodeChar())) { - mCallbackProxy.onUnhandledKeyEvent(event); + switch (target) { + case EventHub.KEYEVENT_UNHANDLED_TYPE: + if (!nativeKeyUp(code, event.getUnicodeChar())) { + mCallbackProxy.onUnhandledKeyEvent(event); + } + break; + case EventHub.KEYEVENT_FOCUS_NODE_TYPE: + nativeSendKeyToFocusNode(code, event.getUnicodeChar(), + event.getRepeatCount(), + event.isShiftPressed(), + event.isAltPressed(), + KEY_ACTION_UP); + break; + } } - } // These values are used to avoid requesting a layout based on old values private int mCurrentViewWidth = 0; @@ -1140,8 +1213,9 @@ final class WebViewCore { mCurrentViewHeight = h; if (needInvalidate) { // ensure {@link #webkitDraw} is called as we were blocking in - // {@link #contentInvalidate} when mCurrentViewWidth is 0 - contentInvalidate(); + // {@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)); @@ -1156,30 +1230,42 @@ final class WebViewCore { // 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; - nativeDraw(mContentPictureB); - int w; - int h; - synchronized (this) { - Picture temp = mContentPictureB; - mContentPictureB = mContentPictureA; - mContentPictureA = temp; - w = mContentPictureA.getWidth(); - h = mContentPictureA.getHeight(); + 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, w, h, - new Point(mCurrentViewWidth, mCurrentViewHeight)) - .sendToTarget(); + WebView.NEW_PICTURE_MSG_ID, draw).sendToTarget(); if (mWebkitScrollX != 0 || mWebkitScrollY != 0) { // as we have the new picture, try to sync the scroll position Message.obtain(mWebView.mPrivateHandler, @@ -1217,37 +1303,18 @@ final class WebViewCore { df = mScrollFilter; } canvas.setDrawFilter(df); - synchronized (this) { - Picture picture = mContentPictureA; - int sc = canvas.save(Canvas.CLIP_SAVE_FLAG); - Rect clip = new Rect(0, 0, picture.getWidth(), picture.getHeight()); - canvas.clipRect(clip, Region.Op.DIFFERENCE); - canvas.drawColor(color); - canvas.restoreToCount(sc); - // experiment commented out - // if (TEST_BUCKET) { - // nativeDrawContentPicture(canvas); - // } else { - canvas.drawPicture(picture); - // } - } + boolean tookTooLong = nativeDrawContent(canvas, color); canvas.setDrawFilter(null); - } - - /* package */ void clearContentPicture() { - // experiment commented out - // if (TEST_BUCKET) { - // nativeClearContentPicture(); - // } - synchronized (this) { - mContentPictureA = new Picture(); + if (tookTooLong && mSplitPictureIsScheduled == false) { + mSplitPictureIsScheduled = true; + sendMessage(EventHub.SPLIT_PICTURE_SET); } } /*package*/ Picture copyContentPicture() { - synchronized (this) { - return new Picture(mContentPictureA); - } + Picture result = new Picture(); + nativeCopyContentToPicture(result); + return result; } static void pauseUpdate(WebViewCore core) { @@ -1263,7 +1330,7 @@ final class WebViewCore { // webcore thread priority is still lowered. if (core != null) { synchronized (core) { - core.mDrawIsScheduled = true; + core.mDrawIsPaused = true; core.mEventHub.removeMessages(EventHub.WEBKIT_DRAW); } } @@ -1278,7 +1345,9 @@ final class WebViewCore { if (core != null) { synchronized (core) { core.mDrawIsScheduled = false; - core.contentInvalidate(); + core.mDrawIsPaused = false; + if (LOGV_ENABLED) Log.v(LOGTAG, "resumeUpdate"); + core.contentDraw(); } } } @@ -1309,7 +1378,7 @@ final class WebViewCore { //------------------------------------------------------------------------- // called from JNI or WebView thread - /* package */ void contentInvalidate() { + /* package */ void contentDraw() { // don't update the Picture until we have an initial width and finish // the first layout if (mCurrentViewWidth == 0 || !mBrowserFrame.firstLayoutDone()) { @@ -1317,14 +1386,14 @@ final class WebViewCore { } // only fire an event if this is our first request synchronized (this) { - if (mDrawIsScheduled) { + if (mDrawIsPaused || mDrawIsScheduled) { return; } mDrawIsScheduled = true; mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW)); } } - + // called by JNI private void contentScrollBy(int dx, int dy) { if (!mBrowserFrame.firstLayoutDone()) { @@ -1397,6 +1466,7 @@ final class WebViewCore { sWebCoreHandler.removeMessages(WebCoreThread.CACHE_TICKER); sWebCoreHandler.sendMessage(sWebCoreHandler .obtainMessage(WebCoreThread.CACHE_TICKER)); + contentDraw(); } // called by JNI @@ -1408,9 +1478,9 @@ final class WebViewCore { } // called by JNI - private void sendViewInvalidate() { + private void sendViewInvalidate(int left, int top, int right, int bottom) { if (mWebView != null) { - mWebView.postInvalidate(); + mWebView.postInvalidate(left, top, right, bottom); } } @@ -1421,7 +1491,7 @@ final class WebViewCore { private native void setViewportSettingsFromNative(); // called by JNI - private void didFirstLayout(String url) { + 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 @@ -1435,7 +1505,7 @@ final class WebViewCore { mWebView.mLastHeightSent, -1.0f)); } - mBrowserFrame.didFirstLayout(url); + mBrowserFrame.didFirstLayout(); // reset the scroll position as it is a new page now mWebkitScrollX = mWebkitScrollY = 0; @@ -1517,7 +1587,16 @@ final class WebViewCore { 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) { @@ -1530,9 +1609,10 @@ final class WebViewCore { } } - // these must be in document space (i.e. not scaled/zoomed. - private native void nativeSetVisibleRect(int x, int y, int width, - int height); + // 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, diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index b367e27..96f3698 100644 --- a/core/java/android/webkit/WebViewDatabase.java +++ b/core/java/android/webkit/WebViewDatabase.java @@ -39,7 +39,7 @@ public class WebViewDatabase { // log tag protected static final String LOGTAG = "webviewdatabase"; - private static final int DATABASE_VERSION = 8; + 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 @@ -47,6 +47,7 @@ public class WebViewDatabase { // 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; @@ -172,7 +173,6 @@ public class WebViewDatabase { mDatabase.beginTransaction(); try { upgradeDatabase(); - bootstrapDatabase(); mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); @@ -200,6 +200,10 @@ public class WebViewDatabase { } 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) { @@ -237,24 +241,26 @@ public class WebViewDatabase { if (oldVersion != 0) { Log.i(LOGTAG, "Upgrading database from version " + oldVersion + " to " - + DATABASE_VERSION + ", which will destroy all old data"); + + 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_COOKIES_ID]); - mDatabase.execSQL("DROP TABLE IF EXISTS cache"); - mDatabase.execSQL("DROP TABLE IF EXISTS " + mTableNames[TABLE_PASSWORD_ID]); - 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.setVersion(DATABASE_VERSION); - } - private static void bootstrapDatabase() { - if (mDatabase != null) { + if (!justPasswords) { // cookies mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_COOKIES_ID] + " (" + ID_COL + " INTEGER PRIMARY KEY, " @@ -265,14 +271,6 @@ public class WebViewDatabase { mDatabase.execSQL("CREATE INDEX cookiesIndex ON " + mTableNames[TABLE_COOKIES_ID] + " (path)"); - // password - 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);"); - // formurl mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_FORMURL_ID] + " (" + ID_COL + " INTEGER PRIMARY KEY, " + FORMURL_URL_COL @@ -295,6 +293,13 @@ public class WebViewDatabase { + 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() { @@ -639,10 +644,11 @@ public class WebViewDatabase { if (cursor.moveToFirst()) { int batchSize = 100; StringBuilder pathStr = new StringBuilder(20 + 16 * batchSize); - pathStr.append("DELETE FROM cache WHERE filepath = ?"); + pathStr.append("DELETE FROM cache WHERE filepath IN (?"); for (int i = 1; i < batchSize; i++) { - pathStr.append(" OR filepath = ?"); + pathStr.append(", ?"); } + pathStr.append(")"); SQLiteStatement statement = mCacheDatabase.compileStatement(pathStr .toString()); // as bindString() uses 1-based index, initialize index to 1 @@ -658,6 +664,7 @@ public class WebViewDatabase { pathList.add(filePath); if (index++ == batchSize) { statement.execute(); + statement.clearBindings(); index = 1; } } while (cursor.moveToNext() && amount > 0); @@ -679,19 +686,20 @@ public class WebViewDatabase { /** * Set password. Tuple (PASSWORD_HOST_COL, PASSWORD_USERNAME_COL) is unique. * - * @param host The host for the password + * @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 host, String username, String password) { - if (host == null || mDatabase == null) { + 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, host); + 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, @@ -702,12 +710,12 @@ public class WebViewDatabase { /** * Retrieve the username and password for a given host * - * @param host The host which passwords applies to + * @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 host) { - if (host == null || mDatabase == null) { + String[] getUsernamePassword(String schemePlusHost) { + if (schemePlusHost == null || mDatabase == null) { return null; } @@ -718,8 +726,8 @@ public class WebViewDatabase { synchronized (mPasswordLock) { String[] ret = null; Cursor cursor = mDatabase.query(mTableNames[TABLE_PASSWORD_ID], - columns, selection, new String[] { host }, null, null, - null); + columns, selection, new String[] { schemePlusHost }, null, + null, null); if (cursor.moveToFirst()) { ret = new String[2]; ret[0] = cursor.getString( 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/DesktopAndroid.java b/core/java/android/webkit/gears/DesktopAndroid.java index 00a9a47..ee8ca49 100644 --- a/core/java/android/webkit/gears/DesktopAndroid.java +++ b/core/java/android/webkit/gears/DesktopAndroid.java @@ -40,8 +40,6 @@ import android.webkit.WebView; public class DesktopAndroid { private static final String TAG = "Gears-J-Desktop"; - private static final String BROWSER = "com.android.browser"; - private static final String BROWSER_ACTIVITY = BROWSER + ".BrowserActivity"; private static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; private static final String ACTION_INSTALL_SHORTCUT = "com.android.launcher.action.INSTALL_SHORTCUT"; @@ -78,11 +76,9 @@ public class DesktopAndroid { String url, String imagePath) { Context context = webview.getContext(); - ComponentName browser = new ComponentName(BROWSER, BROWSER_ACTIVITY); - Intent viewWebPage = new Intent(Intent.ACTION_VIEW); - viewWebPage.setComponent(browser); viewWebPage.setData(Uri.parse(url)); + viewWebPage.addCategory(Intent.CATEGORY_BROWSABLE); Intent intent = new Intent(ACTION_INSTALL_SHORTCUT); intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, viewWebPage); diff --git a/core/java/android/webkit/gears/HttpRequestAndroid.java b/core/java/android/webkit/gears/HttpRequestAndroid.java index 8668c54..30f855f 100644 --- a/core/java/android/webkit/gears/HttpRequestAndroid.java +++ b/core/java/android/webkit/gears/HttpRequestAndroid.java @@ -163,7 +163,20 @@ public final class HttpRequestAndroid { // Setup the connection. This doesn't go to the wire yet - it // doesn't block. try { - connection = (HttpURLConnection) new URL(url).openConnection(); + URL url_object = new URL(url); + // Check that the protocol is indeed HTTP(S). + String protocol = url_object.getProtocol(); + if (protocol == null) { + log("null protocol for URL " + url); + return false; + } + protocol = protocol.toLowerCase(); + if (!"http".equals(protocol) && !"https".equals(protocol)) { + log("Url has wrong protocol: " + url); + return false; + } + + connection = (HttpURLConnection) url_object.openConnection(); connection.setRequestMethod(method); // Manually follow redirects. connection.setInstanceFollowRedirects(false); @@ -197,11 +210,13 @@ public final class HttpRequestAndroid { log("interrupt() called but no child thread"); return; } - if (inBlockingOperation) { - log("Interrupting blocking operation"); - childThread.interrupt(); - } else { - log("Nothing to interrupt"); + synchronized (this) { + if (inBlockingOperation) { + log("Interrupting blocking operation"); + childThread.interrupt(); + } else { + log("Nothing to interrupt"); + } } } @@ -472,7 +487,7 @@ public final class HttpRequestAndroid { String encoding = cacheResult.getEncoding(); // Encoding may not be specified. No default. String contentType = mimeType; - if (encoding != null) { + if (encoding != null && encoding.length() > 0) { contentType += "; charset=" + encoding; } setResponseHeader(KEY_CONTENT_TYPE, contentType); 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 index 95fc30f..288240e 100644 --- a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java +++ b/core/java/android/webkit/gears/UrlInterceptHandlerGears.java @@ -407,6 +407,8 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { true); // forceCache if (cacheResult == null) { + // With the no-cache policy we could end up + // with a null result return null; } @@ -444,8 +446,7 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { // be used for input. cacheResult = CacheManager.getCacheFile(gearsUrl, null); if (cacheResult != null) { - if (logEnabled) - log("Returning surrogate result"); + log("Returning surrogate result"); return cacheResult; } else { // Not an expected condition, but handle gracefully. Perhaps out @@ -476,7 +477,10 @@ public class UrlInterceptHandlerGears implements UrlInterceptHandler { } /** - * Convenience debug function. Calls Android logging mechanism. + * 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) { |