diff options
Diffstat (limited to 'core/java/android/webkit')
63 files changed, 7103 insertions, 5890 deletions
diff --git a/core/java/android/webkit/BrowserFrame.java b/core/java/android/webkit/BrowserFrame.java index dbd2682..9456ae1 100644 --- a/core/java/android/webkit/BrowserFrame.java +++ b/core/java/android/webkit/BrowserFrame.java @@ -16,6 +16,7 @@ package android.webkit; +import android.app.ActivityManager; import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; @@ -31,6 +32,7 @@ import junit.framework.Assert; import java.net.URLEncoder; import java.util.HashMap; +import java.util.Map; import java.util.Iterator; class BrowserFrame extends Handler { @@ -59,7 +61,7 @@ class BrowserFrame extends Handler { private boolean mIsMainFrame; // Attached Javascript interfaces - private HashMap mJSInterfaceMap; + private Map<String, Object> mJSInterfaceMap; // message ids // a message posted when a frame loading is completed @@ -98,20 +100,27 @@ class BrowserFrame extends Handler { * XXX: Called by WebCore thread. */ public BrowserFrame(Context context, WebViewCore w, CallbackProxy proxy, - WebSettings settings) { + WebSettings settings, Map<String, Object> javascriptInterfaces) { // Create a global JWebCoreJavaBridge to handle timers and // cookies in the WebCore thread. if (sJavaBridge == null) { - sJavaBridge = new JWebCoreJavaBridge(); + sJavaBridge = new JWebCoreJavaBridge(context); // set WebCore native cache size - sJavaBridge.setCacheSize(4 * 1024 * 1024); + ActivityManager am = (ActivityManager) context + .getSystemService(Context.ACTIVITY_SERVICE); + if (am.getMemoryClass() > 16) { + sJavaBridge.setCacheSize(8 * 1024 * 1024); + } else { + sJavaBridge.setCacheSize(4 * 1024 * 1024); + } // initialize CacheManager CacheManager.init(context); // create CookieSyncManager with current Context CookieSyncManager.createInstance(context); + // create PluginManager with current Context + PluginManager.getInstance(context); } - AssetManager am = context.getAssets(); - nativeCreateFrame(w, am, proxy.getBackForwardList()); + mJSInterfaceMap = javascriptInterfaces; mSettings = settings; mContext = context; @@ -119,7 +128,10 @@ class BrowserFrame extends Handler { mDatabase = WebViewDatabase.getInstance(context); mWebViewCore = w; - if (WebView.LOGV_ENABLED) { + AssetManager am = context.getAssets(); + nativeCreateFrame(w, am, proxy.getBackForwardList()); + + if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "BrowserFrame constructor: this=" + this); } } @@ -217,7 +229,6 @@ class BrowserFrame extends Handler { private void resetLoadingStates() { mCommitted = true; - mWebViewCore.mEndScaleZoom = mFirstLayoutDone == false; mFirstLayoutDone = true; } @@ -240,7 +251,6 @@ class BrowserFrame extends Handler { // blocking the update in {@link #loadStarted} mWebViewCore.contentDraw(); } - mWebViewCore.mEndScaleZoom = true; } /** @@ -341,17 +351,16 @@ class BrowserFrame extends Handler { switch (msg.what) { case FRAME_COMPLETED: { if (mSettings.getSavePassword() && hasPasswordField()) { - if (WebView.DEBUG) { - Assert.assertNotNull(mCallbackProxy.getBackForwardList() - .getCurrentItem()); - } - WebAddress uri = new WebAddress( - mCallbackProxy.getBackForwardList().getCurrentItem() - .getUrl()); - String schemePlusHost = uri.mScheme + uri.mHost; - String[] up = mDatabase.getUsernamePassword(schemePlusHost); - if (up != null && up[0] != null) { - setUsernamePassword(up[0], up[1]); + WebHistoryItem item = mCallbackProxy.getBackForwardList() + .getCurrentItem(); + if (item != null) { + WebAddress uri = new WebAddress(item.getUrl()); + String schemePlusHost = uri.mScheme + uri.mHost; + String[] up = + mDatabase.getUsernamePassword(schemePlusHost); + if (up != null && up[0] != null) { + setUsernamePassword(up[0], up[1]); + } } } CacheManager.trimCacheIfNeeded(); @@ -463,8 +472,6 @@ class BrowserFrame extends Handler { * @param postData If the method is "POST" postData is sent as the request * body. Is null when empty. * @param cacheMode The cache mode to use when loading this resource. - * @param isHighPriority True if this resource needs to be put at the front - * of the network queue. * @param synchronous True if the load is synchronous. * @return A newly created LoadListener object. */ @@ -474,7 +481,6 @@ class BrowserFrame extends Handler { HashMap headers, byte[] postData, int cacheMode, - boolean isHighPriority, boolean synchronous) { PerfChecker checker = new PerfChecker(); @@ -490,7 +496,7 @@ class BrowserFrame extends Handler { } if (mSettings.getSavePassword() && hasPasswordField()) { try { - if (WebView.DEBUG) { + if (DebugFlags.BROWSER_FRAME) { Assert.assertNotNull(mCallbackProxy.getBackForwardList() .getCurrentItem()); } @@ -538,10 +544,10 @@ class BrowserFrame extends Handler { // is this resource the main-frame top-level page? boolean isMainFramePage = mIsMainFrame; - if (WebView.LOGV_ENABLED) { + if (DebugFlags.BROWSER_FRAME) { Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method=" - + method + ", postData=" + postData + ", isHighPriority=" - + isHighPriority + ", isMainFramePage=" + isMainFramePage); + + method + ", postData=" + postData + ", isMainFramePage=" + + isMainFramePage); } // Create a LoadListener @@ -551,23 +557,17 @@ class BrowserFrame extends Handler { mCallbackProxy.onLoadResource(url); if (LoadListener.getNativeLoaderCount() > MAX_OUTSTANDING_REQUESTS) { + // send an error message, so that loadListener can be deleted + // after this is returned. This is important as LoadListener's + // nativeError will remove the request from its DocLoader's request + // list. But the set up is not done until this method is returned. loadListener.error( android.net.http.EventHandler.ERROR, mContext.getString( com.android.internal.R.string.httpErrorTooManyRequests)); - loadListener.notifyError(); - loadListener.tearDown(); - return null; + return loadListener; } - // during synchronous load, the WebViewCore thread is blocked, so we - // need to endCacheTransaction first so that http thread won't be - // blocked in setupFile() when createCacheFile. - if (synchronous) { - CacheManager.endCacheTransaction(); - } - - FrameLoader loader = new FrameLoader(loadListener, mSettings, - method, isHighPriority); + FrameLoader loader = new FrameLoader(loadListener, mSettings, method); loader.setHeaders(headers); loader.setPostData(postData); // Set the load mode to the mode used for the current page. @@ -581,10 +581,6 @@ class BrowserFrame extends Handler { } checker.responseAlert("startLoadingResource succeed"); - if (synchronous) { - CacheManager.startCacheTransaction(); - } - return !synchronous ? loadListener : null; } @@ -615,6 +611,11 @@ class BrowserFrame extends Handler { mCallbackProxy.onReceivedIcon(icon); } + // Called by JNI when an apple-touch-icon attribute was found. + private void didReceiveTouchIconUrl(String url, boolean precomposed) { + mCallbackProxy.onReceivedTouchIconUrl(url, precomposed); + } + /** * Request a new window from the client. * @return The BrowserFrame object stored in the new WebView. @@ -677,6 +678,7 @@ class BrowserFrame extends Handler { // 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; + private static final int DRAWABLEDIR = 3; String getRawResFilename(int id) { int resid; @@ -689,15 +691,33 @@ class BrowserFrame extends Handler { resid = com.android.internal.R.raw.loaderror; break; + case DRAWABLEDIR: + // use one known resource to find the drawable directory + resid = com.android.internal.R.drawable.btn_check_off; + break; + default: Log.e(LOGTAG, "getRawResFilename got incompatible resource ID"); - return new String(); + return ""; } TypedValue value = new TypedValue(); mContext.getResources().getValue(resid, value, true); + if (id == DRAWABLEDIR) { + String path = value.string.toString(); + int index = path.lastIndexOf('/'); + if (index < 0) { + Log.e(LOGTAG, "Can't find drawable directory."); + return ""; + } + return path.substring(0, index + 1); + } return value.string.toString(); } + private float density() { + return mContext.getResources().getDisplayMetrics().density; + } + //========================================================================== // native functions //========================================================================== diff --git a/core/java/android/webkit/CacheLoader.java b/core/java/android/webkit/CacheLoader.java index 3e1b602..de8f888 100644 --- a/core/java/android/webkit/CacheLoader.java +++ b/core/java/android/webkit/CacheLoader.java @@ -17,6 +17,7 @@ package android.webkit; import android.net.http.Headers; +import android.text.TextUtils; /** * This class is a concrete implementation of StreamLoader that uses a @@ -49,17 +50,22 @@ class CacheLoader extends StreamLoader { @Override protected void buildHeaders(Headers headers) { StringBuilder sb = new StringBuilder(mCacheResult.mimeType); - if (mCacheResult.encoding != null && - mCacheResult.encoding.length() > 0) { + if (!TextUtils.isEmpty(mCacheResult.encoding)) { sb.append(';'); sb.append(mCacheResult.encoding); } headers.setContentType(sb.toString()); - if (mCacheResult.location != null && - mCacheResult.location.length() > 0) { + if (!TextUtils.isEmpty(mCacheResult.location)) { headers.setLocation(mCacheResult.location); } - } + if (!TextUtils.isEmpty(mCacheResult.expiresString)) { + headers.setExpires(mCacheResult.expiresString); + } + + if (!TextUtils.isEmpty(mCacheResult.contentdisposition)) { + headers.setContentDisposition(mCacheResult.contentdisposition); + } + } } diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java index 7897435..75028de 100644 --- a/core/java/android/webkit/CacheManager.java +++ b/core/java/android/webkit/CacheManager.java @@ -51,7 +51,6 @@ public final class CacheManager { private static final String NO_STORE = "no-store"; private static final String NO_CACHE = "no-cache"; - private static final String PRIVATE = "private"; private static final String MAX_AGE = "max-age"; private static long CACHE_THRESHOLD = 6 * 1024 * 1024; @@ -80,12 +79,14 @@ public final class CacheManager { int httpStatusCode; long contentLength; long expires; + String expiresString; String localPath; String lastModified; String etag; String mimeType; String location; String encoding; + String contentdisposition; // these fields are NOT saved to the database InputStream inStream; @@ -108,6 +109,10 @@ public final class CacheManager { return expires; } + public String getExpiresString() { + return expiresString; + } + public String getLastModified() { return lastModified; } @@ -128,6 +133,10 @@ public final class CacheManager { return encoding; } + public String getContentDisposition() { + return contentdisposition; + } + // For out-of-package access to the underlying streams. public InputStream getInputStream() { return inStream; @@ -321,7 +330,7 @@ public final class CacheManager { } } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "getCacheFile for url " + url); } @@ -340,7 +349,7 @@ public final class CacheManager { * @hide - hide createCacheFile since it has a parameter of type headers, which is * in a hidden package. */ - // can be called from any thread + // only called from WebCore thread public static CacheResult createCacheFile(String url, int statusCode, Headers headers, String mimeType, boolean forceCache) { if (!forceCache && mDisabled) { @@ -349,17 +358,25 @@ public final class CacheManager { // according to the rfc 2616, the 303 response MUST NOT be cached. if (statusCode == 303) { + // remove the saved cache if there is any + mDataBase.removeCache(url); return null; } // like the other browsers, do not cache redirects containing a cookie // header. if (checkCacheRedirect(statusCode) && !headers.getSetCookie().isEmpty()) { + // remove the saved cache if there is any + mDataBase.removeCache(url); return null; } CacheResult ret = parseHeaders(statusCode, headers, mimeType); - if (ret != null) { + if (ret == null) { + // this should only happen if the headers has "no-store" in the + // cache-control. remove the saved cache if there is any + mDataBase.removeCache(url); + } else { setupFiles(url, ret); try { ret.outStream = new FileOutputStream(ret.outFile); @@ -403,19 +420,23 @@ public final class CacheManager { } cacheRet.contentLength = cacheRet.outFile.length(); - if (checkCacheRedirect(cacheRet.httpStatusCode)) { + boolean redirect = checkCacheRedirect(cacheRet.httpStatusCode); + if (redirect) { // location is in database, no need to keep the file cacheRet.contentLength = 0; - cacheRet.localPath = new String(); - cacheRet.outFile.delete(); - } else if (cacheRet.contentLength == 0) { - cacheRet.outFile.delete(); + cacheRet.localPath = ""; + } + if ((redirect || cacheRet.contentLength == 0) + && !cacheRet.outFile.delete()) { + Log.e(LOGTAG, cacheRet.outFile.getPath() + " delete failed."); + } + if (cacheRet.contentLength == 0) { return; } mDataBase.addCache(url, cacheRet); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.CACHE_MANAGER) { Log.v(LOGTAG, "saveCacheFile for url " + url); } } @@ -444,7 +465,10 @@ public final class CacheManager { // 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(); + File f = new File(mBaseDir, files[i]); + if (!f.delete()) { + Log.e(LOGTAG, f.getPath() + " delete failed."); + } } } } catch (SecurityException e) { @@ -472,7 +496,10 @@ public final class CacheManager { ArrayList<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT); int size = pathList.size(); for (int i = 0; i < size; i++) { - new File(mBaseDir, pathList.get(i)).delete(); + File f = new File(mBaseDir, pathList.get(i)); + if (!f.delete()) { + Log.e(LOGTAG, f.getPath() + " delete failed."); + } } } } @@ -511,12 +538,7 @@ public final class CacheManager { // cache file. If it is not, resolve the collision. while (file.exists()) { if (checkOldPath) { - // as this is called from http thread through - // createCacheFile, we need endCacheTransaction before - // database access. - WebViewCore.endCacheTransaction(); CacheResult oldResult = mDataBase.getCache(url); - WebViewCore.startCacheTransaction(); if (oldResult != null && oldResult.contentLength > 0) { if (path.equals(oldResult.localPath)) { path = oldResult.localPath; @@ -596,21 +618,27 @@ public final class CacheManager { if (location != null) ret.location = location; ret.expires = -1; - String expires = headers.getExpires(); - if (expires != null) { + ret.expiresString = headers.getExpires(); + if (ret.expiresString != null) { try { - ret.expires = HttpDateTime.parse(expires); + ret.expires = HttpDateTime.parse(ret.expiresString); } catch (IllegalArgumentException ex) { // Take care of the special "-1" and "0" cases - if ("-1".equals(expires) || "0".equals(expires)) { + if ("-1".equals(ret.expiresString) + || "0".equals(ret.expiresString)) { // make it expired, but can be used for history navigation ret.expires = 0; } else { - Log.e(LOGTAG, "illegal expires: " + expires); + Log.e(LOGTAG, "illegal expires: " + ret.expiresString); } } } + String contentDisposition = headers.getContentDisposition(); + if (contentDisposition != null) { + ret.contentdisposition = contentDisposition; + } + String lastModified = headers.getLastModified(); if (lastModified != null) ret.lastModified = lastModified; @@ -628,7 +656,7 @@ public final class CacheManager { // must be re-validated on every load. It does not mean that // the content can not be cached. set to expire 0 means it // can only be used in CACHE_MODE_CACHE_ONLY case - if (NO_CACHE.equals(controls[i]) || PRIVATE.equals(controls[i])) { + if (NO_CACHE.equals(controls[i])) { ret.expires = 0; } else if (controls[i].startsWith(MAX_AGE)) { int separator = controls[i].indexOf('='); diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index 17d3f94..f760b61 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -65,50 +65,62 @@ class CallbackProxy extends Handler { // Keep track of multiple progress updates. private boolean mProgressUpdatePending; // Keep track of the last progress amount. - private volatile int mLatestProgress; + // Start with 100 to indicate it is not in load for the empty page. + private volatile int mLatestProgress = 100; // Back/Forward list private final WebBackForwardList mBackForwardList; // Used to call startActivity during url override. private final Context mContext; // Message Ids - private static final int PAGE_STARTED = 100; - private static final int RECEIVED_ICON = 101; - private static final int RECEIVED_TITLE = 102; - private static final int OVERRIDE_URL = 103; - private static final int AUTH_REQUEST = 104; - private static final int SSL_ERROR = 105; - private static final int PROGRESS = 106; - private static final int UPDATE_VISITED = 107; - private static final int LOAD_RESOURCE = 108; - private static final int CREATE_WINDOW = 109; - private static final int CLOSE_WINDOW = 110; - private static final int SAVE_PASSWORD = 111; - private static final int JS_ALERT = 112; - private static final int JS_CONFIRM = 113; - private static final int JS_PROMPT = 114; - private static final int JS_UNLOAD = 115; - private static final int ASYNC_KEYEVENTS = 116; - private static final int TOO_MANY_REDIRECTS = 117; - private static final int DOWNLOAD_FILE = 118; - private static final int REPORT_ERROR = 119; - private static final int RESEND_POST_DATA = 120; - private static final int PAGE_FINISHED = 121; - private static final int REQUEST_FOCUS = 122; - private static final int SCALE_CHANGED = 123; - private static final int RECEIVED_CERTIFICATE = 124; - private static final int SWITCH_OUT_HISTORY = 125; - private static final int JS_TIMEOUT = 126; + private static final int PAGE_STARTED = 100; + private static final int RECEIVED_ICON = 101; + private static final int RECEIVED_TITLE = 102; + private static final int OVERRIDE_URL = 103; + private static final int AUTH_REQUEST = 104; + private static final int SSL_ERROR = 105; + private static final int PROGRESS = 106; + private static final int UPDATE_VISITED = 107; + private static final int LOAD_RESOURCE = 108; + private static final int CREATE_WINDOW = 109; + private static final int CLOSE_WINDOW = 110; + private static final int SAVE_PASSWORD = 111; + private static final int JS_ALERT = 112; + private static final int JS_CONFIRM = 113; + private static final int JS_PROMPT = 114; + private static final int JS_UNLOAD = 115; + private static final int ASYNC_KEYEVENTS = 116; + private static final int TOO_MANY_REDIRECTS = 117; + private static final int DOWNLOAD_FILE = 118; + private static final int REPORT_ERROR = 119; + private static final int RESEND_POST_DATA = 120; + private static final int PAGE_FINISHED = 121; + private static final int REQUEST_FOCUS = 122; + private static final int SCALE_CHANGED = 123; + private static final int RECEIVED_CERTIFICATE = 124; + private static final int SWITCH_OUT_HISTORY = 125; + private static final int EXCEEDED_DATABASE_QUOTA = 126; + private static final int REACHED_APPCACHE_MAXSIZE = 127; + private static final int JS_TIMEOUT = 128; + private static final int ADD_MESSAGE_TO_CONSOLE = 129; + private static final int GEOLOCATION_PERMISSIONS_SHOW_PROMPT = 130; + private static final int GEOLOCATION_PERMISSIONS_HIDE_PROMPT = 131; + private static final int RECEIVED_TOUCH_ICON_URL = 132; + private static final int GET_VISITED_HISTORY = 133; // Message triggered by the client to resume execution - private static final int NOTIFY = 200; + private static final int NOTIFY = 200; // Result transportation object for returning results across thread // boundaries. - private class ResultTransport<E> { + private static class ResultTransport<E> { // Private result object private E mResult; + public ResultTransport(E defaultResult) { + mResult = defaultResult; + } + public synchronized void setResult(E result) { mResult = result; } @@ -145,6 +157,14 @@ class CallbackProxy extends Handler { } /** + * Get the WebChromeClient. + * @return the current WebChromeClient instance. + */ + public WebChromeClient getWebChromeClient() { + return mWebChromeClient; + } + + /** * Set the client DownloadListener. * @param client An implementation of DownloadListener. */ @@ -229,6 +249,13 @@ class CallbackProxy extends Handler { } break; + case RECEIVED_TOUCH_ICON_URL: + if (mWebChromeClient != null) { + mWebChromeClient.onReceivedTouchIconUrl(mWebView, + (String) msg.obj, msg.arg1 == 1); + } + break; + case RECEIVED_TITLE: if (mWebChromeClient != null) { mWebChromeClient.onReceivedTitle(mWebView, @@ -389,6 +416,63 @@ class CallbackProxy extends Handler { } break; + case EXCEEDED_DATABASE_QUOTA: + if (mWebChromeClient != null) { + HashMap<String, Object> map = + (HashMap<String, Object>) msg.obj; + String databaseIdentifier = + (String) map.get("databaseIdentifier"); + String url = (String) map.get("url"); + long currentQuota = + ((Long) map.get("currentQuota")).longValue(); + long totalUsedQuota = + ((Long) map.get("totalUsedQuota")).longValue(); + long estimatedSize = + ((Long) map.get("estimatedSize")).longValue(); + WebStorage.QuotaUpdater quotaUpdater = + (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + + mWebChromeClient.onExceededDatabaseQuota(url, + databaseIdentifier, currentQuota, estimatedSize, + totalUsedQuota, quotaUpdater); + } + break; + + case REACHED_APPCACHE_MAXSIZE: + if (mWebChromeClient != null) { + HashMap<String, Object> map = + (HashMap<String, Object>) msg.obj; + long spaceNeeded = + ((Long) map.get("spaceNeeded")).longValue(); + long totalUsedQuota = + ((Long) map.get("totalUsedQuota")).longValue(); + WebStorage.QuotaUpdater quotaUpdater = + (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + + mWebChromeClient.onReachedMaxAppCacheSize(spaceNeeded, + totalUsedQuota, quotaUpdater); + } + break; + + case GEOLOCATION_PERMISSIONS_SHOW_PROMPT: + if (mWebChromeClient != null) { + HashMap<String, Object> map = + (HashMap<String, Object>) msg.obj; + String origin = (String) map.get("origin"); + GeolocationPermissions.Callback callback = + (GeolocationPermissions.Callback) + map.get("callback"); + mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, + callback); + } + break; + + case GEOLOCATION_PERMISSIONS_HIDE_PROMPT: + if (mWebChromeClient != null) { + mWebChromeClient.onGeolocationPermissionsHidePrompt(); + } + break; + case JS_ALERT: if (mWebChromeClient != null) { final JsResult res = (JsResult) msg.obj; @@ -563,6 +647,19 @@ class CallbackProxy extends Handler { case SWITCH_OUT_HISTORY: mWebView.switchOutDrawHistory(); break; + + case ADD_MESSAGE_TO_CONSOLE: + String message = msg.getData().getString("message"); + String sourceID = msg.getData().getString("sourceID"); + int lineNumber = msg.getData().getInt("lineNumber"); + mWebChromeClient.addMessageToConsole(message, lineNumber, sourceID); + break; + + case GET_VISITED_HISTORY: + if (mWebChromeClient != null) { + mWebChromeClient.getVisitedHistory((ValueCallback<String[]>)msg.obj); + } + break; } } @@ -605,7 +702,40 @@ class CallbackProxy extends Handler { //-------------------------------------------------------------------------- // Performance probe + private static final boolean PERF_PROBE = false; private long mWebCoreThreadTime; + private long mWebCoreIdleTime; + + /* + * If PERF_PROBE is true, this block needs to be added to MessageQueue.java. + * startWait() and finishWait() should be called before and after wait(). + + private WaitCallback mWaitCallback = null; + public static interface WaitCallback { + void startWait(); + void finishWait(); + } + public final void setWaitCallback(WaitCallback callback) { + mWaitCallback = callback; + } + */ + + // un-comment this block if PERF_PROBE is true + /* + private IdleCallback mIdleCallback = new IdleCallback(); + + private final class IdleCallback implements MessageQueue.WaitCallback { + private long mStartTime = 0; + + public void finishWait() { + mWebCoreIdleTime += SystemClock.uptimeMillis() - mStartTime; + } + + public void startWait() { + mStartTime = SystemClock.uptimeMillis(); + } + } + */ public void onPageStarted(String url, Bitmap favicon) { // Do an unsynchronized quick check to avoid posting if no callback has @@ -614,9 +744,12 @@ class CallbackProxy extends Handler { return; } // Performance probe - if (false) { + if (PERF_PROBE) { mWebCoreThreadTime = SystemClock.currentThreadTimeMillis(); + mWebCoreIdleTime = 0; Network.getInstance(mContext).startTiming(); + // un-comment this if PERF_PROBE is true +// Looper.myQueue().setWaitCallback(mIdleCallback); } Message msg = obtainMessage(PAGE_STARTED); msg.obj = favicon; @@ -631,10 +764,12 @@ class CallbackProxy extends Handler { return; } // Performance probe - if (false) { + if (PERF_PROBE) { + // un-comment this if PERF_PROBE is true +// Looper.myQueue().setWaitCallback(null); Log.d("WebCore", "WebCore thread used " + (SystemClock.currentThreadTimeMillis() - mWebCoreThreadTime) - + " ms"); + + " ms and idled " + mWebCoreIdleTime + " ms"); Network.getInstance(mContext).stopTiming(); } Message msg = obtainMessage(PAGE_FINISHED, url); @@ -693,7 +828,7 @@ class CallbackProxy extends Handler { public boolean shouldOverrideUrlLoading(String url) { // We have a default behavior if no client exists so always send the // message. - ResultTransport<Boolean> res = new ResultTransport<Boolean>(); + ResultTransport<Boolean> res = new ResultTransport<Boolean>(false); Message msg = obtainMessage(OVERRIDE_URL); msg.getData().putString("url", url); msg.obj = res; @@ -834,7 +969,7 @@ class CallbackProxy extends Handler { String password, Message resumeMsg) { // resumeMsg should be null at this point because we want to create it // within the CallbackProxy. - if (WebView.DEBUG) { + if (DebugFlags.CALLBACK_PROXY) { junit.framework.Assert.assertNull(resumeMsg); } resumeMsg = obtainMessage(NOTIFY); @@ -939,6 +1074,24 @@ class CallbackProxy extends Handler { sendMessage(obtainMessage(RECEIVED_ICON, icon)); } + /* package */ void onReceivedTouchIconUrl(String url, boolean precomposed) { + // We should have a current item but we do not want to crash so check + // for null. + WebHistoryItem i = mBackForwardList.getCurrentItem(); + if (i != null) { + if (precomposed || i.getTouchIconUrl() != null) { + i.setTouchIconUrl(url); + } + } + // Do an unsynchronized quick check to avoid posting if no callback has + // been set. + if (mWebChromeClient == null) { + return; + } + sendMessage(obtainMessage(RECEIVED_TOUCH_ICON_URL, + precomposed ? 1 : 0, 0, url)); + } + public void onReceivedTitle(String title) { // Do an unsynchronized quick check to avoid posting if no callback has // been set. @@ -1037,8 +1190,124 @@ class CallbackProxy extends Handler { } /** - * @hide pending API council approval + * Called by WebViewCore to inform the Java side that the current origin + * has overflowed it's database quota. Called in the WebCore thread so + * posts a message to the UI thread that will prompt the WebChromeClient + * for what to do. On return back to C++ side, the WebCore thread will + * sleep pending a new quota value. + * @param url The URL that caused the quota overflow. + * @param databaseIdentifier The identifier of the database that the + * transaction that caused the overflow was running on. + * @param currentQuota The current quota the origin is allowed. + * @param estimatedSize The estimated size of the database. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater An instance of a class encapsulating a callback + * to WebViewCore to run when the decision to allow or deny more + * quota has been made. + */ + public void onExceededDatabaseQuota( + String url, String databaseIdentifier, long currentQuota, + long estimatedSize, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + if (mWebChromeClient == null) { + quotaUpdater.updateQuota(currentQuota); + return; + } + + Message exceededQuota = obtainMessage(EXCEEDED_DATABASE_QUOTA); + HashMap<String, Object> map = new HashMap(); + map.put("databaseIdentifier", databaseIdentifier); + map.put("url", url); + map.put("currentQuota", currentQuota); + map.put("estimatedSize", estimatedSize); + map.put("totalUsedQuota", totalUsedQuota); + map.put("quotaUpdater", quotaUpdater); + exceededQuota.obj = map; + sendMessage(exceededQuota); + } + + /** + * Called by WebViewCore to inform the Java side that the appcache has + * exceeded its max size. + * @param spaceNeeded is the amount of disk space that would be needed + * in order for the last appcache operation to succeed. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater An instance of a class encapsulating a callback + * to WebViewCore to run when the decision to allow or deny a bigger + * app cache size has been made. + */ + public void onReachedMaxAppCacheSize(long spaceNeeded, + long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { + if (mWebChromeClient == null) { + quotaUpdater.updateQuota(0); + return; + } + + Message msg = obtainMessage(REACHED_APPCACHE_MAXSIZE); + HashMap<String, Object> map = new HashMap(); + map.put("spaceNeeded", spaceNeeded); + map.put("totalUsedQuota", totalUsedQuota); + map.put("quotaUpdater", quotaUpdater); + msg.obj = map; + sendMessage(msg); + } + + /** + * Called by WebViewCore to instruct the browser to display a prompt to ask + * the user to set the Geolocation permission state for the given origin. + * @param origin The origin requesting Geolocation permsissions. + * @param callback The callback to call once a permission state has been + * obtained. + */ + public void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback callback) { + if (mWebChromeClient == null) { + return; + } + + Message showMessage = + obtainMessage(GEOLOCATION_PERMISSIONS_SHOW_PROMPT); + HashMap<String, Object> map = new HashMap(); + map.put("origin", origin); + map.put("callback", callback); + showMessage.obj = map; + sendMessage(showMessage); + } + + /** + * Called by WebViewCore to instruct the browser to hide the Geolocation + * permissions prompt. + */ + public void onGeolocationPermissionsHidePrompt() { + if (mWebChromeClient == null) { + return; + } + + Message hideMessage = obtainMessage(GEOLOCATION_PERMISSIONS_HIDE_PROMPT); + sendMessage(hideMessage); + } + + /** + * Called by WebViewCore when we have a message to be added to the JavaScript + * error console. Sends a message to the Java side with the details. + * @param message The message to add to the console. + * @param lineNumber The lineNumber of the source file on which the error + * occurred. + * @param sourceID The filename of the source file in which the error + * occurred. */ + public void addMessageToConsole(String message, int lineNumber, String sourceID) { + if (mWebChromeClient == null) { + return; + } + + Message msg = obtainMessage(ADD_MESSAGE_TO_CONSOLE); + msg.getData().putString("message", message); + msg.getData().putString("sourceID", sourceID); + msg.getData().putInt("lineNumber", lineNumber); + sendMessage(msg); + } + public boolean onJsTimeout() { //always interrupt timedout JS by default if (mWebChromeClient == null) { @@ -1057,4 +1326,13 @@ class CallbackProxy extends Handler { } return result.getResult(); } + + public void getVisitedHistory(ValueCallback<String[]> callback) { + if (mWebChromeClient == null) { + return; + } + Message msg = obtainMessage(GET_VISITED_HISTORY); + msg.obj = callback; + sendMessage(msg); + } } diff --git a/core/java/android/webkit/CertTool.java b/core/java/android/webkit/CertTool.java new file mode 100644 index 0000000..99757d2 --- /dev/null +++ b/core/java/android/webkit/CertTool.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.jce.netscape.NetscapeCertRequest; +import org.bouncycastle.util.encoders.Base64; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.security.Credentials; +import android.util.Log; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; + +class CertTool { + private static final String LOGTAG = "CertTool"; + + private static final AlgorithmIdentifier MD5_WITH_RSA = + new AlgorithmIdentifier(PKCSObjectIdentifiers.md5WithRSAEncryption); + + static final String CERT = Credentials.CERTIFICATE; + static final String PKCS12 = Credentials.PKCS12; + + static String[] getKeyStrengthList() { + return new String[] {"High Grade", "Medium Grade"}; + } + + static String getSignedPublicKey(Context context, int index, String challenge) { + try { + KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize((index == 0) ? 2048 : 1024); + KeyPair pair = generator.genKeyPair(); + + NetscapeCertRequest request = new NetscapeCertRequest(challenge, + MD5_WITH_RSA, pair.getPublic()); + request.sign(pair.getPrivate()); + byte[] signed = request.toASN1Object().getDEREncoded(); + + Credentials.getInstance().install(context, pair); + return new String(Base64.encode(signed)); + } catch (Exception e) { + Log.w(LOGTAG, e); + } + return null; + } + + static void addCertificate(Context context, String type, byte[] value) { + Credentials.getInstance().install(context, type, value); + } + + private CertTool() {} +} diff --git a/core/java/android/webkit/ContentLoader.java b/core/java/android/webkit/ContentLoader.java index f6d7f69..19aa087 100644 --- a/core/java/android/webkit/ContentLoader.java +++ b/core/java/android/webkit/ContentLoader.java @@ -57,6 +57,16 @@ class ContentLoader extends StreamLoader { } + private String errString(Exception ex) { + String exMessage = ex.getMessage(); + String errString = mContext.getString( + com.android.internal.R.string.httpErrorFileNotFound); + if (exMessage != null) { + errString += " " + exMessage; + } + return errString; + } + @Override protected boolean setupStreamAndSendStatus() { Uri uri = Uri.parse(mUrl); @@ -73,28 +83,16 @@ class ContentLoader extends StreamLoader { mDataStream = mContext.getContentResolver().openInputStream(uri); mHandler.status(1, 1, 0, "OK"); } catch (java.io.FileNotFoundException ex) { - mHandler.error( - EventHandler.FILE_NOT_FOUND_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_NOT_FOUND_ERROR, errString(ex)); return false; } catch (java.io.IOException ex) { - mHandler.error( - EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_ERROR, errString(ex)); return false; } catch (RuntimeException ex) { // readExceptionWithFileNotFoundExceptionFromParcel in DatabaseUtils // can throw a serial of RuntimeException. Catch them all here. - mHandler.error( - EventHandler.FILE_ERROR, - mContext.getString( - com.android.internal.R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_ERROR, errString(ex)); return false; } return true; @@ -105,8 +103,7 @@ class ContentLoader extends StreamLoader { if (mContentType != null) { headers.setContentType("text/html"); } - // override the cache-control header set by StreamLoader as content can - // change, we don't want WebKit to cache it + // content can change, we don't want WebKit to cache it headers.setCacheControl("no-store, no-cache"); } diff --git a/core/java/android/webkit/CookieManager.java b/core/java/android/webkit/CookieManager.java index e8c2279..fca591f 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -23,9 +23,12 @@ import android.util.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; /** * CookieManager manages cookies according to RFC2109 spec. @@ -190,6 +193,31 @@ public final class CookieManager { } } + private static final CookieComparator COMPARATOR = new CookieComparator(); + + private static final class CookieComparator implements Comparator<Cookie> { + public int compare(Cookie cookie1, Cookie cookie2) { + // According to RFC 2109, multiple cookies are ordered in a way such + // that those with more specific Path attributes precede those with + // less specific. Ordering with respect to other attributes (e.g., + // Domain) is unspecified. + // As Set is not modified if the two objects are same, we do want to + // assign different value for each cookie. + int diff = cookie2.path.length() - cookie1.path.length(); + if (diff == 0) { + diff = cookie2.domain.length() - cookie1.domain.length(); + if (diff == 0) { + diff = cookie2.name.hashCode() - cookie1.name.hashCode(); + if (diff == 0) { + Log.w(LOGTAG, "Found two cookies with the same value." + + "cookie1=" + cookie1 + " , cookie2=" + cookie2); + } + } + } + return diff; + } + } + private CookieManager() { } @@ -262,7 +290,7 @@ public final class CookieManager { if (!mAcceptCookie || uri == null) { return; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "setCookie: uri: " + uri + " value: " + value); } @@ -401,8 +429,8 @@ public final class CookieManager { long now = System.currentTimeMillis(); boolean secure = HTTPS.equals(uri.mScheme); Iterator<Cookie> iter = cookieList.iterator(); - StringBuilder ret = new StringBuilder(256); + SortedSet<Cookie> cookieSet = new TreeSet<Cookie>(COMPARATOR); while (iter.hasNext()) { Cookie cookie = iter.next(); if (cookie.domainMatch(hostAndPath[0]) && @@ -413,26 +441,33 @@ public final class CookieManager { && (!cookie.secure || secure) && cookie.mode != Cookie.MODE_DELETED) { cookie.lastAcessTime = now; + cookieSet.add(cookie); + } + } - if (ret.length() > 0) { - ret.append(SEMICOLON); - // according to RC2109, SEMICOLON is office separator, - // but when log in yahoo.com, it needs WHITE_SPACE too. - ret.append(WHITE_SPACE); - } - - ret.append(cookie.name); - ret.append(EQUAL); - ret.append(cookie.value); + StringBuilder ret = new StringBuilder(256); + Iterator<Cookie> setIter = cookieSet.iterator(); + while (setIter.hasNext()) { + Cookie cookie = setIter.next(); + if (ret.length() > 0) { + ret.append(SEMICOLON); + // according to RC2109, SEMICOLON is official separator, + // but when log in yahoo.com, it needs WHITE_SPACE too. + ret.append(WHITE_SPACE); } + + ret.append(cookie.name); + ret.append(EQUAL); + ret.append(cookie.value); } + if (ret.length() > 0) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "getCookie: uri: " + uri + " value: " + ret); } return ret.toString(); } else { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "getCookie: uri: " + uri + " But can't find cookie."); } @@ -588,7 +623,7 @@ public final class CookieManager { Iterator<ArrayList<Cookie>> listIter = cookieLists.iterator(); while (listIter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { ArrayList<Cookie> list = listIter.next(); - if (WebView.DEBUG) { + if (DebugFlags.COOKIE_MANAGER) { Iterator<Cookie> iter = list.iterator(); while (iter.hasNext() && count < MAX_RAM_COOKIES_COUNT) { Cookie cookie = iter.next(); @@ -608,7 +643,7 @@ public final class CookieManager { ArrayList<Cookie> retlist = new ArrayList<Cookie>(); if (mapSize >= MAX_RAM_DOMAIN_COUNT || count >= MAX_RAM_COOKIES_COUNT) { - if (WebView.DEBUG) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, count + " cookies used " + byteCount + " bytes with " + mapSize + " domains"); } @@ -616,7 +651,7 @@ public final class CookieManager { int toGo = mapSize / 10 + 1; while (toGo-- > 0){ String domain = domains[toGo].toString(); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_MANAGER) { Log.v(LOGTAG, "delete domain: " + domain + " from RAM cache"); } @@ -798,22 +833,24 @@ public final class CookieManager { // "secure" is a known attribute doesn't use "="; // while sites like live.com uses "secure=" - if (length - index > SECURE_LENGTH + if (length - index >= SECURE_LENGTH && cookieString.substring(index, index + SECURE_LENGTH). equalsIgnoreCase(SECURE)) { index += SECURE_LENGTH; cookie.secure = true; + if (index == length) break; if (cookieString.charAt(index) == EQUAL) index++; continue; } // "httponly" is a known attribute doesn't use "="; // while sites like live.com uses "httponly=" - if (length - index > HTTP_ONLY_LENGTH + if (length - index >= HTTP_ONLY_LENGTH && cookieString.substring(index, index + HTTP_ONLY_LENGTH). equalsIgnoreCase(HTTP_ONLY)) { index += HTTP_ONLY_LENGTH; + if (index == length) break; if (cookieString.charAt(index) == EQUAL) index++; // FIXME: currently only parse the attribute continue; diff --git a/core/java/android/webkit/CookieSyncManager.java b/core/java/android/webkit/CookieSyncManager.java index aa6c76b..14375d2 100644 --- a/core/java/android/webkit/CookieSyncManager.java +++ b/core/java/android/webkit/CookieSyncManager.java @@ -170,7 +170,7 @@ public final class CookieSyncManager extends WebSyncManager { } protected void syncFromRamToFlash() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_SYNC_MANAGER) { Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash STARTS"); } @@ -187,7 +187,7 @@ public final class CookieSyncManager extends WebSyncManager { CookieManager.getInstance().deleteLRUDomain(); syncFromRamToFlash(lruList); - if (WebView.LOGV_ENABLED) { + if (DebugFlags.COOKIE_SYNC_MANAGER) { Log.v(LOGTAG, "CookieSyncManager::syncFromRamToFlash DONE"); } } diff --git a/core/java/android/webkit/DataLoader.java b/core/java/android/webkit/DataLoader.java index dcdc949..6c5d10d 100644 --- a/core/java/android/webkit/DataLoader.java +++ b/core/java/android/webkit/DataLoader.java @@ -16,12 +16,10 @@ package android.webkit; -import org.apache.http.protocol.HTTP; - -import android.net.http.Headers; - import java.io.ByteArrayInputStream; +import org.apache.harmony.luni.util.Base64; + /** * This class is a concrete implementation of StreamLoader that uses the * content supplied as a URL as the source for the stream. The mimetype @@ -30,8 +28,6 @@ import java.io.ByteArrayInputStream; */ class DataLoader extends StreamLoader { - private String mContentType; // Content mimetype, if supplied in URL - /** * Constructor uses the dataURL as the source for an InputStream * @param dataUrl data: URL string optionally containing a mimetype @@ -41,16 +37,20 @@ class DataLoader extends StreamLoader { super(loadListener); String url = dataUrl.substring("data:".length()); - String content; + byte[] data = null; int commaIndex = url.indexOf(','); if (commaIndex != -1) { - mContentType = url.substring(0, commaIndex); - content = url.substring(commaIndex + 1); + String contentType = url.substring(0, commaIndex); + data = url.substring(commaIndex + 1).getBytes(); + loadListener.parseContentTypeHeader(contentType); + if ("base64".equals(loadListener.transferEncoding())) { + data = Base64.decode(data); + } } else { - content = url; + data = url.getBytes(); } - mDataStream = new ByteArrayInputStream(content.getBytes()); - mContentLength = content.length(); + mDataStream = new ByteArrayInputStream(data); + mContentLength = data.length; } @Override @@ -60,10 +60,7 @@ class DataLoader extends StreamLoader { } @Override - protected void buildHeaders(Headers headers) { - if (mContentType != null) { - headers.setContentType(mContentType); - } + protected void buildHeaders(android.net.http.Headers h) { } /** diff --git a/core/java/android/webkit/DateSorter.java b/core/java/android/webkit/DateSorter.java index 750403b..c46702e 100644 --- a/core/java/android/webkit/DateSorter.java +++ b/core/java/android/webkit/DateSorter.java @@ -43,9 +43,6 @@ public class DateSorter { private static final int NUM_DAYS_AGO = 5; - Date mDate = new Date(); - Calendar mCal = Calendar.getInstance(); - /** * @param context Application context */ diff --git a/core/java/android/webkit/DebugFlags.java b/core/java/android/webkit/DebugFlags.java new file mode 100644 index 0000000..8e25395 --- /dev/null +++ b/core/java/android/webkit/DebugFlags.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +/** + * This class is a container for all of the debug flags used in the Java + * components of webkit. These flags must be final in order to ensure that + * the compiler optimizes the code that uses them out of the final executable. + * + * The name of each flags maps directly to the name of the class in which that + * flag is used. + * + */ +class DebugFlags { + + public static final boolean BROWSER_FRAME = false; + public static final boolean CACHE_MANAGER = false; + public static final boolean CALLBACK_PROXY = false; + public static final boolean COOKIE_MANAGER = false; + public static final boolean COOKIE_SYNC_MANAGER = false; + public static final boolean FRAME_LOADER = false; + public static final boolean J_WEB_CORE_JAVA_BRIDGE = false;// HIGHLY VERBOSE + public static final boolean LOAD_LISTENER = false; + public static final boolean NETWORK = false; + public static final boolean SSL_ERROR_HANDLER = false; + public static final boolean STREAM_LOADER = false; + public static final boolean URL_UTIL = false; + public static final boolean WEB_BACK_FORWARD_LIST = false; + public static final boolean WEB_SETTINGS = false; + public static final boolean WEB_SYNC_MANAGER = false; + public static final boolean WEB_TEXT_VIEW = false; + public static final boolean WEB_VIEW = false; + public static final boolean WEB_VIEW_CORE = false; + +} diff --git a/core/java/android/webkit/FileLoader.java b/core/java/android/webkit/FileLoader.java index 54a4c1d..085f1f4 100644 --- a/core/java/android/webkit/FileLoader.java +++ b/core/java/android/webkit/FileLoader.java @@ -72,6 +72,15 @@ class FileLoader extends StreamLoader { } } + private String errString(Exception ex) { + String exMessage = ex.getMessage(); + String errString = mContext.getString(R.string.httpErrorFileNotFound); + if (exMessage != null) { + errString += " " + exMessage; + } + return errString; + } + @Override protected boolean setupStreamAndSendStatus() { try { @@ -95,16 +104,11 @@ class FileLoader extends StreamLoader { mHandler.status(1, 1, 0, "OK"); } catch (java.io.FileNotFoundException ex) { - mHandler.error( - EventHandler.FILE_NOT_FOUND_ERROR, - mContext.getString(R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_NOT_FOUND_ERROR, errString(ex)); return false; } catch (java.io.IOException ex) { - mHandler.error(EventHandler.FILE_ERROR, - mContext.getString(R.string.httpErrorFileNotFound) + - " " + ex.getMessage()); + mHandler.error(EventHandler.FILE_ERROR, errString(ex)); return false; } return true; diff --git a/core/java/android/webkit/FrameLoader.java b/core/java/android/webkit/FrameLoader.java index 66ab021..c1eeb3b 100644 --- a/core/java/android/webkit/FrameLoader.java +++ b/core/java/android/webkit/FrameLoader.java @@ -28,7 +28,6 @@ class FrameLoader { private final LoadListener mListener; private final String mMethod; - private final boolean mIsHighPriority; private final WebSettings mSettings; private Map<String, String> mHeaders; private byte[] mPostData; @@ -52,11 +51,10 @@ class FrameLoader { private static final String LOGTAG = "webkit"; FrameLoader(LoadListener listener, WebSettings settings, - String method, boolean highPriority) { + String method) { mListener = listener; mHeaders = null; mMethod = method; - mIsHighPriority = highPriority; mCacheMode = WebSettings.LOAD_NORMAL; mSettings = settings; } @@ -97,17 +95,6 @@ class FrameLoader { public boolean executeLoad() { String url = mListener.url(); - // Attempt to decode the percent-encoded url. - try { - url = new String(URLUtil.decode(url.getBytes())); - } catch (IllegalArgumentException e) { - // Fail with a bad url error if the decode fails. - mListener.error(EventHandler.ERROR_BAD_URL, - mListener.getContext().getString( - com.android.internal.R.string.httpErrorBadUrl)); - return false; - } - if (URLUtil.isNetworkUrl(url)){ if (mSettings.getBlockNetworkLoads()) { mListener.error(EventHandler.ERROR_BAD_URL, @@ -115,12 +102,19 @@ class FrameLoader { com.android.internal.R.string.httpErrorBadUrl)); return false; } + // Make sure it is correctly URL encoded before sending the request + if (!URLUtil.verifyURLEncoding(url)) { + mListener.error(EventHandler.ERROR_BAD_URL, + mListener.getContext().getString( + com.android.internal.R.string.httpErrorBadUrl)); + return false; + } mNetwork = Network.getInstance(mListener.getContext()); return handleHTTPLoad(); } else if (handleLocalFile(url, mListener, mSettings)) { return true; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader.executeLoad: url protocol not supported:" + mListener.url()); } @@ -134,6 +128,18 @@ class FrameLoader { /* package */ static boolean handleLocalFile(String url, LoadListener loadListener, WebSettings settings) { + // Attempt to decode the percent-encoded url before passing to the + // local loaders. + try { + url = new String(URLUtil.decode(url.getBytes())); + } catch (IllegalArgumentException e) { + loadListener.error(EventHandler.ERROR_BAD_URL, + loadListener.getContext().getString( + com.android.internal.R.string.httpErrorBadUrl)); + // Return true here so we do not trigger an unsupported scheme + // error. + return true; + } if (URLUtil.isAssetUrl(url)) { FileLoader.requestUrl(url, loadListener, loadListener.getContext(), true, settings.getAllowFileAccess()); @@ -166,21 +172,17 @@ class FrameLoader { populateStaticHeaders(); populateHeaders(); - // response was handled by UrlIntercept, don't issue HTTP request - if (handleUrlIntercept()) return true; - // response was handled by Cache, don't issue HTTP request if (handleCache()) { // push the request data down to the LoadListener // as response from the cache could be a redirect // and we may need to initiate a network request if the cache // can't satisfy redirect URL - mListener.setRequestData(mMethod, mHeaders, mPostData, - mIsHighPriority); + mListener.setRequestData(mMethod, mHeaders, mPostData); return true; } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: http " + mMethod + " load for: " + mListener.url()); } @@ -190,7 +192,7 @@ class FrameLoader { try { ret = mNetwork.requestURL(mMethod, mHeaders, - mPostData, mListener, mIsHighPriority); + mPostData, mListener); } catch (android.net.ParseException ex) { error = EventHandler.ERROR_BAD_URL; } catch (java.lang.RuntimeException ex) { @@ -207,11 +209,11 @@ class FrameLoader { } /* - * This function is used by handleUrlInterecpt and handleCache to + * This function is used by handleCache to * setup a load from the byte stream in a CacheResult. */ private void startCacheLoad(CacheResult result) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: loading from cache: " + mListener.url()); } @@ -223,30 +225,6 @@ class FrameLoader { } /* - * This function is used by handleHTTPLoad to allow URL - * interception. This can be used to provide alternative load - * methods such as locally stored versions or for debugging. - * - * Returns true if the response was handled by UrlIntercept. - */ - private boolean handleUrlIntercept() { - // Check if the URL can be served from UrlIntercept. If - // successful, return the data just like a cache hit. - - PluginData data = UrlInterceptRegistry.getPluginData( - mListener.url(), mHeaders); - - if(data != null) { - PluginContentLoader loader = - new PluginContentLoader(mListener, data); - loader.load(); - return true; - } - // Not intercepted. Carry on as normal. - return false; - } - - /* * This function is used by the handleHTTPLoad to setup the cache headers * correctly. * Returns true if the response was handled from the cache @@ -285,7 +263,7 @@ class FrameLoader { // of it's state. If it is not in the cache, then go to the // network. case WebSettings.LOAD_CACHE_ELSE_NETWORK: { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.FRAME_LOADER) { Log.v(LOGTAG, "FrameLoader: checking cache: " + mListener.url()); } diff --git a/core/java/android/webkit/GearsPermissionsManager.java b/core/java/android/webkit/GearsPermissionsManager.java deleted file mode 100644 index 6549cb8..0000000 --- a/core/java/android/webkit/GearsPermissionsManager.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.webkit; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.Editor; -import android.database.ContentObserver; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteStatement; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.provider.Settings; -import android.util.Log; - -import java.io.File; -import java.util.HashSet; - -/** - * Donut-specific hack to keep Gears permissions in sync with the - * system location setting. - */ -class GearsPermissionsManager { - // The application context. - Context mContext; - // The path to gears.so. - private String mGearsPath; - - // The Gears permissions database directory. - private final static String GEARS_DATABASE_DIR = "gears"; - // The Gears permissions database file name. - private final static String GEARS_DATABASE_FILE = "permissions.db"; - // The Gears location permissions table. - private final static String GEARS_LOCATION_ACCESS_TABLE_NAME = - "LocationAccess"; - // The Gears storage access permissions table. - private final static String GEARS_STORAGE_ACCESS_TABLE_NAME = "Access"; - // The Gears permissions db schema version table. - private final static String GEARS_SCHEMA_VERSION_TABLE_NAME = - "VersionInfo"; - // The Gears permission value that denotes "allow access to location". - private static final int GEARS_ALLOW_LOCATION_ACCESS = 1; - // The shared pref name. - private static final String LAST_KNOWN_LOCATION_SETTING = - "lastKnownLocationSystemSetting"; - // The Browser package name. - private static final String BROWSER_PACKAGE_NAME = "com.android.browser"; - // The Secure Settings observer that will be notified when the system - // location setting changes. - private SecureSettingsObserver mSettingsObserver; - // The Google URLs whitelisted for Gears location access. - private static HashSet<String> sGearsWhiteList; - - static { - sGearsWhiteList = new HashSet<String>(); - // NOTE: DO NOT ADD A "/" AT THE END! - sGearsWhiteList.add("http://www.google.com"); - sGearsWhiteList.add("http://www.google.co.uk"); - } - - private static final String LOGTAG = "webcore"; - static final boolean DEBUG = false; - static final boolean LOGV_ENABLED = DEBUG; - - GearsPermissionsManager(Context context, String gearsPath) { - mContext = context; - mGearsPath = gearsPath; - } - - public void doCheckAndStartObserver() { - // Are we running in the browser? - if (!BROWSER_PACKAGE_NAME.equals(mContext.getPackageName())) { - return; - } - // Do the check. - checkGearsPermissions(); - // Install the observer. - mSettingsObserver = new SecureSettingsObserver(); - mSettingsObserver.observe(); - } - - private void checkGearsPermissions() { - // Get the current system settings. - int setting = Settings.Secure.getInt(mContext.getContentResolver(), - Settings.Secure.USE_LOCATION_FOR_SERVICES, -1); - // Check if we need to set the Gears permissions. - if (setting != -1 && locationSystemSettingChanged(setting)) { - setGearsPermissionForGoogleDomains(setting); - } - } - - private boolean locationSystemSettingChanged(int newSetting) { - SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(mContext); - int oldSetting = 0; - oldSetting = prefs.getInt(LAST_KNOWN_LOCATION_SETTING, oldSetting); - if (oldSetting == newSetting) { - return false; - } - Editor ed = prefs.edit(); - ed.putInt(LAST_KNOWN_LOCATION_SETTING, newSetting); - ed.commit(); - return true; - } - - private void setGearsPermissionForGoogleDomains(int systemPermission) { - // Transform the system permission into a boolean flag. When this - // flag is true, it means the origins in gGearsWhiteList are added - // to the Gears location permission table with permission 1 (allowed). - // When the flag is false, the origins in gGearsWhiteList are removed - // from the Gears location permission table. Next time the user - // navigates to one of these origins, she will see the normal Gears - // permission prompt. - boolean addToGearsLocationTable = (systemPermission == 1 ? true : false); - // Build the path to the Gears library. - - File file = new File(mGearsPath).getParentFile(); - if (file == null) { - return; - } - // Build the Gears database file name. - file = new File(file.getAbsolutePath() + File.separator - + GEARS_DATABASE_DIR + File.separator + GEARS_DATABASE_FILE); - // Remember whether or not we need to create the LocationAccess table. - boolean needToCreateTables = false; - if (!file.exists()) { - needToCreateTables = true; - // Create the path or else SQLiteDatabase.openOrCreateDatabase() - // may throw on the device. - file.getParentFile().mkdirs(); - } - // If the database file does not yet exist and the system location - // setting says that the Gears origins need to be removed from the - // location permission table, it means that we don't actually need - // to do anything at all. - if (needToCreateTables && !addToGearsLocationTable) { - return; - } - // Try opening the Gears database. - SQLiteDatabase permissions; - try { - permissions = SQLiteDatabase.openOrCreateDatabase(file, null); - } catch (SQLiteException e) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "Could not open Gears permission DB: " - + e.getMessage()); - } - // Just bail out. - return; - } - // We now have a database open. Begin a transaction. - permissions.beginTransaction(); - try { - if (needToCreateTables) { - // Create the tables. Note that this creates the - // Gears tables for the permissions DB schema version 2. - // The Gears schema upgrade process will take care of the rest. - // First, the storage access table. - SQLiteStatement statement = permissions.compileStatement( - "CREATE TABLE IF NOT EXISTS " - + GEARS_STORAGE_ACCESS_TABLE_NAME - + " (Name TEXT UNIQUE, Value)"); - statement.execute(); - // Next the location access table. - statement = permissions.compileStatement( - "CREATE TABLE IF NOT EXISTS " - + GEARS_LOCATION_ACCESS_TABLE_NAME - + " (Name TEXT UNIQUE, Value)"); - statement.execute(); - // Finally, the schema version table. - statement = permissions.compileStatement( - "CREATE TABLE IF NOT EXISTS " - + GEARS_SCHEMA_VERSION_TABLE_NAME - + " (Name TEXT UNIQUE, Value)"); - statement.execute(); - // Set the schema version to 2. - ContentValues schema = new ContentValues(); - schema.put("Name", "Version"); - schema.put("Value", 2); - permissions.insert(GEARS_SCHEMA_VERSION_TABLE_NAME, null, - schema); - } - - if (addToGearsLocationTable) { - ContentValues permissionValues = new ContentValues(); - - for (String url : sGearsWhiteList) { - permissionValues.put("Name", url); - permissionValues.put("Value", GEARS_ALLOW_LOCATION_ACCESS); - permissions.replace(GEARS_LOCATION_ACCESS_TABLE_NAME, null, - permissionValues); - permissionValues.clear(); - } - } else { - for (String url : sGearsWhiteList) { - permissions.delete(GEARS_LOCATION_ACCESS_TABLE_NAME, "Name=?", - new String[] { url }); - } - } - // Commit the transaction. - permissions.setTransactionSuccessful(); - } catch (SQLiteException e) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "Could not set the Gears permissions: " - + e.getMessage()); - } - } finally { - permissions.endTransaction(); - permissions.close(); - } - } - - class SecureSettingsObserver extends ContentObserver { - SecureSettingsObserver() { - super(new Handler()); - } - - void observe() { - ContentResolver resolver = mContext.getContentResolver(); - resolver.registerContentObserver(Settings.Secure.getUriFor( - Settings.Secure.USE_LOCATION_FOR_SERVICES), false, this); - } - - @Override - public void onChange(boolean selfChange) { - checkGearsPermissions(); - } - } -} diff --git a/core/java/android/webkit/GeolocationPermissions.java b/core/java/android/webkit/GeolocationPermissions.java new file mode 100755 index 0000000..64a9d9b --- /dev/null +++ b/core/java/android/webkit/GeolocationPermissions.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + + +/** + * Implements the Java side of GeolocationPermissions. Simply marshalls calls + * from the UI thread to the WebKit thread. + */ +public final class GeolocationPermissions { + /** + * Callback interface used by the browser to report a Geolocation permission + * state set by the user in response to a permissions prompt. + */ + public interface Callback { + public void invoke(String origin, boolean allow, boolean remember); + }; + + // Log tag + private static final String TAG = "geolocationPermissions"; + + // Global instance + private static GeolocationPermissions sInstance; + + private Handler mHandler; + private Handler mUIHandler; + + // Members used to transfer the origins and permissions between threads. + private Set<String> mOrigins; + private boolean mAllowed; + private Set<String> mOriginsToClear; + private Set<String> mOriginsToAllow; + + // Message ids + static final int GET_ORIGINS = 0; + static final int GET_ALLOWED = 1; + static final int CLEAR = 2; + static final int ALLOW = 3; + static final int CLEAR_ALL = 4; + + // Message ids on the UI thread + static final int RETURN_ORIGINS = 0; + static final int RETURN_ALLOWED = 1; + + private static final String ORIGINS = "origins"; + private static final String ORIGIN = "origin"; + private static final String CALLBACK = "callback"; + private static final String ALLOWED = "allowed"; + + /** + * Gets the singleton instance of the class. + */ + public static GeolocationPermissions getInstance() { + if (sInstance == null) { + sInstance = new GeolocationPermissions(); + } + return sInstance; + } + + /** + * Creates the UI message handler. Must be called on the UI thread. + * @hide + */ + public void createUIHandler() { + if (mUIHandler == null) { + mUIHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // Runs on the UI thread. + switch (msg.what) { + case RETURN_ORIGINS: { + Map values = (Map) msg.obj; + Set origins = (Set) values.get(ORIGINS); + ValueCallback<Set> callback = (ValueCallback<Set>) values.get(CALLBACK); + callback.onReceiveValue(origins); + } break; + case RETURN_ALLOWED: { + Map values = (Map) msg.obj; + Boolean allowed = (Boolean) values.get(ALLOWED); + ValueCallback<Boolean> callback = (ValueCallback<Boolean>) values.get(CALLBACK); + callback.onReceiveValue(allowed); + } break; + } + } + }; + } + } + + /** + * Creates the message handler. Must be called on the WebKit thread. + * @hide + */ + public void createHandler() { + if (mHandler == null) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // Runs on the WebKit thread. + switch (msg.what) { + case GET_ORIGINS: { + getOriginsImpl(); + ValueCallback callback = (ValueCallback) msg.obj; + Set origins = new HashSet(mOrigins); + Map values = new HashMap<String, Object>(); + values.put(CALLBACK, callback); + values.put(ORIGINS, origins); + postUIMessage(Message.obtain(null, RETURN_ORIGINS, values)); + } break; + case GET_ALLOWED: { + Map values = (Map) msg.obj; + String origin = (String) values.get(ORIGIN); + ValueCallback callback = (ValueCallback) values.get(CALLBACK); + getAllowedImpl(origin); + Map retValues = new HashMap<String, Object>(); + retValues.put(CALLBACK, callback); + retValues.put(ALLOWED, new Boolean(mAllowed)); + postUIMessage(Message.obtain(null, RETURN_ALLOWED, retValues)); + } break; + case CLEAR: + nativeClear((String) msg.obj); + break; + case ALLOW: + nativeAllow((String) msg.obj); + break; + case CLEAR_ALL: + nativeClearAll(); + break; + } + } + }; + + if (mOriginsToClear != null) { + for (String origin : mOriginsToClear) { + nativeClear(origin); + } + } + if (mOriginsToAllow != null) { + for (String origin : mOriginsToAllow) { + nativeAllow(origin); + } + } + } + } + + /** + * Utility function to send a message to our handler. + */ + private void postMessage(Message msg) { + assert(mHandler != null); + mHandler.sendMessage(msg); + } + + /** + * Utility function to send a message to the handler on the UI thread + */ + private void postUIMessage(Message msg) { + if (mUIHandler != null) { + mUIHandler.sendMessage(msg); + } + } + + /** + * Gets the set of origins for which Geolocation permissions are stored. + * Note that we represent the origins as strings. These are created using + * WebCore::SecurityOrigin::toString(). As long as all 'HTML 5 modules' + * (Database, Geolocation etc) do so, it's safe to match up origins for the + * purposes of displaying UI. + */ + public void getOrigins(ValueCallback<Set> callback) { + if (callback != null) { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + getOriginsImpl(); + Set origins = new HashSet(mOrigins); + callback.onReceiveValue(origins); + } else { + postMessage(Message.obtain(null, GET_ORIGINS, callback)); + } + } + } + + /** + * Helper method to get the set of origins. + */ + private void getOriginsImpl() { + // Called on the WebKit thread. + mOrigins = nativeGetOrigins(); + } + + /** + * Gets the permission state for the specified origin. + */ + public void getAllowed(String origin, ValueCallback<Boolean> callback) { + if (callback == null) { + return; + } + if (origin == null) { + callback.onReceiveValue(null); + return; + } + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + getAllowedImpl(origin); + callback.onReceiveValue(new Boolean(mAllowed)); + } else { + Map values = new HashMap<String, Object>(); + values.put(ORIGIN, origin); + values.put(CALLBACK, callback); + postMessage(Message.obtain(null, GET_ALLOWED, values)); + } + } + + /** + * Helper method to get the permission state. + */ + private void getAllowedImpl(String origin) { + // Called on the WebKit thread. + mAllowed = nativeGetAllowed(origin); + } + + /** + * Clears the permission state for the specified origin. This method may be + * called before the WebKit thread has intialized the message handler. + * Messages will be queued until this time. + */ + public void clear(String origin) { + // Called on the UI thread. + if (mHandler == null) { + if (mOriginsToClear == null) { + mOriginsToClear = new HashSet<String>(); + } + mOriginsToClear.add(origin); + if (mOriginsToAllow != null) { + mOriginsToAllow.remove(origin); + } + } else { + postMessage(Message.obtain(null, CLEAR, origin)); + } + } + + /** + * Allows the specified origin. This method may be called before the WebKit + * thread has intialized the message handler. Messages will be queued until + * this time. + */ + public void allow(String origin) { + // Called on the UI thread. + if (mHandler == null) { + if (mOriginsToAllow == null) { + mOriginsToAllow = new HashSet<String>(); + } + mOriginsToAllow.add(origin); + if (mOriginsToClear != null) { + mOriginsToClear.remove(origin); + } + } else { + postMessage(Message.obtain(null, ALLOW, origin)); + } + } + + /** + * Clears the permission state for all origins. + */ + public void clearAll() { + // Called on the UI thread. + postMessage(Message.obtain(null, CLEAR_ALL)); + } + + // Native functions, run on the WebKit thread. + private static native Set nativeGetOrigins(); + private static native boolean nativeGetAllowed(String origin); + private static native void nativeClear(String origin); + private static native void nativeAllow(String origin); + private static native void nativeClearAll(); +} diff --git a/core/java/android/webkit/GeolocationService.java b/core/java/android/webkit/GeolocationService.java new file mode 100755 index 0000000..24306f4 --- /dev/null +++ b/core/java/android/webkit/GeolocationService.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.app.ActivityThread; +import android.content.Context; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.util.Log; +import android.webkit.WebView; +import android.webkit.WebViewCore; + + +/** + * Implements the Java side of GeolocationServiceAndroid. + */ +final class GeolocationService implements LocationListener { + + // Log tag + private static final String TAG = "geolocationService"; + + private long mNativeObject; + private LocationManager mLocationManager; + private boolean mIsGpsEnabled; + private boolean mIsRunning; + private boolean mIsNetworkProviderAvailable; + private boolean mIsGpsProviderAvailable; + + /** + * Constructor + * @param nativeObject The native object to which this object will report position updates and + * errors. + */ + public GeolocationService(long nativeObject) { + mNativeObject = nativeObject; + // Register newLocationAvailable with platform service. + ActivityThread thread = ActivityThread.systemMain(); + Context context = thread.getApplication(); + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + if (mLocationManager == null) { + Log.e(TAG, "Could not get location manager."); + } + } + + /** + * Start listening for location updates. + */ + public void start() { + registerForLocationUpdates(); + mIsRunning = true; + } + + /** + * Stop listening for location updates. + */ + public void stop() { + unregisterFromLocationUpdates(); + mIsRunning = false; + } + + /** + * Sets whether to use the GPS. + * @param enable Whether to use the GPS. + */ + public void setEnableGps(boolean enable) { + if (mIsGpsEnabled != enable) { + mIsGpsEnabled = enable; + if (mIsRunning) { + // There's no way to unregister from a single provider, so we can + // only unregister from all, then reregister with all but the GPS. + unregisterFromLocationUpdates(); + registerForLocationUpdates(); + } + } + } + + /** + * LocationListener implementation. + * Called when the location has changed. + * @param location The new location, as a Location object. + */ + public void onLocationChanged(Location location) { + // Callbacks from the system location sevice are queued to this thread, so it's possible + // that we receive callbacks after unregistering. At this point, the native object will no + // longer exist. + if (mIsRunning) { + nativeNewLocationAvailable(mNativeObject, location); + } + } + + /** + * LocationListener implementation. + * Called when the provider status changes. + * @param provider The name of the provider. + * @param status The new status of the provider. + * @param extras an optional Bundle with provider specific data. + */ + public void onStatusChanged(String providerName, int status, Bundle extras) { + boolean isAvailable = (status == LocationProvider.AVAILABLE); + if (LocationManager.NETWORK_PROVIDER.equals(providerName)) { + mIsNetworkProviderAvailable = isAvailable; + } else if (LocationManager.GPS_PROVIDER.equals(providerName)) { + mIsGpsProviderAvailable = isAvailable; + } + maybeReportError("The last location provider is no longer available"); + } + + /** + * LocationListener implementation. + * Called when the provider is enabled. + * @param provider The name of the location provider that is now enabled. + */ + public void onProviderEnabled(String providerName) { + // No need to notify the native side. It's enough to start sending + // valid position fixes again. + if (LocationManager.NETWORK_PROVIDER.equals(providerName)) { + mIsNetworkProviderAvailable = true; + } else if (LocationManager.GPS_PROVIDER.equals(providerName)) { + mIsGpsProviderAvailable = true; + } + } + + /** + * LocationListener implementation. + * Called when the provider is disabled. + * @param provider The name of the location provider that is now disabled. + */ + public void onProviderDisabled(String providerName) { + if (LocationManager.NETWORK_PROVIDER.equals(providerName)) { + mIsNetworkProviderAvailable = false; + } else if (LocationManager.GPS_PROVIDER.equals(providerName)) { + mIsGpsProviderAvailable = false; + } + maybeReportError("The last location provider was disabled"); + } + + /** + * Registers this object with the location service. + */ + private void registerForLocationUpdates() { + try { + mLocationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0, 0, this); + mIsNetworkProviderAvailable = true; + if (mIsGpsEnabled) { + mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); + mIsGpsProviderAvailable = true; + } + } catch(SecurityException e) { + Log.e(TAG, "Caught security exception registering for location updates from system. " + + "This should only happen in DumpRenderTree."); + } + } + + /** + * Unregisters this object from the location service. + */ + private void unregisterFromLocationUpdates() { + mLocationManager.removeUpdates(this); + } + + /** + * Reports an error if neither the network nor the GPS provider is available. + */ + private void maybeReportError(String message) { + // Callbacks from the system location sevice are queued to this thread, so it's possible + // that we receive callbacks after unregistering. At this point, the native object will no + // longer exist. + if (mIsRunning && !mIsNetworkProviderAvailable && !mIsGpsProviderAvailable) { + nativeNewErrorAvailable(mNativeObject, message); + } + } + + // Native functions + private static native void nativeNewLocationAvailable(long nativeObject, Location location); + private static native void nativeNewErrorAvailable(long nativeObject, String message); +} diff --git a/core/java/android/webkit/GoogleLocationSettingManager.java b/core/java/android/webkit/GoogleLocationSettingManager.java new file mode 100644 index 0000000..ecac70a --- /dev/null +++ b/core/java/android/webkit/GoogleLocationSettingManager.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.database.ContentObserver; +import android.os.Handler; +import android.preference.PreferenceManager; +import android.provider.Settings; + +import java.util.HashSet; + +/** + * A class to manage the interaction between the system setting 'Location & + * Security - Share with Google' and the browser. When this setting is set + * to true, we allow Geolocation for Google origins. When this setting is + * set to false, we clear Geolocation permissions for Google origins. + */ +class GoogleLocationSettingManager { + // The observer used to listen to the system setting. + private GoogleLocationSettingObserver mSettingObserver; + + // The value of the system setting that indicates true. + private final static int sSystemSettingTrue = 1; + // The value of the system setting that indicates false. + private final static int sSystemSettingFalse = 0; + // The value of the USE_LOCATION_FOR_SERVICES system setting last read + // by the browser. + private final static String LAST_READ_USE_LOCATION_FOR_SERVICES = + "lastReadUseLocationForServices"; + // The Browser package name. + private static final String BROWSER_PACKAGE_NAME = "com.android.browser"; + // The Google origins we consider. + private static HashSet<String> sGoogleOrigins; + static { + sGoogleOrigins = new HashSet<String>(); + // NOTE: DO NOT ADD A "/" AT THE END! + sGoogleOrigins.add("http://www.google.com"); + sGoogleOrigins.add("http://www.google.co.uk"); + } + + private static GoogleLocationSettingManager sGoogleLocationSettingManager = null; + private static int sRefCount = 0; + + static GoogleLocationSettingManager getInstance() { + if (sGoogleLocationSettingManager == null) { + sGoogleLocationSettingManager = new GoogleLocationSettingManager(); + } + return sGoogleLocationSettingManager; + } + + private GoogleLocationSettingManager() {} + + /** + * Starts the manager. Checks whether the setting has changed and + * installs an observer to listen for future changes. + */ + public void start(Context context) { + // Are we running in the browser? + if (context == null || !BROWSER_PACKAGE_NAME.equals(context.getPackageName())) { + return; + } + // Increase the refCount + sRefCount++; + // Are we already registered? + if (mSettingObserver != null) { + return; + } + // Read and apply the settings if needed. + maybeApplySetting(context); + // Register to receive notifications when the system settings change. + mSettingObserver = new GoogleLocationSettingObserver(); + mSettingObserver.observe(context); + } + + /** + * Stops the manager. + */ + public void stop() { + // Are we already registered? + if (mSettingObserver == null) { + return; + } + if (--sRefCount == 0) { + mSettingObserver.doNotObserve(); + mSettingObserver = null; + } + } + /** + * Checks to see if the system setting has changed and if so, + * updates the Geolocation permissions accordingly. + * @param the Application context + */ + private void maybeApplySetting(Context context) { + int setting = getSystemSetting(context); + if (settingChanged(setting, context)) { + applySetting(setting); + } + } + + /** + * Gets the current system setting for 'Use location for Google services'. + * @param the Application context + * @return The system setting. + */ + private int getSystemSetting(Context context) { + return Settings.Secure.getInt(context.getContentResolver(), + Settings.Secure.USE_LOCATION_FOR_SERVICES, + sSystemSettingFalse); + } + + /** + * Determines whether the supplied setting has changed from the last + * value read by the browser. + * @param setting The setting. + * @param the Application context + * @return Whether the setting has changed from the last value read + * by the browser. + */ + private boolean settingChanged(int setting, Context context) { + SharedPreferences preferences = + PreferenceManager.getDefaultSharedPreferences(context); + // Default to false. If the system setting is false the first time it is ever read by the + // browser, there's nothing to do. + int lastReadSetting = sSystemSettingFalse; + lastReadSetting = preferences.getInt(LAST_READ_USE_LOCATION_FOR_SERVICES, + lastReadSetting); + + if (lastReadSetting == setting) { + return false; + } + + Editor editor = preferences.edit(); + editor.putInt(LAST_READ_USE_LOCATION_FOR_SERVICES, setting); + editor.commit(); + return true; + } + + /** + * Applies the supplied setting to the Geolocation permissions. + * @param setting The setting. + */ + private void applySetting(int setting) { + for (String origin : sGoogleOrigins) { + if (setting == sSystemSettingTrue) { + GeolocationPermissions.getInstance().allow(origin); + } else { + GeolocationPermissions.getInstance().clear(origin); + } + } + } + + /** + * This class implements an observer to listen for changes to the + * system setting. + */ + private class GoogleLocationSettingObserver extends ContentObserver { + private Context mContext; + + GoogleLocationSettingObserver() { + super(new Handler()); + } + + void observe(Context context) { + if (mContext != null) { + return; + } + ContentResolver resolver = context.getContentResolver(); + resolver.registerContentObserver(Settings.Secure.getUriFor( + Settings.Secure.USE_LOCATION_FOR_SERVICES), false, this); + mContext = context; + } + + void doNotObserve() { + if (mContext == null) { + return; + } + ContentResolver resolver = mContext.getContentResolver(); + resolver.unregisterContentObserver(this); + mContext = null; + } + + @Override + public void onChange(boolean selfChange) { + // This may come after the call to doNotObserve() above, + // so mContext may be null. + if (mContext != null) { + maybeApplySetting(mContext); + } + } + } +} diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java new file mode 100644 index 0000000..b7a9065 --- /dev/null +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaPlayer; +import android.media.MediaPlayer.OnPreparedListener; +import android.media.MediaPlayer.OnCompletionListener; +import android.media.MediaPlayer.OnErrorListener; +import android.net.http.EventHandler; +import android.net.http.Headers; +import android.net.http.RequestHandle; +import android.net.http.RequestQueue; +import android.net.http.SslCertificate; +import android.net.http.SslError; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.util.Log; +import android.view.MotionEvent; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsoluteLayout; +import android.widget.FrameLayout; +import android.widget.MediaController; +import android.widget.VideoView; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * <p>Proxy for HTML5 video views. + */ +class HTML5VideoViewProxy extends Handler + implements MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, + MediaPlayer.OnErrorListener { + // Logging tag. + private static final String LOGTAG = "HTML5VideoViewProxy"; + + // Message Ids for WebCore thread -> UI thread communication. + private static final int PLAY = 100; + private static final int SEEK = 101; + private static final int PAUSE = 102; + private static final int ERROR = 103; + private static final int LOAD_DEFAULT_POSTER = 104; + + // Message Ids to be handled on the WebCore thread + private static final int PREPARED = 200; + private static final int ENDED = 201; + private static final int POSTER_FETCHED = 202; + + // The C++ MediaPlayerPrivateAndroid object. + int mNativePointer; + // The handler for WebCore thread messages; + private Handler mWebCoreHandler; + // The WebView instance that created this view. + private WebView mWebView; + // The poster image to be shown when the video is not playing. + // This ref prevents the bitmap from being GC'ed. + private Bitmap mPoster; + // The poster downloader. + private PosterDownloader mPosterDownloader; + // The seek position. + private int mSeekPosition; + // A helper class to control the playback. This executes on the UI thread! + private static final class VideoPlayer { + // The proxy that is currently playing (if any). + private static HTML5VideoViewProxy mCurrentProxy; + // The VideoView instance. This is a singleton for now, at least until + // http://b/issue?id=1973663 is fixed. + private static VideoView mVideoView; + // The progress view. + private static View mProgressView; + // The container for the progress view and video view + private static FrameLayout mLayout; + + private static final WebChromeClient.CustomViewCallback mCallback = + new WebChromeClient.CustomViewCallback() { + public void onCustomViewHidden() { + // At this point the videoview is pretty much destroyed. + // It listens to SurfaceHolder.Callback.SurfaceDestroyed event + // which happens when the video view is detached from its parent + // view. This happens in the WebChromeClient before this method + // is invoked. + mCurrentProxy.playbackEnded(); + mCurrentProxy = null; + mLayout.removeView(mVideoView); + mVideoView = null; + if (mProgressView != null) { + mLayout.removeView(mProgressView); + mProgressView = null; + } + mLayout = null; + } + }; + + public static void play(String url, int time, HTML5VideoViewProxy proxy, + WebChromeClient client) { + if (mCurrentProxy != null) { + // Some other video is already playing. Notify the caller that its playback ended. + proxy.playbackEnded(); + return; + } + mCurrentProxy = proxy; + // Create a FrameLayout that will contain the VideoView and the + // progress view (if any). + mLayout = new FrameLayout(proxy.getContext()); + FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER); + mVideoView = new VideoView(proxy.getContext()); + mVideoView.setWillNotDraw(false); + mVideoView.setMediaController(new MediaController(proxy.getContext())); + mVideoView.setVideoURI(Uri.parse(url)); + mVideoView.setOnCompletionListener(proxy); + mVideoView.setOnPreparedListener(proxy); + mVideoView.setOnErrorListener(proxy); + mVideoView.seekTo(time); + mLayout.addView(mVideoView, layoutParams); + mProgressView = client.getVideoLoadingProgressView(); + if (mProgressView != null) { + mLayout.addView(mProgressView, layoutParams); + mProgressView.setVisibility(View.VISIBLE); + } + mLayout.setVisibility(View.VISIBLE); + mVideoView.start(); + client.onShowCustomView(mLayout, mCallback); + } + + public static void seek(int time, HTML5VideoViewProxy proxy) { + if (mCurrentProxy == proxy && time >= 0 && mVideoView != null) { + mVideoView.seekTo(time); + } + } + + public static void pause(HTML5VideoViewProxy proxy) { + if (mCurrentProxy == proxy && mVideoView != null) { + mVideoView.pause(); + } + } + + public static void onPrepared() { + if (mProgressView == null || mLayout == null) { + return; + } + mProgressView.setVisibility(View.GONE); + mLayout.removeView(mProgressView); + mProgressView = null; + } + } + + // A bunch event listeners for our VideoView + // MediaPlayer.OnPreparedListener + public void onPrepared(MediaPlayer mp) { + VideoPlayer.onPrepared(); + Message msg = Message.obtain(mWebCoreHandler, PREPARED); + Map<String, Object> map = new HashMap<String, Object>(); + map.put("dur", new Integer(mp.getDuration())); + map.put("width", new Integer(mp.getVideoWidth())); + map.put("height", new Integer(mp.getVideoHeight())); + msg.obj = map; + mWebCoreHandler.sendMessage(msg); + } + + // MediaPlayer.OnCompletionListener; + public void onCompletion(MediaPlayer mp) { + playbackEnded(); + } + + // MediaPlayer.OnErrorListener + public boolean onError(MediaPlayer mp, int what, int extra) { + sendMessage(obtainMessage(ERROR)); + return false; + } + + public void playbackEnded() { + Message msg = Message.obtain(mWebCoreHandler, ENDED); + mWebCoreHandler.sendMessage(msg); + } + + // Handler for the messages from WebCore thread to the UI thread. + @Override + public void handleMessage(Message msg) { + // This executes on the UI thread. + switch (msg.what) { + case PLAY: { + String url = (String) msg.obj; + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + VideoPlayer.play(url, mSeekPosition, this, client); + } + break; + } + case SEEK: { + Integer time = (Integer) msg.obj; + mSeekPosition = time; + VideoPlayer.seek(mSeekPosition, this); + break; + } + case PAUSE: { + VideoPlayer.pause(this); + break; + } + case ERROR: { + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + client.onHideCustomView(); + } + break; + } + case LOAD_DEFAULT_POSTER: { + WebChromeClient client = mWebView.getWebChromeClient(); + if (client != null) { + doSetPoster(client.getDefaultVideoPoster()); + } + break; + } + } + } + + // Everything below this comment executes on the WebCore thread, except for + // the EventHandler methods, which are called on the network thread. + + // A helper class that knows how to download posters + private static final class PosterDownloader implements EventHandler { + // The request queue. This is static as we have one queue for all posters. + private static RequestQueue mRequestQueue; + private static int mQueueRefCount = 0; + // The poster URL + private String mUrl; + // The proxy we're doing this for. + private final HTML5VideoViewProxy mProxy; + // The poster bytes. We only touch this on the network thread. + private ByteArrayOutputStream mPosterBytes; + // The request handle. We only touch this on the WebCore thread. + private RequestHandle mRequestHandle; + // The response status code. + private int mStatusCode; + // The response headers. + private Headers mHeaders; + // The handler to handle messages on the WebCore thread. + private Handler mHandler; + + public PosterDownloader(String url, HTML5VideoViewProxy proxy) { + mUrl = url; + mProxy = proxy; + mHandler = new Handler(); + } + // Start the download. Called on WebCore thread. + public void start() { + retainQueue(); + mRequestHandle = mRequestQueue.queueRequest(mUrl, "GET", null, this, null, 0); + } + // Cancel the download if active and release the queue. Called on WebCore thread. + public void cancelAndReleaseQueue() { + if (mRequestHandle != null) { + mRequestHandle.cancel(); + mRequestHandle = null; + } + releaseQueue(); + } + // EventHandler methods. Executed on the network thread. + public void status(int major_version, + int minor_version, + int code, + String reason_phrase) { + mStatusCode = code; + } + + public void headers(Headers headers) { + mHeaders = headers; + } + + public void data(byte[] data, int len) { + if (mPosterBytes == null) { + mPosterBytes = new ByteArrayOutputStream(); + } + mPosterBytes.write(data, 0, len); + } + + public void endData() { + if (mStatusCode == 200) { + if (mPosterBytes.size() > 0) { + Bitmap poster = BitmapFactory.decodeByteArray( + mPosterBytes.toByteArray(), 0, mPosterBytes.size()); + mProxy.doSetPoster(poster); + } + cleanup(); + } else if (mStatusCode >= 300 && mStatusCode < 400) { + // We have a redirect. + mUrl = mHeaders.getLocation(); + if (mUrl != null) { + mHandler.post(new Runnable() { + public void run() { + if (mRequestHandle != null) { + mRequestHandle.setupRedirect(mUrl, mStatusCode, + new HashMap<String, String>()); + } + } + }); + } + } + } + + public void certificate(SslCertificate certificate) { + // Don't care. + } + + public void error(int id, String description) { + cleanup(); + } + + public boolean handleSslErrorRequest(SslError error) { + // Don't care. If this happens, data() will never be called so + // mPosterBytes will never be created, so no need to call cleanup. + return false; + } + // Tears down the poster bytes stream. Called on network thread. + private void cleanup() { + if (mPosterBytes != null) { + try { + mPosterBytes.close(); + } catch (IOException ignored) { + // Ignored. + } finally { + mPosterBytes = null; + } + } + } + + // Queue management methods. Called on WebCore thread. + private void retainQueue() { + if (mRequestQueue == null) { + mRequestQueue = new RequestQueue(mProxy.getContext()); + } + mQueueRefCount++; + } + + private void releaseQueue() { + if (mQueueRefCount == 0) { + return; + } + if (--mQueueRefCount == 0) { + mRequestQueue.shutdown(); + mRequestQueue = null; + } + } + } + + /** + * Private constructor. + * @param webView is the WebView that hosts the video. + * @param nativePtr is the C++ pointer to the MediaPlayerPrivate object. + */ + private HTML5VideoViewProxy(WebView webView, int nativePtr) { + // This handler is for the main (UI) thread. + super(Looper.getMainLooper()); + // Save the WebView object. + mWebView = webView; + // Save the native ptr + mNativePointer = nativePtr; + // create the message handler for this thread + createWebCoreHandler(); + } + + private void createWebCoreHandler() { + mWebCoreHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case PREPARED: { + Map<String, Object> map = (Map<String, Object>) msg.obj; + Integer duration = (Integer) map.get("dur"); + Integer width = (Integer) map.get("width"); + Integer height = (Integer) map.get("height"); + nativeOnPrepared(duration.intValue(), width.intValue(), + height.intValue(), mNativePointer); + break; + } + case ENDED: + nativeOnEnded(mNativePointer); + break; + case POSTER_FETCHED: + Bitmap poster = (Bitmap) msg.obj; + nativeOnPosterFetched(poster, mNativePointer); + break; + } + } + }; + } + + private void doSetPoster(Bitmap poster) { + if (poster == null) { + return; + } + // Save a ref to the bitmap and send it over to the WebCore thread. + mPoster = poster; + Message msg = Message.obtain(mWebCoreHandler, POSTER_FETCHED); + msg.obj = poster; + mWebCoreHandler.sendMessage(msg); + } + + public Context getContext() { + return mWebView.getContext(); + } + + // The public methods below are all called from WebKit only. + /** + * Play a video stream. + * @param url is the URL of the video stream. + */ + public void play(String url) { + if (url == null) { + return; + } + Message message = obtainMessage(PLAY); + message.obj = url; + sendMessage(message); + } + + /** + * Seek into the video stream. + * @param time is the position in the video stream. + */ + public void seek(int time) { + Message message = obtainMessage(SEEK); + message.obj = new Integer(time); + sendMessage(message); + } + + /** + * Pause the playback. + */ + public void pause() { + Message message = obtainMessage(PAUSE); + sendMessage(message); + } + + /** + * Tear down this proxy object. + */ + public void teardown() { + // This is called by the C++ MediaPlayerPrivate dtor. + // Cancel any active poster download. + if (mPosterDownloader != null) { + mPosterDownloader.cancelAndReleaseQueue(); + } + mNativePointer = 0; + } + + /** + * Load the poster image. + * @param url is the URL of the poster image. + */ + public void loadPoster(String url) { + if (url == null) { + Message message = obtainMessage(LOAD_DEFAULT_POSTER); + sendMessage(message); + return; + } + // Cancel any active poster download. + if (mPosterDownloader != null) { + mPosterDownloader.cancelAndReleaseQueue(); + } + // Load the poster asynchronously + mPosterDownloader = new PosterDownloader(url, this); + mPosterDownloader.start(); + } + + /** + * The factory for HTML5VideoViewProxy instances. + * @param webViewCore is the WebViewCore that is requesting the proxy. + * + * @return a new HTML5VideoViewProxy object. + */ + public static HTML5VideoViewProxy getInstance(WebViewCore webViewCore, int nativePtr) { + return new HTML5VideoViewProxy(webViewCore.getWebView(), nativePtr); + } + + private native void nativeOnPrepared(int duration, int width, int height, int nativePointer); + private native void nativeOnEnded(int nativePointer); + private native void nativeOnPosterFetched(Bitmap poster, int nativePointer); +} diff --git a/core/java/android/webkit/HttpAuthHandler.java b/core/java/android/webkit/HttpAuthHandler.java index 84dc9f0..1c17575 100644 --- a/core/java/android/webkit/HttpAuthHandler.java +++ b/core/java/android/webkit/HttpAuthHandler.java @@ -49,8 +49,8 @@ public class HttpAuthHandler extends Handler { // Message id for handling the user response - private final int AUTH_PROCEED = 100; - private final int AUTH_CANCEL = 200; + private static final int AUTH_PROCEED = 100; + private static final int AUTH_CANCEL = 200; /** * Creates a new HTTP authentication handler with an empty diff --git a/core/java/android/webkit/HttpDateTime.java b/core/java/android/webkit/HttpDateTime.java index c6ec2d2..2f46f2b 100644 --- a/core/java/android/webkit/HttpDateTime.java +++ b/core/java/android/webkit/HttpDateTime.java @@ -23,7 +23,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -class HttpDateTime { +/** {@hide} */ +public final class HttpDateTime { /* * Regular expression for parsing HTTP-date. @@ -47,14 +48,16 @@ class HttpDateTime { * Wdy, DD Mon YYYY HH:MM:SS * Wdy Mon (SP)D HH:MM:SS YYYY * Wdy Mon DD HH:MM:SS YYYY GMT + * + * HH can be H if the first digit is zero. */ private static final String HTTP_DATE_RFC_REGEXP = "([0-9]{1,2})[- ]([A-Za-z]{3,3})[- ]([0-9]{2,4})[ ]" - + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])"; + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])"; private static final String HTTP_DATE_ANSIC_REGEXP = "[ ]([A-Za-z]{3,3})[ ]+([0-9]{1,2})[ ]" - + "([0-9][0-9]:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; + + "([0-9]{1,2}:[0-9][0-9]:[0-9][0-9])[ ]([0-9]{2,4})"; /** * The compiled version of the HTTP-date regular expressions. @@ -65,6 +68,12 @@ class HttpDateTime { Pattern.compile(HTTP_DATE_ANSIC_REGEXP); private static class TimeOfDay { + TimeOfDay(int h, int m, int s) { + this.hour = h; + this.minute = m; + this.second = s; + } + int hour; int minute; int second; @@ -76,7 +85,7 @@ class HttpDateTime { int date = 1; int month = Calendar.JANUARY; int year = 1970; - TimeOfDay timeOfDay = new TimeOfDay(); + TimeOfDay timeOfDay; Matcher rfcMatcher = HTTP_DATE_RFC_PATTERN.matcher(timeString); if (rfcMatcher.find()) { @@ -175,21 +184,39 @@ class HttpDateTime { } else { return year + 2000; } - } else - return (yearString.charAt(0) - '0') * 1000 + } else if (yearString.length() == 3) { + // According to RFC 2822, three digit years should be added to 1900. + int year = (yearString.charAt(0) - '0') * 100 + + (yearString.charAt(1) - '0') * 10 + + (yearString.charAt(2) - '0'); + return year + 1900; + } else if (yearString.length() == 4) { + return (yearString.charAt(0) - '0') * 1000 + (yearString.charAt(1) - '0') * 100 + (yearString.charAt(2) - '0') * 10 + (yearString.charAt(3) - '0'); + } else { + return 1970; + } } private static TimeOfDay getTime(String timeString) { - TimeOfDay time = new TimeOfDay(); - time.hour = (timeString.charAt(0) - '0') * 10 - + (timeString.charAt(1) - '0'); - time.minute = (timeString.charAt(3) - '0') * 10 - + (timeString.charAt(4) - '0'); - time.second = (timeString.charAt(6) - '0') * 10 - + (timeString.charAt(7) - '0'); - return time; + // HH might be H + int i = 0; + int hour = timeString.charAt(i++) - '0'; + if (timeString.charAt(i) != ':') + hour = hour * 10 + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int minute = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + // Skip ':' + i++; + + int second = (timeString.charAt(i++) - '0') * 10 + + (timeString.charAt(i++) - '0'); + + return new TimeOfDay(hour, minute, second); } } diff --git a/core/java/android/webkit/JWebCoreJavaBridge.java b/core/java/android/webkit/JWebCoreJavaBridge.java index 1dbd007..f350d13 100644 --- a/core/java/android/webkit/JWebCoreJavaBridge.java +++ b/core/java/android/webkit/JWebCoreJavaBridge.java @@ -16,9 +16,9 @@ package android.webkit; +import android.content.Context; import android.os.Handler; import android.os.Message; -import android.security.CertTool; import android.util.Log; final class JWebCoreJavaBridge extends Handler { @@ -34,14 +34,24 @@ final class JWebCoreJavaBridge extends Handler { // Instant timer is used to implement a timer that needs to fire almost // immediately. private boolean mHasInstantTimer; + // Reference count the pause/resume of timers private int mPauseTimerRefCount; + private boolean mTimerPaused; + private boolean mHasDeferredTimers; + + private Context mContext; + + /* package */ + static final int REFRESH_PLUGINS = 100; + /** * Construct a new JWebCoreJavaBridge to interface with * WebCore timers and cookies. */ - public JWebCoreJavaBridge() { + public JWebCoreJavaBridge(Context context) { + mContext = context; nativeConstructor(); } @@ -51,6 +61,17 @@ final class JWebCoreJavaBridge extends Handler { } /** + * Call native timer callbacks. + */ + private void fireSharedTimer() { + PerfChecker checker = new PerfChecker(); + // clear the flag so that sharedTimerFired() can set a new timer + mHasInstantTimer = false; + sharedTimerFired(); + checker.responseAlert("sharedTimer"); + } + + /** * handleMessage * @param msg The dispatched message. * @@ -60,16 +81,21 @@ final class JWebCoreJavaBridge extends Handler { public void handleMessage(Message msg) { switch (msg.what) { case TIMER_MESSAGE: { - PerfChecker checker = new PerfChecker(); - // clear the flag so that sharedTimerFired() can set a new timer - mHasInstantTimer = false; - sharedTimerFired(); - checker.responseAlert("sharedTimer"); + if (mTimerPaused) { + mHasDeferredTimers = true; + } else { + fireSharedTimer(); + } break; } case FUNCPTR_MESSAGE: nativeServiceFuncPtrQueue(); break; + case REFRESH_PLUGINS: + nativeUpdatePluginDirectories(PluginManager.getInstance(null) + .getPluginDirectories(), ((Boolean) msg.obj) + .booleanValue()); + break; } } @@ -86,7 +112,8 @@ final class JWebCoreJavaBridge extends Handler { */ public void pause() { if (--mPauseTimerRefCount == 0) { - setDeferringTimers(true); + mTimerPaused = true; + mHasDeferredTimers = false; } } @@ -95,7 +122,11 @@ final class JWebCoreJavaBridge extends Handler { */ public void resume() { if (++mPauseTimerRefCount == 1) { - setDeferringTimers(false); + mTimerPaused = false; + if (mHasDeferredTimers) { + mHasDeferredTimers = false; + fireSharedTimer(); + } } } @@ -108,10 +139,9 @@ final class JWebCoreJavaBridge extends Handler { /** * Store a cookie string associated with a url. * @param url The url to be used as a key for the cookie. - * @param docUrl The policy base url used by WebCore. * @param value The cookie string to be stored. */ - private void setCookies(String url, String docUrl, String value) { + private void setCookies(String url, String value) { if (value.contains("\r") || value.contains("\n")) { // for security reason, filter out '\r' and '\n' from the cookie int size = value.length(); @@ -152,11 +182,25 @@ final class JWebCoreJavaBridge extends Handler { } /** + * Returns an array of plugin directoies + */ + private String[] getPluginDirectories() { + return PluginManager.getInstance(null).getPluginDirectories(); + } + + /** + * Returns the path of the plugin data directory + */ + private String getPluginSharedDataDirectory() { + return PluginManager.getInstance(null).getPluginSharedDataDirectory(); + } + + /** * setSharedTimer * @param timemillis The relative time when the timer should fire */ private void setSharedTimer(long timemillis) { - if (WebView.LOGV_ENABLED) Log.v(LOGTAG, "setSharedTimer " + timemillis); + if (DebugFlags.J_WEB_CORE_JAVA_BRIDGE) Log.v(LOGTAG, "setSharedTimer " + timemillis); if (timemillis <= 0) { // we don't accumulate the sharedTimer unless it is a delayed @@ -180,25 +224,27 @@ final class JWebCoreJavaBridge extends Handler { * Stop the shared timer. */ private void stopSharedTimer() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.J_WEB_CORE_JAVA_BRIDGE) { Log.v(LOGTAG, "stopSharedTimer removing all timers"); } removeMessages(TIMER_MESSAGE); mHasInstantTimer = false; + mHasDeferredTimers = false; } private String[] getKeyStrengthList() { - return CertTool.getInstance().getSupportedKeyStrenghs(); + return CertTool.getKeyStrengthList(); } private String getSignedPublicKey(int index, String challenge, String url) { // generateKeyPair expects organizations which we don't have. Ignore url. - return CertTool.getInstance().generateKeyPair(index, challenge, null); + return CertTool.getSignedPublicKey(mContext, index, challenge); } private native void nativeConstructor(); private native void nativeFinalize(); private native void sharedTimerFired(); - private native void setDeferringTimers(boolean defer); + private native void nativeUpdatePluginDirectories(String[] directories, + boolean reload); public native void setNetworkOnLine(boolean online); } diff --git a/core/java/android/webkit/LoadListener.java b/core/java/android/webkit/LoadListener.java index c3f3594..4c17f99 100644 --- a/core/java/android/webkit/LoadListener.java +++ b/core/java/android/webkit/LoadListener.java @@ -28,7 +28,6 @@ import android.net.http.SslError; import android.os.Handler; import android.os.Message; -import android.security.CertTool; import android.util.Log; import android.webkit.CacheManager.CacheResult; @@ -37,14 +36,11 @@ import com.android.internal.R; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Vector; import java.util.regex.Pattern; import java.util.regex.Matcher; -import org.apache.commons.codec.binary.Base64; - class LoadListener extends Handler implements EventHandler { private static final String LOGTAG = "webkit"; @@ -72,12 +68,12 @@ class LoadListener extends Handler implements EventHandler { private static final int HTTP_NOT_FOUND = 404; private static final int HTTP_PROXY_AUTH = 407; - private static HashSet<String> sCertificateMimeTypeMap; + private static HashMap<String, String> sCertificateTypeMap; static { - sCertificateMimeTypeMap = new HashSet<String>(); - sCertificateMimeTypeMap.add("application/x-x509-ca-cert"); - sCertificateMimeTypeMap.add("application/x-x509-user-cert"); - sCertificateMimeTypeMap.add("application/x-pkcs12"); + sCertificateTypeMap = new HashMap<String, String>(); + sCertificateTypeMap.put("application/x-x509-ca-cert", CertTool.CERT); + sCertificateTypeMap.put("application/x-x509-user-cert", CertTool.CERT); + sCertificateTypeMap.put("application/x-pkcs12", CertTool.PKCS12); } private static int sNativeLoaderCount; @@ -101,6 +97,7 @@ class LoadListener extends Handler implements EventHandler { private boolean mAuthFailed; // indicates that the prev. auth failed private CacheLoader mCacheLoader; private CacheManager.CacheResult mCacheResult; + private boolean mFromCache = false; private HttpAuthHeader mAuthHeader; private int mErrorID = OK; private String mErrorDescription; @@ -113,7 +110,6 @@ class LoadListener extends Handler implements EventHandler { private String mMethod; private Map<String, String> mRequestHeaders; private byte[] mPostData; - private boolean mIsHighPriority; // Flag to indicate that this load is synchronous. private boolean mSynchronous; private Vector<Message> mMessageQueue; @@ -142,15 +138,13 @@ class LoadListener extends Handler implements EventHandler { LoadListener(Context context, BrowserFrame frame, String url, int nativeLoader, boolean synchronous, boolean isMainPageLoader) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener constructor url=" + url); } mContext = context; mBrowserFrame = frame; setUrl(url); mNativeLoader = nativeLoader; - mMimeType = ""; - mEncoding = ""; mSynchronous = synchronous; if (synchronous) { mMessageQueue = new Vector<Message>(); @@ -293,7 +287,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void headers(Headers headers) { - if (WebView.LOGV_ENABLED) Log.v(LOGTAG, "LoadListener.headers"); + if (DebugFlags.LOAD_LISTENER) Log.v(LOGTAG, "LoadListener.headers"); sendMessageInternal(obtainMessage(MSG_CONTENT_HEADERS, headers)); } @@ -301,8 +295,6 @@ class LoadListener extends Handler implements EventHandler { private void handleHeaders(Headers headers) { if (mCancelled) return; mHeaders = headers; - mMimeType = ""; - mEncoding = ""; ArrayList<String> cookies = headers.getSetCookie(); for (int i = 0; i < cookies.size(); ++i) { @@ -322,8 +314,8 @@ class LoadListener extends Handler implements EventHandler { // If we have one of "generic" MIME types, try to deduce // the right MIME type from the file extension (if any): - if (mMimeType.equalsIgnoreCase("text/plain") || - mMimeType.equalsIgnoreCase("application/octet-stream")) { + if (mMimeType.equals("text/plain") || + mMimeType.equals("application/octet-stream")) { // for attachment, use the filename in the Content-Disposition // to guess the mimetype @@ -339,17 +331,14 @@ class LoadListener extends Handler implements EventHandler { if (newMimeType != null) { mMimeType = newMimeType; } - } else if (mMimeType.equalsIgnoreCase("text/vnd.wap.wml")) { + } else if (mMimeType.equals("text/vnd.wap.wml")) { // As we don't support wml, render it as plain text mMimeType = "text/plain"; } else { - // XXX: Until the servers send us either correct xhtml or - // text/html, treat application/xhtml+xml as text/html. // It seems that xhtml+xml and vnd.wap.xhtml+xml mime // subtypes are used interchangeably. So treat them the same. - if (mMimeType.equalsIgnoreCase("application/xhtml+xml") || - mMimeType.equals("application/vnd.wap.xhtml+xml")) { - mMimeType = "text/html"; + if (mMimeType.equals("application/vnd.wap.xhtml+xml")) { + mMimeType = "application/xhtml+xml"; } } } else { @@ -419,11 +408,10 @@ class LoadListener extends Handler implements EventHandler { mStatusCode == HTTP_MOVED_PERMANENTLY || mStatusCode == HTTP_TEMPORARY_REDIRECT) && mNativeLoader != 0) { - // Content arriving from a StreamLoader (eg File, Cache or Data) - // will not be cached as they have the header: - // cache-control: no-store - mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, - headers, mMimeType, false); + if (!mFromCache && mRequestHandle != null) { + mCacheResult = CacheManager.createCacheFile(mUrl, mStatusCode, + headers, mMimeType, false); + } if (mCacheResult != null) { mCacheResult.encoding = mEncoding; } @@ -450,7 +438,7 @@ class LoadListener extends Handler implements EventHandler { */ public void status(int majorVersion, int minorVersion, int code, /* Status-Code value */ String reasonPhrase) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener: from: " + mUrl + " major: " + majorVersion + " minor: " + minorVersion @@ -464,6 +452,9 @@ class LoadListener extends Handler implements EventHandler { status.put("reason", reasonPhrase); // New status means new data. Clear the old. mDataBuilder.clear(); + mMimeType = ""; + mEncoding = ""; + mTransferEncoding = ""; sendMessageInternal(obtainMessage(MSG_STATUS, status)); } @@ -507,7 +498,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void error(int id, String description) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.error url:" + url() + " id:" + id + " description:" + description); } @@ -535,23 +526,10 @@ class LoadListener extends Handler implements EventHandler { * mDataBuilder is a thread-safe structure. */ public void data(byte[] data, int length) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.data(): url: " + url()); } - // Decode base64 data - // Note: It's fine that we only decode base64 here and not in the other - // data call because the only caller of the stream version is not - // base64 encoded. - if ("base64".equalsIgnoreCase(mTransferEncoding)) { - if (length < data.length) { - byte[] trimmedData = new byte[length]; - System.arraycopy(data, 0, trimmedData, 0, length); - data = trimmedData; - } - data = Base64.decodeBase64(data); - length = data.length; - } // Synchronize on mData because commitLoad may write mData to WebCore // and we don't want to replace mData or mDataLength at the same time // as a write. @@ -573,7 +551,7 @@ class LoadListener extends Handler implements EventHandler { * directly */ public void endData() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.endData(): url: " + url()); } sendMessageInternal(obtainMessage(MSG_CONTENT_FINISHED)); @@ -626,7 +604,8 @@ class LoadListener extends Handler implements EventHandler { // before calling it. if (mCacheLoader != null) { mCacheLoader.load(); - if (WebView.LOGV_ENABLED) { + mFromCache = true; + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener cache load url=" + url()); } return; @@ -646,6 +625,7 @@ class LoadListener extends Handler implements EventHandler { * serviced by the Cache. */ /* package */ void setCacheLoader(CacheLoader c) { mCacheLoader = c; + mFromCache = true; } /** @@ -662,6 +642,8 @@ class LoadListener extends Handler implements EventHandler { // Go ahead and set the cache loader to null in case the result is // null. mCacheLoader = null; + // reset the flag + mFromCache = false; if (result != null) { // The contents of the cache may need to be revalidated so just @@ -676,12 +658,13 @@ class LoadListener extends Handler implements EventHandler { CacheManager.HEADER_KEY_IFNONEMATCH) && !headers.containsKey( CacheManager.HEADER_KEY_IFMODIFIEDSINCE)) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "FrameLoader: HTTP URL in cache " + "and usable: " + url()); } // Load the cached file mCacheLoader.load(); + mFromCache = true; return true; } } @@ -695,12 +678,23 @@ class LoadListener extends Handler implements EventHandler { * directly */ public boolean handleSslErrorRequest(SslError error) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.handleSslErrorRequest(): url:" + url() + " primary error: " + error.getPrimaryError() + " certificate: " + error.getCertificate()); } + // Check the cached preference table before sending a message. This + // will prevent waiting for an already available answer. + if (Network.getInstance(mContext).checkSslPrefTable(this, error)) { + return true; + } + // Do not post a message for a synchronous request. This will cause a + // deadlock. Just bail on the request. + if (isSynchronous()) { + mRequestHandle.handleSslErrorResponse(false); + return true; + } sendMessageInternal(obtainMessage(MSG_SSL_ERROR, error)); // if it has been canceled, return false so that the network thread // won't be blocked. If it is not canceled, save the mRequestHandle @@ -773,7 +767,7 @@ class LoadListener extends Handler implements EventHandler { * are null, cancel the request. */ void handleAuthResponse(String username, String password) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.handleAuthResponse: url: " + mUrl + " username: " + username + " password: " + password); @@ -823,14 +817,12 @@ class LoadListener extends Handler implements EventHandler { * @param method * @param headers * @param postData - * @param isHighPriority */ void setRequestData(String method, Map<String, String> headers, - byte[] postData, boolean isHighPriority) { + byte[] postData) { mMethod = method; mRequestHeaders = headers; mPostData = postData; - mIsHighPriority = isHighPriority; } /** @@ -870,7 +862,7 @@ class LoadListener extends Handler implements EventHandler { } void attachRequestHandle(RequestHandle requestHandle) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.attachRequestHandle(): " + "requestHandle: " + requestHandle); } @@ -878,7 +870,7 @@ class LoadListener extends Handler implements EventHandler { } void detachRequestHandle() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.detachRequestHandle(): " + "requestHandle: " + mRequestHandle); } @@ -917,7 +909,7 @@ class LoadListener extends Handler implements EventHandler { */ static boolean willLoadFromCache(String url) { boolean inCache = CacheManager.getCacheFile(url, null) != null; - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "willLoadFromCache: " + url + " in cache: " + inCache); } @@ -938,6 +930,10 @@ class LoadListener extends Handler implements EventHandler { return mMimeType; } + String transferEncoding() { + return mTransferEncoding; + } + /* * Return the size of the content being downloaded. This represents the * full content size, even under the situation where the download has been @@ -965,9 +961,9 @@ class LoadListener extends Handler implements EventHandler { // This commits the headers without checking the response status code. private void commitHeaders() { - if (mIsMainPageLoader && sCertificateMimeTypeMap.contains(mMimeType)) { + if (mIsMainPageLoader && sCertificateTypeMap.containsKey(mMimeType)) { // In the case of downloading certificate, we will save it to the - // Keystore in commitLoad. Do not call webcore. + // KeyStore in commitLoad. Do not call webcore. return; } @@ -992,8 +988,7 @@ class LoadListener extends Handler implements EventHandler { // pass content-type content-length and content-encoding final int nativeResponse = nativeCreateResponse( mUrl, statusCode, mStatusText, - mMimeType, mContentLength, mEncoding, - mCacheResult == null ? 0 : mCacheResult.expires / 1000); + mMimeType, mContentLength, mEncoding); if (mHeaders != null) { mHeaders.getHeaders(new Headers.HeaderCallback() { public void header(String name, String value) { @@ -1011,26 +1006,28 @@ class LoadListener extends Handler implements EventHandler { private void commitLoad() { if (mCancelled) return; - if (mIsMainPageLoader && sCertificateMimeTypeMap.contains(mMimeType)) { - // In the case of downloading certificate, we will save it to the - // Keystore and stop the current loading so that it will not - // generate a new history page - byte[] cert = new byte[mDataBuilder.getByteSize()]; - int position = 0; - ByteArrayBuilder.Chunk c; - while (true) { - c = mDataBuilder.getFirstChunk(); - if (c == null) break; - - if (c.mLength != 0) { - System.arraycopy(c.mArray, 0, cert, position, c.mLength); - position += c.mLength; + if (mIsMainPageLoader) { + String type = sCertificateTypeMap.get(mMimeType); + if (type != null) { + // In the case of downloading certificate, we will save it to + // the KeyStore and stop the current loading so that it will not + // generate a new history page + byte[] cert = new byte[mDataBuilder.getByteSize()]; + int offset = 0; + while (true) { + ByteArrayBuilder.Chunk c = mDataBuilder.getFirstChunk(); + if (c == null) break; + + if (c.mLength != 0) { + System.arraycopy(c.mArray, 0, cert, offset, c.mLength); + offset += c.mLength; + } + mDataBuilder.releaseChunk(c); } - mDataBuilder.releaseChunk(c); + CertTool.addCertificate(mContext, type, cert); + mBrowserFrame.stopLoading(); + return; } - CertTool.getInstance().addCertificate(cert, mContext); - mBrowserFrame.stopLoading(); - return; } // Give the data to WebKit now @@ -1115,7 +1112,7 @@ class LoadListener extends Handler implements EventHandler { * EventHandler's method call. */ public void cancel() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { if (mRequestHandle == null) { Log.v(LOGTAG, "LoadListener.cancel(): no requestHandle"); } else { @@ -1221,7 +1218,7 @@ class LoadListener extends Handler implements EventHandler { // Network.requestURL. Network network = Network.getInstance(getContext()); if (!network.requestURL(mMethod, mRequestHeaders, - mPostData, this, mIsHighPriority)) { + mPostData, this)) { // Signal a bad url error if we could not load the // redirection. handleError(EventHandler.ERROR_BAD_URL, @@ -1247,7 +1244,7 @@ class LoadListener extends Handler implements EventHandler { tearDown(); } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.onRedirect(): redirect to: " + redirectTo); } @@ -1260,8 +1257,8 @@ class LoadListener extends Handler implements EventHandler { private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("^((?:[xX]-)?[a-zA-Z\\*]+/[\\w\\+\\*-]+[\\.[\\w\\+-]+]*)$"); - private void parseContentTypeHeader(String contentType) { - if (WebView.LOGV_ENABLED) { + /* package */ void parseContentTypeHeader(String contentType) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "LoadListener.parseContentTypeHeader: " + "contentType: " + contentType); } @@ -1282,13 +1279,14 @@ class LoadListener extends Handler implements EventHandler { mEncoding = contentType.substring(i + 1); } // Trim excess whitespace. - mEncoding = mEncoding.trim(); + mEncoding = mEncoding.trim().toLowerCase(); if (i < contentType.length() - 1) { // for data: uri the mimeType and encoding have // the form image/jpeg;base64 or text/plain;charset=utf-8 // or text/html;charset=utf-8;base64 - mTransferEncoding = contentType.substring(i + 1).trim(); + mTransferEncoding = + contentType.substring(i + 1).trim().toLowerCase(); } } else { mMimeType = contentType; @@ -1308,6 +1306,8 @@ class LoadListener extends Handler implements EventHandler { guessMimeType(); } } + // Ensure mMimeType is lower case. + mMimeType = mMimeType.toLowerCase(); } /** @@ -1397,7 +1397,8 @@ class LoadListener extends Handler implements EventHandler { */ private boolean ignoreCallbacks() { return (mCancelled || mAuthHeader != null || - (mStatusCode > 300 && mStatusCode < 400)); + // Allow 305 (Use Proxy) to call through. + (mStatusCode > 300 && mStatusCode < 400 && mStatusCode != 305)); } /** @@ -1438,7 +1439,7 @@ class LoadListener extends Handler implements EventHandler { mMimeType = "text/html"; String newMimeType = guessMimeTypeFromExtension(mUrl); if (newMimeType != null) { - mMimeType = newMimeType; + mMimeType = newMimeType; } } } @@ -1448,23 +1449,12 @@ class LoadListener extends Handler implements EventHandler { */ private String guessMimeTypeFromExtension(String url) { // PENDING: need to normalize url - if (WebView.LOGV_ENABLED) { + if (DebugFlags.LOAD_LISTENER) { Log.v(LOGTAG, "guessMimeTypeFromExtension: url = " + url); } - String mimeType = - MimeTypeMap.getSingleton().getMimeTypeFromExtension( - MimeTypeMap.getFileExtensionFromUrl(url)); - - if (mimeType != null) { - // XXX: Until the servers send us either correct xhtml or - // text/html, treat application/xhtml+xml as text/html. - if (mimeType.equals("application/xhtml+xml")) { - mimeType = "text/html"; - } - } - - return mimeType; + return MimeTypeMap.getSingleton().getMimeTypeFromExtension( + MimeTypeMap.getFileExtensionFromUrl(url)); } /** @@ -1483,7 +1473,7 @@ class LoadListener extends Handler implements EventHandler { * Cycle through our messages for synchronous loads. */ /* package */ void loadSynchronousMessages() { - if (WebView.DEBUG && !mSynchronous) { + if (DebugFlags.LOAD_LISTENER && !mSynchronous) { throw new AssertionError(); } // Note: this can be called twice if it is a synchronous network load, @@ -1510,12 +1500,11 @@ class LoadListener extends Handler implements EventHandler { * @param expectedLength An estimate of the content length or the length * given by the server. * @param encoding HTTP encoding. - * @param expireTime HTTP expires converted to seconds since the epoch. * @return The native response pointer. */ private native int nativeCreateResponse(String url, int statusCode, String statusText, String mimeType, long expectedLength, - String encoding, long expireTime); + String encoding); /** * Add a response header to the native object. diff --git a/core/java/android/webkit/MimeTypeMap.java b/core/java/android/webkit/MimeTypeMap.java index 9fdde61..fffba1b 100644 --- a/core/java/android/webkit/MimeTypeMap.java +++ b/core/java/android/webkit/MimeTypeMap.java @@ -22,7 +22,7 @@ import java.util.regex.Pattern; /** * Two-way map that maps MIME-types to file extensions and vice versa. */ -public /* package */ class MimeTypeMap { +public class MimeTypeMap { /** * Singleton MIME-type map instance: @@ -39,7 +39,6 @@ public /* package */ class MimeTypeMap { */ private HashMap<String, String> mExtensionToMimeTypeMap; - /** * Creates a new MIME-type map. */ @@ -50,7 +49,10 @@ public /* package */ class MimeTypeMap { /** * Returns the file extension or an empty string iff there is no - * extension. + * extension. This method is a convenience method for obtaining the + * extension of a url and has undefined results for other Strings. + * @param url + * @return The file extension of the given url. */ public static String getFileExtensionFromUrl(String url) { if (url != null && url.length() > 0) { @@ -80,8 +82,7 @@ public /* package */ class MimeTypeMap { * Load an entry into the map. This does not check if the item already * exists, it trusts the caller! */ - private void loadEntry(String mimeType, String extension, - boolean textType) { + private void loadEntry(String mimeType, String extension) { // // if we have an existing x --> y mapping, we do not want to // override it with another mapping x --> ? @@ -94,18 +95,12 @@ public /* package */ class MimeTypeMap { mMimeTypeToExtensionMap.put(mimeType, extension); } - // - // here, we don't want to map extensions to text MIME types; - // otherwise, we will start replacing generic text/plain and - // text/html with text MIME types that our platform does not - // understand. - // - if (!textType) { - mExtensionToMimeTypeMap.put(extension, mimeType); - } + mExtensionToMimeTypeMap.put(extension, mimeType); } /** + * Return true if the given MIME type has an entry in the map. + * @param mimeType A MIME type (i.e. text/plain) * @return True iff there is a mimeType entry in the map. */ public boolean hasMimeType(String mimeType) { @@ -117,7 +112,9 @@ public /* package */ class MimeTypeMap { } /** - * @return The extension for the MIME type or null iff there is none. + * Return the MIME type for the given extension. + * @param extension A file extension without the leading '.' + * @return The MIME type for the given extension or null iff there is none. */ public String getMimeTypeFromExtension(String extension) { if (extension != null && extension.length() > 0) { @@ -128,18 +125,23 @@ public /* package */ class MimeTypeMap { } /** + * Return true if the given extension has a registered MIME type. + * @param extension A file extension without the leading '.' * @return True iff there is an extension entry in the map. */ public boolean hasExtension(String extension) { if (extension != null && extension.length() > 0) { return mExtensionToMimeTypeMap.containsKey(extension); } - return false; } /** - * @return The MIME type for the extension or null iff there is none. + * Return the registered extension for the given MIME type. Note that some + * MIME types map to multiple extensions. This call will return the most + * common extension for the given MIME type. + * @param mimeType A MIME type (i.e. text/plain) + * @return The extension for the given MIME type or null iff there is none. */ public String getExtensionFromMimeType(String mimeType) { if (mimeType != null && mimeType.length() > 0) { @@ -150,6 +152,7 @@ public /* package */ class MimeTypeMap { } /** + * Get the singleton instance of MimeTypeMap. * @return The singleton instance of the MIME-type map. */ public static MimeTypeMap getSingleton() { @@ -164,341 +167,341 @@ public /* package */ class MimeTypeMap { // mail.google.com/a/google.com // // and "active" MIME types (due to potential security issues). - // - // Also, notice that not all data from this table is actually - // added (see loadEntry method for more details). - sMimeTypeMap.loadEntry("application/andrew-inset", "ez", false); - sMimeTypeMap.loadEntry("application/dsptype", "tsp", false); - sMimeTypeMap.loadEntry("application/futuresplash", "spl", false); - sMimeTypeMap.loadEntry("application/hta", "hta", false); - sMimeTypeMap.loadEntry("application/mac-binhex40", "hqx", false); - sMimeTypeMap.loadEntry("application/mac-compactpro", "cpt", false); - sMimeTypeMap.loadEntry("application/mathematica", "nb", false); - sMimeTypeMap.loadEntry("application/msaccess", "mdb", false); - sMimeTypeMap.loadEntry("application/oda", "oda", false); - sMimeTypeMap.loadEntry("application/ogg", "ogg", false); - sMimeTypeMap.loadEntry("application/pdf", "pdf", false); - sMimeTypeMap.loadEntry("application/pgp-keys", "key", false); - sMimeTypeMap.loadEntry("application/pgp-signature", "pgp", false); - sMimeTypeMap.loadEntry("application/pics-rules", "prf", false); - sMimeTypeMap.loadEntry("application/rar", "rar", false); - sMimeTypeMap.loadEntry("application/rdf+xml", "rdf", false); - sMimeTypeMap.loadEntry("application/rss+xml", "rss", false); - sMimeTypeMap.loadEntry("application/zip", "zip", false); + sMimeTypeMap.loadEntry("application/andrew-inset", "ez"); + sMimeTypeMap.loadEntry("application/dsptype", "tsp"); + sMimeTypeMap.loadEntry("application/futuresplash", "spl"); + sMimeTypeMap.loadEntry("application/hta", "hta"); + sMimeTypeMap.loadEntry("application/mac-binhex40", "hqx"); + sMimeTypeMap.loadEntry("application/mac-compactpro", "cpt"); + sMimeTypeMap.loadEntry("application/mathematica", "nb"); + sMimeTypeMap.loadEntry("application/msaccess", "mdb"); + sMimeTypeMap.loadEntry("application/oda", "oda"); + sMimeTypeMap.loadEntry("application/ogg", "ogg"); + sMimeTypeMap.loadEntry("application/pdf", "pdf"); + sMimeTypeMap.loadEntry("application/pgp-keys", "key"); + sMimeTypeMap.loadEntry("application/pgp-signature", "pgp"); + sMimeTypeMap.loadEntry("application/pics-rules", "prf"); + sMimeTypeMap.loadEntry("application/rar", "rar"); + sMimeTypeMap.loadEntry("application/rdf+xml", "rdf"); + sMimeTypeMap.loadEntry("application/rss+xml", "rss"); + sMimeTypeMap.loadEntry("application/zip", "zip"); sMimeTypeMap.loadEntry("application/vnd.android.package-archive", - "apk", false); - sMimeTypeMap.loadEntry("application/vnd.cinderella", "cdy", false); - sMimeTypeMap.loadEntry("application/vnd.ms-pki.stl", "stl", false); + "apk"); + sMimeTypeMap.loadEntry("application/vnd.cinderella", "cdy"); + sMimeTypeMap.loadEntry("application/vnd.ms-pki.stl", "stl"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.database", "odb", - false); + "application/vnd.oasis.opendocument.database", "odb"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.formula", "odf", - false); + "application/vnd.oasis.opendocument.formula", "odf"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.graphics", "odg", - false); + "application/vnd.oasis.opendocument.graphics", "odg"); sMimeTypeMap.loadEntry( "application/vnd.oasis.opendocument.graphics-template", - "otg", false); + "otg"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.image", "odi", false); + "application/vnd.oasis.opendocument.image", "odi"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.spreadsheet", "ods", - false); + "application/vnd.oasis.opendocument.spreadsheet", "ods"); sMimeTypeMap.loadEntry( "application/vnd.oasis.opendocument.spreadsheet-template", - "ots", false); + "ots"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text", "odt"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-master", "odm"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-template", "ott"); + sMimeTypeMap.loadEntry( + "application/vnd.oasis.opendocument.text-web", "oth"); + sMimeTypeMap.loadEntry("application/msword", "doc"); + sMimeTypeMap.loadEntry("application/msword", "dot"); + sMimeTypeMap.loadEntry( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "docx"); + sMimeTypeMap.loadEntry( + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "dotx"); + sMimeTypeMap.loadEntry("application/vnd.ms-excel", "xls"); + sMimeTypeMap.loadEntry("application/vnd.ms-excel", "xlt"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text", "odt", false); + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "xlsx"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text-master", "odm", - false); + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "xltx"); + sMimeTypeMap.loadEntry("application/vnd.ms-powerpoint", "ppt"); + sMimeTypeMap.loadEntry("application/vnd.ms-powerpoint", "pot"); + sMimeTypeMap.loadEntry("application/vnd.ms-powerpoint", "pps"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text-template", "ott", - false); + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "pptx"); sMimeTypeMap.loadEntry( - "application/vnd.oasis.opendocument.text-web", "oth", - false); - sMimeTypeMap.loadEntry("application/vnd.rim.cod", "cod", false); - sMimeTypeMap.loadEntry("application/vnd.smaf", "mmf", false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.calc", "sdc", - false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.draw", "sda", - false); + "application/vnd.openxmlformats-officedocument.presentationml.template", + "potx"); sMimeTypeMap.loadEntry( - "application/vnd.stardivision.impress", "sdd", false); + "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "ppsx"); + sMimeTypeMap.loadEntry("application/vnd.rim.cod", "cod"); + sMimeTypeMap.loadEntry("application/vnd.smaf", "mmf"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.calc", "sdc"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.draw", "sda"); sMimeTypeMap.loadEntry( - "application/vnd.stardivision.impress", "sdp", false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.math", "smf", - false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "sdw", - false); - sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", "vor", - false); + "application/vnd.stardivision.impress", "sdd"); sMimeTypeMap.loadEntry( - "application/vnd.stardivision.writer-global", "sgl", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.calc", "sxc", - false); + "application/vnd.stardivision.impress", "sdp"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.math", "smf"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", + "sdw"); + sMimeTypeMap.loadEntry("application/vnd.stardivision.writer", + "vor"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.calc.template", "stc", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.draw", "sxd", - false); + "application/vnd.stardivision.writer-global", "sgl"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.calc", "sxc"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.draw.template", "std", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.impress", "sxi", - false); + "application/vnd.sun.xml.calc.template", "stc"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.draw", "sxd"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.impress.template", "sti", false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.math", "sxm", - false); - sMimeTypeMap.loadEntry("application/vnd.sun.xml.writer", "sxw", - false); + "application/vnd.sun.xml.draw.template", "std"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.impress", "sxi"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.writer.global", "sxg", false); + "application/vnd.sun.xml.impress.template", "sti"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.math", "sxm"); + sMimeTypeMap.loadEntry("application/vnd.sun.xml.writer", "sxw"); sMimeTypeMap.loadEntry( - "application/vnd.sun.xml.writer.template", "stw", false); - sMimeTypeMap.loadEntry("application/vnd.visio", "vsd", false); - sMimeTypeMap.loadEntry("application/x-abiword", "abw", false); - sMimeTypeMap.loadEntry("application/x-apple-diskimage", "dmg", - false); - sMimeTypeMap.loadEntry("application/x-bcpio", "bcpio", false); - sMimeTypeMap.loadEntry("application/x-bittorrent", "torrent", - false); - sMimeTypeMap.loadEntry("application/x-cdf", "cdf", false); - sMimeTypeMap.loadEntry("application/x-cdlink", "vcd", false); - sMimeTypeMap.loadEntry("application/x-chess-pgn", "pgn", false); - sMimeTypeMap.loadEntry("application/x-cpio", "cpio", false); - sMimeTypeMap.loadEntry("application/x-debian-package", "deb", - false); - sMimeTypeMap.loadEntry("application/x-debian-package", "udeb", - false); - sMimeTypeMap.loadEntry("application/x-director", "dcr", false); - sMimeTypeMap.loadEntry("application/x-director", "dir", false); - sMimeTypeMap.loadEntry("application/x-director", "dxr", false); - sMimeTypeMap.loadEntry("application/x-dms", "dms", false); - sMimeTypeMap.loadEntry("application/x-doom", "wad", false); - sMimeTypeMap.loadEntry("application/x-dvi", "dvi", false); - sMimeTypeMap.loadEntry("application/x-flac", "flac", false); - sMimeTypeMap.loadEntry("application/x-font", "pfa", false); - sMimeTypeMap.loadEntry("application/x-font", "pfb", false); - sMimeTypeMap.loadEntry("application/x-font", "gsf", false); - sMimeTypeMap.loadEntry("application/x-font", "pcf", false); - sMimeTypeMap.loadEntry("application/x-font", "pcf.Z", false); - sMimeTypeMap.loadEntry("application/x-freemind", "mm", false); - sMimeTypeMap.loadEntry("application/x-futuresplash", "spl", false); - sMimeTypeMap.loadEntry("application/x-gnumeric", "gnumeric", false); - sMimeTypeMap.loadEntry("application/x-go-sgf", "sgf", false); - sMimeTypeMap.loadEntry("application/x-graphing-calculator", "gcf", - false); - sMimeTypeMap.loadEntry("application/x-gtar", "gtar", false); - sMimeTypeMap.loadEntry("application/x-gtar", "tgz", false); - sMimeTypeMap.loadEntry("application/x-gtar", "taz", false); - sMimeTypeMap.loadEntry("application/x-hdf", "hdf", false); - sMimeTypeMap.loadEntry("application/x-ica", "ica", false); - sMimeTypeMap.loadEntry("application/x-internet-signup", "ins", - false); - sMimeTypeMap.loadEntry("application/x-internet-signup", "isp", - false); - sMimeTypeMap.loadEntry("application/x-iphone", "iii", false); - sMimeTypeMap.loadEntry("application/x-iso9660-image", "iso", false); - sMimeTypeMap.loadEntry("application/x-jmol", "jmz", false); - sMimeTypeMap.loadEntry("application/x-kchart", "chrt", false); - sMimeTypeMap.loadEntry("application/x-killustrator", "kil", false); - sMimeTypeMap.loadEntry("application/x-koan", "skp", false); - sMimeTypeMap.loadEntry("application/x-koan", "skd", false); - sMimeTypeMap.loadEntry("application/x-koan", "skt", false); - sMimeTypeMap.loadEntry("application/x-koan", "skm", false); - sMimeTypeMap.loadEntry("application/x-kpresenter", "kpr", false); - sMimeTypeMap.loadEntry("application/x-kpresenter", "kpt", false); - sMimeTypeMap.loadEntry("application/x-kspread", "ksp", false); - sMimeTypeMap.loadEntry("application/x-kword", "kwd", false); - sMimeTypeMap.loadEntry("application/x-kword", "kwt", false); - sMimeTypeMap.loadEntry("application/x-latex", "latex", false); - sMimeTypeMap.loadEntry("application/x-lha", "lha", false); - sMimeTypeMap.loadEntry("application/x-lzh", "lzh", false); - sMimeTypeMap.loadEntry("application/x-lzx", "lzx", false); - sMimeTypeMap.loadEntry("application/x-maker", "frm", false); - sMimeTypeMap.loadEntry("application/x-maker", "maker", false); - sMimeTypeMap.loadEntry("application/x-maker", "frame", false); - sMimeTypeMap.loadEntry("application/x-maker", "fb", false); - sMimeTypeMap.loadEntry("application/x-maker", "book", false); - sMimeTypeMap.loadEntry("application/x-maker", "fbdoc", false); - sMimeTypeMap.loadEntry("application/x-mif", "mif", false); - sMimeTypeMap.loadEntry("application/x-ms-wmd", "wmd", false); - sMimeTypeMap.loadEntry("application/x-ms-wmz", "wmz", false); - sMimeTypeMap.loadEntry("application/x-msi", "msi", false); - sMimeTypeMap.loadEntry("application/x-ns-proxy-autoconfig", "pac", - false); - sMimeTypeMap.loadEntry("application/x-nwc", "nwc", false); - sMimeTypeMap.loadEntry("application/x-object", "o", false); - sMimeTypeMap.loadEntry("application/x-oz-application", "oza", - false); - sMimeTypeMap.loadEntry("application/x-pkcs12", "p12", false); - sMimeTypeMap.loadEntry("application/x-pkcs7-certreqresp", "p7r", - false); - sMimeTypeMap.loadEntry("application/x-pkcs7-crl", "crl", false); - sMimeTypeMap.loadEntry("application/x-quicktimeplayer", "qtl", - false); - sMimeTypeMap.loadEntry("application/x-shar", "shar", false); - sMimeTypeMap.loadEntry("application/x-stuffit", "sit", false); - sMimeTypeMap.loadEntry("application/x-sv4cpio", "sv4cpio", false); - sMimeTypeMap.loadEntry("application/x-sv4crc", "sv4crc", false); - sMimeTypeMap.loadEntry("application/x-tar", "tar", false); - sMimeTypeMap.loadEntry("application/x-texinfo", "texinfo", false); - sMimeTypeMap.loadEntry("application/x-texinfo", "texi", false); - sMimeTypeMap.loadEntry("application/x-troff", "t", false); - sMimeTypeMap.loadEntry("application/x-troff", "roff", false); - sMimeTypeMap.loadEntry("application/x-troff-man", "man", false); - sMimeTypeMap.loadEntry("application/x-ustar", "ustar", false); - sMimeTypeMap.loadEntry("application/x-wais-source", "src", false); - sMimeTypeMap.loadEntry("application/x-wingz", "wz", false); + "application/vnd.sun.xml.writer.global", "sxg"); sMimeTypeMap.loadEntry( - "application/x-webarchive", "webarchive", false); // added - sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt", false); - sMimeTypeMap.loadEntry("application/x-x509-user-cert", "crt", false); - sMimeTypeMap.loadEntry("application/x-xcf", "xcf", false); - sMimeTypeMap.loadEntry("application/x-xfig", "fig", false); - sMimeTypeMap.loadEntry("audio/basic", "snd", false); - sMimeTypeMap.loadEntry("audio/midi", "mid", false); - sMimeTypeMap.loadEntry("audio/midi", "midi", false); - sMimeTypeMap.loadEntry("audio/midi", "kar", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mpga", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mpega", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mp2", false); - sMimeTypeMap.loadEntry("audio/mpeg", "mp3", false); - sMimeTypeMap.loadEntry("audio/mpeg", "m4a", false); - sMimeTypeMap.loadEntry("audio/mpegurl", "m3u", false); - sMimeTypeMap.loadEntry("audio/prs.sid", "sid", false); - sMimeTypeMap.loadEntry("audio/x-aiff", "aif", false); - sMimeTypeMap.loadEntry("audio/x-aiff", "aiff", false); - sMimeTypeMap.loadEntry("audio/x-aiff", "aifc", false); - sMimeTypeMap.loadEntry("audio/x-gsm", "gsm", false); - sMimeTypeMap.loadEntry("audio/x-mpegurl", "m3u", false); - sMimeTypeMap.loadEntry("audio/x-ms-wma", "wma", false); - sMimeTypeMap.loadEntry("audio/x-ms-wax", "wax", false); - sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ra", false); - sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "rm", false); - sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ram", false); - sMimeTypeMap.loadEntry("audio/x-realaudio", "ra", false); - sMimeTypeMap.loadEntry("audio/x-scpls", "pls", false); - sMimeTypeMap.loadEntry("audio/x-sd2", "sd2", false); - sMimeTypeMap.loadEntry("audio/x-wav", "wav", false); - sMimeTypeMap.loadEntry("image/bmp", "bmp", false); // added - sMimeTypeMap.loadEntry("image/gif", "gif", false); - sMimeTypeMap.loadEntry("image/ico", "cur", false); // added - sMimeTypeMap.loadEntry("image/ico", "ico", false); // added - sMimeTypeMap.loadEntry("image/ief", "ief", false); - sMimeTypeMap.loadEntry("image/jpeg", "jpeg", false); - sMimeTypeMap.loadEntry("image/jpeg", "jpg", false); - sMimeTypeMap.loadEntry("image/jpeg", "jpe", false); - sMimeTypeMap.loadEntry("image/pcx", "pcx", false); - sMimeTypeMap.loadEntry("image/png", "png", false); - sMimeTypeMap.loadEntry("image/svg+xml", "svg", false); - sMimeTypeMap.loadEntry("image/svg+xml", "svgz", false); - sMimeTypeMap.loadEntry("image/tiff", "tiff", false); - sMimeTypeMap.loadEntry("image/tiff", "tif", false); - sMimeTypeMap.loadEntry("image/vnd.djvu", "djvu", false); - sMimeTypeMap.loadEntry("image/vnd.djvu", "djv", false); - sMimeTypeMap.loadEntry("image/vnd.wap.wbmp", "wbmp", false); - sMimeTypeMap.loadEntry("image/x-cmu-raster", "ras", false); - sMimeTypeMap.loadEntry("image/x-coreldraw", "cdr", false); - sMimeTypeMap.loadEntry("image/x-coreldrawpattern", "pat", false); - sMimeTypeMap.loadEntry("image/x-coreldrawtemplate", "cdt", false); - sMimeTypeMap.loadEntry("image/x-corelphotopaint", "cpt", false); - sMimeTypeMap.loadEntry("image/x-icon", "ico", false); - sMimeTypeMap.loadEntry("image/x-jg", "art", false); - sMimeTypeMap.loadEntry("image/x-jng", "jng", false); - sMimeTypeMap.loadEntry("image/x-ms-bmp", "bmp", false); - sMimeTypeMap.loadEntry("image/x-photoshop", "psd", false); - sMimeTypeMap.loadEntry("image/x-portable-anymap", "pnm", false); - sMimeTypeMap.loadEntry("image/x-portable-bitmap", "pbm", false); - sMimeTypeMap.loadEntry("image/x-portable-graymap", "pgm", false); - sMimeTypeMap.loadEntry("image/x-portable-pixmap", "ppm", false); - sMimeTypeMap.loadEntry("image/x-rgb", "rgb", false); - sMimeTypeMap.loadEntry("image/x-xbitmap", "xbm", false); - sMimeTypeMap.loadEntry("image/x-xpixmap", "xpm", false); - sMimeTypeMap.loadEntry("image/x-xwindowdump", "xwd", false); - sMimeTypeMap.loadEntry("model/iges", "igs", false); - sMimeTypeMap.loadEntry("model/iges", "iges", false); - sMimeTypeMap.loadEntry("model/mesh", "msh", false); - sMimeTypeMap.loadEntry("model/mesh", "mesh", false); - sMimeTypeMap.loadEntry("model/mesh", "silo", false); - sMimeTypeMap.loadEntry("text/calendar", "ics", true); - sMimeTypeMap.loadEntry("text/calendar", "icz", true); - sMimeTypeMap.loadEntry("text/comma-separated-values", "csv", true); - sMimeTypeMap.loadEntry("text/css", "css", true); - sMimeTypeMap.loadEntry("text/h323", "323", true); - sMimeTypeMap.loadEntry("text/iuls", "uls", true); - sMimeTypeMap.loadEntry("text/mathml", "mml", true); + "application/vnd.sun.xml.writer.template", "stw"); + sMimeTypeMap.loadEntry("application/vnd.visio", "vsd"); + sMimeTypeMap.loadEntry("application/x-abiword", "abw"); + sMimeTypeMap.loadEntry("application/x-apple-diskimage", "dmg"); + sMimeTypeMap.loadEntry("application/x-bcpio", "bcpio"); + sMimeTypeMap.loadEntry("application/x-bittorrent", "torrent"); + sMimeTypeMap.loadEntry("application/x-cdf", "cdf"); + sMimeTypeMap.loadEntry("application/x-cdlink", "vcd"); + sMimeTypeMap.loadEntry("application/x-chess-pgn", "pgn"); + sMimeTypeMap.loadEntry("application/x-cpio", "cpio"); + sMimeTypeMap.loadEntry("application/x-debian-package", "deb"); + sMimeTypeMap.loadEntry("application/x-debian-package", "udeb"); + sMimeTypeMap.loadEntry("application/x-director", "dcr"); + sMimeTypeMap.loadEntry("application/x-director", "dir"); + sMimeTypeMap.loadEntry("application/x-director", "dxr"); + sMimeTypeMap.loadEntry("application/x-dms", "dms"); + sMimeTypeMap.loadEntry("application/x-doom", "wad"); + sMimeTypeMap.loadEntry("application/x-dvi", "dvi"); + sMimeTypeMap.loadEntry("application/x-flac", "flac"); + sMimeTypeMap.loadEntry("application/x-font", "pfa"); + sMimeTypeMap.loadEntry("application/x-font", "pfb"); + sMimeTypeMap.loadEntry("application/x-font", "gsf"); + sMimeTypeMap.loadEntry("application/x-font", "pcf"); + sMimeTypeMap.loadEntry("application/x-font", "pcf.Z"); + sMimeTypeMap.loadEntry("application/x-freemind", "mm"); + sMimeTypeMap.loadEntry("application/x-futuresplash", "spl"); + sMimeTypeMap.loadEntry("application/x-gnumeric", "gnumeric"); + sMimeTypeMap.loadEntry("application/x-go-sgf", "sgf"); + sMimeTypeMap.loadEntry("application/x-graphing-calculator", "gcf"); + sMimeTypeMap.loadEntry("application/x-gtar", "gtar"); + sMimeTypeMap.loadEntry("application/x-gtar", "tgz"); + sMimeTypeMap.loadEntry("application/x-gtar", "taz"); + sMimeTypeMap.loadEntry("application/x-hdf", "hdf"); + sMimeTypeMap.loadEntry("application/x-ica", "ica"); + sMimeTypeMap.loadEntry("application/x-internet-signup", "ins"); + sMimeTypeMap.loadEntry("application/x-internet-signup", "isp"); + sMimeTypeMap.loadEntry("application/x-iphone", "iii"); + sMimeTypeMap.loadEntry("application/x-iso9660-image", "iso"); + sMimeTypeMap.loadEntry("application/x-jmol", "jmz"); + sMimeTypeMap.loadEntry("application/x-kchart", "chrt"); + sMimeTypeMap.loadEntry("application/x-killustrator", "kil"); + sMimeTypeMap.loadEntry("application/x-koan", "skp"); + sMimeTypeMap.loadEntry("application/x-koan", "skd"); + sMimeTypeMap.loadEntry("application/x-koan", "skt"); + sMimeTypeMap.loadEntry("application/x-koan", "skm"); + sMimeTypeMap.loadEntry("application/x-kpresenter", "kpr"); + sMimeTypeMap.loadEntry("application/x-kpresenter", "kpt"); + sMimeTypeMap.loadEntry("application/x-kspread", "ksp"); + sMimeTypeMap.loadEntry("application/x-kword", "kwd"); + sMimeTypeMap.loadEntry("application/x-kword", "kwt"); + sMimeTypeMap.loadEntry("application/x-latex", "latex"); + sMimeTypeMap.loadEntry("application/x-lha", "lha"); + sMimeTypeMap.loadEntry("application/x-lzh", "lzh"); + sMimeTypeMap.loadEntry("application/x-lzx", "lzx"); + sMimeTypeMap.loadEntry("application/x-maker", "frm"); + sMimeTypeMap.loadEntry("application/x-maker", "maker"); + sMimeTypeMap.loadEntry("application/x-maker", "frame"); + sMimeTypeMap.loadEntry("application/x-maker", "fb"); + sMimeTypeMap.loadEntry("application/x-maker", "book"); + sMimeTypeMap.loadEntry("application/x-maker", "fbdoc"); + sMimeTypeMap.loadEntry("application/x-mif", "mif"); + sMimeTypeMap.loadEntry("application/x-ms-wmd", "wmd"); + sMimeTypeMap.loadEntry("application/x-ms-wmz", "wmz"); + sMimeTypeMap.loadEntry("application/x-msi", "msi"); + sMimeTypeMap.loadEntry("application/x-ns-proxy-autoconfig", "pac"); + sMimeTypeMap.loadEntry("application/x-nwc", "nwc"); + sMimeTypeMap.loadEntry("application/x-object", "o"); + sMimeTypeMap.loadEntry("application/x-oz-application", "oza"); + sMimeTypeMap.loadEntry("application/x-pkcs12", "p12"); + sMimeTypeMap.loadEntry("application/x-pkcs7-certreqresp", "p7r"); + sMimeTypeMap.loadEntry("application/x-pkcs7-crl", "crl"); + sMimeTypeMap.loadEntry("application/x-quicktimeplayer", "qtl"); + sMimeTypeMap.loadEntry("application/x-shar", "shar"); + sMimeTypeMap.loadEntry("application/x-stuffit", "sit"); + sMimeTypeMap.loadEntry("application/x-sv4cpio", "sv4cpio"); + sMimeTypeMap.loadEntry("application/x-sv4crc", "sv4crc"); + sMimeTypeMap.loadEntry("application/x-tar", "tar"); + sMimeTypeMap.loadEntry("application/x-texinfo", "texinfo"); + sMimeTypeMap.loadEntry("application/x-texinfo", "texi"); + sMimeTypeMap.loadEntry("application/x-troff", "t"); + sMimeTypeMap.loadEntry("application/x-troff", "roff"); + sMimeTypeMap.loadEntry("application/x-troff-man", "man"); + sMimeTypeMap.loadEntry("application/x-ustar", "ustar"); + sMimeTypeMap.loadEntry("application/x-wais-source", "src"); + sMimeTypeMap.loadEntry("application/x-wingz", "wz"); + sMimeTypeMap.loadEntry("application/x-webarchive", "webarchive"); + sMimeTypeMap.loadEntry("application/x-x509-ca-cert", "crt"); + sMimeTypeMap.loadEntry("application/x-x509-user-cert", "crt"); + sMimeTypeMap.loadEntry("application/x-xcf", "xcf"); + sMimeTypeMap.loadEntry("application/x-xfig", "fig"); + sMimeTypeMap.loadEntry("application/xhtml+xml", "xhtml"); + sMimeTypeMap.loadEntry("audio/basic", "snd"); + sMimeTypeMap.loadEntry("audio/midi", "mid"); + sMimeTypeMap.loadEntry("audio/midi", "midi"); + sMimeTypeMap.loadEntry("audio/midi", "kar"); + sMimeTypeMap.loadEntry("audio/mpeg", "mpga"); + sMimeTypeMap.loadEntry("audio/mpeg", "mpega"); + sMimeTypeMap.loadEntry("audio/mpeg", "mp2"); + sMimeTypeMap.loadEntry("audio/mpeg", "mp3"); + sMimeTypeMap.loadEntry("audio/mpeg", "m4a"); + sMimeTypeMap.loadEntry("audio/mpegurl", "m3u"); + sMimeTypeMap.loadEntry("audio/prs.sid", "sid"); + sMimeTypeMap.loadEntry("audio/x-aiff", "aif"); + sMimeTypeMap.loadEntry("audio/x-aiff", "aiff"); + sMimeTypeMap.loadEntry("audio/x-aiff", "aifc"); + sMimeTypeMap.loadEntry("audio/x-gsm", "gsm"); + sMimeTypeMap.loadEntry("audio/x-mpegurl", "m3u"); + sMimeTypeMap.loadEntry("audio/x-ms-wma", "wma"); + sMimeTypeMap.loadEntry("audio/x-ms-wax", "wax"); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ra"); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "rm"); + sMimeTypeMap.loadEntry("audio/x-pn-realaudio", "ram"); + sMimeTypeMap.loadEntry("audio/x-realaudio", "ra"); + sMimeTypeMap.loadEntry("audio/x-scpls", "pls"); + sMimeTypeMap.loadEntry("audio/x-sd2", "sd2"); + sMimeTypeMap.loadEntry("audio/x-wav", "wav"); + sMimeTypeMap.loadEntry("image/bmp", "bmp"); + sMimeTypeMap.loadEntry("image/gif", "gif"); + sMimeTypeMap.loadEntry("image/ico", "cur"); + sMimeTypeMap.loadEntry("image/ico", "ico"); + sMimeTypeMap.loadEntry("image/ief", "ief"); + sMimeTypeMap.loadEntry("image/jpeg", "jpeg"); + sMimeTypeMap.loadEntry("image/jpeg", "jpg"); + sMimeTypeMap.loadEntry("image/jpeg", "jpe"); + sMimeTypeMap.loadEntry("image/pcx", "pcx"); + sMimeTypeMap.loadEntry("image/png", "png"); + sMimeTypeMap.loadEntry("image/svg+xml", "svg"); + sMimeTypeMap.loadEntry("image/svg+xml", "svgz"); + sMimeTypeMap.loadEntry("image/tiff", "tiff"); + sMimeTypeMap.loadEntry("image/tiff", "tif"); + sMimeTypeMap.loadEntry("image/vnd.djvu", "djvu"); + sMimeTypeMap.loadEntry("image/vnd.djvu", "djv"); + sMimeTypeMap.loadEntry("image/vnd.wap.wbmp", "wbmp"); + sMimeTypeMap.loadEntry("image/x-cmu-raster", "ras"); + sMimeTypeMap.loadEntry("image/x-coreldraw", "cdr"); + sMimeTypeMap.loadEntry("image/x-coreldrawpattern", "pat"); + sMimeTypeMap.loadEntry("image/x-coreldrawtemplate", "cdt"); + sMimeTypeMap.loadEntry("image/x-corelphotopaint", "cpt"); + sMimeTypeMap.loadEntry("image/x-icon", "ico"); + sMimeTypeMap.loadEntry("image/x-jg", "art"); + sMimeTypeMap.loadEntry("image/x-jng", "jng"); + sMimeTypeMap.loadEntry("image/x-ms-bmp", "bmp"); + sMimeTypeMap.loadEntry("image/x-photoshop", "psd"); + sMimeTypeMap.loadEntry("image/x-portable-anymap", "pnm"); + sMimeTypeMap.loadEntry("image/x-portable-bitmap", "pbm"); + sMimeTypeMap.loadEntry("image/x-portable-graymap", "pgm"); + sMimeTypeMap.loadEntry("image/x-portable-pixmap", "ppm"); + sMimeTypeMap.loadEntry("image/x-rgb", "rgb"); + sMimeTypeMap.loadEntry("image/x-xbitmap", "xbm"); + sMimeTypeMap.loadEntry("image/x-xpixmap", "xpm"); + sMimeTypeMap.loadEntry("image/x-xwindowdump", "xwd"); + sMimeTypeMap.loadEntry("model/iges", "igs"); + sMimeTypeMap.loadEntry("model/iges", "iges"); + sMimeTypeMap.loadEntry("model/mesh", "msh"); + sMimeTypeMap.loadEntry("model/mesh", "mesh"); + sMimeTypeMap.loadEntry("model/mesh", "silo"); + sMimeTypeMap.loadEntry("text/calendar", "ics"); + sMimeTypeMap.loadEntry("text/calendar", "icz"); + sMimeTypeMap.loadEntry("text/comma-separated-values", "csv"); + sMimeTypeMap.loadEntry("text/css", "css"); + sMimeTypeMap.loadEntry("text/h323", "323"); + sMimeTypeMap.loadEntry("text/iuls", "uls"); + sMimeTypeMap.loadEntry("text/mathml", "mml"); // add it first so it will be the default for ExtensionFromMimeType - sMimeTypeMap.loadEntry("text/plain", "txt", true); - sMimeTypeMap.loadEntry("text/plain", "asc", true); - sMimeTypeMap.loadEntry("text/plain", "text", true); - sMimeTypeMap.loadEntry("text/plain", "diff", true); - sMimeTypeMap.loadEntry("text/plain", "pot", true); - sMimeTypeMap.loadEntry("text/richtext", "rtx", true); - sMimeTypeMap.loadEntry("text/rtf", "rtf", true); - sMimeTypeMap.loadEntry("text/texmacs", "ts", true); - sMimeTypeMap.loadEntry("text/text", "phps", true); - sMimeTypeMap.loadEntry("text/tab-separated-values", "tsv", true); - sMimeTypeMap.loadEntry("text/x-bibtex", "bib", true); - sMimeTypeMap.loadEntry("text/x-boo", "boo", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "h++", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "hpp", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "hxx", true); - sMimeTypeMap.loadEntry("text/x-c++hdr", "hh", true); - sMimeTypeMap.loadEntry("text/x-c++src", "c++", true); - sMimeTypeMap.loadEntry("text/x-c++src", "cpp", true); - sMimeTypeMap.loadEntry("text/x-c++src", "cxx", true); - sMimeTypeMap.loadEntry("text/x-chdr", "h", true); - sMimeTypeMap.loadEntry("text/x-component", "htc", true); - sMimeTypeMap.loadEntry("text/x-csh", "csh", true); - sMimeTypeMap.loadEntry("text/x-csrc", "c", true); - sMimeTypeMap.loadEntry("text/x-dsrc", "d", true); - sMimeTypeMap.loadEntry("text/x-haskell", "hs", true); - sMimeTypeMap.loadEntry("text/x-java", "java", true); - sMimeTypeMap.loadEntry("text/x-literate-haskell", "lhs", true); - sMimeTypeMap.loadEntry("text/x-moc", "moc", true); - sMimeTypeMap.loadEntry("text/x-pascal", "p", true); - sMimeTypeMap.loadEntry("text/x-pascal", "pas", true); - sMimeTypeMap.loadEntry("text/x-pcs-gcd", "gcd", true); - sMimeTypeMap.loadEntry("text/x-setext", "etx", true); - sMimeTypeMap.loadEntry("text/x-tcl", "tcl", true); - sMimeTypeMap.loadEntry("text/x-tex", "tex", true); - sMimeTypeMap.loadEntry("text/x-tex", "ltx", true); - sMimeTypeMap.loadEntry("text/x-tex", "sty", true); - sMimeTypeMap.loadEntry("text/x-tex", "cls", true); - sMimeTypeMap.loadEntry("text/x-vcalendar", "vcs", true); - sMimeTypeMap.loadEntry("text/x-vcard", "vcf", true); - sMimeTypeMap.loadEntry("video/3gpp", "3gp", false); - sMimeTypeMap.loadEntry("video/3gpp", "3g2", false); - sMimeTypeMap.loadEntry("video/dl", "dl", false); - sMimeTypeMap.loadEntry("video/dv", "dif", false); - sMimeTypeMap.loadEntry("video/dv", "dv", false); - sMimeTypeMap.loadEntry("video/fli", "fli", false); - sMimeTypeMap.loadEntry("video/mpeg", "mpeg", false); - sMimeTypeMap.loadEntry("video/mpeg", "mpg", false); - sMimeTypeMap.loadEntry("video/mpeg", "mpe", false); - sMimeTypeMap.loadEntry("video/mp4", "mp4", false); - sMimeTypeMap.loadEntry("video/mpeg", "VOB", false); - sMimeTypeMap.loadEntry("video/quicktime", "qt", false); - sMimeTypeMap.loadEntry("video/quicktime", "mov", false); - sMimeTypeMap.loadEntry("video/vnd.mpegurl", "mxu", false); - sMimeTypeMap.loadEntry("video/x-la-asf", "lsf", false); - sMimeTypeMap.loadEntry("video/x-la-asf", "lsx", false); - sMimeTypeMap.loadEntry("video/x-mng", "mng", false); - sMimeTypeMap.loadEntry("video/x-ms-asf", "asf", false); - sMimeTypeMap.loadEntry("video/x-ms-asf", "asx", false); - sMimeTypeMap.loadEntry("video/x-ms-wm", "wm", false); - sMimeTypeMap.loadEntry("video/x-ms-wmv", "wmv", false); - sMimeTypeMap.loadEntry("video/x-ms-wmx", "wmx", false); - sMimeTypeMap.loadEntry("video/x-ms-wvx", "wvx", false); - sMimeTypeMap.loadEntry("video/x-msvideo", "avi", false); - sMimeTypeMap.loadEntry("video/x-sgi-movie", "movie", false); - sMimeTypeMap.loadEntry("x-conference/x-cooltalk", "ice", false); - sMimeTypeMap.loadEntry("x-epoc/x-sisx-app", "sisx", false); + sMimeTypeMap.loadEntry("text/plain", "txt"); + sMimeTypeMap.loadEntry("text/plain", "asc"); + sMimeTypeMap.loadEntry("text/plain", "text"); + sMimeTypeMap.loadEntry("text/plain", "diff"); + sMimeTypeMap.loadEntry("text/plain", "po"); // reserve "pot" for vnd.ms-powerpoint + sMimeTypeMap.loadEntry("text/richtext", "rtx"); + sMimeTypeMap.loadEntry("text/rtf", "rtf"); + sMimeTypeMap.loadEntry("text/texmacs", "ts"); + sMimeTypeMap.loadEntry("text/text", "phps"); + sMimeTypeMap.loadEntry("text/tab-separated-values", "tsv"); + sMimeTypeMap.loadEntry("text/xml", "xml"); + sMimeTypeMap.loadEntry("text/x-bibtex", "bib"); + sMimeTypeMap.loadEntry("text/x-boo", "boo"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "h++"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hpp"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hxx"); + sMimeTypeMap.loadEntry("text/x-c++hdr", "hh"); + sMimeTypeMap.loadEntry("text/x-c++src", "c++"); + sMimeTypeMap.loadEntry("text/x-c++src", "cpp"); + sMimeTypeMap.loadEntry("text/x-c++src", "cxx"); + sMimeTypeMap.loadEntry("text/x-chdr", "h"); + sMimeTypeMap.loadEntry("text/x-component", "htc"); + sMimeTypeMap.loadEntry("text/x-csh", "csh"); + sMimeTypeMap.loadEntry("text/x-csrc", "c"); + sMimeTypeMap.loadEntry("text/x-dsrc", "d"); + sMimeTypeMap.loadEntry("text/x-haskell", "hs"); + sMimeTypeMap.loadEntry("text/x-java", "java"); + sMimeTypeMap.loadEntry("text/x-literate-haskell", "lhs"); + sMimeTypeMap.loadEntry("text/x-moc", "moc"); + sMimeTypeMap.loadEntry("text/x-pascal", "p"); + sMimeTypeMap.loadEntry("text/x-pascal", "pas"); + sMimeTypeMap.loadEntry("text/x-pcs-gcd", "gcd"); + sMimeTypeMap.loadEntry("text/x-setext", "etx"); + sMimeTypeMap.loadEntry("text/x-tcl", "tcl"); + sMimeTypeMap.loadEntry("text/x-tex", "tex"); + sMimeTypeMap.loadEntry("text/x-tex", "ltx"); + sMimeTypeMap.loadEntry("text/x-tex", "sty"); + sMimeTypeMap.loadEntry("text/x-tex", "cls"); + sMimeTypeMap.loadEntry("text/x-vcalendar", "vcs"); + sMimeTypeMap.loadEntry("text/x-vcard", "vcf"); + sMimeTypeMap.loadEntry("video/3gpp", "3gp"); + sMimeTypeMap.loadEntry("video/3gpp", "3g2"); + sMimeTypeMap.loadEntry("video/dl", "dl"); + sMimeTypeMap.loadEntry("video/dv", "dif"); + sMimeTypeMap.loadEntry("video/dv", "dv"); + sMimeTypeMap.loadEntry("video/fli", "fli"); + sMimeTypeMap.loadEntry("video/mpeg", "mpeg"); + sMimeTypeMap.loadEntry("video/mpeg", "mpg"); + sMimeTypeMap.loadEntry("video/mpeg", "mpe"); + sMimeTypeMap.loadEntry("video/mp4", "mp4"); + sMimeTypeMap.loadEntry("video/mpeg", "VOB"); + sMimeTypeMap.loadEntry("video/quicktime", "qt"); + sMimeTypeMap.loadEntry("video/quicktime", "mov"); + sMimeTypeMap.loadEntry("video/vnd.mpegurl", "mxu"); + sMimeTypeMap.loadEntry("video/x-la-asf", "lsf"); + sMimeTypeMap.loadEntry("video/x-la-asf", "lsx"); + sMimeTypeMap.loadEntry("video/x-mng", "mng"); + sMimeTypeMap.loadEntry("video/x-ms-asf", "asf"); + sMimeTypeMap.loadEntry("video/x-ms-asf", "asx"); + sMimeTypeMap.loadEntry("video/x-ms-wm", "wm"); + sMimeTypeMap.loadEntry("video/x-ms-wmv", "wmv"); + sMimeTypeMap.loadEntry("video/x-ms-wmx", "wmx"); + sMimeTypeMap.loadEntry("video/x-ms-wvx", "wvx"); + sMimeTypeMap.loadEntry("video/x-msvideo", "avi"); + sMimeTypeMap.loadEntry("video/x-sgi-movie", "movie"); + sMimeTypeMap.loadEntry("x-conference/x-cooltalk", "ice"); + sMimeTypeMap.loadEntry("x-epoc/x-sisx-app", "sisx"); } return sMimeTypeMap; diff --git a/core/java/android/webkit/MockGeolocation.java b/core/java/android/webkit/MockGeolocation.java new file mode 100644 index 0000000..fbda492 --- /dev/null +++ b/core/java/android/webkit/MockGeolocation.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +/** + * This class is simply a container for the methods used to configure WebKit's + * mock Geolocation service for use in LayoutTests. + * @hide + */ +public final class MockGeolocation { + + // Global instance of a MockGeolocation + private static MockGeolocation sMockGeolocation; + + /** + * Set the position for the mock Geolocation service. + */ + public void setPosition(double latitude, double longitude, double accuracy) { + // This should only ever be called on the WebKit thread. + nativeSetPosition(latitude, longitude, accuracy); + } + + /** + * Set the error for the mock Geolocation service. + */ + public void setError(int code, String message) { + // This should only ever be called on the WebKit thread. + nativeSetError(code, message); + } + + /** + * Get the global instance of MockGeolocation. + * @return The global MockGeolocation instance. + */ + public static MockGeolocation getInstance() { + if (sMockGeolocation == null) { + sMockGeolocation = new MockGeolocation(); + } + return sMockGeolocation; + } + + // Native functions + private static native void nativeSetPosition(double latitude, double longitude, double accuracy); + private static native void nativeSetError(int code, String message); +} diff --git a/core/java/android/webkit/Network.java b/core/java/android/webkit/Network.java index c9b80ce..af0cb1e 100644 --- a/core/java/android/webkit/Network.java +++ b/core/java/android/webkit/Network.java @@ -132,11 +132,11 @@ class Network { * XXX: Must be created in the same thread as WebCore!!!!! */ private Network(Context context) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(Thread.currentThread(). getName().equals(WebViewCore.THREAD_NAME)); } - mSslErrorHandler = new SslErrorHandler(this); + mSslErrorHandler = new SslErrorHandler(); mHttpAuthHandler = new HttpAuthHandler(this); mRequestQueue = new RequestQueue(context); @@ -149,14 +149,12 @@ class Network { * @param headers The http headers. * @param postData The body of the request. * @param loader A LoadListener for receiving the results of the request. - * @param isHighPriority True if this is high priority request. * @return True if the request was successfully queued. */ public boolean requestURL(String method, Map<String, String> headers, byte [] postData, - LoadListener loader, - boolean isHighPriority) { + LoadListener loader) { String url = loader.url(); @@ -188,7 +186,7 @@ class Network { RequestHandle handle = q.queueRequest( url, loader.getWebAddress(), method, headers, loader, - bodyProvider, bodyLength, isHighPriority); + bodyProvider, bodyLength); loader.attachRequestHandle(handle); if (loader.isSynchronous()) { @@ -232,7 +230,7 @@ class Network { * connecting through the proxy. */ public synchronized void setProxyUsername(String proxyUsername) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(isValidProxySet()); } @@ -252,7 +250,7 @@ class Network { * connecting through the proxy. */ public synchronized void setProxyPassword(String proxyPassword) { - if (WebView.DEBUG) { + if (DebugFlags.NETWORK) { Assert.assertTrue(isValidProxySet()); } @@ -266,7 +264,7 @@ class Network { * @return True iff succeeds. */ public boolean saveState(Bundle outState) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.NETWORK) { Log.v(LOGTAG, "Network.saveState()"); } @@ -280,7 +278,7 @@ class Network { * @return True iff succeeds. */ public boolean restoreState(Bundle inState) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.NETWORK) { Log.v(LOGTAG, "Network.restoreState()"); } @@ -300,12 +298,20 @@ class Network { * @param loader The loader that resulted in SSL errors. */ public void handleSslErrorRequest(LoadListener loader) { - if (WebView.DEBUG) Assert.assertNotNull(loader); + if (DebugFlags.NETWORK) Assert.assertNotNull(loader); if (loader != null) { mSslErrorHandler.handleSslErrorRequest(loader); } } + /* package */ boolean checkSslPrefTable(LoadListener loader, + SslError error) { + if (loader != null && error != null) { + return mSslErrorHandler.checkSslPrefTable(loader, error); + } + return false; + } + /** * Handles authentication requests on their way up to the user (the user * must provide credentials). @@ -313,7 +319,7 @@ class Network { * authentication request. */ public void handleAuthRequest(LoadListener loader) { - if (WebView.DEBUG) Assert.assertNotNull(loader); + if (DebugFlags.NETWORK) Assert.assertNotNull(loader); if (loader != null) { mHttpAuthHandler.handleAuthRequest(loader); } diff --git a/core/java/android/webkit/Plugin.java b/core/java/android/webkit/Plugin.java index f83da99..34a30a9 100644 --- a/core/java/android/webkit/Plugin.java +++ b/core/java/android/webkit/Plugin.java @@ -26,7 +26,11 @@ import android.webkit.WebView; /** * Represents a plugin (Java equivalent of the PluginPackageAndroid * C++ class in libs/WebKitLib/WebKit/WebCore/plugins/android/) + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ +@Deprecated public class Plugin { public interface PreferencesClickHandler { public void handleClickEvent(Context context); @@ -38,6 +42,11 @@ public class Plugin { private String mDescription; private PreferencesClickHandler mHandler; + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public Plugin(String name, String path, String fileName, @@ -49,49 +58,103 @@ public class Plugin { mHandler = new DefaultClickHandler(); } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String toString() { return mName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getName() { return mName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getPath() { return mPath; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getFileName() { return mFileName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public String getDescription() { return mDescription; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setName(String name) { mName = name; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setPath(String path) { mPath = path; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setFileName(String fileName) { mFileName = fileName; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setDescription(String description) { mDescription = description; } + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void setClickHandler(PreferencesClickHandler handler) { mHandler = handler; } /** * Invokes the click handler for this plugin. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public void dispatchClickEvent(Context context) { if (mHandler != null) { mHandler.handleClickEvent(context); @@ -100,11 +163,15 @@ public class Plugin { /** * Default click handler. The plugins should implement their own. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated private class DefaultClickHandler implements PreferencesClickHandler, DialogInterface.OnClickListener { private AlertDialog mDialog; - + @Deprecated public void handleClickEvent(Context context) { // Show a simple popup dialog containing the description // string of the plugin. @@ -117,7 +184,11 @@ public class Plugin { .show(); } } - + /** + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ + @Deprecated public void onClick(DialogInterface dialog, int which) { mDialog.dismiss(); mDialog = null; diff --git a/core/java/android/webkit/PluginActivity.java b/core/java/android/webkit/PluginActivity.java new file mode 100644 index 0000000..cda7b59 --- /dev/null +++ b/core/java/android/webkit/PluginActivity.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +/** + * This activity is invoked when a plugin elects to go into full screen mode. + * @hide + */ +public class PluginActivity extends Activity { + + /* package */ static final String INTENT_EXTRA_PACKAGE_NAME = + "android.webkit.plugin.PACKAGE_NAME"; + /* package */ static final String INTENT_EXTRA_CLASS_NAME = + "android.webkit.plugin.CLASS_NAME"; + /* package */ static final String INTENT_EXTRA_NPP_INSTANCE = + "android.webkit.plugin.NPP_INSTANCE"; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + final Intent intent = getIntent(); + if (intent == null) { + // No intent means no class to lookup. + finish(); + } + final String packageName = + intent.getStringExtra(INTENT_EXTRA_PACKAGE_NAME); + final String className = intent.getStringExtra(INTENT_EXTRA_CLASS_NAME); + final int npp = intent.getIntExtra(INTENT_EXTRA_NPP_INSTANCE, -1); + // Retrieve the PluginStub implemented in packageName.className + PluginStub stub = + PluginUtil.getPluginStub(this, packageName, className); + + if (stub != null) { + View pluginView = stub.getFullScreenView(npp, this); + if (pluginView != null) { + setContentView(pluginView); + } else { + // No custom full-sreen view returned by the plugin, odd but + // just in case, finish the activity. + finish(); + } + } else { + finish(); + } + } +} diff --git a/core/java/android/webkit/PluginContentLoader.java b/core/java/android/webkit/PluginContentLoader.java deleted file mode 100644 index 2069599..0000000 --- a/core/java/android/webkit/PluginContentLoader.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.webkit; - -import android.net.http.Headers; - -import java.io.InputStream; -import java.util.*; - -import org.apache.http.util.CharArrayBuffer; - -/** - * This class is a concrete implementation of StreamLoader that uses a - * PluginData object as the source for the stream. - */ -class PluginContentLoader extends StreamLoader { - - private PluginData mData; // Content source - - /** - * Constructs a PluginDataLoader for use when loading content from - * a plugin. - * - * @param loadListener LoadListener to pass the content to - * @param data PluginData used as the source for the content. - */ - PluginContentLoader(LoadListener loadListener, PluginData data) { - super(loadListener); - mData = data; - } - - @Override - protected boolean setupStreamAndSendStatus() { - mDataStream = mData.getInputStream(); - mContentLength = mData.getContentLength(); - mHandler.status(1, 1, mData.getStatusCode(), "OK"); - return true; - } - - @Override - protected void buildHeaders(Headers headers) { - // Crate a CharArrayBuffer with an arbitrary initial capacity. - CharArrayBuffer buffer = new CharArrayBuffer(100); - Iterator<Map.Entry<String, String[]>> responseHeadersIt = - mData.getHeaders().entrySet().iterator(); - while (responseHeadersIt.hasNext()) { - Map.Entry<String, String[]> entry = responseHeadersIt.next(); - // Headers.parseHeader() expects lowercase keys, so keys - // such as "Accept-Ranges" will fail to parse. - // - // UrlInterceptHandler instances supply a mapping of - // lowercase key to [ unmodified key, value ], so for - // Headers.parseHeader() to succeed, we need to construct - // a string using the key (i.e. entry.getKey()) and the - // element denoting the header value in the - // [ unmodified key, value ] pair (i.e. entry.getValue()[1). - // - // The reason why UrlInterceptHandler instances supply such a - // mapping in the first place is historical. Early versions of - // the Gears plugin used java.net.HttpURLConnection, which always - // returned headers names as capitalized strings. When these were - // fed back into webkit, they failed to parse. - // - // Mewanwhile, Gears was modified to use Apache HTTP library - // instead, so this design is now obsolete. Changing it however, - // would require changes to the Gears C++ codebase and QA-ing and - // submitting a new binary to the Android tree. Given the - // timelines for the next Android release, we will not do this - // for now. - // - // TODO: fix C++ Gears to remove the need for this - // design. - String keyValue = entry.getKey() + ": " + entry.getValue()[1]; - buffer.ensureCapacity(keyValue.length()); - buffer.append(keyValue); - // Parse it into the header container. - headers.parseHeader(buffer); - // Clear the buffer - buffer.clear(); - } - } -} diff --git a/core/java/android/webkit/PluginData.java b/core/java/android/webkit/PluginData.java index 2b539fe..2dd445e 100644 --- a/core/java/android/webkit/PluginData.java +++ b/core/java/android/webkit/PluginData.java @@ -28,7 +28,10 @@ import java.util.Map; * status code. The PluginData class is the container for all these * parts. * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ +@Deprecated public final class PluginData { /** * The content stream. @@ -47,10 +50,6 @@ public final class PluginData { private Map<String, String[]> mHeaders; /** - * The index of the header value in the above mapping. - */ - private int mHeaderValueIndex; - /** * The associated HTTP response code. */ private int mStatusCode; @@ -63,7 +62,11 @@ public final class PluginData { * @param headers The response headers. Map of * lowercase header name to [ unmodified header name, header value] * @param length The HTTP response status code. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public PluginData( InputStream stream, long length, @@ -79,7 +82,11 @@ public final class PluginData { * Returns the input stream that contains the plugin content. * * @return An InputStream instance with the plugin content. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public InputStream getInputStream() { return mStream; } @@ -88,7 +95,11 @@ public final class PluginData { * Returns the length of the plugin content. * * @return the length of the plugin content. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public long getContentLength() { return mContentLength; } @@ -100,7 +111,11 @@ public final class PluginData { * @return A Map<String, String[]> containing all headers. The * mapping is 'lowercase header name' to ['unmodified header * name', header value]. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public Map<String, String[]> getHeaders() { return mHeaders; } @@ -109,7 +124,11 @@ public final class PluginData { * Returns the HTTP status code for the response. * * @return The HTTP statue code, e.g 200. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public int getStatusCode() { return mStatusCode; } diff --git a/core/java/android/webkit/PluginList.java b/core/java/android/webkit/PluginList.java index a9d3d8c..a61b07b 100644 --- a/core/java/android/webkit/PluginList.java +++ b/core/java/android/webkit/PluginList.java @@ -24,27 +24,43 @@ import java.util.List; * A simple list of initialized plugins. This list gets * populated when the plugins are initialized (at * browser startup, at the moment). + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ +@Deprecated public class PluginList { private ArrayList<Plugin> mPlugins; /** * Public constructor. Initializes the list of plugins. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public PluginList() { mPlugins = new ArrayList<Plugin>(); } /** * Returns the list of plugins as a java.util.List. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized List getList() { return mPlugins; } /** * Adds a plugin to the list. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void addPlugin(Plugin plugin) { if (!mPlugins.contains(plugin)) { mPlugins.add(plugin); @@ -53,7 +69,11 @@ public class PluginList { /** * Removes a plugin from the list. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void removePlugin(Plugin plugin) { int location = mPlugins.indexOf(plugin); if (location != -1) { @@ -63,14 +83,22 @@ public class PluginList { /** * Clears the plugin list. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void clear() { mPlugins.clear(); } /** * Dispatches the click event to the appropriate plugin. + * + * @deprecated This interface was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public synchronized void pluginClicked(Context context, int position) { try { Plugin plugin = mPlugins.get(position); diff --git a/core/java/android/webkit/PluginManager.java b/core/java/android/webkit/PluginManager.java new file mode 100644 index 0000000..4588f46 --- /dev/null +++ b/core/java/android/webkit/PluginManager.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import java.util.ArrayList; +import java.util.List; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.pm.Signature; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.SystemProperties; +import android.util.Log; + +/** + * Class for managing the relationship between the {@link WebView} and installed + * plugins in the system. You can find this class through + * {@link PluginManager#getInstance}. + * + * @hide pending API solidification + */ +public class PluginManager { + + /** + * Service Action: A plugin wishes to be loaded in the WebView must provide + * {@link android.content.IntentFilter IntentFilter} that accepts this + * action in their AndroidManifest.xml. + * <p> + * TODO: we may change this to a new PLUGIN_ACTION if this is going to be + * public. + */ + @SdkConstant(SdkConstantType.SERVICE_ACTION) + public static final String PLUGIN_ACTION = "android.webkit.PLUGIN"; + + /** + * A plugin wishes to be loaded in the WebView must provide this permission + * in their AndroidManifest.xml. + */ + public static final String PLUGIN_PERMISSION = "android.webkit.permission.PLUGIN"; + + private static final String LOGTAG = "webkit"; + + private static PluginManager mInstance = null; + + private final Context mContext; + + private ArrayList<PackageInfo> mPackageInfoCache; + + // Only plugin matches one of the signatures in the list can be loaded + // inside the WebView process + private static final String SIGNATURE_1 = "308204c5308203ada003020102020900d7cb412f75f4887e300d06092a864886f70d010105050030819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f7261746564301e170d3039313030313030323331345a170d3337303231363030323331345a30819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f726174656430820120300d06092a864886f70d01010105000382010d0030820108028201010099724f3e05bbd78843794f357776e04b340e13cb1c9ccb3044865180d7d8fec8166c5bbd876da8b80aa71eb6ba3d4d3455c9a8de162d24a25c4c1cd04c9523affd06a279fc8f0d018f242486bdbb2dbfbf6fcb21ed567879091928b876f7ccebc7bccef157366ebe74e33ae1d7e9373091adab8327482154afc0693a549522f8c796dd84d16e24bb221f5dbb809ca56dd2b6e799c5fa06b6d9c5c09ada54ea4c5db1523a9794ed22a3889e5e05b29f8ee0a8d61efe07ae28f65dece2ff7edc5b1416d7c7aad7f0d35e8f4a4b964dbf50ae9aa6d620157770d974131b3e7e3abd6d163d65758e2f0822db9c88598b9db6263d963d13942c91fc5efe34fc1e06e3020103a382010630820102301d0603551d0e041604145af418e419a639e1657db960996364a37ef20d403081d20603551d230481ca3081c780145af418e419a639e1657db960996364a37ef20d40a181a3a481a030819d310b3009060355040613025553311330110603550408130a43616c69666f726e69613111300f0603550407130853616e204a6f736531233021060355040a131a41646f62652053797374656d7320496e636f72706f7261746564311c301a060355040b1313496e666f726d6174696f6e2053797374656d73312330210603550403131a41646f62652053797374656d7320496e636f72706f7261746564820900d7cb412f75f4887e300c0603551d13040530030101ff300d06092a864886f70d0101050500038201010076c2a11fe303359689c2ebc7b2c398eff8c3f9ad545cdbac75df63bf7b5395b6988d1842d6aa1556d595b5692e08224d667a4c9c438f05e74906c53dd8016dde7004068866f01846365efd146e9bfaa48c9ecf657f87b97c757da11f225c4a24177bf2d7188e6cce2a70a1e8a841a14471eb51457398b8a0addd8b6c8c1538ca8f1e40b4d8b960009ea22c188d28924813d2c0b4a4d334b7cf05507e1fcf0a06fe946c7ffc435e173af6fc3e3400643710acc806f830a14788291d46f2feed9fb5c70423ca747ed1572d752894ac1f19f93989766308579393fabb43649aa8806a313b1ab9a50922a44c2467b9062037f2da0d484d9ffd8fe628eeea629ba637"; + + private static final Signature[] SIGNATURES = new Signature[] { + new Signature(SIGNATURE_1) + }; + + private PluginManager(Context context) { + mContext = context; + mPackageInfoCache = new ArrayList<PackageInfo>(); + } + + public static synchronized PluginManager getInstance(Context context) { + if (mInstance == null) { + if (context == null) { + throw new IllegalStateException( + "First call to PluginManager need a valid context."); + } + mInstance = new PluginManager(context); + } + return mInstance; + } + + /** + * Signal the WebCore thread to refresh its list of plugins. Use this if the + * directory contents of one of the plugin directories has been modified and + * needs its changes reflecting. May cause plugin load and/or unload. + * + * @param reloadOpenPages Set to true to reload all open pages. + */ + public void refreshPlugins(boolean reloadOpenPages) { + BrowserFrame.sJavaBridge.obtainMessage( + JWebCoreJavaBridge.REFRESH_PLUGINS, reloadOpenPages) + .sendToTarget(); + } + + String[] getPluginDirectories() { + + ArrayList<String> directories = new ArrayList<String>(); + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> plugins = pm.queryIntentServices(new Intent( + PLUGIN_ACTION), PackageManager.GET_SERVICES); + + synchronized(mPackageInfoCache) { + + // clear the list of existing packageInfo objects + mPackageInfoCache.clear(); + + for (ResolveInfo info : plugins) { + ServiceInfo serviceInfo = info.serviceInfo; + if (serviceInfo == null) { + Log.w(LOGTAG, "Ignore bad plugin"); + continue; + } + PackageInfo pkgInfo; + try { + pkgInfo = pm.getPackageInfo(serviceInfo.packageName, + PackageManager.GET_PERMISSIONS + | PackageManager.GET_SIGNATURES); + } catch (NameNotFoundException e) { + Log.w(LOGTAG, "Cant find plugin: " + serviceInfo.packageName); + continue; + } + if (pkgInfo == null) { + continue; + } + String directory = pkgInfo.applicationInfo.dataDir + "/lib"; + if (directories.contains(directory)) { + continue; + } + String permissions[] = pkgInfo.requestedPermissions; + if (permissions == null) { + continue; + } + boolean permissionOk = false; + for (String permit : permissions) { + if (PLUGIN_PERMISSION.equals(permit)) { + permissionOk = true; + break; + } + } + if (!permissionOk) { + continue; + } + Signature signatures[] = pkgInfo.signatures; + if (signatures == null) { + continue; + } + if (SystemProperties.getBoolean("ro.secure", false)) { + boolean signatureMatch = false; + for (Signature signature : signatures) { + for (int i = 0; i < SIGNATURES.length; i++) { + if (SIGNATURES[i].equals(signature)) { + signatureMatch = true; + break; + } + } + } + if (!signatureMatch) { + continue; + } + } + mPackageInfoCache.add(pkgInfo); + directories.add(directory); + } + } + + return directories.toArray(new String[directories.size()]); + } + + String getPluginsAPKName(String pluginLib) { + + // basic error checking on input params + if (pluginLib == null || pluginLib.length() == 0) { + return null; + } + + // must be synchronized to ensure the consistency of the cache + synchronized(mPackageInfoCache) { + for (PackageInfo pkgInfo : mPackageInfoCache) { + if (pluginLib.startsWith(pkgInfo.applicationInfo.dataDir)) { + return pkgInfo.packageName; + } + } + } + + // if no apk was found then return null + return null; + } + + String getPluginSharedDataDirectory() { + return mContext.getDir("plugins", 0).getPath(); + } +} diff --git a/core/java/android/webkit/PluginStub.java b/core/java/android/webkit/PluginStub.java new file mode 100644 index 0000000..3887d44 --- /dev/null +++ b/core/java/android/webkit/PluginStub.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.content.Context; +import android.view.View; + +/** + * This interface is used to implement plugins in a WebView. A plugin + * package may extend this class and implement the abstract functions to create + * embedded or fullscreeen views displayed in a WebView. The PluginStub + * implementation will be provided the same NPP instance that is created + * through the native interface. + */ +public interface PluginStub { + + /** + * Return a custom embedded view to draw the plugin. + * @param NPP The native NPP instance. + * @param context The current application's Context. + * @return A custom View that will be managed by WebView. + */ + public abstract View getEmbeddedView(int NPP, Context context); + + /** + * Return a custom full-screen view to be displayed when the user requests + * a plugin display as full-screen. Note that the application may choose not + * to display this View as completely full-screen. + * @param NPP The native NPP instance. + * @param context The current application's Context. + * @return A custom View that will be managed by the application. + */ + public abstract View getFullScreenView(int NPP, Context context); +} diff --git a/core/java/android/webkit/PluginUtil.java b/core/java/android/webkit/PluginUtil.java new file mode 100644 index 0000000..8fdbd67 --- /dev/null +++ b/core/java/android/webkit/PluginUtil.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.webkit; + +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.util.Log; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +class PluginUtil { + + private static final String LOGTAG = "PluginUtil"; + + /** + * + * @param packageName the name of the apk where the class can be found + * @param className the fully qualified name of a subclass of PluginStub + */ + /* package */ + static PluginStub getPluginStub(Context context, String packageName, + String className) { + try { + Context pluginContext = context.createPackageContext(packageName, + Context.CONTEXT_INCLUDE_CODE | + Context.CONTEXT_IGNORE_SECURITY); + ClassLoader pluginCL = pluginContext.getClassLoader(); + + Class<?> stubClass = pluginCL.loadClass(className); + Object stubObject = stubClass.newInstance(); + + if (stubObject instanceof PluginStub) { + return (PluginStub) stubObject; + } else { + Log.e(LOGTAG, "The plugin class is not of type PluginStub"); + } + } catch (Exception e) { + // Any number of things could have happened. Log the exception and + // return null. Careful not to use Log.e(LOGTAG, "String", e) + // because that reports the exception to the checkin service. + Log.e(LOGTAG, Log.getStackTraceString(e)); + } + return null; + } +} diff --git a/core/java/android/webkit/SslErrorHandler.java b/core/java/android/webkit/SslErrorHandler.java index 5f84bbe..90ed65d 100644 --- a/core/java/android/webkit/SslErrorHandler.java +++ b/core/java/android/webkit/SslErrorHandler.java @@ -42,11 +42,6 @@ public class SslErrorHandler extends Handler { private static final String LOGTAG = "network"; /** - * Network. - */ - private Network mNetwork; - - /** * Queue of loaders that experience SSL-related problems. */ private LinkedList<LoadListener> mLoaderQueue; @@ -57,13 +52,15 @@ public class SslErrorHandler extends Handler { private Bundle mSslPrefTable; // Message id for handling the response - private final int HANDLE_RESPONSE = 100; + private static final int HANDLE_RESPONSE = 100; @Override public void handleMessage(Message msg) { switch (msg.what) { case HANDLE_RESPONSE: - handleSslErrorResponse(msg.arg1 == 1); + LoadListener loader = (LoadListener) msg.obj; + handleSslErrorResponse(loader, loader.sslError(), + msg.arg1 == 1); fastProcessQueuedSslErrors(); break; } @@ -72,9 +69,7 @@ public class SslErrorHandler extends Handler { /** * Creates a new error handler with an empty loader queue. */ - /* package */ SslErrorHandler(Network network) { - mNetwork = network; - + /* package */ SslErrorHandler() { mLoaderQueue = new LinkedList<LoadListener>(); mSslPrefTable = new Bundle(); } @@ -83,7 +78,7 @@ public class SslErrorHandler extends Handler { * Saves this handler's state into a map. * @return True iff succeeds. */ - /* package */ boolean saveState(Bundle outState) { + /* package */ synchronized boolean saveState(Bundle outState) { boolean success = (outState != null); if (success) { // TODO? @@ -97,7 +92,7 @@ public class SslErrorHandler extends Handler { * Restores this handler's state from a map. * @return True iff succeeds. */ - /* package */ boolean restoreState(Bundle inState) { + /* package */ synchronized boolean restoreState(Bundle inState) { boolean success = (inState != null); if (success) { success = inState.containsKey("ssl-error-handler"); @@ -120,7 +115,7 @@ public class SslErrorHandler extends Handler { * Handles SSL error(s) on the way up to the user. */ /* package */ synchronized void handleSslErrorRequest(LoadListener loader) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.SSL_ERROR_HANDLER) { Log.v(LOGTAG, "SslErrorHandler.handleSslErrorRequest(): " + "url=" + loader.url()); } @@ -134,6 +129,28 @@ public class SslErrorHandler extends Handler { } /** + * Check the preference table for a ssl error that has already been shown + * to the user. + */ + /* package */ synchronized boolean checkSslPrefTable(LoadListener loader, + SslError error) { + final String host = loader.host(); + final int primary = error.getPrimaryError(); + + if (DebugFlags.SSL_ERROR_HANDLER) { + Assert.assertTrue(host != null && primary != 0); + } + + if (mSslPrefTable.containsKey(host)) { + if (primary <= mSslPrefTable.getInt(host)) { + handleSslErrorResponse(loader, error, true); + return true; + } + } + return false; + } + + /** * Processes queued SSL-error confirmation requests in * a tight loop while there is no need to ask the user. */ @@ -151,28 +168,24 @@ public class SslErrorHandler extends Handler { if (loader != null) { // if this loader has been cancelled if (loader.cancelled()) { - // go to the following loader in the queue + // go to the following loader in the queue. Make sure this + // loader has been removed from the queue. + mLoaderQueue.remove(loader); return true; } SslError error = loader.sslError(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertNotNull(error); } - int primary = error.getPrimaryError(); - String host = loader.host(); - - if (WebView.DEBUG) { - Assert.assertTrue(host != null && primary != 0); - } - - if (mSslPrefTable.containsKey(host)) { - if (primary <= mSslPrefTable.getInt(host)) { - handleSslErrorResponse(true); - return true; - } + // checkSslPrefTable will handle the ssl error response if the + // answer is available. It does not remove the loader from the + // queue. + if (checkSslPrefTable(loader, error)) { + mLoaderQueue.remove(loader); + return true; } // if we do not have information on record, ask @@ -189,7 +202,7 @@ public class SslErrorHandler extends Handler { * Proceed with the SSL certificate. */ public void proceed() { - sendMessage(obtainMessage(HANDLE_RESPONSE, 1, 0)); + sendMessage(obtainMessage(HANDLE_RESPONSE, 1, 0, mLoaderQueue.poll())); } /** @@ -197,19 +210,20 @@ public class SslErrorHandler extends Handler { * the error. */ public void cancel() { - sendMessage(obtainMessage(HANDLE_RESPONSE, 0, 0)); + sendMessage(obtainMessage(HANDLE_RESPONSE, 0, 0, mLoaderQueue.poll())); } /** * Handles SSL error(s) on the way down from the user. */ - /* package */ synchronized void handleSslErrorResponse(boolean proceed) { - LoadListener loader = mLoaderQueue.poll(); - if (WebView.DEBUG) { + /* package */ synchronized void handleSslErrorResponse(LoadListener loader, + SslError error, boolean proceed) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertNotNull(loader); + Assert.assertNotNull(error); } - if (WebView.LOGV_ENABLED) { + if (DebugFlags.SSL_ERROR_HANDLER) { Log.v(LOGTAG, "SslErrorHandler.handleSslErrorResponse():" + " proceed: " + proceed + " url:" + loader.url()); @@ -218,16 +232,16 @@ public class SslErrorHandler extends Handler { if (!loader.cancelled()) { if (proceed) { // update the user's SSL error preference table - int primary = loader.sslError().getPrimaryError(); + int primary = error.getPrimaryError(); String host = loader.host(); - if (WebView.DEBUG) { + if (DebugFlags.SSL_ERROR_HANDLER) { Assert.assertTrue(host != null && primary != 0); } boolean hasKey = mSslPrefTable.containsKey(host); if (!hasKey || primary > mSslPrefTable.getInt(host)) { - mSslPrefTable.putInt(host, new Integer(primary)); + mSslPrefTable.putInt(host, primary); } } loader.handleSslErrorResponse(proceed); diff --git a/core/java/android/webkit/StreamLoader.java b/core/java/android/webkit/StreamLoader.java index 705157c..623ff29 100644 --- a/core/java/android/webkit/StreamLoader.java +++ b/core/java/android/webkit/StreamLoader.java @@ -102,7 +102,7 @@ abstract class StreamLoader extends Handler { // to pass data to the loader mData = new byte[8192]; sendHeaders(); - while (!sendData()); + while (!sendData() && !mHandler.cancelled()); closeStreamAndSendEndData(); mHandler.loadSynchronousMessages(); } @@ -113,9 +113,13 @@ abstract class StreamLoader extends Handler { * @see android.os.Handler#handleMessage(android.os.Message) */ public void handleMessage(Message msg) { - if (WebView.DEBUG && mHandler.isSynchronous()) { + if (DebugFlags.STREAM_LOADER && mHandler.isSynchronous()) { throw new AssertionError(); } + if (mHandler.cancelled()) { + closeStreamAndSendEndData(); + return; + } switch(msg.what) { case MSG_STATUS: if (setupStreamAndSendStatus()) { @@ -153,7 +157,6 @@ abstract class StreamLoader extends Handler { if (mContentLength > 0) { headers.setContentLength(mContentLength); } - headers.setCacheControl(NO_STORE); buildHeaders(headers); mHandler.headers(headers); } diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java index 9889fe9..232ed36 100644 --- a/core/java/android/webkit/URLUtil.java +++ b/core/java/android/webkit/URLUtil.java @@ -61,7 +61,7 @@ public final class URLUtil { webAddress = new WebAddress(inUrl); } catch (ParseException ex) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.URL_UTIL) { Log.v(LOGTAG, "smartUrlFilter: failed to parse url = " + inUrl); } return retVal; @@ -126,6 +126,32 @@ public final class URLUtil { return retData; } + /** + * @return True iff the url is correctly URL encoded + */ + static boolean verifyURLEncoding(String url) { + int count = url.length(); + if (count == 0) { + return false; + } + + int index = url.indexOf('%'); + while (index >= 0 && index < count) { + if (index < count - 2) { + try { + parseHex((byte) url.charAt(++index)); + parseHex((byte) url.charAt(++index)); + } catch (IllegalArgumentException e) { + return false; + } + } else { + return false; + } + index = url.indexOf('%', index + 1); + } + return true; + } + private static int parseHex(byte b) { if (b >= '0' && b <= '9') return (b - '0'); if (b >= 'A' && b <= 'F') return (b - 'A' + 10); @@ -146,6 +172,7 @@ public final class URLUtil { * requests from a file url. * @deprecated Cookieless proxy is no longer supported. */ + @Deprecated public static boolean isCookielessProxyUrl(String url) { return (null != url) && url.startsWith(PROXY_BASE); } diff --git a/core/java/android/webkit/UrlInterceptHandler.java b/core/java/android/webkit/UrlInterceptHandler.java index 766ed7e..78bab04 100644 --- a/core/java/android/webkit/UrlInterceptHandler.java +++ b/core/java/android/webkit/UrlInterceptHandler.java @@ -20,6 +20,11 @@ import android.webkit.CacheManager.CacheResult; import android.webkit.PluginData; import java.util.Map; +/** + * @deprecated This interface was inteded to be used by Gears. Since Gears was + * deprecated, so is this class. + */ +@Deprecated public interface UrlInterceptHandler { /** @@ -30,8 +35,8 @@ public interface UrlInterceptHandler { * @param url URL string. * @param headers The headers associated with the request. May be null. * @return The CacheResult containing the surrogate response. - * @deprecated Use PluginData getPluginData(String url, - * Map<String, String> headers); instead + * + * @deprecated Do not use, this interface is deprecated. */ @Deprecated public CacheResult service(String url, Map<String, String> headers); @@ -44,6 +49,9 @@ public interface UrlInterceptHandler { * @param url URL string. * @param headers The headers associated with the request. May be null. * @return The PluginData containing the surrogate response. + * + * @deprecated Do not use, this interface is deprecated. */ + @Deprecated public PluginData getPluginData(String url, Map<String, String> headers); } diff --git a/core/java/android/webkit/UrlInterceptRegistry.java b/core/java/android/webkit/UrlInterceptRegistry.java index 31005bb..eca5acd 100644 --- a/core/java/android/webkit/UrlInterceptRegistry.java +++ b/core/java/android/webkit/UrlInterceptRegistry.java @@ -24,6 +24,11 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.Map; +/** + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. + */ +@Deprecated public final class UrlInterceptRegistry { private final static String LOGTAG = "intercept"; @@ -42,7 +47,11 @@ public final class UrlInterceptRegistry { * set the flag to control whether url intercept is enabled or disabled * * @param disabled true to disable the cache + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized void setUrlInterceptDisabled(boolean disabled) { mDisabled = disabled; } @@ -51,7 +60,11 @@ public final class UrlInterceptRegistry { * get the state of the url intercept, enabled or disabled * * @return return if it is disabled + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized boolean urlInterceptDisabled() { return mDisabled; } @@ -62,7 +75,11 @@ public final class UrlInterceptRegistry { * * @param handler The new UrlInterceptHandler object * @return true if the handler was not previously registered. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized boolean registerHandler( UrlInterceptHandler handler) { if (!getHandlers().contains(handler)) { @@ -78,7 +95,11 @@ public final class UrlInterceptRegistry { * * @param handler A previously registered UrlInterceptHandler. * @return true if the handler was found and removed from the list. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized boolean unregisterHandler( UrlInterceptHandler handler) { return getHandlers().remove(handler); @@ -89,8 +110,9 @@ public final class UrlInterceptRegistry { * UrlInterceptHandler interested, or null if none are. * * @return A CacheResult containing surrogate content. - * @deprecated Use PluginData getPluginData( String url, - * Map<String, String> headers) instead. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ @Deprecated public static synchronized CacheResult getSurrogate( @@ -115,7 +137,11 @@ public final class UrlInterceptRegistry { * intercepts are disabled. * * @return A PluginData instance containing surrogate content. + * + * @deprecated This class was intended to be used by Gears. Since Gears was + * deprecated, so is this class. */ + @Deprecated public static synchronized PluginData getPluginData( String url, Map<String, String> headers) { if (urlInterceptDisabled()) { diff --git a/core/java/android/webkit/ValueCallback.java b/core/java/android/webkit/ValueCallback.java new file mode 100644 index 0000000..1a167e8 --- /dev/null +++ b/core/java/android/webkit/ValueCallback.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +/** + * A callback interface used to returns values asynchronously + */ +public interface ValueCallback<T> { + /** + * Invoked when we have the result + */ + public void onReceiveValue(T value); +}; diff --git a/core/java/android/webkit/ViewManager.java b/core/java/android/webkit/ViewManager.java new file mode 100644 index 0000000..6a838c3 --- /dev/null +++ b/core/java/android/webkit/ViewManager.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.content.Context; +import android.view.View; +import android.widget.AbsoluteLayout; + +import java.util.ArrayList; + +class ViewManager { + private final WebView mWebView; + private final ArrayList<ChildView> mChildren = new ArrayList<ChildView>(); + private boolean mHidden; + + class ChildView { + int x; + int y; + int width; + int height; + View mView; // generic view to show + + ChildView() { + } + + void setBounds(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + void attachView(int x, int y, int width, int height) { + if (mView == null) { + return; + } + setBounds(x, y, width, height); + final AbsoluteLayout.LayoutParams lp = + new AbsoluteLayout.LayoutParams(ctvD(width), ctvD(height), + ctvX(x), ctvY(y)); + mWebView.mPrivateHandler.post(new Runnable() { + public void run() { + // This method may be called multiple times. If the view is + // already attached, just set the new LayoutParams, + // otherwise attach the view and add it to the list of + // children. + if (mView.getParent() != null) { + mView.setLayoutParams(lp); + } else { + attachViewOnUIThread(lp); + } + } + }); + } + + void attachViewOnUIThread(AbsoluteLayout.LayoutParams lp) { + mWebView.addView(mView, lp); + mChildren.add(this); + } + + void removeView() { + if (mView == null) { + return; + } + mWebView.mPrivateHandler.post(new Runnable() { + public void run() { + removeViewOnUIThread(); + } + }); + } + + void removeViewOnUIThread() { + mWebView.removeView(mView); + mChildren.remove(this); + } + } + + ViewManager(WebView w) { + mWebView = w; + } + + ChildView createView() { + return new ChildView(); + } + + /** + * Shorthand for calling mWebView.contentToViewDimension. Used when + * obtaining a view dimension from a content dimension, whether it be in x + * or y. + */ + private int ctvD(int val) { + return mWebView.contentToViewDimension(val); + } + + /** + * Shorthand for calling mWebView.contentToViewX. Used when obtaining a + * view x coordinate from a content x coordinate. + */ + private int ctvX(int val) { + return mWebView.contentToViewX(val); + } + + /** + * Shorthand for calling mWebView.contentToViewY. Used when obtaining a + * view y coordinate from a content y coordinate. + */ + private int ctvY(int val) { + return mWebView.contentToViewY(val); + } + + void scaleAll() { + for (ChildView v : mChildren) { + View view = v.mView; + AbsoluteLayout.LayoutParams lp = + (AbsoluteLayout.LayoutParams) view.getLayoutParams(); + lp.width = ctvD(v.width); + lp.height = ctvD(v.height); + lp.x = ctvX(v.x); + lp.y = ctvY(v.y); + view.setLayoutParams(lp); + } + } + + void hideAll() { + if (mHidden) { + return; + } + for (ChildView v : mChildren) { + v.mView.setVisibility(View.GONE); + } + mHidden = true; + } + + void showAll() { + if (!mHidden) { + return; + } + for (ChildView v : mChildren) { + v.mView.setVisibility(View.VISIBLE); + } + mHidden = false; + } +} diff --git a/core/java/android/webkit/WebBackForwardList.java b/core/java/android/webkit/WebBackForwardList.java index ffd6a11..62a5531 100644 --- a/core/java/android/webkit/WebBackForwardList.java +++ b/core/java/android/webkit/WebBackForwardList.java @@ -137,7 +137,7 @@ public class WebBackForwardList implements Cloneable, Serializable { // when removing the first item, we can assert that the index is 0. // This lets us change the current index without having to query the // native BackForwardList. - if (WebView.DEBUG && (index != 0)) { + if (DebugFlags.WEB_BACK_FORWARD_LIST && (index != 0)) { throw new AssertionError(); } final WebHistoryItem h = mArray.remove(index); diff --git a/core/java/android/webkit/WebChromeClient.java b/core/java/android/webkit/WebChromeClient.java index 9d9763c..92676aa 100644 --- a/core/java/android/webkit/WebChromeClient.java +++ b/core/java/android/webkit/WebChromeClient.java @@ -18,6 +18,7 @@ package android.webkit; import android.graphics.Bitmap; import android.os.Message; +import android.view.View; public class WebChromeClient { @@ -44,6 +45,42 @@ public class WebChromeClient { public void onReceivedIcon(WebView view, Bitmap icon) {} /** + * Notify the host application of the url for an apple-touch-icon. + * @param view The WebView that initiated the callback. + * @param url The icon url. + * @param precomposed True if the url is for a precomposed touch icon. + */ + public void onReceivedTouchIconUrl(WebView view, String url, + boolean precomposed) {} + + /** + * A callback interface used by the host application to notify + * the current page that its custom view has been dismissed. + */ + public interface CustomViewCallback { + /** + * Invoked when the host application dismisses the + * custom view. + */ + public void onCustomViewHidden(); + } + + /** + * Notify the host application that the current page would + * like to show a custom View. + * @param view is the View object to be shown. + * @param callback is the callback to be invoked if and when the view + * is dismissed. + */ + public void onShowCustomView(View view, CustomViewCallback callback) {}; + + /** + * Notify the host application that the current page would + * like to hide its custom view. + */ + public void onHideCustomView() {} + + /** * Request the host application to create a new Webview. The host * application should handle placement of the new WebView in the view * system. The default behavior returns null. @@ -158,6 +195,52 @@ public class WebChromeClient { return false; } + /** + * Tell the client that the database quota for the origin has been exceeded. + * @param url The URL that triggered the notification + * @param databaseIdentifier The identifier of the database that caused the + * quota overflow. + * @param currentQuota The current quota for the origin. + * @param estimatedSize The estimated size of the database. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater A callback to inform the WebCore thread that a new + * quota is available. This callback must always be executed at some + * point to ensure that the sleeping WebCore thread is woken up. + */ + public void onExceededDatabaseQuota(String url, String databaseIdentifier, + long currentQuota, long estimatedSize, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + // This default implementation passes the current quota back to WebCore. + // WebCore will interpret this that new quota was declined. + quotaUpdater.updateQuota(currentQuota); + } + + /** + * Tell the client that the Application Cache has exceeded its max size. + * @param spaceNeeded is the amount of disk space that would be needed + * in order for the last appcache operation to succeed. + * @param totalUsedQuota is the sum of all origins' quota. + * @param quotaUpdater A callback to inform the WebCore thread that a new + * app cache size is available. This callback must always be executed at + * some point to ensure that the sleeping WebCore thread is woken up. + */ + public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, + WebStorage.QuotaUpdater quotaUpdater) { + quotaUpdater.updateQuota(0); + } + + /** + * Instructs the client to show a prompt to ask the user to set the + * Geolocation permission state for the specified origin. + */ + public void onGeolocationPermissionsShowPrompt(String origin, + GeolocationPermissions.Callback callback) {} + + /** + * Instructs the client to hide the Geolocation permissions prompt. + */ + public void onGeolocationPermissionsHidePrompt() {} + /** * Tell the client that a JavaScript execution timeout has occured. And the * client may decide whether or not to interrupt the execution. If the @@ -167,9 +250,43 @@ public class WebChromeClient { * will continue to occur if the script does not finish at the next check * point. * @return boolean Whether the JavaScript execution should be interrupted. - * @hide pending API Council approval */ public boolean onJsTimeout() { return true; } + + /** + * Add a JavaScript error message to the console. Clients should override + * this to process the log message as they see fit. + * @param message The error message to report. + * @param lineNumber The line number of the error. + * @param sourceID The name of the source file that caused the error. + */ + public void addMessageToConsole(String message, int lineNumber, String sourceID) {} + + /** + * Ask the host application for an icon to represent a <video> element. + * This icon will be used if the Web page did not specify a poster attribute. + * + * @return Bitmap The icon or null if no such icon is available. + */ + public Bitmap getDefaultVideoPoster() { + return null; + } + + /** + * Ask the host application for a custom progress view to show while + * a <video> is loading. + * + * @return View The progress view. + */ + public View getVideoLoadingProgressView() { + return null; + } + + /** Obtains a list of all visited history items, used for link coloring + */ + public void getVisitedHistory(ValueCallback<String[]> callback) { + } + } diff --git a/core/java/android/webkit/WebHistoryItem.java b/core/java/android/webkit/WebHistoryItem.java index fd26b98..abd8237 100644 --- a/core/java/android/webkit/WebHistoryItem.java +++ b/core/java/android/webkit/WebHistoryItem.java @@ -39,6 +39,8 @@ public class WebHistoryItem implements Cloneable { private Bitmap mFavicon; // The pre-flattened data used for saving the state. private byte[] mFlattenedData; + // The apple-touch-icon url for use when adding the site to the home screen + private String mTouchIconUrl; /** * Basic constructor that assigns a unique id to the item. Called by JNI @@ -127,6 +129,14 @@ public class WebHistoryItem implements Cloneable { } /** + * Return the touch icon url. + * @hide + */ + public String getTouchIconUrl() { + return mTouchIconUrl; + } + + /** * Set the favicon. * @param icon A Bitmap containing the favicon for this history item. * Note: The VM ensures 32-bit atomic read/write operations so we don't have @@ -137,6 +147,14 @@ public class WebHistoryItem implements Cloneable { } /** + * Set the touch icon url. + * @hide + */ + /*package*/ void setTouchIconUrl(String url) { + mTouchIconUrl = url; + } + + /** * Get the pre-flattened data. * Note: The VM ensures 32-bit atomic read/write operations so we don't have * to synchronize this method. diff --git a/core/java/android/webkit/WebIconDatabase.java b/core/java/android/webkit/WebIconDatabase.java index d284f5e..6cc6bb4 100644 --- a/core/java/android/webkit/WebIconDatabase.java +++ b/core/java/android/webkit/WebIconDatabase.java @@ -37,7 +37,7 @@ public final class WebIconDatabase { private final EventHandler mEventHandler = new EventHandler(); // Class to handle messages before WebCore is ready - private class EventHandler extends Handler { + private static class EventHandler extends Handler { // Message ids static final int OPEN = 0; static final int CLOSE = 1; diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 51c4293..6f3262a 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -17,14 +17,13 @@ package android.webkit; import android.content.Context; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Build; import android.os.Handler; import android.os.Message; import android.provider.Checkin; - import java.lang.SecurityException; - import java.util.Locale; /** @@ -75,7 +74,6 @@ public class WebSettings { * FAR makes 100% looking like in 240dpi * MEDIUM makes 100% looking like in 160dpi * CLOSE makes 100% looking like in 120dpi - * @hide Pending API council approval */ public enum ZoomDensity { FAR(150), // 240dpi @@ -150,7 +148,6 @@ public class WebSettings { private String mUserAgent; private boolean mUseDefaultUserAgent; private String mAcceptLanguage; - private String mPluginsPath = ""; private int mMinimumFontSize = 8; private int mMinimumLogicalFontSize = 8; private int mDefaultFontSize = 16; @@ -165,6 +162,17 @@ public class WebSettings { private boolean mUseWideViewport = false; private boolean mSupportMultipleWindows = false; private boolean mShrinksStandaloneImagesToFit = false; + // HTML5 API flags + private boolean mAppCacheEnabled = false; + private boolean mDatabaseEnabled = false; + private boolean mDomStorageEnabled = false; + private boolean mWorkersEnabled = false; // only affects V8. + private boolean mGeolocationEnabled = true; + // HTML5 configuration parameters + private long mAppCacheMaxSize = Long.MAX_VALUE; + private String mAppCachePath = ""; + private String mDatabasePath = ""; + private String mGeolocationDatabasePath = ""; // 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. @@ -179,9 +187,13 @@ public class WebSettings { private boolean mSupportZoom = true; private boolean mBuiltInZoomControls = false; private boolean mAllowFileAccess = true; + private boolean mLoadWithOverviewMode = false; + + // private WebSettings, not accessible by the host activity + static private int mDoubleTapToastCount = 3; - // The Gears permissions manager. Only in Donut. - static GearsPermissionsManager sGearsPermissionsManager; + private static final String PREF_FILE = "WebViewSettings"; + private static final String DOUBLE_TAP_TOAST_COUNT = "double_tap_toast_count"; // Class to handle messages before WebCore is ready. private class EventHandler { @@ -189,6 +201,8 @@ public class WebSettings { static final int SYNC = 0; // Message id for setting priority static final int PRIORITY = 1; + // Message id for writing double-tap toast count + static final int SET_DOUBLE_TAP_TOAST_COUNT = 2; // Actual WebCore thread handler private Handler mHandler; @@ -203,7 +217,6 @@ public class WebSettings { switch (msg.what) { case SYNC: synchronized (WebSettings.this) { - checkGearsPermissions(); if (mBrowserFrame.mNativeFrame != 0) { nativeSync(mBrowserFrame.mNativeFrame); } @@ -215,6 +228,16 @@ public class WebSettings { setRenderPriority(); break; } + + case SET_DOUBLE_TAP_TOAST_COUNT: { + SharedPreferences.Editor editor = mContext + .getSharedPreferences(PREF_FILE, + Context.MODE_PRIVATE).edit(); + editor.putInt(DOUBLE_TAP_TOAST_COUNT, + mDoubleTapToastCount); + editor.commit(); + break; + } } } }; @@ -251,13 +274,13 @@ public class WebSettings { // User agent strings. private static final String DESKTOP_USERAGENT = - "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en)" - + " AppleWebKit/528.5+ (KHTML, like Gecko) Version/3.1.2" - + " Safari/525.20.1"; + "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us)" + + " AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0" + + " Safari/530.17"; 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"; + "Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_0 like Mac OS X; en-us)" + + " AppleWebKit/528.18 (KHTML, like Gecko) Version/4.0" + + " Mobile/7A341 Safari/528.16"; private static Locale sLocale; private static Object sLockForLocaleSettings; @@ -351,11 +374,13 @@ public class WebSettings { // default to "en" buffer.append("en"); } - - final String model = Build.MODEL; - if (model.length() > 0) { - buffer.append("; "); - buffer.append(model); + // add the model for the release build + if ("REL".equals(Build.VERSION.CODENAME)) { + final String model = Build.MODEL; + if (model.length() > 0) { + buffer.append("; "); + buffer.append(model); + } } final String id = Build.ID; if (id.length() > 0) { @@ -425,6 +450,20 @@ public class WebSettings { } /** + * Set whether the WebView loads a page with overview mode. + */ + public void setLoadWithOverviewMode(boolean overview) { + mLoadWithOverviewMode = overview; + } + + /** + * Returns true if this WebView loads page with overview mode + */ + public boolean getLoadWithOverviewMode() { + return mLoadWithOverviewMode; + } + + /** * Store whether the WebView is saving form data. */ public void setSaveFormData(boolean save) { @@ -480,7 +519,6 @@ public class WebSettings { * thread. * @param zoom A ZoomDensity value * @see WebSettings.ZoomDensity - * @hide Pending API council approval */ public void setDefaultZoom(ZoomDensity zoom) { if (mDefaultZoom != zoom) { @@ -494,7 +532,6 @@ public class WebSettings { * thread. * @return A ZoomDensity value * @see WebSettings.ZoomDensity - * @hide Pending API council approval */ public ZoomDensity getDefaultZoom() { return mDefaultZoom; @@ -939,13 +976,144 @@ public class WebSettings { } /** - * Set a custom path to plugins used by the WebView. The client - * must ensure it exists before this call. - * @param pluginsPath String path to the directory containing plugins. + * TODO: need to add @Deprecated */ public synchronized void setPluginsPath(String pluginsPath) { - if (pluginsPath != null && !pluginsPath.equals(mPluginsPath)) { - mPluginsPath = pluginsPath; + } + + /** + * Set the path to where database storage API databases should be saved. + * This will update WebCore when the Sync runs in the C++ side. + * @param databasePath String path to the directory where databases should + * be saved. May be the empty string but should never be null. + */ + public synchronized void setDatabasePath(String databasePath) { + if (databasePath != null && !databasePath.equals(mDatabasePath)) { + mDatabasePath = databasePath; + postSync(); + } + } + + /** + * Set the path where the Geolocation permissions database should be saved. + * This will update WebCore when the Sync runs in the C++ side. + * @param databasePath String path to the directory where the Geolocation + * permissions database should be saved. May be the empty string but + * should never be null. + */ + public synchronized void setGeolocationDatabasePath(String databasePath) { + if (databasePath != null && !databasePath.equals(mDatabasePath)) { + mGeolocationDatabasePath = databasePath; + postSync(); + } + } + + /** + * Tell the WebView to enable Application Caches API. + * @param flag True if the WebView should enable Application Caches. + */ + public synchronized void setAppCacheEnabled(boolean flag) { + if (mAppCacheEnabled != flag) { + mAppCacheEnabled = flag; + postSync(); + } + } + + /** + * Set a custom path to the Application Caches files. The client + * must ensure it exists before this call. + * @param appCachePath String path to the directory containing Application + * Caches files. The appCache path can be the empty string but should not + * be null. Passing null for this parameter will result in a no-op. + */ + public synchronized void setAppCachePath(String appCachePath) { + if (appCachePath != null && !appCachePath.equals(mAppCachePath)) { + mAppCachePath = appCachePath; + postSync(); + } + } + + /** + * Set the maximum size for the Application Caches content. + * @param appCacheMaxSize the maximum size in bytes. + */ + public synchronized void setAppCacheMaxSize(long appCacheMaxSize) { + if (appCacheMaxSize != mAppCacheMaxSize) { + mAppCacheMaxSize = appCacheMaxSize; + postSync(); + } + } + + /** + * Set whether the database storage API is enabled. + * @param flag boolean True if the WebView should use the database storage + * API. + */ + public synchronized void setDatabaseEnabled(boolean flag) { + if (mDatabaseEnabled != flag) { + mDatabaseEnabled = flag; + postSync(); + } + } + + /** + * Set whether the DOM storage API is enabled. + * @param flag boolean True if the WebView should use the DOM storage + * API. + */ + public synchronized void setDomStorageEnabled(boolean flag) { + if (mDomStorageEnabled != flag) { + mDomStorageEnabled = flag; + postSync(); + } + } + + /** + * Returns true if the DOM Storage API's are enabled. + * @return True if the DOM Storage API's are enabled. + */ + public synchronized boolean getDomStorageEnabled() { + return mDomStorageEnabled; + } + + /** + * Return the path to where database storage API databases are saved for + * the current WebView. + * @return the String path to the database storage API databases. + */ + public synchronized String getDatabasePath() { + return mDatabasePath; + } + + /** + * Returns true if database storage API is enabled. + * @return True if the database storage API is enabled. + */ + public synchronized boolean getDatabaseEnabled() { + return mDatabaseEnabled; + } + + /** + * Tell the WebView to enable WebWorkers API. + * @param flag True if the WebView should enable WebWorkers. + * Note that this flag only affects V8. JSC does not have + * an equivalent setting. + * @hide pending api council approval + */ + public synchronized void setWorkersEnabled(boolean flag) { + if (mWorkersEnabled != flag) { + mWorkersEnabled = flag; + postSync(); + } + } + + /** + * Sets whether Geolocation is enabled. + * @param flag Whether Geolocation should be enabled. + */ + public synchronized void setGeolocationEnabled(boolean flag) { + if (mGeolocationEnabled != flag) { + mGeolocationEnabled = flag; postSync(); } } @@ -967,11 +1135,10 @@ public class WebSettings { } /** - * Return the current path used for plugins in the WebView. - * @return The string path to the WebView plugins. + * TODO: need to add @Deprecated */ public synchronized String getPluginsPath() { - return mPluginsPath; + return ""; } /** @@ -1146,6 +1313,19 @@ public class WebSettings { } } + int getDoubleTapToastCount() { + return mDoubleTapToastCount; + } + + void setDoubleTapToastCount(int count) { + if (mDoubleTapToastCount != count) { + mDoubleTapToastCount = count; + // write the settings in the non-UI thread + mEventHandler.sendMessage(Message.obtain(null, + EventHandler.SET_DOUBLE_TAP_TOAST_COUNT)); + } + } + /** * Transfer messages from the queue to the new WebCoreThread. Called from * WebCore thread. @@ -1153,15 +1333,31 @@ public class WebSettings { /*package*/ synchronized void syncSettingsAndCreateHandler(BrowserFrame frame) { mBrowserFrame = frame; - if (WebView.DEBUG) { + if (DebugFlags.WEB_SETTINGS) { junit.framework.Assert.assertTrue(frame.mNativeFrame != 0); } - checkGearsPermissions(); + + GoogleLocationSettingManager.getInstance().start(mContext); + + SharedPreferences sp = mContext.getSharedPreferences(PREF_FILE, + Context.MODE_PRIVATE); + if (mDoubleTapToastCount > 0) { + mDoubleTapToastCount = sp.getInt(DOUBLE_TAP_TOAST_COUNT, + mDoubleTapToastCount); + } nativeSync(frame.mNativeFrame); mSyncPending = false; mEventHandler.createHandler(); } + /** + * Let the Settings object know that our owner is being destroyed. + */ + /*package*/ + synchronized void onDestroyed() { + GoogleLocationSettingManager.getInstance().stop(); + } + private int pin(int size) { // FIXME: 72 is just an arbitrary max text size value. if (size < 1) { @@ -1172,23 +1368,6 @@ public class WebSettings { return size; } - private void checkGearsPermissions() { - // Did we already check the permissions at startup? - if (sGearsPermissionsManager != null) { - return; - } - // Is the pluginsPath sane? - String pluginsPath = getPluginsPath(); - if (pluginsPath == null || pluginsPath.length() == 0) { - // We don't yet have a meaningful plugin path, so - // we can't do anything about the Gears permissions. - return; - } - sGearsPermissionsManager = - new GearsPermissionsManager(mContext, pluginsPath); - sGearsPermissionsManager.doCheckAndStartObserver(); - } - /* Post a SYNC message to handle syncing the native settings. */ private synchronized void postSync() { // Only post if a sync is not pending diff --git a/core/java/android/webkit/WebStorage.java b/core/java/android/webkit/WebStorage.java new file mode 100644 index 0000000..a182287 --- /dev/null +++ b/core/java/android/webkit/WebStorage.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +import java.util.Collection; +import java.util.Map; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Functionality for manipulating the webstorage databases. + */ +public final class WebStorage { + + /** + * Encapsulates a callback function to be executed when a new quota is made + * available. We primarily want this to allow us to call back the sleeping + * WebCore thread from outside the WebViewCore class (as the native call + * is private). It is imperative that this the setDatabaseQuota method is + * executed once a decision to either allow or deny new quota is made, + * otherwise the WebCore thread will remain asleep. + */ + public interface QuotaUpdater { + public void updateQuota(long newQuota); + }; + + // Log tag + private static final String TAG = "webstorage"; + + // Global instance of a WebStorage + private static WebStorage sWebStorage; + + // Message ids + static final int UPDATE = 0; + static final int SET_QUOTA_ORIGIN = 1; + static final int DELETE_ORIGIN = 2; + static final int DELETE_ALL = 3; + static final int GET_ORIGINS = 4; + static final int GET_USAGE_ORIGIN = 5; + static final int GET_QUOTA_ORIGIN = 6; + + // Message ids on the UI thread + static final int RETURN_ORIGINS = 0; + static final int RETURN_USAGE_ORIGIN = 1; + static final int RETURN_QUOTA_ORIGIN = 2; + + private static final String ORIGINS = "origins"; + private static final String ORIGIN = "origin"; + private static final String CALLBACK = "callback"; + private static final String USAGE = "usage"; + private static final String QUOTA = "quota"; + + private Map <String, Origin> mOrigins; + + private Handler mHandler = null; + private Handler mUIHandler = null; + + static class Origin { + String mOrigin = null; + long mQuota = 0; + long mUsage = 0; + + public Origin(String origin, long quota, long usage) { + mOrigin = origin; + mQuota = quota; + mUsage = usage; + } + + public Origin(String origin, long quota) { + mOrigin = origin; + mQuota = quota; + } + + public Origin(String origin) { + mOrigin = origin; + } + + public String getOrigin() { + return mOrigin; + } + + public long getQuota() { + return mQuota; + } + + public long getUsage() { + return mUsage; + } + } + + /** + * @hide + * Message handler, UI side + */ + public void createUIHandler() { + if (mUIHandler == null) { + mUIHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case RETURN_ORIGINS: { + Map values = (Map) msg.obj; + Map origins = (Map) values.get(ORIGINS); + ValueCallback<Map> callback = (ValueCallback<Map>) values.get(CALLBACK); + callback.onReceiveValue(origins); + } break; + + case RETURN_USAGE_ORIGIN: { + Map values = (Map) msg.obj; + ValueCallback<Long> callback = (ValueCallback<Long>) values.get(CALLBACK); + callback.onReceiveValue((Long)values.get(USAGE)); + } break; + + case RETURN_QUOTA_ORIGIN: { + Map values = (Map) msg.obj; + ValueCallback<Long> callback = (ValueCallback<Long>) values.get(CALLBACK); + callback.onReceiveValue((Long)values.get(QUOTA)); + } break; + } + } + }; + } + } + + /** + * @hide + * Message handler, webcore side + */ + public void createHandler() { + if (mHandler == null) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SET_QUOTA_ORIGIN: { + Origin website = (Origin) msg.obj; + nativeSetQuotaForOrigin(website.getOrigin(), + website.getQuota()); + } break; + + case DELETE_ORIGIN: { + Origin website = (Origin) msg.obj; + nativeDeleteOrigin(website.getOrigin()); + } break; + + case DELETE_ALL: + nativeDeleteAllData(); + break; + + case GET_ORIGINS: { + syncValues(); + ValueCallback callback = (ValueCallback) msg.obj; + Map origins = new HashMap(mOrigins); + Map values = new HashMap<String, Object>(); + values.put(CALLBACK, callback); + values.put(ORIGINS, origins); + postUIMessage(Message.obtain(null, RETURN_ORIGINS, values)); + } break; + + case GET_USAGE_ORIGIN: { + syncValues(); + Map values = (Map) msg.obj; + String origin = (String) values.get(ORIGIN); + ValueCallback callback = (ValueCallback) values.get(CALLBACK); + Origin website = mOrigins.get(origin); + Map retValues = new HashMap<String, Object>(); + retValues.put(CALLBACK, callback); + if (website != null) { + long usage = website.getUsage(); + retValues.put(USAGE, new Long(usage)); + } + postUIMessage(Message.obtain(null, RETURN_USAGE_ORIGIN, retValues)); + } break; + + case GET_QUOTA_ORIGIN: { + syncValues(); + Map values = (Map) msg.obj; + String origin = (String) values.get(ORIGIN); + ValueCallback callback = (ValueCallback) values.get(CALLBACK); + Origin website = mOrigins.get(origin); + Map retValues = new HashMap<String, Object>(); + retValues.put(CALLBACK, callback); + if (website != null) { + long quota = website.getQuota(); + retValues.put(QUOTA, new Long(quota)); + } + postUIMessage(Message.obtain(null, RETURN_QUOTA_ORIGIN, retValues)); + } break; + + case UPDATE: + syncValues(); + break; + } + } + }; + } + } + + /* + * When calling getOrigins(), getUsageForOrigin() and getQuotaForOrigin(), + * we need to get the values from webcore, but we cannot block while doing so + * as we used to do, as this could result in a full deadlock (other webcore + * messages received while we are still blocked here, see http://b/2127737). + * + * We have to do everything asynchronously, by providing a callback function. + * We post a message on the webcore thread (mHandler) that will get the result + * from webcore, and we post it back on the UI thread (using mUIHandler). + * We can then use the callback function to return the value. + */ + + /** + * Returns a list of origins having a database + */ + public void getOrigins(ValueCallback<Map> callback) { + if (callback != null) { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + syncValues(); + callback.onReceiveValue(mOrigins); + } else { + postMessage(Message.obtain(null, GET_ORIGINS, callback)); + } + } + } + + /** + * Returns a list of origins having a database + * should only be called from WebViewCore. + */ + Collection<Origin> getOriginsSync() { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + update(); + return mOrigins.values(); + } + return null; + } + + /** + * Returns the use for a given origin + */ + public void getUsageForOrigin(String origin, ValueCallback<Long> callback) { + if (callback == null) { + return; + } + if (origin == null) { + callback.onReceiveValue(null); + return; + } + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + syncValues(); + Origin website = mOrigins.get(origin); + callback.onReceiveValue(new Long(website.getUsage())); + } else { + HashMap values = new HashMap<String, Object>(); + values.put(ORIGIN, origin); + values.put(CALLBACK, callback); + postMessage(Message.obtain(null, GET_USAGE_ORIGIN, values)); + } + } + + /** + * Returns the quota for a given origin + */ + public void getQuotaForOrigin(String origin, ValueCallback<Long> callback) { + if (callback == null) { + return; + } + if (origin == null) { + callback.onReceiveValue(null); + return; + } + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + syncValues(); + Origin website = mOrigins.get(origin); + callback.onReceiveValue(new Long(website.getUsage())); + } else { + HashMap values = new HashMap<String, Object>(); + values.put(ORIGIN, origin); + values.put(CALLBACK, callback); + postMessage(Message.obtain(null, GET_QUOTA_ORIGIN, values)); + } + } + + /** + * Set the quota for a given origin + */ + public void setQuotaForOrigin(String origin, long quota) { + if (origin != null) { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + nativeSetQuotaForOrigin(origin, quota); + } else { + postMessage(Message.obtain(null, SET_QUOTA_ORIGIN, + new Origin(origin, quota))); + } + } + } + + /** + * Delete a given origin + */ + public void deleteOrigin(String origin) { + if (origin != null) { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + nativeDeleteOrigin(origin); + } else { + postMessage(Message.obtain(null, DELETE_ORIGIN, + new Origin(origin))); + } + } + } + + /** + * Delete all databases + */ + public void deleteAllData() { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + nativeDeleteAllData(); + } else { + postMessage(Message.obtain(null, DELETE_ALL)); + } + } + + /** + * Utility function to send a message to our handler + */ + private void postMessage(Message msg) { + if (mHandler != null) { + mHandler.sendMessage(msg); + } + } + + /** + * Utility function to send a message to the handler on the UI thread + */ + private void postUIMessage(Message msg) { + if (mUIHandler != null) { + mUIHandler.sendMessage(msg); + } + } + + /** + * Get the global instance of WebStorage. + * @return A single instance of WebStorage. + */ + public static WebStorage getInstance() { + if (sWebStorage == null) { + sWebStorage = new WebStorage(); + } + return sWebStorage; + } + + /** + * @hide + * Post a Sync request + */ + public void update() { + if (WebViewCore.THREAD_NAME.equals(Thread.currentThread().getName())) { + syncValues(); + } else { + postMessage(Message.obtain(null, UPDATE)); + } + } + + /** + * Run on the webcore thread + * set the local values with the current ones + */ + private void syncValues() { + Set<String> tmp = nativeGetOrigins(); + mOrigins = new HashMap<String, Origin>(); + for (String origin : tmp) { + Origin website = new Origin(origin, + nativeGetUsageForOrigin(origin), + nativeGetQuotaForOrigin(origin)); + mOrigins.put(origin, website); + } + } + + // Native functions + private static native Set nativeGetOrigins(); + private static native long nativeGetUsageForOrigin(String origin); + private static native long nativeGetQuotaForOrigin(String origin); + private static native void nativeSetQuotaForOrigin(String origin, long quota); + private static native void nativeDeleteOrigin(String origin); + private static native void nativeDeleteAllData(); +} diff --git a/core/java/android/webkit/WebSyncManager.java b/core/java/android/webkit/WebSyncManager.java index ded17ed..d3ec603 100644 --- a/core/java/android/webkit/WebSyncManager.java +++ b/core/java/android/webkit/WebSyncManager.java @@ -47,7 +47,7 @@ abstract class WebSyncManager implements Runnable { @Override public void handleMessage(Message msg) { if (msg.what == SYNC_MESSAGE) { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager sync ***"); } syncFromRamToFlash(); @@ -94,7 +94,7 @@ abstract class WebSyncManager implements Runnable { * sync() forces sync manager to sync now */ public void sync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager sync ***"); } if (mHandler == null) { @@ -109,7 +109,7 @@ abstract class WebSyncManager implements Runnable { * resetSync() resets sync manager's timer */ public void resetSync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager resetSync ***"); } if (mHandler == null) { @@ -124,7 +124,7 @@ abstract class WebSyncManager implements Runnable { * startSync() requests sync manager to start sync */ public void startSync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager startSync ***, Ref count:" + mStartSyncRefCount); } @@ -142,7 +142,7 @@ abstract class WebSyncManager implements Runnable { * the queue to break the sync loop */ public void stopSync() { - if (WebView.LOGV_ENABLED) { + if (DebugFlags.WEB_SYNC_MANAGER) { Log.v(LOGTAG, "*** WebSyncManager stopSync ***, Ref count:" + mStartSyncRefCount); } diff --git a/core/java/android/webkit/TextDialog.java b/core/java/android/webkit/WebTextView.java index 99de56d..e0d41c2 100644 --- a/core/java/android/webkit/TextDialog.java +++ b/core/java/android/webkit/WebTextView.java @@ -16,15 +16,16 @@ package android.webkit; +import com.android.internal.widget.EditableInputConnection; + import android.content.Context; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.ColorFilter; import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.Rect; -import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.RectShape; import android.text.Editable; import android.text.InputFilter; import android.text.Selection; @@ -32,13 +33,18 @@ import android.text.Spannable; import android.text.TextPaint; import android.text.TextUtils; import android.text.method.MovementMethod; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.InputMethodManager; +import android.text.method.Touch; +import android.util.Log; +import android.view.Gravity; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputConnection; import android.widget.AbsoluteLayout.LayoutParams; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; @@ -47,11 +53,13 @@ import android.widget.TextView; import java.util.ArrayList; /** - * TextDialog is a specialized version of EditText used by WebView + * WebTextView is a specialized version of EditText used by WebView * to overlay html textfields (and textareas) to use our standard * text editing. */ -/* package */ class TextDialog extends AutoCompleteTextView { +/* package */ class WebTextView extends AutoCompleteTextView { + + static final String LOGTAG = "webtextview"; private WebView mWebView; private boolean mSingle; @@ -62,13 +70,29 @@ import java.util.ArrayList; // on the enter key. The method for blocking unmatched key ups prevents // the shift key from working properly. private boolean mGotEnterDown; - // mScrollToAccommodateCursor being set to false prevents us from scrolling - // the cursor on screen when using the trackball to select a textfield. - private boolean mScrollToAccommodateCursor; private int mMaxLength; // Keep track of the text before the change so we know whether we actually // need to send down the DOM events. private String mPreChange; + private Drawable mBackground; + // Variables for keeping track of the touch down, to send to the WebView + // when a drag starts + private float mDragStartX; + private float mDragStartY; + private long mDragStartTime; + private boolean mDragSent; + // True if the most recent drag event has caused either the TextView to + // scroll or the web page to scroll. Gets reset after a touch down. + private boolean mScrolled; + // Gets set to true when the the IME jumps to the next textfield. When this + // happens, the next time the user hits a key it is okay for the focus + // pointer to not match the WebTextView's node pointer + boolean mOkayForFocusNotToMatch; + // Whether or not a selection change was generated from webkit. If it was, + // we do not need to pass the selection back to webkit. + private boolean mFromWebKit; + private boolean mGotTouchDown; + private boolean mInSetTextAndKeepSelection; // Array to store the final character added in onTextChanged, so that its // KeyEvents may be determined. private char[] mCharacter = new char[1]; @@ -79,39 +103,14 @@ import java.util.ArrayList; private static final InputFilter[] NO_FILTERS = new InputFilter[0]; /** - * Create a new TextDialog. - * @param context The Context for this TextDialog. + * Create a new WebTextView. + * @param context The Context for this WebTextView. * @param webView The WebView that created this. */ - /* package */ TextDialog(Context context, WebView webView) { + /* package */ WebTextView(Context context, WebView webView) { super(context); mWebView = webView; - ShapeDrawable background = new ShapeDrawable(new RectShape()); - Paint shapePaint = background.getPaint(); - shapePaint.setStyle(Paint.Style.STROKE); - ColorDrawable color = new ColorDrawable(Color.WHITE); - Drawable[] array = new Drawable[2]; - array[0] = color; - array[1] = background; - LayerDrawable layers = new LayerDrawable(array); - // Hide WebCore's text behind this and allow the WebView - // to draw its own focusring. - setBackgroundDrawable(layers); - // Align the text better with the text behind it, so moving - // off of the textfield will not appear to move the text. - setPadding(3, 2, 0, 0); mMaxLength = -1; - // Turn on subpixel text, and turn off kerning, so it better matches - // the text in webkit. - TextPaint paint = getPaint(); - int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG | - Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG; - paint.setFlags(flags); - // Set the text color to black, regardless of the theme. This ensures - // that other applications that use embedded WebViews will properly - // display the text in textfields. - setTextColor(Color.BLACK); - setImeOptions(EditorInfo.IME_ACTION_NONE); } @Override @@ -122,10 +121,39 @@ import java.util.ArrayList; // Treat ACTION_DOWN and ACTION MULTIPLE the same boolean down = event.getAction() != KeyEvent.ACTION_UP; int keyCode = event.getKeyCode(); + + boolean isArrowKey = false; + switch(keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + case KeyEvent.KEYCODE_DPAD_RIGHT: + case KeyEvent.KEYCODE_DPAD_UP: + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!mWebView.nativeCursorMatchesFocus()) { + return down ? mWebView.onKeyDown(keyCode, event) : mWebView + .onKeyUp(keyCode, event); + + } + isArrowKey = true; + break; + } + if (!isArrowKey && !mOkayForFocusNotToMatch + && mWebView.nativeFocusNodePointer() != mNodePointer) { + mWebView.nativeClearCursor(); + // Do not call remove() here, which hides the soft keyboard. If + // the soft keyboard is being displayed, the user will still want + // it there. + mWebView.removeView(this); + mWebView.requestFocus(); + return mWebView.dispatchKeyEvent(event); + } + // After a jump to next textfield and the first key press, the cursor + // and focus will once again match, so reset this value. + mOkayForFocusNotToMatch = false; + Spannable text = (Spannable) getText(); int oldLength = text.length(); // Normally the delete key's dom events are sent via onTextChanged. - // However, if the length is zero, the text did not change, so we + // However, if the length is zero, the text did not change, so we // go ahead and pass the key down immediately. if (KeyEvent.KEYCODE_DEL == keyCode && 0 == oldLength) { sendDomEvent(event); @@ -151,6 +179,10 @@ import java.util.ArrayList; if (isPopupShowing()) { return super.dispatchKeyEvent(event); } + if (!mWebView.nativeCursorMatchesFocus()) { + return down ? mWebView.onKeyDown(keyCode, event) : mWebView + .onKeyUp(keyCode, event); + } // Center key should be passed to a potential onClick if (!down) { mWebView.shortPressOnTextField(); @@ -177,7 +209,7 @@ import java.util.ArrayList; oldText = ""; } if (super.dispatchKeyEvent(event)) { - // If the TextDialog handled the key it was either an alphanumeric + // If the WebTextView handled the key it was either an alphanumeric // key, a delete, or a movement within the text. All of those are // ok to pass to javascript. @@ -187,27 +219,15 @@ import java.util.ArrayList; // so do not pass down to javascript, and instead // return true. If it is an arrow key or a delete key, we can go // ahead and pass it down. - boolean isArrowKey; - switch(keyCode) { - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - isArrowKey = true; - break; - case KeyEvent.KEYCODE_ENTER: - // For multi-line text boxes, newlines will - // trigger onTextChanged for key down (which will send both - // key up and key down) but not key up. - mGotEnterDown = true; - default: - isArrowKey = false; - break; + if (KeyEvent.KEYCODE_ENTER == keyCode) { + // For multi-line text boxes, newlines will + // trigger onTextChanged for key down (which will send both + // key up and key down) but not key up. + mGotEnterDown = true; } if (maxedOut && !isArrowKey && keyCode != KeyEvent.KEYCODE_DEL) { if (oldEnd == oldStart) { // Return true so the key gets dropped. - mScrollToAccommodateCursor = true; return true; } else if (!oldText.equals(getText().toString())) { // FIXME: This makes the text work properly, but it @@ -221,36 +241,33 @@ import java.util.ArrayList; int newEnd = Selection.getSelectionEnd(span); mWebView.replaceTextfieldText(0, oldLength, span.toString(), newStart, newEnd); - mScrollToAccommodateCursor = true; return true; } } + /* FIXME: + * In theory, we would like to send the events for the arrow keys. + * However, the TextView can arbitrarily change the selection (i.e. + * long press followed by using the trackball). Therefore, we keep + * in sync with the TextView via onSelectionChanged. If we also + * send the DOM event, we lose the correct selection. if (isArrowKey) { // Arrow key does not change the text, but we still want to send // the DOM events. sendDomEvent(event); } - mScrollToAccommodateCursor = true; + */ return true; } - // FIXME: TextViews return false for up and down key events even though - // they change the selection. Since we don't want the get out of sync - // with WebCore's notion of the current selection, reset the selection - // to what it was before the key event. - Selection.setSelection(text, oldStart, oldEnd); // Ignore the key up event for newlines. This prevents // multiple newlines in the native textarea. if (mGotEnterDown && !down) { return true; } // if it is a navigation key, pass it to WebView - if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT - || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT - || keyCode == KeyEvent.KEYCODE_DPAD_UP - || keyCode == KeyEvent.KEYCODE_DPAD_DOWN) { + if (isArrowKey) { // WebView check the trackballtime in onKeyDown to avoid calling - // native from both trackball and key handling. As this is called - // from TextDialog, we always want WebView to check with native. + // native from both trackball and key handling. As this is called + // from WebTextView, we always want WebView to check with native. // Reset trackballtime to ensure it. mWebView.resetTrackballTime(); return down ? mWebView.onKeyDown(keyCode, event) : mWebView @@ -260,50 +277,87 @@ import java.util.ArrayList; } /** - * Create a fake touch up event at (x,y) with respect to this TextDialog. - * This is used by WebView to act as though a touch event which happened - * before we placed the TextDialog actually hit it, so that it can place - * the cursor accordingly. - */ - /* package */ void fakeTouchEvent(float x, float y) { - // We need to ensure that there is a Layout, since the Layout is used - // in determining where to place the cursor. - if (getLayout() == null) { - measure(mWidthSpec, mHeightSpec); - } - // Create a fake touch up, which is used to place the cursor. - MotionEvent ev = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, - x, y, 0); - onTouchEvent(ev); - ev.recycle(); - } - - /** - * Determine whether this TextDialog currently represents the node + * Determine whether this WebTextView currently represents the node * represented by ptr. * @param ptr Pointer to a node to compare to. - * @return boolean Whether this TextDialog already represents the node + * @return boolean Whether this WebTextView already represents the node * pointed to by ptr. */ /* package */ boolean isSameTextField(int ptr) { return ptr == mNodePointer; } + @Override public InputConnection onCreateInputConnection( + EditorInfo outAttrs) { + InputConnection connection = super.onCreateInputConnection(outAttrs); + if (mWebView != null) { + // Use the name of the textfield + the url. Use backslash as an + // arbitrary separator. + outAttrs.fieldName = mWebView.nativeFocusCandidateName() + "\\" + + mWebView.getUrl(); + } + return connection; + } + @Override - public boolean onPreDraw() { - if (getLayout() == null) { - measure(mWidthSpec, mHeightSpec); + public void onEditorAction(int actionCode) { + switch (actionCode) { + case EditorInfo.IME_ACTION_NEXT: + mWebView.nativeMoveCursorToNextTextInput(); + // Preemptively rebuild the WebTextView, so that the action will + // be set properly. + mWebView.rebuildWebTextView(); + // Since the cursor will no longer be in the same place as the + // focus, set the focus controller back to inactive + mWebView.setFocusControllerInactive(); + mWebView.invalidate(); + mOkayForFocusNotToMatch = true; + break; + case EditorInfo.IME_ACTION_DONE: + super.onEditorAction(actionCode); + break; + case EditorInfo.IME_ACTION_GO: + // Send an enter and hide the soft keyboard + InputMethodManager.getInstance(mContext) + .hideSoftInputFromWindow(getWindowToken(), 0); + sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_ENTER)); + sendDomEvent(new KeyEvent(KeyEvent.ACTION_UP, + KeyEvent.KEYCODE_ENTER)); + + default: + break; } - return super.onPreDraw(); } - + + @Override + protected void onSelectionChanged(int selStart, int selEnd) { + // This code is copied from TextView.onDraw(). That code does not get + // executed, however, because the WebTextView does not draw, allowing + // webkit's drawing to show through. + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null && imm.isActive(this)) { + Spannable sp = (Spannable) getText(); + int candStart = EditableInputConnection.getComposingSpanStart(sp); + int candEnd = EditableInputConnection.getComposingSpanEnd(sp); + imm.updateSelection(this, selStart, selEnd, candStart, candEnd); + } + if (!mFromWebKit && mWebView != null) { + if (DebugFlags.WEB_TEXT_VIEW) { + Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart + + " selEnd=" + selEnd); + } + mWebView.setSelection(selStart, selEnd); + } + } + @Override protected void onTextChanged(CharSequence s,int start,int before,int count){ super.onTextChanged(s, start, before, count); String postChange = s.toString(); // Prevent calls to setText from invoking onTextChanged (since this will // mean we are on a different textfield). Also prevent the change when - // going from a textfield with a string of text to one with a smaller + // going from a textfield with a string of text to one with a smaller // limit on text length from registering the onTextChanged event. if (mPreChange == null || mPreChange.equals(postChange) || (mMaxLength > -1 && mPreChange.length() > mMaxLength && @@ -311,8 +365,7 @@ import java.util.ArrayList; return; } mPreChange = postChange; - // This was simply a delete or a cut, so just delete the - // selection. + // This was simply a delete or a cut, so just delete the selection. if (before > 0 && 0 == count) { mWebView.deleteSelection(start, start + before); // For this and all changes to the text, update our cache @@ -337,24 +390,113 @@ import java.util.ArrayList; start + count - charactersFromKeyEvents, start + count - charactersFromKeyEvents); } else { - // This corrects the selection which may have been affected by the + // This corrects the selection which may have been affected by the // trackball or auto-correct. - mWebView.setSelection(start, start + before); + if (DebugFlags.WEB_TEXT_VIEW) { + Log.v(LOGTAG, "onTextChanged start=" + start + + " start + before=" + (start + before)); + } + if (!mInSetTextAndKeepSelection) { + mWebView.setSelection(start, start + before); + } } - updateCachedTextfield(); - if (cannotUseKeyEvents) { - return; + if (!cannotUseKeyEvents) { + int length = events.length; + for (int i = 0; i < length; i++) { + // We never send modifier keys to native code so don't send them + // here either. + if (!KeyEvent.isModifierKey(events[i].getKeyCode())) { + sendDomEvent(events[i]); + } + } } - int length = events.length; - for (int i = 0; i < length; i++) { - // We never send modifier keys to native code so don't send them - // here either. - if (!KeyEvent.isModifierKey(events[i].getKeyCode())) { - sendDomEvent(events[i]); + updateCachedTextfield(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + super.onTouchEvent(event); + // This event may be the start of a drag, so store it to pass to the + // WebView if it is. + mDragStartX = event.getX(); + mDragStartY = event.getY(); + mDragStartTime = event.getEventTime(); + mDragSent = false; + mScrolled = false; + mGotTouchDown = true; + break; + case MotionEvent.ACTION_MOVE: + int slop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + Spannable buffer = getText(); + int initialScrollX = Touch.getInitialScrollX(this, buffer); + int initialScrollY = Touch.getInitialScrollY(this, buffer); + super.onTouchEvent(event); + int dx = Math.abs(mScrollX - initialScrollX); + int dy = Math.abs(mScrollY - initialScrollY); + // Use a smaller slop when checking to see if we've moved far enough + // to scroll the text, because experimentally, slop has shown to be + // to big for the case of a small textfield. + int smallerSlop = slop/2; + if (dx > smallerSlop || dy > smallerSlop) { + if (mWebView != null) { + float maxScrollX = (float) Touch.getMaxScrollX(this, + getLayout(), mScrollY); + if (DebugFlags.WEB_TEXT_VIEW) { + Log.v(LOGTAG, "onTouchEvent x=" + mScrollX + " y=" + + mScrollY + " maxX=" + maxScrollX); + } + mWebView.scrollFocusedTextInput(maxScrollX > 0 ? + mScrollX / maxScrollX : 0, mScrollY); + } + mScrolled = true; + return true; + } + if (Math.abs((int) event.getX() - mDragStartX) < slop + && Math.abs((int) event.getY() - mDragStartY) < slop) { + // If the user has not scrolled further than slop, we should not + // send the drag. Instead, do nothing, and when the user lifts + // their finger, we will change the selection. + return true; + } + if (mWebView != null) { + // Only want to set the initial state once. + if (!mDragSent) { + mWebView.initiateTextFieldDrag(mDragStartX, mDragStartY, + mDragStartTime); + mDragSent = true; + } + boolean scrolled = mWebView.textFieldDrag(event); + if (scrolled) { + mScrolled = true; + cancelLongPress(); + return true; + } + } + return false; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + if (!mScrolled) { + // If the page scrolled, or the TextView scrolled, we do not + // want to change the selection + cancelLongPress(); + if (mGotTouchDown && mWebView != null) { + mWebView.touchUpOnTextField(event); + } + } + // Necessary for the WebView to reset its state + if (mWebView != null && mDragSent) { + mWebView.onTouchEvent(event); } + mGotTouchDown = false; + break; + default: + break; } + return true; } - + @Override public boolean onTrackballEvent(MotionEvent event) { if (isPopupShowing()) { @@ -363,29 +505,23 @@ import java.util.ArrayList; if (event.getAction() != MotionEvent.ACTION_MOVE) { return false; } + // If the Cursor is not on the text input, webview should handle the + // trackball + if (!mWebView.nativeCursorMatchesFocus()) { + return mWebView.onTrackballEvent(event); + } Spannable text = (Spannable) getText(); MovementMethod move = getMovementMethod(); if (move != null && getLayout() != null && move.onTrackballEvent(this, text, event)) { - // Need to pass down the selection, which has changed. - // FIXME: This should work, but does not, so we set the selection - // in onTextChanged. - //int start = Selection.getSelectionStart(text); - //int end = Selection.getSelectionEnd(text); - //mWebView.setSelection(start, end); + // Selection is changed in onSelectionChanged return true; } - // If the user is in a textfield, and the movement method is not - // handling the trackball events, it means they are at the end of the - // field and continuing to move the trackball. In this case, we should - // not scroll the cursor on screen bc the user may be attempting to - // scroll the page, possibly in the opposite direction of the cursor. - mScrollToAccommodateCursor = false; return false; } /** - * Remove this TextDialog from its host WebView, and return + * Remove this WebTextView from its host WebView, and return * focus to the host. */ /* package */ void remove() { @@ -394,11 +530,6 @@ import java.util.ArrayList; getWindowToken(), 0); mWebView.removeView(this); mWebView.requestFocus(); - mScrollToAccommodateCursor = false; - } - - /* package */ void enableScrollOnScreen(boolean enable) { - mScrollToAccommodateCursor = enable; } /* package */ void bringIntoView() { @@ -407,14 +538,6 @@ import java.util.ArrayList; } } - @Override - public boolean requestRectangleOnScreen(Rect rectangle) { - if (mScrollToAccommodateCursor) { - return super.requestRectangleOnScreen(rectangle); - } - return false; - } - /** * Send the DOM events for the specified event. * @param event KeyEvent to be translated into a DOM event. @@ -425,7 +548,7 @@ import java.util.ArrayList; /** * Always use this instead of setAdapter, as this has features specific to - * the TextDialog. + * the WebTextView. */ public void setAdapterCustom(AutoCompleteAdapter adapter) { if (adapter != null) { @@ -476,7 +599,65 @@ import java.util.ArrayList; if (inPassword) { setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo. TYPE_TEXT_VARIATION_PASSWORD); + createBackground(); + } + // For password fields, draw the WebTextView. For others, just show + // webkit's drawing. + setWillNotDraw(!inPassword); + setBackgroundDrawable(inPassword ? mBackground : null); + // For non-password fields, avoid the invals from TextView's blinking + // cursor + setCursorVisible(inPassword); + } + + /** + * Private class used for the background of a password textfield. + */ + private static class OutlineDrawable extends Drawable { + public void draw(Canvas canvas) { + Rect bounds = getBounds(); + Paint paint = new Paint(); + paint.setAntiAlias(true); + // Draw the background. + paint.setColor(Color.WHITE); + canvas.drawRect(bounds, paint); + // Draw the outline. + paint.setStyle(Paint.Style.STROKE); + paint.setColor(Color.BLACK); + canvas.drawRect(bounds, paint); + } + // Always want it to be opaque. + public int getOpacity() { + return PixelFormat.OPAQUE; } + // These are needed because they are abstract in Drawable. + public void setAlpha(int alpha) { } + public void setColorFilter(ColorFilter cf) { } + } + + /** + * Create a background for the WebTextView and set up the paint for drawing + * the text. This way, we can see the password transformation of the + * system, which (optionally) shows the actual text before changing to dots. + * The background is necessary to hide the webkit-drawn text beneath. + */ + private void createBackground() { + if (mBackground != null) { + return; + } + mBackground = new OutlineDrawable(); + + setGravity(Gravity.CENTER_VERTICAL); + // Turn on subpixel text, and turn off kerning, so it better matches + // the text in webkit. + TextPaint paint = getPaint(); + int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG | + Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG; + paint.setFlags(flags); + // Set the text color to black, regardless of the theme. This ensures + // that other applications that use embedded WebViews will properly + // display the text in password textfields. + setTextColor(Color.BLACK); } /* package */ void setMaxLength(int maxLength) { @@ -491,16 +672,16 @@ import java.util.ArrayList; /** * Set the pointer for this node so it can be determined which node this - * TextDialog represents. + * WebTextView represents. * @param ptr Integer representing the pointer to the node which this - * TextDialog represents. + * WebTextView represents. */ /* package */ void setNodePointer(int ptr) { mNodePointer = ptr; } /** - * Determine the position and size of TextDialog, and add it to the + * Determine the position and size of WebTextView, and add it to the * WebView's view heirarchy. All parameters are presumed to be in * view coordinates. Also requests Focus and sets the cursor to not * request to be in view. @@ -531,6 +712,19 @@ import java.util.ArrayList; } /** + * Set the selection, and disable our onSelectionChanged action. + */ + /* package */ void setSelectionFromWebKit(int start, int end) { + if (start < 0 || end < 0) return; + Spannable text = (Spannable) getText(); + int length = text.length(); + if (start > length || end > length) return; + mFromWebKit = true; + Selection.setSelection(text, start, end); + mFromWebKit = false; + } + + /** * Set whether this is a single-line textfield or a multi-line textarea. * Textfields scroll horizontally, and do not handle the enter key. * Textareas behave oppositely. @@ -540,10 +734,26 @@ import java.util.ArrayList; public void setSingleLine(boolean single) { int inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; - if (!single) { + if (single) { + int action = mWebView.nativeTextFieldAction(); + switch (action) { + // Keep in sync with CachedRoot::ImeAction + case 0: // NEXT + setImeOptions(EditorInfo.IME_ACTION_NEXT); + break; + case 1: // GO + setImeOptions(EditorInfo.IME_ACTION_GO); + break; + case -1: // FAILURE + case 2: // DONE + setImeOptions(EditorInfo.IME_ACTION_DONE); + break; + } + } else { inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; + setImeOptions(EditorInfo.IME_ACTION_NONE); } mSingle = single; setHorizontallyScrolling(single); @@ -551,8 +761,8 @@ import java.util.ArrayList; } /** - * Set the text for this TextDialog, and set the selection to (start, end) - * @param text Text to go into this TextDialog. + * Set the text for this WebTextView, and set the selection to (start, end) + * @param text Text to go into this WebTextView. * @param start Beginning of the selection. * @param end End of the selection. */ @@ -569,6 +779,10 @@ import java.util.ArrayList; } else if (start > length) { start = length; } + if (DebugFlags.WEB_TEXT_VIEW) { + Log.v(LOGTAG, "setText start=" + start + + " end=" + end); + } Selection.setSelection(span, start, end); } @@ -580,14 +794,26 @@ import java.util.ArrayList; /* package */ void setTextAndKeepSelection(String text) { mPreChange = text.toString(); Editable edit = (Editable) getText(); + mInSetTextAndKeepSelection = true; edit.replace(0, edit.length(), text); + mInSetTextAndKeepSelection = false; updateCachedTextfield(); } - + /** * Update the cache to reflect the current text. */ /* package */ void updateCachedTextfield() { mWebView.updateCachedTextfield(getText().toString()); } + + @Override + public boolean requestRectangleOnScreen(Rect rectangle) { + // don't scroll while in zoom animation. When it is done, we will adjust + // the WebTextView if it is in editing mode. + if (!mWebView.inAnimateZoom()) { + return super.requestRectangleOnScreen(rectangle); + } + return false; + } } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index 2892051..142dffb 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -25,12 +25,11 @@ import android.database.DataSetObserver; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Path; import android.graphics.Picture; import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; +import android.graphics.drawable.Drawable; import android.net.http.SslCertificate; import android.net.Uri; import android.os.Bundle; @@ -45,6 +44,7 @@ import android.text.Spannable; import android.util.AttributeSet; import android.util.EventLog; import android.util.Log; +import android.util.TypedValue; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -58,14 +58,13 @@ import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.animation.AlphaAnimation; import android.view.inputmethod.InputMethodManager; -import android.webkit.TextDialog.AutoCompleteAdapter; +import android.webkit.WebTextView.AutoCompleteAdapter; import android.webkit.WebViewCore.EventHub; import android.widget.AbsoluteLayout; import android.widget.Adapter; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.FrameLayout; -import android.widget.ImageView; import android.widget.ListView; import android.widget.Scroller; import android.widget.Toast; @@ -80,11 +79,11 @@ import java.io.FileOutputStream; import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; +import java.util.Map; /** - * <p>A View that displays web pages. This class is the basis upon which you + * <p>A View that displays web pages. This class is the basis upon which you * can roll your own web browser or simply display some online content within your Activity. * It uses the WebKit rendering engine to display * web pages and includes methods to navigate forward and backward @@ -93,7 +92,7 @@ import java.util.List; * {@link #getSettings() WebSettings}.{@link WebSettings#setBuiltInZoomControls(boolean)} * (introduced in API version 3). * <p>Note that, in order for your Activity to access the Internet and load web pages - * in a WebView, you must add the <var>INTERNET</var> permissions to your + * in a WebView, you must add the <var>INTERNET</var> permissions to your * Android Manifest file:</p> * <pre><uses-permission android:name="android.permission.INTERNET" /></pre> * @@ -195,7 +194,7 @@ import java.util.List; * changes, and then just leave the WebView alone. It'll automatically * re-orient itself as appropriate.</p> */ -public class WebView extends AbsoluteLayout +public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener { @@ -205,62 +204,52 @@ public class WebView extends AbsoluteLayout // true means redraw the screen all-the-time. Only with AUTO_REDRAW_HACK private boolean mAutoRedraw; - // keep debugging parameters near the top of the file static final String LOGTAG = "webview"; - static final boolean DEBUG = false; - static final boolean LOGV_ENABLED = DEBUG; - private class ExtendedZoomControls extends FrameLayout { + private static class ExtendedZoomControls extends FrameLayout { public ExtendedZoomControls(Context context, AttributeSet attrs) { super(context, attrs); LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(com.android.internal.R.layout.zoom_magnify, this, true); - mZoomControls = (ZoomControls) findViewById(com.android.internal.R.id.zoomControls); - mZoomMagnify = (ImageView) findViewById(com.android.internal.R.id.zoomMagnify); + mPlusMinusZoomControls = (ZoomControls) findViewById( + com.android.internal.R.id.zoomControls); + findViewById(com.android.internal.R.id.zoomMagnify).setVisibility( + View.GONE); } - + public void show(boolean showZoom, boolean canZoomOut) { - mZoomControls.setVisibility(showZoom ? View.VISIBLE : View.GONE); - mZoomMagnify.setVisibility(canZoomOut ? View.VISIBLE : View.GONE); + mPlusMinusZoomControls.setVisibility( + showZoom ? View.VISIBLE : View.GONE); fade(View.VISIBLE, 0.0f, 1.0f); } - + public void hide() { fade(View.GONE, 1.0f, 0.0f); } - + private void fade(int visibility, float startAlpha, float endAlpha) { AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha); anim.setDuration(500); startAnimation(anim); setVisibility(visibility); } - - public void setIsZoomMagnifyEnabled(boolean isEnabled) { - mZoomMagnify.setEnabled(isEnabled); - } - + public boolean hasFocus() { - return mZoomControls.hasFocus() || mZoomMagnify.hasFocus(); + return mPlusMinusZoomControls.hasFocus(); } - + public void setOnZoomInClickListener(OnClickListener listener) { - mZoomControls.setOnZoomInClickListener(listener); + mPlusMinusZoomControls.setOnZoomInClickListener(listener); } - + public void setOnZoomOutClickListener(OnClickListener listener) { - mZoomControls.setOnZoomOutClickListener(listener); - } - - public void setOnZoomMagnifyClickListener(OnClickListener listener) { - mZoomMagnify.setOnClickListener(listener); + mPlusMinusZoomControls.setOnZoomOutClickListener(listener); } - ZoomControls mZoomControls; - ImageView mZoomMagnify; + ZoomControls mPlusMinusZoomControls; } - + /** * Transportation object for returning WebView across thread boundaries. */ @@ -300,13 +289,13 @@ public class WebView extends AbsoluteLayout private WebViewCore mWebViewCore; // Handler for dispatching UI messages. /* package */ final Handler mPrivateHandler = new PrivateHandler(); - private TextDialog mTextEntry; + private WebTextView mWebTextView; // Used to ignore changes to webkit text that arrives to the UI side after // more key events. private int mTextGeneration; - // The list of loaded plugins. - private static PluginList sPluginList; + // Used by WebViewCore to create child views. + /* package */ final ViewManager mViewManager; /** * Position of the last touch event. @@ -326,15 +315,27 @@ public class WebView extends AbsoluteLayout /** * The minimum elapsed time before sending another ACTION_MOVE event to - * WebViewCore + * WebViewCore. This really should be tuned for each type of the devices. + * For example in Google Map api test case, it takes Dream device at least + * 150ms to do a full cycle in the WebViewCore by processing a touch event, + * triggering the layout and drawing the picture. While the same process + * takes 60+ms on the current high speed device. If we make + * TOUCH_SENT_INTERVAL too small, there will be multiple touch events sent + * to WebViewCore queue and the real layout and draw events will be pushed + * to further, which slows down the refresh rate. Choose 50 to favor the + * current high speed devices. For Dream like devices, 100 is a better + * choice. Maybe make this in the buildspec later. */ - private static final int TOUCH_SENT_INTERVAL = 100; + private static final int TOUCH_SENT_INTERVAL = 50; /** * Helper class to get velocity for fling */ VelocityTracker mVelocityTracker; private int mMaximumFling; + private float mLastVelocity; + private float mLastVelX; + private float mLastVelY; /** * Touch mode @@ -345,16 +346,9 @@ public class WebView extends AbsoluteLayout private static final int TOUCH_DRAG_MODE = 3; private static final int TOUCH_SHORTPRESS_START_MODE = 4; private static final int TOUCH_SHORTPRESS_MODE = 5; - private static final int TOUCH_DOUBLECLICK_MODE = 6; + private static final int TOUCH_DOUBLE_TAP_MODE = 6; private static final int TOUCH_DONE_MODE = 7; private static final int TOUCH_SELECT_MODE = 8; - // touch mode values specific to scale+scroll - private static final int FIRST_SCROLL_ZOOM = 9; - private static final int SCROLL_ZOOM_ANIMATION_IN = 9; - private static final int SCROLL_ZOOM_ANIMATION_OUT = 10; - private static final int SCROLL_ZOOM_OUT = 11; - private static final int LAST_SCROLL_ZOOM = 11; - // end of touch mode values specific to scale+scroll // Whether to forward the touch events to WebCore private boolean mForwardTouchEvents = false; @@ -362,20 +356,28 @@ public class WebView extends AbsoluteLayout // Whether to prevent drag during touch. The initial value depends on // mForwardTouchEvents. If WebCore wants touch events, we assume it will // take control of touch events unless it says no for touch down event. - private boolean mPreventDrag; + private static final int PREVENT_DRAG_NO = 0; + private static final int PREVENT_DRAG_MAYBE_YES = 1; + private static final int PREVENT_DRAG_YES = 2; + private int mPreventDrag = PREVENT_DRAG_NO; + + // To keep track of whether the current drag was initiated by a WebTextView, + // so that we know not to hide the cursor + boolean mDragFromTextInput; - // If updateTextEntry gets called while we are out of focus, use this - // variable to remember to do it next time we gain focus. - private boolean mNeedsUpdateTextEntry = false; - - // Whether or not to draw the focus ring. - private boolean mDrawFocusRing = true; + // Whether or not to draw the cursor ring. + private boolean mDrawCursorRing = true; + + // true if onPause has been called (and not onResume) + private boolean mIsPaused; /** * Customizable constant */ // pre-computed square of ViewConfiguration.getScaledTouchSlop() private int mTouchSlopSquare; + // pre-computed square of ViewConfiguration.getScaledDoubleTapSlop() + private int mDoubleTapSlopSquare; // pre-computed density adjusted navigation slop private int mNavSlop; // This should be ViewConfiguration.getTapTimeout() @@ -389,7 +391,7 @@ public class WebView extends AbsoluteLayout // needed to avoid flinging after a pause of no movement private static final int MIN_FLING_TIME = 250; // The time that the Zoom Controls are visible before fading away - private static final long ZOOM_CONTROLS_TIMEOUT = + private static final long ZOOM_CONTROLS_TIMEOUT = ViewConfiguration.getZoomControlsTimeout(); // The amount of content to overlap between two screens when going through // pages with the space bar, in pixels. @@ -410,7 +412,7 @@ public class WebView extends AbsoluteLayout private int mContentWidth; // cache of value from WebViewCore private int mContentHeight; // cache of value from WebViewCore - // Need to have the separate control for horizontal and vertical scrollbar + // Need to have the separate control for horizontal and vertical scrollbar // style than the View's single scrollbar style private boolean mOverlayHorizontalScrollbar = true; private boolean mOverlayVerticalScrollbar = false; @@ -421,55 +423,53 @@ public class WebView extends AbsoluteLayout private static final int STD_SPEED = 480; // pixels per second // time for the longest scroll animation private static final int MAX_DURATION = 750; // milliseconds + private static final int SLIDE_TITLE_DURATION = 500; // milliseconds private Scroller mScroller; private boolean mWrapContent; - // true if we should call webcore to draw the content, false means we have - // requested something but it isn't ready to draw yet. - private WebViewCore.FocusData mFocusData; /** * Private message ids */ - private static final int REMEMBER_PASSWORD = 1; - private static final int NEVER_REMEMBER_PASSWORD = 2; - private static final int SWITCH_TO_SHORTPRESS = 3; - private static final int SWITCH_TO_LONGPRESS = 4; - private static final int UPDATE_TEXT_ENTRY_ADAPTER = 6; - private static final int SWITCH_TO_ENTER = 7; - private static final int RESUME_WEBCORE_UPDATE = 8; + private static final int REMEMBER_PASSWORD = 1; + private static final int NEVER_REMEMBER_PASSWORD = 2; + private static final int SWITCH_TO_SHORTPRESS = 3; + private static final int SWITCH_TO_LONGPRESS = 4; + private static final int RELEASE_SINGLE_TAP = 5; + private static final int REQUEST_FORM_DATA = 6; + private static final int RESUME_WEBCORE_UPDATE = 7; //! arg1=x, arg2=y - static final int SCROLL_TO_MSG_ID = 10; - static final int SCROLL_BY_MSG_ID = 11; + static final int SCROLL_TO_MSG_ID = 10; + static final int SCROLL_BY_MSG_ID = 11; //! arg1=x, arg2=y - static final int SPAWN_SCROLL_TO_MSG_ID = 12; + static final int SPAWN_SCROLL_TO_MSG_ID = 12; //! arg1=x, arg2=y - static final int SYNC_SCROLL_TO_MSG_ID = 13; - static final int NEW_PICTURE_MSG_ID = 14; - static final int UPDATE_TEXT_ENTRY_MSG_ID = 15; - static final int WEBCORE_INITIALIZED_MSG_ID = 16; - static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17; - static final int DID_FIRST_LAYOUT_MSG_ID = 18; - static final int RECOMPUTE_FOCUS_MSG_ID = 19; - static final int NOTIFY_FOCUS_SET_MSG_ID = 20; - static final int MARK_NODE_INVALID_ID = 21; - static final int UPDATE_CLIPBOARD = 22; - static final int LONG_PRESS_ENTER = 23; - static final int PREVENT_TOUCH_ID = 24; - static final int WEBCORE_NEED_TOUCH_EVENTS = 25; + static final int SYNC_SCROLL_TO_MSG_ID = 13; + static final int NEW_PICTURE_MSG_ID = 14; + static final int UPDATE_TEXT_ENTRY_MSG_ID = 15; + static final int WEBCORE_INITIALIZED_MSG_ID = 16; + static final int UPDATE_TEXTFIELD_TEXT_MSG_ID = 17; + static final int MOVE_OUT_OF_PLUGIN = 19; + static final int CLEAR_TEXT_ENTRY = 20; + static final int UPDATE_TEXT_SELECTION_MSG_ID = 21; + static final int UPDATE_CLIPBOARD = 22; + static final int LONG_PRESS_CENTER = 23; + static final int PREVENT_TOUCH_ID = 24; + static final int WEBCORE_NEED_TOUCH_EVENTS = 25; // obj=Rect in doc coordinates - static final int INVAL_RECT_MSG_ID = 26; - + static final int INVAL_RECT_MSG_ID = 26; + static final int REQUEST_KEYBOARD = 27; + static final String[] HandlerDebugString = { - "REMEMBER_PASSWORD", // = 1; - "NEVER_REMEMBER_PASSWORD", // = 2; - "SWITCH_TO_SHORTPRESS", // = 3; - "SWITCH_TO_LONGPRESS", // = 4; - "5", - "UPDATE_TEXT_ENTRY_ADAPTER", // = 6; - "SWITCH_TO_ENTER", // = 7; - "RESUME_WEBCORE_UPDATE", // = 8; + "REMEMBER_PASSWORD", // = 1; + "NEVER_REMEMBER_PASSWORD", // = 2; + "SWITCH_TO_SHORTPRESS", // = 3; + "SWITCH_TO_LONGPRESS", // = 4; + "RELEASE_SINGLE_TAP", // = 5; + "REQUEST_FORM_DATA", // = 6; + "SWITCH_TO_CLICK", // = 7; + "RESUME_WEBCORE_UPDATE", // = 8; "9", "SCROLL_TO_MSG_ID", // = 10; "SCROLL_BY_MSG_ID", // = 11; @@ -479,31 +479,40 @@ public class WebView extends AbsoluteLayout "UPDATE_TEXT_ENTRY_MSG_ID", // = 15; "WEBCORE_INITIALIZED_MSG_ID", // = 16; "UPDATE_TEXTFIELD_TEXT_MSG_ID", // = 17; - "DID_FIRST_LAYOUT_MSG_ID", // = 18; - "RECOMPUTE_FOCUS_MSG_ID", // = 19; - "NOTIFY_FOCUS_SET_MSG_ID", // = 20; - "MARK_NODE_INVALID_ID", // = 21; + "18", // = 18; + "MOVE_OUT_OF_PLUGIN", // = 19; + "CLEAR_TEXT_ENTRY", // = 20; + "UPDATE_TEXT_SELECTION_MSG_ID", // = 21; "UPDATE_CLIPBOARD", // = 22; - "LONG_PRESS_ENTER", // = 23; + "LONG_PRESS_CENTER", // = 23; "PREVENT_TOUCH_ID", // = 24; "WEBCORE_NEED_TOUCH_EVENTS", // = 25; - "INVAL_RECT_MSG_ID" // = 26; + "INVAL_RECT_MSG_ID", // = 26; + "REQUEST_KEYBOARD" // = 27; }; - // width which view is considered to be fully zoomed out - static final int ZOOM_OUT_WIDTH = 1008; - // default scale limit. Depending on the display density private static float DEFAULT_MAX_ZOOM_SCALE; private static float DEFAULT_MIN_ZOOM_SCALE; // scale limit, which can be set through viewport meta tag in the web page private float mMaxZoomScale; private float mMinZoomScale; - private boolean mMinZoomScaleFixed = false; + private boolean mMinZoomScaleFixed = true; // initial scale in percent. 0 means using default. private int mInitialScale = 0; + // while in the zoom overview mode, the page's width is fully fit to the + // current window. The page is alive, in another words, you can click to + // follow the links. Double tap will toggle between zoom overview mode and + // the last zoom scale. + boolean mInZoomOverview = false; + + // ideally mZoomOverviewWidth should be mContentWidth. But sites like espn, + // engadget always have wider mContentWidth no matter what viewport size is. + int mZoomOverviewWidth = WebViewCore.DEFAULT_VIEWPORT_WIDTH; + float mLastScale; + // default scale. Depending on the display density. static int DEFAULT_SCALE_PERCENT; private float mDefaultScale; @@ -518,6 +527,8 @@ public class WebView extends AbsoluteLayout private float mZoomScale; private float mInvInitialZoomScale; private float mInvFinalZoomScale; + private int mInitialScrollX; + private int mInitialScrollY; private long mZoomStart; private static final int ZOOM_ANIMATION_LENGTH = 500; @@ -530,7 +541,7 @@ public class WebView extends AbsoluteLayout private static final int SNAP_X_LOCK = 4; private static final int SNAP_Y_LOCK = 5; private boolean mSnapPositive; - + // Used to match key downs and key ups private boolean mGotKeyDown; @@ -553,7 +564,7 @@ public class WebView extends AbsoluteLayout * URI scheme for map address */ public static final String SCHEME_GEO = "geo:0,0?q="; - + private int mBackgroundColor = Color.WHITE; // Used to notify listeners of a new picture. @@ -570,7 +581,8 @@ public class WebView extends AbsoluteLayout public void onNewPicture(WebView view, Picture picture); } - public class HitTestResult { + // FIXME: Want to make this public, but need to change the API file. + public /*static*/ class HitTestResult { /** * Default HitTestResult, where the target is unknown */ @@ -640,8 +652,7 @@ public class WebView extends AbsoluteLayout private ExtendedZoomControls mZoomControls; private Runnable mZoomControlRunnable; - private ZoomButtonsController mZoomButtonsController; - private ImageView mZoomOverviewButton; + private ZoomButtonsController mZoomButtonsController; // These keep track of the center point of the zoom. They are used to // determine the point around which we should zoom. @@ -664,11 +675,11 @@ public class WebView extends AbsoluteLayout } else { zoomOut(); } - + updateZoomButtonsEnabled(); } }; - + /** * Construct a new WebView with a Context object. * @param context A Context object used to access application assets. @@ -693,24 +704,33 @@ public class WebView extends AbsoluteLayout * @param defStyle The default style resource ID. */ public WebView(Context context, AttributeSet attrs, int defStyle) { + this(context, attrs, defStyle, null); + } + + /** + * Construct a new WebView with layout parameters, a default style and a set + * of custom Javscript interfaces to be added to the WebView at initialization + * time. This guraratees that these interfaces will be available when the JS + * context is initialized. + * @param context A Context object used to access application assets. + * @param attrs An AttributeSet passed to our parent. + * @param defStyle The default style resource ID. + * @param javascriptInterfaces is a Map of intareface names, as keys, and + * object implementing those interfaces, as values. + * @hide pending API council approval. + */ + protected WebView(Context context, AttributeSet attrs, int defStyle, + Map<String, Object> javascriptInterfaces) { super(context, attrs, defStyle); init(); mCallbackProxy = new CallbackProxy(context, this); - mWebViewCore = new WebViewCore(context, this, mCallbackProxy); + mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces); mDatabase = WebViewDatabase.getInstance(context); - mFocusData = new WebViewCore.FocusData(); - mFocusData.mFrame = 0; - mFocusData.mNode = 0; - mFocusData.mX = 0; - mFocusData.mY = 0; mScroller = new Scroller(context); - initZoomController(context); - } + mViewManager = new ViewManager(this); - private void initZoomController(Context context) { - // Create the buttons controller mZoomButtonsController = new ZoomButtonsController(this); mZoomButtonsController.setOnZoomListener(mZoomListener); // ZoomButtonsController positions the buttons at the bottom, but in @@ -723,30 +743,11 @@ public class WebView extends AbsoluteLayout params; frameParams.gravity = Gravity.RIGHT; } - - // Create the accessory buttons - LayoutInflater inflater = - (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - ViewGroup container = mZoomButtonsController.getContainer(); - inflater.inflate(com.android.internal.R.layout.zoom_browser_accessory_buttons, container); - mZoomOverviewButton = - (ImageView) container.findViewById(com.android.internal.R.id.zoom_page_overview); - mZoomOverviewButton.setOnClickListener( - new View.OnClickListener() { - public void onClick(View v) { - mZoomButtonsController.setVisible(false); - zoomScrollOut(); - if (mLogEvent) { - Checkin.updateStats(mContext.getContentResolver(), - Checkin.Stats.Tag.BROWSER_ZOOM_OVERVIEW, 1, 0.0); - } - } - }); } private void updateZoomButtonsEnabled() { boolean canZoomIn = mActualScale < mMaxZoomScale; - boolean canZoomOut = mActualScale > mMinZoomScale; + boolean canZoomOut = mActualScale > mMinZoomScale && !mInZoomOverview; if (!canZoomIn && !canZoomOut) { // Hide the zoom in and out buttons, as well as the fit to page // button, if the page cannot zoom @@ -760,8 +761,6 @@ public class WebView extends AbsoluteLayout mZoomButtonsController.setZoomInEnabled(canZoomIn); mZoomButtonsController.setZoomOutEnabled(canZoomOut); } - mZoomOverviewButton.setVisibility(canZoomScrollOut() ? View.VISIBLE: - View.GONE); } private void init() { @@ -772,9 +771,11 @@ public class WebView extends AbsoluteLayout setLongClickable(true); final ViewConfiguration configuration = ViewConfiguration.get(getContext()); - final int slop = configuration.getScaledTouchSlop(); + int slop = configuration.getScaledTouchSlop(); mTouchSlopSquare = slop * slop; mMinLockSnapReverseDistance = slop; + slop = configuration.getScaledDoubleTapSlop(); + mDoubleTapSlopSquare = slop * slop; final float density = getContext().getResources().getDisplayMetrics().density; // use one line height, 16 based on our current default font, for how // far we allow a touch be away from the edge of a link @@ -908,8 +909,9 @@ public class WebView extends AbsoluteLayout /* * Return the width of the view where the content of WebView should render * to. + * Note: this can be called from WebCoreThread. */ - private int getViewWidth() { + /* package */ int getViewWidth() { if (!isVerticalScrollBarEnabled() || mOverlayVerticalScrollbar) { return getWidth(); } else { @@ -918,15 +920,35 @@ public class WebView extends AbsoluteLayout } /* + * returns the height of the titlebarview (if any). Does not care about + * scrolling + */ + private int getTitleHeight() { + return mTitleBar != null ? mTitleBar.getHeight() : 0; + } + + /* + * Return the amount of the titlebarview (if any) that is visible + */ + private int getVisibleTitleHeight() { + return Math.max(getTitleHeight() - mScrollY, 0); + } + + /* * Return the height of the view where the content of WebView should render - * to. + * to. Note that this excludes mTitleBar, if there is one. + * Note: this can be called from WebCoreThread. */ - private int getViewHeight() { - if (!isHorizontalScrollBarEnabled() || mOverlayHorizontalScrollbar) { - return getHeight(); - } else { - return getHeight() - getHorizontalScrollbarHeight(); + /* package */ int getViewHeight() { + return getViewHeightWithTitle() - getVisibleTitleHeight(); + } + + private int getViewHeightWithTitle() { + int height = getHeight(); + if (isHorizontalScrollBarEnabled() && !mOverlayHorizontalScrollbar) { + height -= getHorizontalScrollbarHeight(); } + return height; } /** @@ -996,7 +1018,7 @@ public class WebView extends AbsoluteLayout clearTextEntry(); if (mWebViewCore != null) { // Set the handlers to null before destroying WebViewCore so no - // more messages will be posted. + // more messages will be posted. mCallbackProxy.setWebViewClient(null); mCallbackProxy.setWebChromeClient(null); // Tell WebViewCore to destroy itself @@ -1027,12 +1049,23 @@ public class WebView extends AbsoluteLayout /** * If platform notifications are enabled, this should be called - * from onPause() or onStop(). + * from the Activity's onPause() or onStop(). */ public static void disablePlatformNotifications() { Network.disablePlatformNotifications(); } - + + /** + * Sets JavaScript engine flags. + * + * @param flags JS engine flags in a String + * + * @hide pending API solidification + */ + public void setJsFlags(String flags) { + mWebViewCore.sendMessage(EventHub.SET_JS_FLAGS, flags); + } + /** * Inform WebView of the network state. This is used to set * the javascript property window.navigator.isOnline and @@ -1045,7 +1078,7 @@ public class WebView extends AbsoluteLayout } /** - * Save the state of this WebView used in + * 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 @@ -1078,6 +1111,12 @@ public class WebView extends AbsoluteLayout ArrayList<byte[]> history = new ArrayList<byte[]>(size); for (int i = 0; i < size; i++) { WebHistoryItem item = list.getItemAtIndex(i); + if (null == item) { + // FIXME: this shouldn't happen + // need to determine how item got set to null + Log.w(LOGTAG, "saveState: Unexpected null history item."); + return null; + } byte[] data = item.getFlattenedData(); if (data == null) { // It would be very odd to not have any data for a given history @@ -1123,6 +1162,9 @@ public class WebView extends AbsoluteLayout b.putInt("scrollX", mScrollX); b.putInt("scrollY", mScrollY); b.putFloat("scale", mActualScale); + if (mInZoomOverview) { + b.putFloat("lastScale", mLastScale); + } return true; } return false; @@ -1167,6 +1209,13 @@ public class WebView extends AbsoluteLayout // onSizeChanged() is called, the rest will be set // correctly mActualScale = scale; + float lastScale = b.getFloat("lastScale", -1.0f); + if (lastScale > 0) { + mInZoomOverview = true; + mLastScale = lastScale; + } else { + mInZoomOverview = false; + } invalidate(); return true; } @@ -1176,10 +1225,10 @@ public class WebView extends AbsoluteLayout /** * Restore the state of this WebView from the given map used in - * {@link android.app.Activity#onRestoreInstanceState}. This method should - * be called to restore the state of the WebView before using the object. If - * it is called after the WebView has had a chance to build state (load - * pages, create a back/forward list, etc.) there may be undesirable + * {@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. @@ -1240,6 +1289,9 @@ public class WebView extends AbsoluteLayout * @param url The url of the resource to load. */ public void loadUrl(String url) { + if (url == null) { + return; + } switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.LOAD_URL, url); clearTextEntry(); @@ -1249,18 +1301,16 @@ public class WebView extends AbsoluteLayout * Load the url with postData using "POST" method into the WebView. If url * is not a network url, it will be loaded with {link * {@link #loadUrl(String)} instead. - * + * * @param url The url of the resource to load. * @param postData The data will be passed to "POST" request. - * - * @hide pending API solidification */ public void postUrl(String url, byte[] postData) { if (URLUtil.isNetworkUrl(url)) { switchOutDrawHistory(); - HashMap arg = new HashMap(); - arg.put("url", url); - arg.put("data", postData); + WebViewCore.PostUrlData arg = new WebViewCore.PostUrlData(); + arg.mUrl = url; + arg.mPostData = postData; mWebViewCore.sendMessage(EventHub.POST_URL, arg); clearTextEntry(); } else { @@ -1294,7 +1344,7 @@ public class WebView extends AbsoluteLayout * able to access asset files. If the baseUrl is anything other than * http(s)/ftp(s)/about/javascript as scheme, you can access asset files for * sub resources. - * + * * @param baseUrl Url to resolve relative paths with, if null defaults to * "about:blank" * @param data A String of data in the given encoding. @@ -1305,18 +1355,18 @@ public class WebView extends AbsoluteLayout */ public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String failUrl) { - + if (baseUrl != null && baseUrl.toLowerCase().startsWith("data:")) { loadData(data, mimeType, encoding); return; } switchOutDrawHistory(); - HashMap arg = new HashMap(); - arg.put("baseUrl", baseUrl); - arg.put("data", data); - arg.put("mimeType", mimeType); - arg.put("encoding", encoding); - arg.put("failUrl", failUrl); + WebViewCore.BaseUrlData arg = new WebViewCore.BaseUrlData(); + arg.mBaseUrl = baseUrl; + arg.mData = data; + arg.mMimeType = mimeType; + arg.mEncoding = encoding; + arg.mFailUrl = failUrl; mWebViewCore.sendMessage(EventHub.LOAD_DATA, arg); clearTextEntry(); } @@ -1335,6 +1385,7 @@ public class WebView extends AbsoluteLayout * Reload the current url. */ public void reload() { + clearTextEntry(); switchOutDrawHistory(); mWebViewCore.sendMessage(EventHub.RELOAD); } @@ -1426,7 +1477,7 @@ public class WebView extends AbsoluteLayout ignoreSnapshot ? 1 : 0); } } - + private boolean extendScroll(int y) { int finalY = mScroller.getFinalY(); int newY = pinLocY(finalY + y); @@ -1435,7 +1486,7 @@ public class WebView extends AbsoluteLayout mScroller.extendDuration(computeDuration(0, y)); return true; } - + /** * Scroll the contents of the view up by half the view size * @param top true to jump to the top of the page @@ -1445,7 +1496,7 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) { return false; } - nativeClearFocus(-1, -1); + nativeClearCursor(); // start next trackball movement from page edge if (top) { // go to the top of the document return pinScrollTo(mScrollX, 0, true, 0); @@ -1459,10 +1510,10 @@ public class WebView extends AbsoluteLayout y = -h / 2; } mUserScroll = true; - return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) + return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } - + /** * Scroll the contents of the view down by half the page size * @param bottom true to jump to bottom of page @@ -1472,7 +1523,7 @@ public class WebView extends AbsoluteLayout if (mNativeClass == 0) { return false; } - nativeClearFocus(-1, -1); + nativeClearCursor(); // start next trackball movement from page edge if (bottom) { return pinScrollTo(mScrollX, mContentHeight, true, 0); } @@ -1485,7 +1536,7 @@ public class WebView extends AbsoluteLayout y = h / 2; } mUserScroll = true; - return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) + return mScroller.isFinished() ? pinScrollBy(0, y, true, 0) : extendScroll(y); } @@ -1498,7 +1549,7 @@ public class WebView extends AbsoluteLayout mContentHeight = 0; mWebViewCore.sendMessage(EventHub.CLEAR_CONTENT); } - + /** * Return a new picture that captures the current display of the webview. * This is a copy of the display, and will be unaffected if the webview @@ -1509,7 +1560,7 @@ public class WebView extends AbsoluteLayout * bounds of the view. */ public Picture capturePicture() { - if (null == mWebViewCore) return null; // check for out of memory tab + if (null == mWebViewCore) return null; // check for out of memory tab return mWebViewCore.copyContentPicture(); } @@ -1517,17 +1568,17 @@ public class WebView extends AbsoluteLayout * Return true if the browser is displaying a TextView for text input. */ private boolean inEditingMode() { - return mTextEntry != null && mTextEntry.getParent() != null - && mTextEntry.hasFocus(); + return mWebTextView != null && mWebTextView.getParent() != null + && mWebTextView.hasFocus(); } private void clearTextEntry() { if (inEditingMode()) { - mTextEntry.remove(); + mWebTextView.remove(); } } - /** + /** * Return the current scale of the WebView * @return The current scale. */ @@ -1568,7 +1619,7 @@ public class WebView extends AbsoluteLayout } /** - * Return a HitTestResult based on the current focus node. If a HTML::a tag + * Return a HitTestResult based on the current cursor node. If a HTML::a tag * is found and the anchor has a non-javascript url, the HitTestResult type * is set to SRC_ANCHOR_TYPE and the url is set in the "extra" field. If the * anchor does not have a url or if it is a javascript url, the type will @@ -1591,26 +1642,26 @@ public class WebView extends AbsoluteLayout } HitTestResult result = new HitTestResult(); - - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsTextField || node.mIsTextArea) { + if (nativeHasCursorNode()) { + if (nativeCursorIsTextInput()) { result.setType(HitTestResult.EDIT_TEXT_TYPE); - } else if (node.mText != null) { - String text = node.mText; - if (text.startsWith(SCHEME_TEL)) { - result.setType(HitTestResult.PHONE_TYPE); - result.setExtra(text.substring(SCHEME_TEL.length())); - } else if (text.startsWith(SCHEME_MAILTO)) { - result.setType(HitTestResult.EMAIL_TYPE); - result.setExtra(text.substring(SCHEME_MAILTO.length())); - } else if (text.startsWith(SCHEME_GEO)) { - result.setType(HitTestResult.GEO_TYPE); - result.setExtra(URLDecoder.decode(text - .substring(SCHEME_GEO.length()))); - } else if (node.mIsAnchor) { - result.setType(HitTestResult.SRC_ANCHOR_TYPE); - result.setExtra(text); + } else { + String text = nativeCursorText(); + if (text != null) { + if (text.startsWith(SCHEME_TEL)) { + result.setType(HitTestResult.PHONE_TYPE); + result.setExtra(text.substring(SCHEME_TEL.length())); + } else if (text.startsWith(SCHEME_MAILTO)) { + result.setType(HitTestResult.EMAIL_TYPE); + result.setExtra(text.substring(SCHEME_MAILTO.length())); + } else if (text.startsWith(SCHEME_GEO)) { + result.setType(HitTestResult.GEO_TYPE); + result.setExtra(URLDecoder.decode(text + .substring(SCHEME_GEO.length()))); + } else if (nativeCursorIsAnchor()) { + result.setType(HitTestResult.SRC_ANCHOR_TYPE); + result.setExtra(text); + } } } } @@ -1618,12 +1669,12 @@ public class WebView extends AbsoluteLayout if (type == HitTestResult.UNKNOWN_TYPE || type == HitTestResult.SRC_ANCHOR_TYPE) { // Now check to see if it is an image. - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); String text = nativeImageURI(contentX, contentY); if (text != null) { - result.setType(type == HitTestResult.UNKNOWN_TYPE ? - HitTestResult.IMAGE_TYPE : + result.setType(type == HitTestResult.UNKNOWN_TYPE ? + HitTestResult.IMAGE_TYPE : HitTestResult.SRC_IMAGE_ANCHOR_TYPE); result.setExtra(text); } @@ -1635,37 +1686,36 @@ public class WebView extends AbsoluteLayout * Request the href of an anchor element due to getFocusNodePath returning * "href." If hrefMsg is null, this method returns immediately and does not * dispatch hrefMsg to its target. - * + * * @param hrefMsg This message will be dispatched with the result of the * request as the data member with "url" as key. The result can * be null. */ + // FIXME: API change required to change the name of this function. We now + // look at the cursor node, and not the focus node. Also, what is + // getFocusNodePath? public void requestFocusNodeHref(Message hrefMsg) { if (hrefMsg == null || mNativeClass == 0) { return; } - if (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); - } + if (nativeCursorIsAnchor()) { + mWebViewCore.sendMessage(EventHub.REQUEST_CURSOR_HREF, + nativeCursorFramePointer(), nativeCursorNodePointer(), + hrefMsg); } } - + /** * Request the url of the image last touched by the user. msg will be sent * to its target with a String representing the url as its object. - * + * * @param msg This message will be dispatched with the result of the request * as the data member with "url" as key. The result can be null. */ public void requestImageRef(Message msg) { - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); + if (0 == mNativeClass) return; // client isn't initialized + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); String ref = nativeImageURI(contentX, contentY); Bundle data = msg.getData(); data.putString("url", ref); @@ -1696,34 +1746,160 @@ public class WebView extends AbsoluteLayout // Expects y in view coordinates private int pinLocY(int y) { - return pinLoc(y, getViewHeight(), computeVerticalScrollRange()); + int titleH = getTitleHeight(); + // if the titlebar is still visible, just pin against 0 + if (y <= titleH) { + return Math.max(y, 0); + } + // convert to 0-based coordinate (subtract the title height) + // pin(), and then add the title height back in + return pinLoc(y - titleH, getViewHeight(), + computeVerticalScrollRange()) + titleH; + } + + /** + * A title bar which is embedded in this WebView, and scrolls along with it + * vertically, but not horizontally. + */ + private View mTitleBar; + + /** + * Since we draw the title bar ourselves, we removed the shadow from the + * browser's activity. We do want a shadow at the bottom of the title bar, + * or at the top of the screen if the title bar is not visible. This + * drawable serves that purpose. + */ + private Drawable mTitleShadow; + + /** + * Add or remove a title bar to be embedded into the WebView, and scroll + * along with it vertically, while remaining in view horizontally. Pass + * null to remove the title bar from the WebView, and return to drawing + * the WebView normally without translating to account for the title bar. + * @hide + */ + public void setEmbeddedTitleBar(View v) { + if (mTitleBar == v) return; + if (mTitleBar != null) { + removeView(mTitleBar); + } + if (null != v) { + addView(v, new AbsoluteLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0)); + if (mTitleShadow == null) { + mTitleShadow = (Drawable) mContext.getResources().getDrawable( + com.android.internal.R.drawable.title_bar_shadow); + } + } + mTitleBar = v; + } + + /** + * Given a distance in view space, convert it to content space. Note: this + * does not reflect translation, just scaling, so this should not be called + * with coordinates, but should be called for dimensions like width or + * height. + */ + private int viewToContentDimension(int d) { + return Math.round(d * mInvActualScale); + } + + /** + * Given an x coordinate in view space, convert it to content space. Also + * may be used for absolute heights (such as for the WebTextView's + * textSize, which is unaffected by the height of the title bar). + */ + /*package*/ int viewToContentX(int x) { + return viewToContentDimension(x); + } + + /** + * Given a y coordinate in view space, convert it to content space. + * Takes into account the height of the title bar if there is one + * embedded into the WebView. + */ + /*package*/ int viewToContentY(int y) { + return viewToContentDimension(y - getTitleHeight()); + } + + /** + * Given a distance in content space, convert it to view space. Note: this + * does not reflect translation, just scaling, so this should not be called + * with coordinates, but should be called for dimensions like width or + * height. + */ + /*package*/ int contentToViewDimension(int d) { + return Math.round(d * mActualScale); + } + + /** + * Given an x coordinate in content space, convert it to view + * space. + */ + /*package*/ int contentToViewX(int x) { + return contentToViewDimension(x); } - /*package*/ int viewToContent(int x) { - return Math.round(x * mInvActualScale); + /** + * Given a y coordinate in content space, convert it to view + * space. Takes into account the height of the title bar. + */ + /*package*/ int contentToViewY(int y) { + return contentToViewDimension(y) + getTitleHeight(); } - private int contentToView(int x) { - return Math.round(x * mActualScale); + private Rect contentToViewRect(Rect x) { + return new Rect(contentToViewX(x.left), contentToViewY(x.top), + contentToViewX(x.right), contentToViewY(x.bottom)); } + /* To invalidate a rectangle in content coordinates, we need to transform + the rect into view coordinates, so we can then call invalidate(...). + + Normally, we would just call contentToView[XY](...), which eventually + calls Math.round(coordinate * mActualScale). However, for invalidates, + we need to account for the slop that occurs with antialiasing. To + address that, we are a little more liberal in the size of the rect that + we invalidate. + + This liberal calculation calls floor() for the top/left, and ceil() for + the bottom/right coordinates. This catches the possible extra pixels of + antialiasing that we might have missed with just round(). + */ + // Called by JNI to invalidate the View, given rectangle coordinates in // content space private void viewInvalidate(int l, int t, int r, int b) { - invalidate(contentToView(l), contentToView(t), contentToView(r), - contentToView(b)); + final float scale = mActualScale; + final int dy = getTitleHeight(); + invalidate((int)Math.floor(l * scale), + (int)Math.floor(t * scale) + dy, + (int)Math.ceil(r * scale), + (int)Math.ceil(b * scale) + dy); } // Called by JNI to invalidate the View after a delay, given rectangle // coordinates in content space private void viewInvalidateDelayed(long delay, int l, int t, int r, int b) { - postInvalidateDelayed(delay, contentToView(l), contentToView(t), - contentToView(r), contentToView(b)); + final float scale = mActualScale; + final int dy = getTitleHeight(); + postInvalidateDelayed(delay, + (int)Math.floor(l * scale), + (int)Math.floor(t * scale) + dy, + (int)Math.ceil(r * scale), + (int)Math.ceil(b * scale) + dy); } - private Rect contentToView(Rect x) { - return new Rect(contentToView(x.left), contentToView(x.top) - , contentToView(x.right), contentToView(x.bottom)); + private void invalidateContentRect(Rect r) { + viewInvalidate(r.left, r.top, r.right, r.bottom); + } + + // stop the scroll animation, and don't let a subsequent fling add + // to the existing velocity + private void abortAnimation() { + mScroller.abortAnimation(); + mLastVelocity = 0; } /* call from webcoreview.draw(), so we're still executing in the UI thread @@ -1734,7 +1910,7 @@ public class WebView extends AbsoluteLayout if ((w | h) == 0) { return; } - + // don't abort a scroll animation if we didn't change anything if (mContentWidth != w || mContentHeight != h) { // record new dimensions @@ -1748,12 +1924,15 @@ public class WebView extends AbsoluteLayout int oldY = mScrollY; mScrollX = pinLocX(mScrollX); mScrollY = pinLocY(mScrollY); - // android.util.Log.d("skia", "recordNewContentSize - - // abortAnimation"); - mScroller.abortAnimation(); // just in case if (oldX != mScrollX || oldY != mScrollY) { sendOurVisibleRect(); } + if (!mScroller.isFinished()) { + // We are in the middle of a scroll. Repin the final scroll + // position. + mScroller.setFinalX(pinLocX(mScroller.getFinalX())); + mScroller.setFinalY(pinLocY(mScroller.getFinalY())); + } } } contentSizeChanged(updateLayout); @@ -1785,7 +1964,8 @@ public class WebView extends AbsoluteLayout int oldY = mScrollY; float ratio = scale * mInvActualScale; // old inverse float sx = ratio * oldX + (ratio - 1) * mZoomCenterX; - float sy = ratio * oldY + (ratio - 1) * mZoomCenterY; + float sy = ratio * oldY + (ratio - 1) + * (mZoomCenterY - getTitleHeight()); // now update our new scale and inverse if (scale != mActualScale && !mPreviewZoomOnly) { @@ -1794,7 +1974,10 @@ public class WebView extends AbsoluteLayout mActualScale = scale; mInvActualScale = 1 / scale; - // as we don't have animation for scaling, don't do animation + // Scale all the child views + mViewManager.scaleAll(); + + // as we don't have animation for scaling, don't do animation // for scrolling, as it causes weird intermediate state // pinScrollTo(Math.round(sx), Math.round(sy)); mScrollX = pinLocX(Math.round(sx)); @@ -1815,18 +1998,21 @@ public class WebView extends AbsoluteLayout 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)) { + Point pos = new Point(rect.left, rect.top); mWebViewCore.sendMessage(EventHub.SET_SCROLL_OFFSET, - rect.left, rect.top); + nativeMoveGeneration(), 0, pos); mLastVisibleRectSent = rect; } Rect globalRect = new Rect(); if (getGlobalVisibleRect(globalRect) && !globalRect.equals(mLastGlobalRect)) { + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "sendOurVisibleRect=(" + globalRect.left + "," + + globalRect.top + ",r=" + globalRect.right + ",b=" + + globalRect.bottom); + } // TODO: the global offset is only used by windowRect() // in ChromeClientAndroid ; other clients such as touch // and mouse events could return view + screen relative points. @@ -1841,15 +2027,30 @@ public class WebView extends AbsoluteLayout Point p = new Point(); getGlobalVisibleRect(r, p); r.offset(-p.x, -p.y); + if (mFindIsUp) { + r.bottom -= mFindHeight; + } } // Sets r to be our visible rectangle in content coordinates private void calcOurContentVisibleRect(Rect r) { calcOurVisibleRect(r); - r.left = viewToContent(r.left); - r.top = viewToContent(r.top); - r.right = viewToContent(r.right); - r.bottom = viewToContent(r.bottom); + r.left = viewToContentX(r.left); + // viewToContentY will remove the total height of the title bar. Add + // the visible height back in to account for the fact that if the title + // bar is partially visible, the part of the visible rect which is + // displaying our content is displaced by that amount. + r.top = viewToContentY(r.top + getVisibleTitleHeight()); + r.right = viewToContentX(r.right); + r.bottom = viewToContentY(r.bottom); + } + + static class ViewSizeData { + int mWidth; + int mHeight; + int mTextWrapWidth; + float mScale; + boolean mIgnoreHeight; } /** @@ -1859,7 +2060,8 @@ public class WebView extends AbsoluteLayout * @return true if new values were sent */ private boolean sendViewSizeZoom() { - int newWidth = Math.round(getViewWidth() * mInvActualScale); + int viewWidth = getViewWidth(); + int newWidth = Math.round(viewWidth * mInvActualScale); int newHeight = Math.round(getViewHeight() * mInvActualScale); /* * Because the native side may have already done a layout before the @@ -1874,8 +2076,17 @@ public class WebView extends AbsoluteLayout } // Avoid sending another message if the dimensions have not changed. if (newWidth != mLastWidthSent || newHeight != mLastHeightSent) { - mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, - newWidth, newHeight, new Float(mActualScale)); + ViewSizeData data = new ViewSizeData(); + data.mWidth = newWidth; + data.mHeight = newHeight; + // while in zoom overview mode, the text are wrapped to the screen + // width matching mLastScale. So that we don't trigger re-flow while + // toggling between overview mode and normal mode. + data.mTextWrapWidth = mInZoomOverview ? Math.round(viewWidth + / mLastScale) : newWidth; + data.mScale = mActualScale; + data.mIgnoreHeight = mZoomScale != 0 && !mHeightCanMeasure; + mWebViewCore.sendMessage(EventHub.VIEW_SIZE_CHANGED, data); mLastWidthSent = newWidth; mLastHeightSent = newHeight; return true; @@ -1888,26 +2099,40 @@ public class WebView extends AbsoluteLayout if (mDrawHistory) { return mHistoryWidth; } else { - return contentToView(mContentWidth); + // to avoid rounding error caused unnecessary scrollbar, use floor + return (int) Math.floor(mContentWidth * mActualScale); } } - // Make sure this stays in sync with the actual height of the FindDialog. - private static final int FIND_HEIGHT = 79; - @Override protected int computeVerticalScrollRange() { if (mDrawHistory) { return mHistoryHeight; } else { - int height = contentToView(mContentHeight); - if (mFindIsUp) { - height += FIND_HEIGHT; - } - return height; + // to avoid rounding error caused unnecessary scrollbar, use floor + return (int) Math.floor(mContentHeight * mActualScale); } } + @Override + protected int computeVerticalScrollOffset() { + return Math.max(mScrollY - getTitleHeight(), 0); + } + + @Override + protected int computeVerticalScrollExtent() { + return getViewHeight(); + } + + /** @hide */ + @Override + protected void onDrawVerticalScrollBar(Canvas canvas, + Drawable scrollBar, + int l, int t, int r, int b) { + scrollBar.setBounds(l, t + getVisibleTitleHeight(), r, b); + scrollBar.draw(canvas); + } + /** * Get the url for the current page. This is not always the same as the url * passed to WebViewClient.onPageStarted because although the load for @@ -1918,10 +2143,10 @@ 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 + * 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. @@ -1953,13 +2178,22 @@ public class WebView extends AbsoluteLayout } /** + * Get the touch icon url for the apple-touch-icon <link> element. + * @hide + */ + public String getTouchIconUrl() { + WebHistoryItem h = mCallbackProxy.getBackForwardList().getCurrentItem(); + return h != null ? h.getTouchIconUrl() : null; + } + + /** * Get the progress for the current page. * @return The progress for the current page between 0 and 100. */ public int getProgress() { return mCallbackProxy.getProgress(); } - + /** * @return the height of the HTML content. */ @@ -1968,22 +2202,75 @@ public class WebView extends AbsoluteLayout } /** - * Pause all layout, parsing, and javascript timers. This can be useful if - * the WebView is not visible or the application has been paused. + * @return the width of the HTML content. + * @hide + */ + public int getContentWidth() { + return mContentWidth; + } + + /** + * Pause all layout, parsing, and javascript timers for all webviews. This + * is a global requests, not restricted to just this webview. This can be + * useful if the application has been paused. */ public void pauseTimers() { mWebViewCore.sendMessage(EventHub.PAUSE_TIMERS); } /** - * Resume all layout, parsing, and javascript timers. This will resume - * dispatching all timers. + * Resume all layout, parsing, and javascript timers for all webviews. + * This will resume dispatching all timers. */ public void resumeTimers() { mWebViewCore.sendMessage(EventHub.RESUME_TIMERS); } /** + * Call this to pause any extra processing associated with this view and + * its associated DOM/plugins/javascript/etc. For example, if the view is + * taken offscreen, this could be called to reduce unnecessary CPU and/or + * network traffic. When the view is again "active", call onResume(). + * + * Note that this differs from pauseTimers(), which affects all views/DOMs + * @hide + */ + public void onPause() { + if (!mIsPaused) { + mIsPaused = true; + mWebViewCore.sendMessage(EventHub.ON_PAUSE); + } + } + + /** + * Call this to balanace a previous call to onPause() + * @hide + */ + public void onResume() { + if (mIsPaused) { + mIsPaused = false; + mWebViewCore.sendMessage(EventHub.ON_RESUME); + } + } + + /** + * Returns true if the view is paused, meaning onPause() was called. Calling + * onResume() sets the paused state back to false. + * @hide + */ + public boolean isPaused() { + return mIsPaused; + } + + /** + * Call this to inform the view that memory is low so that it can + * free any available memory. + */ + public void freeMemory() { + mWebViewCore.sendMessage(EventHub.FREE_MEMORY); + } + + /** * Clear the resource cache. Note that the cache is per-application, so * this will clear the cache for all WebViews used. * @@ -2004,7 +2291,7 @@ public class WebView extends AbsoluteLayout public void clearFormData() { if (inEditingMode()) { AutoCompleteAdapter adapter = null; - mTextEntry.setAdapterCustom(adapter); + mWebTextView.setAdapterCustom(adapter); } } @@ -2038,12 +2325,13 @@ public class WebView extends AbsoluteLayout /* * Highlight and scroll to the next occurance of String in findAll. - * Wraps the page infinitely, and scrolls. Must be called after + * Wraps the page infinitely, and scrolls. Must be called after * calling findAll. * * @param forward Direction to search. */ public void findNext(boolean forward) { + if (0 == mNativeClass) return; // client isn't initialized nativeFindNext(forward); } @@ -2054,7 +2342,12 @@ public class WebView extends AbsoluteLayout * that were found. */ public int findAll(String find) { - mFindIsUp = true; + if (0 == mNativeClass) return 0; // client isn't initialized + if (mFindIsUp == false) { + recordNewContentSize(mContentWidth, mContentHeight + mFindHeight, + false); + mFindIsUp = true; + } int result = nativeFindAll(find.toLowerCase(), find.toUpperCase()); invalidate(); return result; @@ -2063,12 +2356,10 @@ public class WebView extends AbsoluteLayout // Used to know whether the find dialog is open. Affects whether // or not we draw the highlights for matches. private boolean mFindIsUp; + private int mFindHeight; - 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 + * Return the first substring consisting of the address of a physical * location. Currently, only addresses in the United States are detected, * and consist of: * - a house number @@ -2081,21 +2372,51 @@ public class WebView extends AbsoluteLayout * All names must be correctly capitalized, and the zip code, if present, * must be valid for the state. The street type must be a standard USPS * spelling or abbreviation. The state or territory must also be spelled - * or abbreviated using USPS standards. The house number may not exceed + * or abbreviated using USPS standards. The house number may not exceed * five digits. * @param addr The string to search for addresses. * * @return the address, or if no address is found, return null. */ public static String findAddress(String addr) { - return WebViewCore.nativeFindAddress(addr); + return findAddress(addr, false); + } + + /** + * @hide + * Return the first substring consisting of the address of a physical + * location. Currently, only addresses in the United States are detected, + * and consist of: + * - a house number + * - a street name + * - a street type (Road, Circle, etc), either spelled out or abbreviated + * - a city name + * - a state or territory, either spelled out or two-letter abbr. + * - an optional 5 digit or 9 digit zip code. + * + * Names are optionally capitalized, and the zip code, if present, + * must be valid for the state. The street type must be a standard USPS + * spelling or abbreviation. The state or territory must also be spelled + * or abbreviated using USPS standards. The house number may not exceed + * five digits. + * @param addr The string to search for addresses. + * @param caseInsensitive addr Set to true to make search ignore case. + * + * @return the address, or if no address is found, return null. + */ + public static String findAddress(String addr, boolean caseInsensitive) { + return WebViewCore.nativeFindAddress(addr, caseInsensitive); } /* * Clear the highlighting surrounding text matches created by findAll. */ public void clearMatches() { - mFindIsUp = false; + if (mFindIsUp) { + recordNewContentSize(mContentWidth, mContentHeight - mFindHeight, + false); + 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. @@ -2104,6 +2425,16 @@ public class WebView extends AbsoluteLayout } /** + * @hide + */ + public void setFindDialogHeight(int height) { + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "setFindDialogHeight height=" + height); + } + mFindHeight = height; + } + + /** * Query the document to see if it contains any image references. The * message object will be dispatched with arg1 being set to 1 if images * were found and 0 if the document does not reference any images. @@ -2145,7 +2476,6 @@ public class WebView extends AbsoluteLayout private boolean pinScrollBy(int dx, int dy, boolean animate, int animationDuration) { return pinScrollTo(mScrollX + dx, mScrollY + dy, animate, animationDuration); } - // helper to pin the scrollTo parameters (already in view coordinates) // returns true if the scroll was changed private boolean pinScrollTo(int x, int y, boolean animate, int animationDuration) { @@ -2157,15 +2487,14 @@ public class WebView extends AbsoluteLayout if ((dx | dy) == 0) { return false; } - - if (true && animate) { + if (animate) { // Log.d(LOGTAG, "startScroll: " + dx + " " + dy); - mScroller.startScroll(mScrollX, mScrollY, dx, dy, animationDuration > 0 ? animationDuration : computeDuration(dx, dy)); + awakenScrollBars(mScroller.getDuration()); invalidate(); } else { - mScroller.abortAnimation(); // just in case + abortAnimation(); // just in case scrollTo(x, y); } return true; @@ -2173,16 +2502,16 @@ public class WebView extends AbsoluteLayout // Scale from content to view coordinates, and pin. // Also called by jni webview.cpp - private void setContentScrollBy(int cx, int cy, boolean animate) { + private boolean setContentScrollBy(int cx, int cy, boolean animate) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture // is used in the view system. // TODO: as we switchOutDrawHistory when trackball or navigation // keys are hit, this should be safe. Right? - return; + return false; } - cx = contentToView(cx); - cy = contentToView(cy); + cx = contentToViewDimension(cx); + cy = contentToViewDimension(cy); if (mHeightCanMeasure) { // move our visible rect according to scroll request if (cy != 0) { @@ -2196,17 +2525,18 @@ public class WebView extends AbsoluteLayout // FIXME: Why do we only scroll horizontally if there is no // vertical scroll? // Log.d(LOGTAG, "setContentScrollBy cy=" + cy); - if (cy == 0 && cx != 0) { - pinScrollBy(cx, 0, animate, 0); - } + return cy == 0 && cx != 0 && pinScrollBy(cx, 0, animate, 0); } else { - pinScrollBy(cx, cy, animate, 0); + return pinScrollBy(cx, cy, animate, 0); } } // scale from content to view coordinates, and pin - // return true if pin caused the final x/y different than the request cx/cy; - // return false if the view scroll to the exact position as it is requested. + // return true if pin caused the final x/y different than the request cx/cy, + // and a future scroll may reach the request cx/cy after our size has + // changed + // return false if the view scroll to the exact position as it is requested, + // where negative numbers are taken to mean 0 private boolean setContentScrollTo(int cx, int cy) { if (mDrawHistory) { // disallow WebView to change the scroll position as History Picture @@ -2216,12 +2546,35 @@ public class WebView extends AbsoluteLayout // saved scroll position, it is ok to skip this. return false; } - int vx = contentToView(cx); - int vy = contentToView(cy); + int vx; + int vy; + if ((cx | cy) == 0) { + // If the page is being scrolled to (0,0), do not add in the title + // bar's height, and simply scroll to (0,0). (The only other work + // in contentToView_ is to multiply, so this would not change 0.) + vx = 0; + vy = 0; + } else { + vx = contentToViewX(cx); + vy = contentToViewY(cy); + } // Log.d(LOGTAG, "content scrollTo [" + cx + " " + cy + "] view=[" + // vx + " " + vy + "]"); + // Some mobile sites attempt to scroll the title bar off the page by + // scrolling to (0,1). If we are at the top left corner of the + // page, assume this is an attempt to scroll off the title bar, and + // animate the title bar off screen slowly enough that the user can see + // it. + if (cx == 0 && cy == 1 && mScrollX == 0 && mScrollY == 0) { + pinScrollTo(vx, vy, true, SLIDE_TITLE_DURATION); + // Since we are animating, we have not yet reached the desired + // scroll position. Do not return true to request another attempt + return false; + } pinScrollTo(vx, vy, false, 0); - if (mScrollX != vx || mScrollY != vy) { + // If the request was to scroll to a negative coordinate, treat it as if + // it was a request to scroll to 0 + if ((mScrollX != vx && cx >= 0) || (mScrollY != vy && cy >= 0)) { return true; } else { return false; @@ -2235,8 +2588,8 @@ public class WebView extends AbsoluteLayout // is used in the view system. return; } - int vx = contentToView(cx); - int vy = contentToView(cy); + int vx = contentToViewX(cx); + int vy = contentToViewY(cy); pinScrollTo(vx, vy, true, 0); } @@ -2253,12 +2606,12 @@ public class WebView extends AbsoluteLayout } if (mHeightCanMeasure) { - if (getMeasuredHeight() != contentToView(mContentHeight) + if (getMeasuredHeight() != contentToViewDimension(mContentHeight) && updateLayout) { requestLayout(); } } else if (mWidthCanMeasure) { - if (getMeasuredWidth() != contentToView(mContentWidth) + if (getMeasuredWidth() != contentToViewDimension(mContentWidth) && updateLayout) { requestLayout(); } @@ -2299,6 +2652,16 @@ public class WebView extends AbsoluteLayout } /** + * Gets the chrome handler. + * @return the current WebChromeClient instance. + * + * @hide API council approval. + */ + public WebChromeClient getWebChromeClient() { + return mCallbackProxy.getWebChromeClient(); + } + + /** * Set the Picture listener. This is an interface used to receive * notifications of a new Picture. * @param listener An implementation of WebView.PictureListener. @@ -2343,10 +2706,9 @@ public class WebView extends AbsoluteLayout * @param interfaceName The name to used to expose the class in Javascript */ public void addJavascriptInterface(Object obj, String interfaceName) { - // Use Hashmap rather than Bundle as Bundles can't cope with Objects - HashMap arg = new HashMap(); - arg.put("object", obj); - arg.put("interfaceName", interfaceName); + WebViewCore.JSInterfaceData arg = new WebViewCore.JSInterfaceData(); + arg.mObject = obj; + arg.mInterfaceName = interfaceName; mWebViewCore.sendMessage(EventHub.ADD_JS_INTERFACE, arg); } @@ -2363,26 +2725,19 @@ public class WebView extends AbsoluteLayout /** * Return the list of currently loaded plugins. * @return The list of currently loaded plugins. + * + * @deprecated This was used for Gears, which has been deprecated. */ + @Deprecated public static synchronized PluginList getPluginList() { - if (sPluginList == null) { - sPluginList = new PluginList(); - } - return sPluginList; + return null; } /** - * Signal the WebCore thread to refresh its list of plugins. Use - * this if the directory contents of one of the plugin directories - * has been modified and needs its changes reflecting. May cause - * plugin load and/or unload. - * @param reloadOpenPages Set to true to reload all open pages. + * @deprecated This was used for Gears, which has been deprecated. */ - public void refreshPlugins(boolean reloadOpenPages) { - if (mWebViewCore != null) { - mWebViewCore.sendMessage(EventHub.REFRESH_PLUGINS, reloadOpenPages); - } - } + @Deprecated + public void refreshPlugins(boolean reloadOpenPages) { } //------------------------------------------------------------------------- // Override View methods @@ -2390,44 +2745,56 @@ public class WebView extends AbsoluteLayout @Override protected void finalize() throws Throwable { - destroy(); + try { + destroy(); + } finally { + super.finalize(); + } } - + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + if (child == mTitleBar) { + // When drawing the title bar, move it horizontally to always show + // at the top of the WebView. + mTitleBar.offsetLeftAndRight(mScrollX - mTitleBar.getLeft()); + } + return super.drawChild(canvas, child, drawingTime); + } + @Override protected void onDraw(Canvas canvas) { // if mNativeClass is 0, the WebView has been destroyed. Do nothing. if (mNativeClass == 0) { return; } - if (mWebViewCore.mEndScaleZoom) { - mWebViewCore.mEndScaleZoom = false; - if (mTouchMode >= FIRST_SCROLL_ZOOM - && mTouchMode <= LAST_SCROLL_ZOOM) { - setHorizontalScrollBarEnabled(true); - setVerticalScrollBarEnabled(true); - mTouchMode = TOUCH_DONE_MODE; - } + int saveCount = canvas.save(); + if (mTitleBar != null) { + canvas.translate(0, (int) mTitleBar.getHeight()); + } + // Update the buttons in the picture, so when we draw the picture + // to the screen, they are in the correct state. + // Tell the native side if user is a) touching the screen, + // b) pressing the trackball down, or c) pressing the enter key + // If the cursor is on a button, we need to draw it in the pressed + // state. + // If mNativeClass is 0, we should not reach here, so we do not + // need to check it again. + nativeRecordButtons(hasFocus() && hasWindowFocus(), + mTouchMode == TOUCH_SHORTPRESS_START_MODE + || mTrackballDown || mGotCenterDown, false); + drawCoreAndCursorRing(canvas, mBackgroundColor, mDrawCursorRing); + canvas.restoreToCount(saveCount); + + // Now draw the shadow. + if (mTitleBar != null) { + int y = mScrollY + getVisibleTitleHeight(); + int height = (int) (5f * getContext().getResources() + .getDisplayMetrics().density); + mTitleShadow.setBounds(mScrollX, y, mScrollX + getWidth(), + y + height); + mTitleShadow.draw(canvas); } - int sc = canvas.save(); - if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { - scrollZoomDraw(canvas); - } else { - nativeRecomputeFocus(); - // Update the buttons in the picture, so when we draw the picture - // to the screen, they are in the correct state. - // Tell the native side if user is a) touching the screen, - // b) pressing the trackball down, or c) pressing the enter key - // If the focus is a button, we need to draw it in the pressed - // state. - // If mNativeClass is 0, we should not reach here, so we do not - // need to check it again. - nativeRecordButtons(hasFocus() && hasWindowFocus(), - mTouchMode == TOUCH_SHORTPRESS_START_MODE - || mTrackballDown || mGotEnterDown, false); - drawCoreAndFocusRing(canvas, mBackgroundColor, mDrawFocusRing); - } - canvas.restoreToCount(sc); - if (AUTO_REDRAW_HACK && mAutoRedraw) { invalidate(); } @@ -2443,15 +2810,36 @@ public class WebView extends AbsoluteLayout @Override public boolean performLongClick() { + if (mNativeClass != 0 && nativeCursorIsTextInput()) { + // Send the click so that the textfield is in focus + // FIXME: When we start respecting changes to the native textfield's + // selection, need to make sure that this does not change it. + mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), + nativeCursorNodePointer()); + rebuildWebTextView(); + } if (inEditingMode()) { - return mTextEntry.performLongClick(); + return mWebTextView.performLongClick(); } else { return super.performLongClick(); } } - private void drawCoreAndFocusRing(Canvas canvas, int color, - boolean drawFocus) { + boolean inAnimateZoom() { + return mZoomScale != 0; + } + + /** + * Need to adjust the WebTextView after a change in zoom, since mActualScale + * has changed. This is especially important for password fields, which are + * drawn by the WebTextView, since it conveys more information than what + * webkit draws. Thus we need to reposition it to show in the correct + * place. + */ + private boolean mNeedToAdjustWebTextView; + + private void drawCoreAndCursorRing(Canvas canvas, int color, + boolean drawCursorRing) { if (mDrawHistory) { canvas.scale(mActualScale, mActualScale); canvas.drawPicture(mHistoryPicture); @@ -2459,41 +2847,79 @@ public class WebView extends AbsoluteLayout } boolean animateZoom = mZoomScale != 0; - boolean animateScroll = !mScroller.isFinished() + boolean animateScroll = !mScroller.isFinished() || mVelocityTracker != null; if (animateZoom) { float zoomScale; int interval = (int) (SystemClock.uptimeMillis() - mZoomStart); if (interval < ZOOM_ANIMATION_LENGTH) { float ratio = (float) interval / ZOOM_ANIMATION_LENGTH; - zoomScale = 1.0f / (mInvInitialZoomScale + zoomScale = 1.0f / (mInvInitialZoomScale + (mInvFinalZoomScale - mInvInitialZoomScale) * ratio); invalidate(); } else { zoomScale = mZoomScale; // set mZoomScale to be 0 as we have done animation mZoomScale = 0; + // call invalidate() again to draw with the final filters + invalidate(); + if (mNeedToAdjustWebTextView) { + mNeedToAdjustWebTextView = false; + Rect contentBounds = nativeFocusCandidateNodeBounds(); + Rect vBox = contentToViewRect(contentBounds); + Rect visibleRect = new Rect(); + calcOurVisibleRect(visibleRect); + if (visibleRect.contains(vBox)) { + // As a result of the zoom, the textfield is now on + // screen. Place the WebTextView in its new place, + // accounting for our new scroll/zoom values. + mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + contentToViewDimension( + nativeFocusCandidateTextSize())); + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), + vBox.height()); + // If it is a password field, start drawing the + // WebTextView once again. + if (nativeFocusCandidateIsPassword()) { + mWebTextView.setInPassword(true); + } + } else { + // The textfield is now off screen. The user probably + // was not zooming to see the textfield better. Remove + // the WebTextView. If the user types a key, and the + // textfield is still in focus, we will reconstruct + // the WebTextView and scroll it back on screen. + mWebTextView.remove(); + } + } } - float scale = (mActualScale - zoomScale) * mInvActualScale; - float tx = scale * (mZoomCenterX + mScrollX); - float ty = scale * (mZoomCenterY + mScrollY); - - // this block pins the translate to "legal" bounds. This makes the - // animation a bit non-obvious, but it means we won't pop when the - // "real" zoom takes effect - if (true) { - // canvas.translate(mScrollX, mScrollY); - tx -= mScrollX; - ty -= mScrollY; - tx = -pinLoc(-Math.round(tx), getViewWidth(), Math - .round(mContentWidth * zoomScale)); - ty = -pinLoc(-Math.round(ty), getViewHeight(), Math - .round(mContentHeight * zoomScale)); - tx += mScrollX; - ty += mScrollY; - } + // calculate the intermediate scroll position. As we need to use + // zoomScale, we can't use pinLocX/Y directly. Copy the logic here. + float scale = zoomScale * mInvInitialZoomScale; + int tx = Math.round(scale * (mInitialScrollX + mZoomCenterX) + - mZoomCenterX); + tx = -pinLoc(tx, getViewWidth(), Math.round(mContentWidth + * zoomScale)) + mScrollX; + int titleHeight = getTitleHeight(); + int ty = Math.round(scale + * (mInitialScrollY + mZoomCenterY - titleHeight) + - (mZoomCenterY - titleHeight)); + ty = -(ty <= titleHeight ? Math.max(ty, 0) : pinLoc(ty + - titleHeight, getViewHeight(), Math.round(mContentHeight + * zoomScale)) + titleHeight) + mScrollY; canvas.translate(tx, ty); canvas.scale(zoomScale, zoomScale); + if (inEditingMode() && !mNeedToAdjustWebTextView + && mZoomScale != 0) { + // The WebTextView is up. Keep track of this so we can adjust + // its size and placement when we finish zooming + mNeedToAdjustWebTextView = true; + // If it is in password mode, turn it off so it does not draw + // misplaced. + if (nativeFocusCandidateIsPassword()) { + mWebTextView.setInPassword(false); + } + } } else { canvas.scale(mActualScale, mActualScale); } @@ -2502,14 +2928,14 @@ public class WebView extends AbsoluteLayout animateScroll); if (mNativeClass == 0) return; - if (mShiftIsPressed) { + if (mShiftIsPressed && !animateZoom) { if (mTouchSelection) { nativeDrawSelectionRegion(canvas); } else { - nativeDrawSelection(canvas, mSelectX, mSelectY, - mExtendSelection); + nativeDrawSelection(canvas, mInvActualScale, getTitleHeight(), + mSelectX, mSelectY, mExtendSelection); } - } else if (drawFocus) { + } else if (drawCursorRing) { if (mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mTouchMode = TOUCH_SHORTPRESS_MODE; HitTestResult hitTest = getHitTestResult(); @@ -2520,7 +2946,7 @@ public class WebView extends AbsoluteLayout LONG_PRESS_TIMEOUT); } } - nativeDrawFocusRing(canvas); + nativeDrawCursorRing(canvas); } // When the FindDialog is up, only draw the matches if we are not in // the process of scrolling them into view. @@ -2529,363 +2955,6 @@ public class WebView extends AbsoluteLayout } } - private native void nativeDrawMatches(Canvas canvas); - - private float scrollZoomGridScale(float invScale) { - float griddedInvScale = (int) (invScale * SCROLL_ZOOM_GRID) - / (float) SCROLL_ZOOM_GRID; - return 1.0f / griddedInvScale; - } - - private float scrollZoomX(float scale) { - int width = getViewWidth(); - float maxScrollZoomX = mContentWidth * scale - width; - int maxX = mContentWidth - width; - return -(maxScrollZoomX > 0 ? mZoomScrollX * maxScrollZoomX / maxX - : maxScrollZoomX / 2); - } - - private float scrollZoomY(float scale) { - int height = getViewHeight(); - float maxScrollZoomY = mContentHeight * scale - height; - int maxY = mContentHeight - height; - return -(maxScrollZoomY > 0 ? mZoomScrollY * maxScrollZoomY / maxY - : maxScrollZoomY / 2); - } - - private void drawMagnifyFrame(Canvas canvas, Rect frame, Paint paint) { - final float ADORNMENT_LEN = 16.0f; - float width = frame.width(); - float height = frame.height(); - Path path = new Path(); - path.moveTo(-ADORNMENT_LEN, -ADORNMENT_LEN); - path.lineTo(0, 0); - path.lineTo(width, 0); - path.lineTo(width + ADORNMENT_LEN, -ADORNMENT_LEN); - path.moveTo(-ADORNMENT_LEN, height + ADORNMENT_LEN); - path.lineTo(0, height); - path.lineTo(width, height); - path.lineTo(width + ADORNMENT_LEN, height + ADORNMENT_LEN); - path.moveTo(0, 0); - path.lineTo(0, height); - path.moveTo(width, 0); - path.lineTo(width, height); - path.offset(frame.left, frame.top); - canvas.drawPath(path, paint); - } - - // Returns frame surrounding magified portion of screen while - // scroll-zoom is enabled. The frame is also used to center the - // zoom-in zoom-out points at the start and end of the animation. - private Rect scrollZoomFrame(int width, int height, float halfScale) { - Rect scrollFrame = new Rect(); - scrollFrame.set(mZoomScrollX, mZoomScrollY, - mZoomScrollX + width, mZoomScrollY + height); - if (mContentWidth * mZoomScrollLimit < width) { - float scale = zoomFrameScaleX(width, halfScale, 1.0f); - float offsetX = (width * scale - width) * 0.5f; - scrollFrame.left -= offsetX; - scrollFrame.right += offsetX; - } - if (mContentHeight * mZoomScrollLimit < height) { - float scale = zoomFrameScaleY(height, halfScale, 1.0f); - float offsetY = (height * scale - height) * 0.5f; - scrollFrame.top -= offsetY; - scrollFrame.bottom += offsetY; - } - return scrollFrame; - } - - private float zoomFrameScaleX(int width, float halfScale, float noScale) { - // mContentWidth > width > mContentWidth * mZoomScrollLimit - if (mContentWidth <= width) { - return halfScale; - } - float part = (width - mContentWidth * mZoomScrollLimit) - / (width * (1 - mZoomScrollLimit)); - return halfScale * part + noScale * (1.0f - part); - } - - private float zoomFrameScaleY(int height, float halfScale, float noScale) { - if (mContentHeight <= height) { - return halfScale; - } - float part = (height - mContentHeight * mZoomScrollLimit) - / (height * (1 - mZoomScrollLimit)); - return halfScale * part + noScale * (1.0f - part); - } - - private float scrollZoomMagScale(float invScale) { - return (invScale * 2 + mInvActualScale) / 3; - } - - private void scrollZoomDraw(Canvas canvas) { - float invScale = mZoomScrollInvLimit; - int elapsed = 0; - if (mTouchMode != SCROLL_ZOOM_OUT) { - elapsed = (int) Math.min(System.currentTimeMillis() - - mZoomScrollStart, SCROLL_ZOOM_DURATION); - float transitionScale = (mZoomScrollInvLimit - mInvActualScale) - * elapsed / SCROLL_ZOOM_DURATION; - if (mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { - invScale = mInvActualScale + transitionScale; - } else { /* if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) */ - invScale = mZoomScrollInvLimit - transitionScale; - } - } - float scale = scrollZoomGridScale(invScale); - invScale = 1.0f / scale; - int width = getViewWidth(); - int height = getViewHeight(); - float halfScale = scrollZoomMagScale(invScale); - Rect scrollFrame = scrollZoomFrame(width, height, halfScale); - if (elapsed == SCROLL_ZOOM_DURATION) { - if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN) { - setHorizontalScrollBarEnabled(true); - setVerticalScrollBarEnabled(true); - updateTextEntry(); - scrollTo((int) (scrollFrame.centerX() * mActualScale) - - (width >> 1), (int) (scrollFrame.centerY() - * mActualScale) - (height >> 1)); - mTouchMode = TOUCH_DONE_MODE; - } else { - mTouchMode = SCROLL_ZOOM_OUT; - } - } - float newX = scrollZoomX(scale); - float newY = scrollZoomY(scale); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "scrollZoomDraw scale=" + scale + " + (" + newX - + ", " + newY + ") mZoomScroll=(" + mZoomScrollX + ", " - + mZoomScrollY + ")" + " invScale=" + invScale + " scale=" - + scale); - } - canvas.translate(newX, newY); - canvas.scale(scale, scale); - boolean animating = mTouchMode != SCROLL_ZOOM_OUT; - if (mDrawHistory) { - int sc = canvas.save(Canvas.CLIP_SAVE_FLAG); - Rect clip = new Rect(0, 0, mHistoryPicture.getWidth(), - mHistoryPicture.getHeight()); - canvas.clipRect(clip, Region.Op.DIFFERENCE); - canvas.drawColor(mBackgroundColor); - canvas.restoreToCount(sc); - canvas.drawPicture(mHistoryPicture); - } else { - mWebViewCore.drawContentPicture(canvas, mBackgroundColor, - animating, true); - } - if (mTouchMode == TOUCH_DONE_MODE) { - return; - } - Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); - paint.setStyle(Paint.Style.STROKE); - paint.setStrokeWidth(30.0f); - paint.setARGB(0x50, 0, 0, 0); - int maxX = mContentWidth - width; - int maxY = mContentHeight - height; - if (true) { // experiment: draw hint to place finger off magnify area - drawMagnifyFrame(canvas, scrollFrame, paint); - } else { - canvas.drawRect(scrollFrame, paint); - } - int sc = canvas.save(); - canvas.clipRect(scrollFrame); - float halfX = (float) mZoomScrollX / maxX; - if (mContentWidth * mZoomScrollLimit < width) { - halfX = zoomFrameScaleX(width, 0.5f, halfX); - } - float halfY = (float) mZoomScrollY / maxY; - if (mContentHeight * mZoomScrollLimit < height) { - halfY = zoomFrameScaleY(height, 0.5f, halfY); - } - canvas.scale(halfScale, halfScale, mZoomScrollX + width * halfX - , mZoomScrollY + height * halfY); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "scrollZoomDraw halfScale=" + halfScale + " w/h=(" - + width + ", " + height + ") half=(" + halfX + ", " - + halfY + ")"); - } - if (mDrawHistory) { - canvas.drawPicture(mHistoryPicture); - } else { - mWebViewCore.drawContentPicture(canvas, mBackgroundColor, - animating, false); - } - canvas.restoreToCount(sc); - if (mTouchMode != SCROLL_ZOOM_OUT) { - invalidate(); - } - } - - private void zoomScrollTap(float x, float y) { - float scale = scrollZoomGridScale(mZoomScrollInvLimit); - float left = scrollZoomX(scale); - float top = scrollZoomY(scale); - int width = getViewWidth(); - int height = getViewHeight(); - x -= width * scale / 2; - y -= height * scale / 2; - mZoomScrollX = Math.min(mContentWidth - width - , Math.max(0, (int) ((x - left) / scale))); - mZoomScrollY = Math.min(mContentHeight - height - , Math.max(0, (int) ((y - top) / scale))); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "zoomScrollTap scale=" + scale + " + (" + left - + ", " + top + ") mZoomScroll=(" + mZoomScrollX + ", " - + mZoomScrollY + ")" + " x=" + x + " y=" + y); - } - } - - private boolean canZoomScrollOut() { - if (mContentWidth == 0 || mContentHeight == 0) { - return false; - } - int width = getViewWidth(); - int height = getViewHeight(); - float x = (float) width / (float) mContentWidth; - float y = (float) height / (float) mContentHeight; - mZoomScrollLimit = Math.max(DEFAULT_MIN_ZOOM_SCALE, Math.min(x, y)); - mZoomScrollInvLimit = 1.0f / mZoomScrollLimit; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "canZoomScrollOut" - + " mInvActualScale=" + mInvActualScale - + " mZoomScrollLimit=" + mZoomScrollLimit - + " mZoomScrollInvLimit=" + mZoomScrollInvLimit - + " mContentWidth=" + mContentWidth - + " mContentHeight=" + mContentHeight - ); - } - // don't zoom out unless magnify area is at least half as wide - // or tall as content - float limit = mZoomScrollLimit * 2; - return mContentWidth >= width * limit - || mContentHeight >= height * limit; - } - - private void startZoomScrollOut() { - setHorizontalScrollBarEnabled(false); - setVerticalScrollBarEnabled(false); - if (getSettings().getBuiltInZoomControls()) { - if (mZoomButtonsController.isVisible()) { - mZoomButtonsController.setVisible(false); - } - } else { - if (mZoomControlRunnable != null) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - } - if (mZoomControls != null) { - mZoomControls.hide(); - } - } - int width = getViewWidth(); - int height = getViewHeight(); - int halfW = width >> 1; - mLastTouchX = halfW; - int halfH = height >> 1; - mLastTouchY = halfH; - mScroller.abortAnimation(); - mZoomScrollStart = System.currentTimeMillis(); - Rect zoomFrame = scrollZoomFrame(width, height - , scrollZoomMagScale(mZoomScrollInvLimit)); - mZoomScrollX = Math.max(0, (int) ((mScrollX + halfW) * mInvActualScale) - - (zoomFrame.width() >> 1)); - mZoomScrollY = Math.max(0, (int) ((mScrollY + halfH) * mInvActualScale) - - (zoomFrame.height() >> 1)); - scrollTo(0, 0); // triggers inval, starts animation - clearTextEntry(); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "startZoomScrollOut mZoomScroll=(" - + mZoomScrollX + ", " + mZoomScrollY +")"); - } - } - - private void zoomScrollOut() { - if (canZoomScrollOut() == false) { - mTouchMode = TOUCH_DONE_MODE; - return; - } - startZoomScrollOut(); - mTouchMode = SCROLL_ZOOM_ANIMATION_OUT; - invalidate(); - } - - private void moveZoomScrollWindow(float x, float y) { - if (Math.abs(x - mLastZoomScrollRawX) < 1.5f - && Math.abs(y - mLastZoomScrollRawY) < 1.5f) { - return; - } - mLastZoomScrollRawX = x; - mLastZoomScrollRawY = y; - int oldX = mZoomScrollX; - int oldY = mZoomScrollY; - int width = getViewWidth(); - int height = getViewHeight(); - int maxZoomX = mContentWidth - width; - if (maxZoomX > 0) { - int maxScreenX = width - (int) Math.ceil(width - * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow-X" - + " maxScreenX=" + maxScreenX + " width=" + width - + " mZoomScrollLimit=" + mZoomScrollLimit + " x=" + x); - } - x += maxScreenX * mLastScrollX / maxZoomX - mLastTouchX; - x *= Math.max(maxZoomX / maxScreenX, mZoomScrollInvLimit); - mZoomScrollX = Math.max(0, Math.min(maxZoomX, (int) x)); - } - int maxZoomY = mContentHeight - height; - if (maxZoomY > 0) { - int maxScreenY = height - (int) Math.ceil(height - * mZoomScrollLimit) - SCROLL_ZOOM_FINGER_BUFFER; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow-Y" - + " maxScreenY=" + maxScreenY + " height=" + height - + " mZoomScrollLimit=" + mZoomScrollLimit + " y=" + y); - } - y += maxScreenY * mLastScrollY / maxZoomY - mLastTouchY; - y *= Math.max(maxZoomY / maxScreenY, mZoomScrollInvLimit); - mZoomScrollY = Math.max(0, Math.min(maxZoomY, (int) y)); - } - if (oldX != mZoomScrollX || oldY != mZoomScrollY) { - invalidate(); - } - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveZoomScrollWindow" - + " scrollTo=(" + mZoomScrollX + ", " + mZoomScrollY + ")" - + " mLastTouch=(" + mLastTouchX + ", " + mLastTouchY + ")" - + " maxZoom=(" + maxZoomX + ", " + maxZoomY + ")" - + " last=("+mLastScrollX+", "+mLastScrollY+")" - + " x=" + x + " y=" + y); - } - } - - private void setZoomScrollIn() { - mZoomScrollStart = System.currentTimeMillis(); - } - - private float mZoomScrollLimit; - private float mZoomScrollInvLimit; - private int mLastScrollX; - private int mLastScrollY; - private long mZoomScrollStart; - private int mZoomScrollX; - private int mZoomScrollY; - private float mLastZoomScrollRawX = -1000.0f; - private float mLastZoomScrollRawY = -1000.0f; - // The zoomed scale varies from 1.0 to DEFAULT_MIN_ZOOM_SCALE == 0.25. - // The zoom animation duration SCROLL_ZOOM_DURATION == 0.5. - // Two pressures compete for gridding; a high frame rate (e.g. 20 fps) - // and minimizing font cache allocations (fewer frames is better). - // A SCROLL_ZOOM_GRID of 6 permits about 20 zoom levels over 0.5 seconds: - // the inverse of: 1.0, 1.16, 1.33, 1.5, 1.67, 1.84, 2.0, etc. to 4.0 - private static final int SCROLL_ZOOM_GRID = 6; - private static final int SCROLL_ZOOM_DURATION = 500; - // Make it easier to get to the bottom of a document by reserving a 32 - // pixel buffer, for when the starting drag is a bit below the bottom of - // the magnify frame. - private static final int SCROLL_ZOOM_FINGER_BUFFER = 32; - // draw history private boolean mDrawHistory = false; private Picture mHistoryPicture = null; @@ -2900,7 +2969,7 @@ public class WebView extends AbsoluteLayout // Should only be called in UI thread void switchOutDrawHistory() { if (null == mWebViewCore) return; // CallbackProxy may trigger this - if (mDrawHistory) { + if (mDrawHistory && mWebViewCore.pictureReady()) { mDrawHistory = false; invalidate(); int oldScrollX = mScrollX; @@ -2916,72 +2985,29 @@ public class WebView extends AbsoluteLayout } } - /** - * Class representing the node which is focused. - */ - private class FocusNode { - public FocusNode() { - mBounds = new Rect(); - } - // Only to be called by JNI - private void setAll(boolean isTextField, boolean isTextArea, boolean - isPassword, boolean isAnchor, boolean isRtlText, int maxLength, - int textSize, int boundsX, int boundsY, int boundsRight, int - boundsBottom, int nodePointer, int framePointer, String text, - String name, int rootTextGeneration) { - mIsTextField = isTextField; - mIsTextArea = isTextArea; - mIsPassword = isPassword; - mIsAnchor = isAnchor; - mIsRtlText = isRtlText; - - mMaxLength = maxLength; - mTextSize = textSize; - - mBounds.set(boundsX, boundsY, boundsRight, boundsBottom); - - - mNodePointer = nodePointer; - mFramePointer = framePointer; - mText = text; - mName = name; - mRootTextGeneration = rootTextGeneration; - } - public boolean mIsTextField; - public boolean mIsTextArea; - public boolean mIsPassword; - public boolean mIsAnchor; - public boolean mIsRtlText; - - public int mSelectionStart; - public int mSelectionEnd; - public int mMaxLength; - public int mTextSize; - - public Rect mBounds; - - public int mNodePointer; - public int mFramePointer; - public String mText; - public String mName; - public int mRootTextGeneration; - } - - // Warning: ONLY use mFocusNode AFTER calling nativeUpdateFocusNode(), - // and ONLY if it returns true; - private FocusNode mFocusNode = new FocusNode(); - + WebViewCore.CursorData cursorData() { + WebViewCore.CursorData result = new WebViewCore.CursorData(); + result.mMoveGeneration = nativeMoveGeneration(); + result.mFrame = nativeCursorFramePointer(); + Point position = nativeCursorPosition(); + result.mX = position.x; + result.mY = position.y; + return result; + } + /** * Delete text from start to end in the focused textfield. If there is no - * focus, or if start == end, silently fail. If start and end are out of + * focus, or if start == end, silently fail. If start and end are out of * order, swap them. * @param start Beginning of selection to delete. * @param end End of selection to delete. */ /* package */ void deleteSelection(int start, int end) { mTextGeneration++; - mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, start, end, - new WebViewCore.FocusData(mFocusData)); + WebViewCore.TextSelectionData data + = new WebViewCore.TextSelectionData(start, end); + mWebViewCore.sendMessage(EventHub.DELETE_SELECTION, mTextGeneration, 0, + data); } /** @@ -2991,119 +3017,128 @@ public class WebView extends AbsoluteLayout * @param end End of selection. */ /* package */ void setSelection(int start, int end) { - mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end, - new WebViewCore.FocusData(mFocusData)); + mWebViewCore.sendMessage(EventHub.SET_SELECTION, start, end); } // Called by JNI when a touch event puts a textfield into focus. - private void displaySoftKeyboard() { + private void displaySoftKeyboard(boolean isTextView) { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mTextEntry, 0); - mTextEntry.enableScrollOnScreen(true); - // Now we need to fake a touch event to place the cursor where the - // user touched. - AbsoluteLayout.LayoutParams lp = (AbsoluteLayout.LayoutParams) - mTextEntry.getLayoutParams(); - if (lp != null) { - // Take the last touch and adjust for the location of the - // TextDialog. - float x = mLastTouchX + (float) (mScrollX - lp.x); - float y = mLastTouchY + (float) (mScrollY - lp.y); - mTextEntry.fakeTouchEvent(x, y); - } - } - - private void updateTextEntry() { - if (mTextEntry == null) { - mTextEntry = new TextDialog(mContext, WebView.this); - // Initialize our generation number. - mTextGeneration = 0; + + if (isTextView) { + if (mWebTextView == null) return; + + imm.showSoftInput(mWebTextView, 0); + if (mInZoomOverview) { + // if in zoom overview mode, call doDoubleTap() to bring it back + // to normal mode so that user can enter text. + doDoubleTap(); + } + } + else { // used by plugins + imm.showSoftInput(this, 0); } - // If we do not have focus, do nothing until we gain focus. - if (!hasFocus() && !mTextEntry.hasFocus() - || (mTouchMode >= FIRST_SCROLL_ZOOM - && mTouchMode <= LAST_SCROLL_ZOOM)) { - mNeedsUpdateTextEntry = true; + } + + // Called by WebKit to instruct the UI to hide the keyboard + private void hideSoftKeyboard() { + InputMethodManager imm = (InputMethodManager) + getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + + imm.hideSoftInputFromWindow(this.getWindowToken(), 0); + } + + /* + * This method checks the current focus and cursor and potentially rebuilds + * mWebTextView to have the appropriate properties, such as password, + * multiline, and what text it contains. It also removes it if necessary. + */ + /* package */ void rebuildWebTextView() { + // If the WebView does not have focus, do nothing until it gains focus. + if (!hasFocus() && (null == mWebTextView || !mWebTextView.hasFocus())) { return; } boolean alreadyThere = inEditingMode(); - if (0 == mNativeClass || !nativeUpdateFocusNode()) { + // inEditingMode can only return true if mWebTextView is non-null, + // so we can safely call remove() if (alreadyThere) + if (0 == mNativeClass || !nativeFocusCandidateIsTextInput()) { if (alreadyThere) { - mTextEntry.remove(); + mWebTextView.remove(); } return; } - FocusNode node = mFocusNode; - if (!node.mIsTextField && !node.mIsTextArea) { - if (alreadyThere) { - mTextEntry.remove(); - } - return; + // At this point, we know we have found an input field, so go ahead + // and create the WebTextView if necessary. + if (mWebTextView == null) { + mWebTextView = new WebTextView(mContext, WebView.this); + // Initialize our generation number. + mTextGeneration = 0; } - mTextEntry.setTextSize(contentToView(node.mTextSize)); - Rect visibleRect = sendOurVisibleRect(); + mWebTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, + contentToViewDimension(nativeFocusCandidateTextSize())); + Rect visibleRect = new Rect(); + calcOurContentVisibleRect(visibleRect); // Note that sendOurVisibleRect calls viewToContent, so the coordinates // should be in content coordinates. - if (!Rect.intersects(node.mBounds, visibleRect)) { - // Node is not on screen, so do not bother. - return; + Rect bounds = nativeFocusCandidateNodeBounds(); + if (!Rect.intersects(bounds, visibleRect)) { + mWebTextView.bringIntoView(); } - int x = node.mBounds.left; - int y = node.mBounds.top; - int width = node.mBounds.width(); - int height = node.mBounds.height(); - if (alreadyThere && mTextEntry.isSameTextField(node.mNodePointer)) { + String text = nativeFocusCandidateText(); + int nodePointer = nativeFocusCandidatePointer(); + if (alreadyThere && mWebTextView.isSameTextField(nodePointer)) { // It is possible that we have the same textfield, but it has moved, // i.e. In the case of opening/closing the screen. // In that case, we need to set the dimensions, but not the other // aspects. // We also need to restore the selection, which gets wrecked by // calling setTextEntryRect. - Spannable spannable = (Spannable) mTextEntry.getText(); + Spannable spannable = (Spannable) mWebTextView.getText(); int start = Selection.getSelectionStart(spannable); int end = Selection.getSelectionEnd(spannable); - setTextEntryRect(x, y, width, height); // If the text has been changed by webkit, update it. However, if // there has been more UI text input, ignore it. We will receive // another update when that text is recognized. - if (node.mText != null && !node.mText.equals(spannable.toString()) - && node.mRootTextGeneration == mTextGeneration) { - mTextEntry.setTextAndKeepSelection(node.mText); + if (text != null && !text.equals(spannable.toString()) + && nativeTextGeneration() == mTextGeneration) { + mWebTextView.setTextAndKeepSelection(text); } else { Selection.setSelection(spannable, start, end); } } else { - String text = node.mText; - setTextEntryRect(x, y, width, height); - mTextEntry.setGravity(node.mIsRtlText ? Gravity.RIGHT : - Gravity.NO_GRAVITY); + Rect vBox = contentToViewRect(bounds); + mWebTextView.setRect(vBox.left, vBox.top, vBox.width(), + vBox.height()); + mWebTextView.setGravity(nativeFocusCandidateIsRtlText() ? + Gravity.RIGHT : Gravity.NO_GRAVITY); // this needs to be called before update adapter thread starts to - // ensure the mTextEntry has the same node pointer - mTextEntry.setNodePointer(node.mNodePointer); + // ensure the mWebTextView has the same node pointer + mWebTextView.setNodePointer(nodePointer); int maxLength = -1; - if (node.mIsTextField) { - maxLength = node.mMaxLength; + boolean isTextField = nativeFocusCandidateIsTextField(); + if (isTextField) { + maxLength = nativeFocusCandidateMaxLength(); + String name = nativeFocusCandidateName(); if (mWebViewCore.getSettings().getSaveFormData() - && node.mName != null) { - HashMap data = new HashMap(); - data.put("text", node.mText); + && name != null) { Message update = mPrivateHandler.obtainMessage( - UPDATE_TEXT_ENTRY_ADAPTER, node.mNodePointer, 0, - data); - UpdateTextEntryAdapter updater = new UpdateTextEntryAdapter( - node.mName, getUrl(), update); + REQUEST_FORM_DATA, nodePointer); + RequestFormData updater = new RequestFormData(name, + getUrl(), update); Thread t = new Thread(updater); t.start(); } } - mTextEntry.setMaxLength(maxLength); + mWebTextView.setMaxLength(maxLength); AutoCompleteAdapter adapter = null; - mTextEntry.setAdapterCustom(adapter); - mTextEntry.setSingleLine(node.mIsTextField); - mTextEntry.setInPassword(node.mIsPassword); + mWebTextView.setAdapterCustom(adapter); + mWebTextView.setSingleLine(isTextField); + mWebTextView.setInPassword(nativeFocusCandidateIsPassword()); if (null == text) { - mTextEntry.setText("", 0, 0); + mWebTextView.setText("", 0, 0); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "rebuildWebTextView null == text"); + } } else { // Change to true to enable the old style behavior, where // entering a textfield/textarea always set the selection to the @@ -3114,24 +3149,35 @@ public class WebView extends AbsoluteLayout // textarea. Testing out a new behavior, where textfields set // selection at the end, and textareas at the beginning. if (false) { - mTextEntry.setText(text, 0, text.length()); - } else if (node.mIsTextField) { + mWebTextView.setText(text, 0, text.length()); + } else if (isTextField) { int length = text.length(); - mTextEntry.setText(text, length, length); + mWebTextView.setText(text, length, length); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "rebuildWebTextView length=" + length); + } } else { - mTextEntry.setText(text, 0, 0); + mWebTextView.setText(text, 0, 0); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "rebuildWebTextView !isTextField"); + } } } - mTextEntry.requestFocus(); + mWebTextView.requestFocus(); } } - private class UpdateTextEntryAdapter implements Runnable { + /* + * This class requests an Adapter for the WebTextView which shows past + * entries stored in the database. It is a Runnable so that it can be done + * in its own thread, without slowing down the UI. + */ + private class RequestFormData implements Runnable { private String mName; private String mUrl; private Message mUpdateMessage; - public UpdateTextEntryAdapter(String name, String url, Message msg) { + public RequestFormData(String name, String url, Message msg) { mName = name; mUrl = url; mUpdateMessage = msg; @@ -3142,29 +3188,21 @@ public class WebView extends AbsoluteLayout if (pastEntries.size() > 0) { AutoCompleteAdapter adapter = new AutoCompleteAdapter(mContext, pastEntries); - ((HashMap) mUpdateMessage.obj).put("adapter", adapter); + mUpdateMessage.obj = adapter; mUpdateMessage.sendToTarget(); } } } - private void setTextEntryRect(int x, int y, int width, int height) { - x = contentToView(x); - y = contentToView(y); - width = contentToView(width); - height = contentToView(height); - mTextEntry.setRect(x, y, width, height); - } - - // This is used to determine long press with the enter key, or - // a center key. Does not affect long press with the trackball/touch. - private boolean mGotEnterDown = false; + // This is used to determine long press with the center key. Does not + // affect long press with the trackball/touch. + private boolean mGotCenterDown = false; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyDown at " + System.currentTimeMillis() - + ", " + event); + + ", " + event + ", unicode=" + event.getUnicodeChar()); } if (mNativeClass == 0) { @@ -3182,37 +3220,33 @@ public class WebView extends AbsoluteLayout // 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; + // 2. the host application wants to handle it; if (event.isSystem() - || mCallbackProxy.uiOverrideKeyEvent(event) - || (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM)) { + || mCallbackProxy.uiOverrideKeyEvent(event)) { return false; } - if (mShiftIsPressed == false && nativeFocusNodeWantsKeyEvents() == false - && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + if (mShiftIsPressed == false && nativeCursorWantsKeyEvents() == false + && (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT)) { mExtendSelection = false; mShiftIsPressed = true; - if (nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - mSelectX = contentToView(node.mBounds.left); - mSelectY = contentToView(node.mBounds.top); + if (nativeHasCursorNode()) { + Rect rect = nativeCursorNodeBounds(); + mSelectX = contentToViewX(rect.left); + mSelectY = contentToViewY(rect.top); } else { mSelectX = mScrollX + (int) mLastTouchX; mSelectY = mScrollY + (int) mLastTouchY; } - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - nativeClearFocus(contentX, contentY); + nativeHideCursor(); } 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())) { + if (navHandledKey(keyCode, 1, false, event.getEventTime(), false)) { playSoundEffect(keyCodeToSoundsEffect(keyCode)); return true; } @@ -3220,13 +3254,12 @@ public class WebView extends AbsoluteLayout return false; } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { switchOutDrawHistory(); if (event.getRepeatCount() == 0) { - mGotEnterDown = true; + mGotCenterDown = true; mPrivateHandler.sendMessageDelayed(mPrivateHandler - .obtainMessage(LONG_PRESS_ENTER), LONG_PRESS_TIMEOUT); + .obtainMessage(LONG_PRESS_CENTER), LONG_PRESS_TIMEOUT); // Already checked mNativeClass, so we do not need to check it // again. nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); @@ -3236,6 +3269,15 @@ public class WebView extends AbsoluteLayout return false; } + if (keyCode != KeyEvent.KEYCODE_SHIFT_LEFT + && keyCode != KeyEvent.KEYCODE_SHIFT_RIGHT) { + // turn off copy select if a shift-key combo is pressed + mExtendSelection = mShiftIsPressed = false; + if (mTouchMode == TOUCH_SELECT_MODE) { + mTouchMode = TOUCH_INIT_MODE; + } + } + if (getSettings().getNavDump()) { switch (keyCode) { case KeyEvent.KEYCODE_4: @@ -3264,8 +3306,30 @@ public class WebView extends AbsoluteLayout } } + if (nativeCursorIsPlugin()) { + nativeUpdatePluginReceivesEvents(); + invalidate(); + } else if (nativeCursorIsTextInput()) { + // This message will put the node in focus, for the DOM's notion + // of focus, and make the focuscontroller active + mWebViewCore.sendMessage(EventHub.CLICK, nativeCursorFramePointer(), + nativeCursorNodePointer()); + // This will bring up the WebTextView and put it in focus, for + // our view system's notion of focus + rebuildWebTextView(); + // Now we need to pass the event to it + return mWebTextView.onKeyDown(keyCode, event); + } else if (nativeHasFocusNode()) { + // In this case, the cursor is not on a text input, but the focus + // might be. Check it, and if so, hand over to the WebTextView. + rebuildWebTextView(); + if (inEditingMode()) { + return mWebTextView.onKeyDown(keyCode, event); + } + } + // TODO: should we pass all the keys to DOM or check the meta tag - if (nativeFocusNodeWantsKeyEvents() || true) { + if (nativeCursorWantsKeyEvents() || true) { // pass the key to DOM mWebViewCore.sendMessage(EventHub.KEY_DOWN, event); // return true as DOM handles the key @@ -3278,20 +3342,19 @@ public class WebView extends AbsoluteLayout @Override public boolean onKeyUp(int keyCode, KeyEvent event) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "keyUp at " + System.currentTimeMillis() - + ", " + event); + + ", " + event + ", unicode=" + event.getUnicodeChar()); } 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 + // special CALL handling when cursor node's href is "tel:XXX" + if (keyCode == KeyEvent.KEYCODE_CALL && nativeHasCursorNode()) { + String text = nativeCursorText(); + if (!nativeCursorIsTextInput() && text != null && text.startsWith(SCHEME_TEL)) { Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(text)); getContext().startActivity(intent); @@ -3306,19 +3369,7 @@ public class WebView extends AbsoluteLayout return false; } - // special handling in scroll_zoom state - if (mTouchMode >= FIRST_SCROLL_ZOOM && mTouchMode <= LAST_SCROLL_ZOOM) { - if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode - && mTouchMode != SCROLL_ZOOM_ANIMATION_IN) { - setZoomScrollIn(); - mTouchMode = SCROLL_ZOOM_ANIMATION_IN; - invalidate(); - return true; - } - return false; - } - - if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { if (commitCopy()) { return true; @@ -3332,55 +3383,42 @@ public class WebView extends AbsoluteLayout return false; } - if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER) { + if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { // remove the long press message first - mPrivateHandler.removeMessages(LONG_PRESS_ENTER); - mGotEnterDown = false; + mPrivateHandler.removeMessages(LONG_PRESS_CENTER); + mGotCenterDown = 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; - } + if (mShiftIsPressed) { + return false; } + // perform the single click Rect visibleRect = sendOurVisibleRect(); // Note that sendOurVisibleRect calls viewToContent, so the // coordinates should be in content coordinates. - if (nativeUpdateFocusNode()) { - if (Rect.intersects(mFocusNode.mBounds, visibleRect)) { - nativeSetFollowedLink(true); - mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, - EventHub.BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP, 0, - new WebViewCore.FocusData(mFocusData)); - playSoundEffect(SoundEffectConstants.CLICK); - if (!mCallbackProxy.uiOverrideUrlLoading(mFocusNode.mText)) { - // use CLICK instead of KEY_DOWN/KEY_UP so that we can - // trigger mouse click events - mWebViewCore.sendMessage(EventHub.CLICK); - } - } - return true; + if (!nativeCursorIntersects(visibleRect)) { + return false; } - // Bubble up the key event as WebView doesn't handle it - return false; + nativeSetFollowedLink(true); + nativeUpdatePluginReceivesEvents(); + WebViewCore.CursorData data = cursorData(); + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, data); + playSoundEffect(SoundEffectConstants.CLICK); + boolean isTextInput = nativeCursorIsTextInput(); + if (isTextInput || !mCallbackProxy.uiOverrideUrlLoading( + nativeCursorText())) { + mWebViewCore.sendMessage(EventHub.CLICK, data.mFrame, + nativeCursorNodePointer()); + } + if (isTextInput) { + rebuildWebTextView(); + displaySoftKeyboard(true); + } + return true; } // TODO: should we pass all the keys to DOM or check the meta tag - if (nativeFocusNodeWantsKeyEvents() || true) { + if (nativeCursorWantsKeyEvents() || true) { // pass the key to DOM mWebViewCore.sendMessage(EventHub.KEY_UP, event); // return true as DOM handles the key @@ -3390,16 +3428,15 @@ public class WebView extends AbsoluteLayout // Bubble up the key event as WebView doesn't handle it return false; } - + /** * @hide */ public void emulateShiftHeld() { + if (0 == mNativeClass) return; // client isn't initialized mExtendSelection = false; mShiftIsPressed = true; - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - nativeClearFocus(contentX, contentY); + nativeHideCursor(); } private boolean commitCopy() { @@ -3447,16 +3484,13 @@ public class WebView extends AbsoluteLayout // Clean up the zoom controller mZoomButtonsController.setVisible(false); } - + // Implementation for OnHierarchyChangeListener public void onChildViewAdded(View parent, View child) {} - + public void onChildViewRemoved(View p, View child) { if (child == this) { - if (inEditingMode()) { - clearTextEntry(); - mNeedsUpdateTextEntry = true; - } + clearTextEntry(); } } @@ -3469,26 +3503,25 @@ public class WebView extends AbsoluteLayout public void onGlobalFocusChanged(View oldFocus, View newFocus) { } - // To avoid drawing the focus ring, and remove the TextView when our window + // To avoid drawing the cursor ring, and remove the TextView when our window // loses focus. @Override public void onWindowFocusChanged(boolean hasWindowFocus) { if (hasWindowFocus) { if (hasFocus()) { // If our window regained focus, and we have focus, then begin - // drawing the focus ring, and restore the TextView if - // necessary. - mDrawFocusRing = true; - if (mNeedsUpdateTextEntry) { - updateTextEntry(); - } + // drawing the cursor ring + mDrawCursorRing = true; if (mNativeClass != 0) { nativeRecordButtons(true, false, true); + if (inEditingMode()) { + mWebViewCore.sendMessage(EventHub.SET_ACTIVE, 1, 0); + } } } else { // If our window gained focus, but we do not have it, do not - // draw the focus ring. - mDrawFocusRing = false; + // draw the cursor ring. + mDrawCursorRing = false; // We do not call nativeRecordButtons here because we assume // that when we lost focus, or window focus, it got called with // false for the first parameter @@ -3497,39 +3530,49 @@ public class WebView extends AbsoluteLayout if (getSettings().getBuiltInZoomControls() && !mZoomButtonsController.isVisible()) { /* * The zoom controls come in their own window, so our window - * loses focus. Our policy is to not draw the focus ring if + * loses focus. Our policy is to not draw the cursor ring if * our window is not focused, but this is an exception since * the user can still navigate the web page with the zoom * controls showing. */ - // If our window has lost focus, stop drawing the focus ring - mDrawFocusRing = false; + // If our window has lost focus, stop drawing the cursor ring + mDrawCursorRing = false; } mGotKeyDown = false; mShiftIsPressed = false; if (mNativeClass != 0) { nativeRecordButtons(false, false, true); } + setFocusControllerInactive(); } invalidate(); super.onWindowFocusChanged(hasWindowFocus); } + /* + * Pass a message to WebCore Thread, telling the WebCore::Page's + * FocusController to be "inactive" so that it will + * not draw the blinking cursor. It gets set to "active" to draw the cursor + * in WebViewCore.cpp, when the WebCore thread receives key events/clicks. + */ + /* package */ void setFocusControllerInactive() { + // Do not need to also check whether mWebViewCore is null, because + // mNativeClass is only set if mWebViewCore is non null + if (mNativeClass == 0) return; + mWebViewCore.sendMessage(EventHub.SET_ACTIVE, 0, 0); + } + @Override protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "MT focusChanged " + focused + ", " + direction); } if (focused) { // When we regain focus, if we have window focus, resume drawing - // the focus ring, and add the TextView if necessary. + // the cursor ring if (hasWindowFocus()) { - mDrawFocusRing = true; - if (mNeedsUpdateTextEntry) { - updateTextEntry(); - mNeedsUpdateTextEntry = false; - } + mDrawCursorRing = true; if (mNativeClass != 0) { nativeRecordButtons(true, false, true); } @@ -3540,12 +3583,13 @@ public class WebView extends AbsoluteLayout } } else { // When we lost focus, unless focus went to the TextView (which is - // true if we are in editing mode), stop drawing the focus ring. + // true if we are in editing mode), stop drawing the cursor ring. if (!inEditingMode()) { - mDrawFocusRing = false; + mDrawCursorRing = false; if (mNativeClass != 0) { nativeRecordButtons(false, false, true); } + setFocusControllerInactive(); } mGotKeyDown = false; } @@ -3557,13 +3601,20 @@ public class WebView extends AbsoluteLayout protected void onSizeChanged(int w, int h, int ow, int oh) { super.onSizeChanged(w, h, ow, oh); // Center zooming to the center of the screen. - mZoomCenterX = getViewWidth() * .5f; - mZoomCenterY = getViewHeight() * .5f; + if (mZoomScale == 0) { // unless we're already zooming + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + } // update mMinZoomScale if the minimum zoom scale is not fixed if (!mMinZoomScaleFixed) { - mMinZoomScale = (float) getViewWidth() - / Math.max(ZOOM_OUT_WIDTH, mContentWidth); + // when change from narrow screen to wide screen, the new viewWidth + // can be wider than the old content width. We limit the minimum + // scale to 1.0f. The proper minimum scale will be calculated when + // the new picture shows up. + mMinZoomScale = Math.min(1.0f, (float) getViewWidth() + / (mDrawHistory ? mHistoryPicture.getWidth() + : mZoomOverviewWidth)); } // we always force, in case our height changed, in which case we still @@ -3574,10 +3625,11 @@ public class WebView extends AbsoluteLayout @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); + sendOurVisibleRect(); } - - + + @Override public boolean dispatchKeyEvent(KeyEvent event) { boolean dispatch = true; @@ -3621,7 +3673,7 @@ public class WebView extends AbsoluteLayout return false; } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, ev + " at " + ev.getEventTime() + " mTouchMode=" + mTouchMode); } @@ -3638,20 +3690,17 @@ public class WebView extends AbsoluteLayout if (x > getViewWidth() - 1) { x = getViewWidth() - 1; } - if (y > getViewHeight() - 1) { - y = getViewHeight() - 1; + if (y > getViewHeightWithTitle() - 1) { + y = getViewHeightWithTitle() - 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)) { + if (mForwardTouchEvents && (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); + ted.mX = viewToContentX((int) x + mScrollX); + ted.mY = viewToContentY((int) y + mScrollY); mWebViewCore.sendMessage(EventHub.TOUCH_EVENT, ted); mLastSentTouchTime = eventTime; } @@ -3661,15 +3710,11 @@ public class WebView extends AbsoluteLayout switch (action) { case MotionEvent.ACTION_DOWN: { - if (mTouchMode == SCROLL_ZOOM_ANIMATION_IN - || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { - // no interaction while animation is in progress - break; - } else if (mTouchMode == SCROLL_ZOOM_OUT) { - mLastScrollX = mZoomScrollX; - mLastScrollY = mZoomScrollY; - // If two taps are close, ignore the first tap - } else if (!mScroller.isFinished()) { + mPreventDrag = PREVENT_DRAG_NO; + if (!mScroller.isFinished()) { + // stop the current scroll animation, but if this is + // the start of a fling, allow it to add to the current + // fling's velocity mScroller.abortAnimation(); mTouchMode = TOUCH_DRAG_START_MODE; mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE); @@ -3677,22 +3722,35 @@ public class WebView extends AbsoluteLayout mSelectX = mScrollX + (int) x; mSelectY = mScrollY + (int) y; mTouchMode = TOUCH_SELECT_MODE; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "select=" + mSelectX + "," + mSelectY); } - nativeMoveSelection(viewToContent(mSelectX) - , viewToContent(mSelectY), false); + nativeMoveSelection(viewToContentX(mSelectX), + viewToContentY(mSelectY), false); mTouchSelection = mExtendSelection = true; + } else if (mPrivateHandler.hasMessages(RELEASE_SINGLE_TAP)) { + mPrivateHandler.removeMessages(RELEASE_SINGLE_TAP); + if (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare) { + mTouchMode = TOUCH_DOUBLE_TAP_MODE; + } else { + // commit the short press action for the previous tap + doShortPress(); + // continue, mTouchMode should be still TOUCH_INIT_MODE + } } else { mTouchMode = TOUCH_INIT_MODE; - mPreventDrag = mForwardTouchEvents; + mPreventDrag = mForwardTouchEvents ? PREVENT_DRAG_MAYBE_YES + : PREVENT_DRAG_NO; + mWebViewCore.sendMessage( + EventHub.UPDATE_FRAME_CACHE_IF_LOADING); if (mLogEvent && eventTime - mLastTouchUpTime < 1000) { EventLog.writeEvent(EVENT_LOG_DOUBLE_TAP_DURATION, (eventTime - mLastTouchUpTime), eventTime); } } // Trigger the link - if (mTouchMode == TOUCH_INIT_MODE) { + if (mTouchMode == TOUCH_INIT_MODE + || mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mPrivateHandler.sendMessageDelayed(mPrivateHandler .obtainMessage(SWITCH_TO_SHORTPRESS), TAP_TIMEOUT); } @@ -3705,40 +3763,38 @@ public class WebView extends AbsoluteLayout break; } case MotionEvent.ACTION_MOVE: { - if (mTouchMode == TOUCH_DONE_MODE - || mTouchMode == SCROLL_ZOOM_ANIMATION_IN - || mTouchMode == SCROLL_ZOOM_ANIMATION_OUT) { + if (mTouchMode == TOUCH_DONE_MODE) { // no dragging during scroll zoom animation break; } - if (mTouchMode == SCROLL_ZOOM_OUT) { - // while fully zoomed out, move the virtual window - moveZoomScrollWindow(x, y); - break; - } mVelocityTracker.addMovement(ev); if (mTouchMode != TOUCH_DRAG_MODE) { if (mTouchMode == TOUCH_SELECT_MODE) { mSelectX = mScrollX + (int) x; mSelectY = mScrollY + (int) y; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "xtend=" + mSelectX + "," + mSelectY); } - nativeMoveSelection(viewToContent(mSelectX) - , viewToContent(mSelectY), true); + nativeMoveSelection(viewToContentX(mSelectX), + viewToContentY(mSelectY), true); invalidate(); break; } - if (mPreventDrag || (deltaX * deltaX + deltaY * deltaY) - < mTouchSlopSquare) { + if ((deltaX * deltaX + deltaY * deltaY) < mTouchSlopSquare) { + break; + } + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + // track mLastTouchTime as we may need to do fling at + // ACTION_UP + mLastTouchTime = eventTime; break; } - if (mTouchMode == TOUCH_SHORTPRESS_MODE || mTouchMode == TOUCH_SHORTPRESS_START_MODE) { mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); - } else if (mTouchMode == TOUCH_INIT_MODE) { + } else if (mTouchMode == TOUCH_INIT_MODE + || mTouchMode == TOUCH_DOUBLE_TAP_MODE) { mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); } @@ -3755,24 +3811,22 @@ public class WebView extends AbsoluteLayout mTouchMode = TOUCH_DRAG_MODE; WebViewCore.pauseUpdate(mWebViewCore); - int contentX = viewToContent((int) x + mScrollX); - int contentY = viewToContent((int) y + mScrollY); - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } - nativeClearFocus(contentX, contentY); - // remove the zoom anchor if there is any - if (mZoomScale != 0) { - mWebViewCore - .sendMessage(EventHub.SET_SNAP_ANCHOR, 0, 0); + if (!mDragFromTextInput) { + nativeHideCursor(); } WebSettings settings = getSettings(); if (settings.supportZoom() && settings.getBuiltInZoomControls() && !mZoomButtonsController.isVisible() - && (canZoomScrollOut() || - mMinZoomScale < mMaxZoomScale)) { + && mMinZoomScale < mMaxZoomScale) { mZoomButtonsController.setVisible(true); + int count = settings.getDoubleTapToastCount(); + if (mInZoomOverview && count > 0) { + settings.setDoubleTapToastCount(--count); + Toast.makeText(mContext, + com.android.internal.R.string.double_tap_toast, + Toast.LENGTH_LONG).show(); + } } } @@ -3796,7 +3850,7 @@ public class WebView extends AbsoluteLayout } // reverse direction means lock in the snap mode if ((ax > MAX_SLOPE_FOR_DIAG * ay) && - ((mSnapPositive && + ((mSnapPositive && deltaX < -mMinLockSnapReverseDistance) || (!mSnapPositive && deltaX > mMinLockSnapReverseDistance))) { @@ -3810,9 +3864,9 @@ public class WebView extends AbsoluteLayout } // reverse direction means lock in the snap mode if ((ay > MAX_SLOPE_FOR_DIAG * ax) && - ((mSnapPositive && + ((mSnapPositive && deltaY < -mMinLockSnapReverseDistance) - || (!mSnapPositive && + || (!mSnapPositive && deltaY > mMinLockSnapReverseDistance))) { mSnapScrollMode = SNAP_Y_LOCK; } @@ -3821,11 +3875,25 @@ public class WebView extends AbsoluteLayout if (mSnapScrollMode == SNAP_X || mSnapScrollMode == SNAP_X_LOCK) { - scrollBy(deltaX, 0); + if (deltaX == 0) { + // keep the scrollbar on the screen even there is no + // scroll + awakenScrollBars(ViewConfiguration + .getScrollDefaultDelay(), false); + } else { + scrollBy(deltaX, 0); + } mLastTouchX = x; } else if (mSnapScrollMode == SNAP_Y || mSnapScrollMode == SNAP_Y_LOCK) { - scrollBy(0, deltaY); + if (deltaY == 0) { + // keep the scrollbar on the screen even there is no + // scroll + awakenScrollBars(ViewConfiguration + .getScrollDefaultDelay(), false); + } else { + scrollBy(0, deltaY); + } mLastTouchY = y; } else { scrollBy(deltaX, deltaY); @@ -3838,12 +3906,11 @@ public class WebView extends AbsoluteLayout if (!getSettings().getBuiltInZoomControls()) { boolean showPlusMinus = mMinZoomScale < mMaxZoomScale; - boolean showMagnify = canZoomScrollOut(); - if (mZoomControls != null && (showPlusMinus || showMagnify)) { + if (mZoomControls != null && showPlusMinus) { if (mZoomControls.getVisibility() == View.VISIBLE) { mPrivateHandler.removeCallbacks(mZoomControlRunnable); } else { - mZoomControls.show(showPlusMinus, showMagnify); + mZoomControls.show(showPlusMinus, false); } mPrivateHandler.postDelayed(mZoomControlRunnable, ZOOM_CONTROLS_TIMEOUT); @@ -3851,6 +3918,9 @@ public class WebView extends AbsoluteLayout } if (done) { + // keep the scrollbar on the screen even there is no scroll + awakenScrollBars(ViewConfiguration.getScrollDefaultDelay(), + false); // return false to indicate that we can't pan out of the // view space return false; @@ -3860,41 +3930,54 @@ public class WebView extends AbsoluteLayout case MotionEvent.ACTION_UP: { mLastTouchUpTime = eventTime; switch (mTouchMode) { - case TOUCH_INIT_MODE: // tap - case TOUCH_SHORTPRESS_START_MODE: - case TOUCH_SHORTPRESS_MODE: + case TOUCH_DOUBLE_TAP_MODE: // double tap mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); - mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mTouchMode = TOUCH_DONE_MODE; - doShortPress(); + doDoubleTap(); break; case TOUCH_SELECT_MODE: commitCopy(); mTouchSelection = false; break; - case SCROLL_ZOOM_ANIMATION_IN: - case SCROLL_ZOOM_ANIMATION_OUT: - // no action during scroll animation - break; - case SCROLL_ZOOM_OUT: - if (LOGV_ENABLED) { - Log.v(LOGTAG, "ACTION_UP SCROLL_ZOOM_OUT" - + " eventTime - mLastTouchTime=" - + (eventTime - mLastTouchTime)); - } - // for now, always zoom back when the drag completes - if (true || eventTime - mLastTouchTime < TAP_TIMEOUT) { - // but if we tap, zoom in where we tap - if (eventTime - mLastTouchTime < TAP_TIMEOUT) { - zoomScrollTap(x, y); + case TOUCH_INIT_MODE: // tap + case TOUCH_SHORTPRESS_START_MODE: + case TOUCH_SHORTPRESS_MODE: + mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); + mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); + if ((deltaX * deltaX + deltaY * deltaY) > mTouchSlopSquare) { + Log.w(LOGTAG, "Miss a drag as we are waiting for" + + " WebCore's response for touch down."); + if (computeHorizontalScrollExtent() < computeHorizontalScrollRange() + || computeVerticalScrollExtent() < computeVerticalScrollRange()) { + // we will not rewrite drag code here, but we + // will try fling if it applies. + WebViewCore.pauseUpdate(mWebViewCore); + // fall through to TOUCH_DRAG_MODE + } else { + break; + } + } else { + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + // if mPreventDrag is not confirmed, treat it as + // no so that it won't block tap or double tap. + mPreventDrag = PREVENT_DRAG_NO; } - // start zooming in back to the original view - setZoomScrollIn(); - mTouchMode = SCROLL_ZOOM_ANIMATION_IN; - invalidate(); + if (mPreventDrag == PREVENT_DRAG_NO) { + if (mTouchMode == TOUCH_INIT_MODE) { + mPrivateHandler.sendMessageDelayed( + mPrivateHandler.obtainMessage( + RELEASE_SINGLE_TAP), + ViewConfiguration.getDoubleTapTimeout()); + } else { + mTouchMode = TOUCH_DONE_MODE; + doShortPress(); + } + } + break; } - break; case TOUCH_DRAG_MODE: + // redraw in high-quality, as we're done dragging + invalidate(); // if the user waits a while w/o moving before the // up, we don't want to do a fling if (eventTime - mLastTouchTime <= MIN_FLING_TIME) { @@ -3902,6 +3985,7 @@ public class WebView extends AbsoluteLayout doFling(); break; } + mLastVelocity = 0; WebViewCore.resumeUpdate(mWebViewCore); break; case TOUCH_DRAG_START_MODE: @@ -3926,27 +4010,19 @@ public class WebView extends AbsoluteLayout mVelocityTracker.recycle(); mVelocityTracker = null; } - if (mTouchMode == SCROLL_ZOOM_OUT || - mTouchMode == SCROLL_ZOOM_ANIMATION_IN) { - scrollTo(mZoomScrollX, mZoomScrollY); - } else if (mTouchMode == TOUCH_DRAG_MODE) { + if (mTouchMode == TOUCH_DRAG_MODE) { WebViewCore.resumeUpdate(mWebViewCore); } mPrivateHandler.removeMessages(SWITCH_TO_SHORTPRESS); mPrivateHandler.removeMessages(SWITCH_TO_LONGPRESS); mTouchMode = TOUCH_DONE_MODE; - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } - nativeClearFocus(contentX, contentY); + nativeHideCursor(); break; } } return true; } - + private long mTrackballFirstTime = 0; private long mTrackballLastTime = 0; private float mTrackballRemainsX = 0.0f; @@ -3968,14 +4044,14 @@ public class WebView extends AbsoluteLayout private boolean mShiftIsPressed = false; private boolean mTrackballDown = false; private long mTrackballUpTime = 0; - private long mLastFocusTime = 0; - private Rect mLastFocusBounds; + private long mLastCursorTime = 0; + private Rect mLastCursorBounds; // Set by default; BrowserActivity clears to interpret trackball data - // directly for movement. Currently, the framework only passes + // directly for movement. Currently, the framework only passes // arrow key events, not trackball events, from one child to the next private boolean mMapTrackballToArrowKeys = true; - + public void setMapTrackballToArrowKeys(boolean setMap) { mMapTrackballToArrowKeys = setMap; } @@ -3993,26 +4069,29 @@ public class WebView extends AbsoluteLayout return true; } if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mPrivateHandler.removeMessages(SWITCH_TO_ENTER); + if (mShiftIsPressed) { + return true; // discard press if copy in progress + } mTrackballDown = true; - if (mNativeClass != 0) { - nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); + if (mNativeClass == 0) { + return false; } - if (time - mLastFocusTime <= TRACKBALL_TIMEOUT - && !mLastFocusBounds.equals(nativeGetFocusRingBounds())) { - nativeSelectBestAt(mLastFocusBounds); + nativeRecordButtons(hasFocus() && hasWindowFocus(), true, true); + if (time - mLastCursorTime <= TRACKBALL_TIMEOUT + && !mLastCursorBounds.equals(nativeGetCursorRingBounds())) { + nativeSelectBestAt(mLastCursorBounds); } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent down ev=" + ev - + " time=" + time - + " mLastFocusTime=" + mLastFocusTime); + + " time=" + time + + " mLastCursorTime=" + mLastCursorTime); } if (isInTouchMode()) requestFocusFromTouch(); return false; // let common code in onKeyDown at it - } + } if (ev.getAction() == MotionEvent.ACTION_UP) { - // LONG_PRESS_ENTER is set in common onKeyDown - mPrivateHandler.removeMessages(LONG_PRESS_ENTER); + // LONG_PRESS_CENTER is set in common onKeyDown + mPrivateHandler.removeMessages(LONG_PRESS_CENTER); mTrackballDown = false; mTrackballUpTime = time; if (mShiftIsPressed) { @@ -4021,43 +4100,39 @@ public class WebView extends AbsoluteLayout } else { mExtendSelection = true; } + return true; // discard press if copy in progress } - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent up ev=" + ev - + " time=" + time + + " time=" + time ); } return false; // let common code in onKeyUp at it } if (mMapTrackballToArrowKeys && mShiftIsPressed == false) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent gmail quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent gmail quit"); return false; } - // no move if we're still waiting on SWITCH_TO_ENTER timeout - if (mTouchMode == TOUCH_DOUBLECLICK_MODE) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent 2 click quit"); - return true; - } if (mTrackballDown) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent down quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent down quit"); return true; // discard move if trackball is down } if (time - mTrackballUpTime < TRACKBALL_TIMEOUT) { - if (LOGV_ENABLED) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); + if (DebugFlags.WEB_VIEW) Log.v(LOGTAG, "onTrackballEvent up timeout quit"); return true; } // TODO: alternatively we can do panning as touch does switchOutDrawHistory(); if (time - mTrackballLastTime > TRACKBALL_TIMEOUT) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, "onTrackballEvent time=" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "onTrackballEvent time=" + time + " last=" + mTrackballLastTime); } mTrackballFirstTime = time; mTrackballXMove = mTrackballYMove = 0; } mTrackballLastTime = time; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "onTrackballEvent ev=" + ev + " time=" + time); } mTrackballRemainsX += ev.getX(); @@ -4065,7 +4140,7 @@ public class WebView extends AbsoluteLayout doTrackball(time); return true; } - + void moveSelection(float xRate, float yRate) { if (mNativeClass == 0) return; @@ -4079,8 +4154,8 @@ public class WebView extends AbsoluteLayout , mSelectX)); mSelectY = Math.min(maxY, Math.max(mScrollY - SELECT_CURSOR_OFFSET , mSelectY)); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "moveSelection" + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "moveSelection" + " mSelectX=" + mSelectX + " mSelectY=" + mSelectY + " mScrollX=" + mScrollX @@ -4089,13 +4164,13 @@ public class WebView extends AbsoluteLayout + " yRate=" + yRate ); } - nativeMoveSelection(viewToContent(mSelectX) - , viewToContent(mSelectY), mExtendSelection); + nativeMoveSelection(viewToContentX(mSelectX), + viewToContentY(mSelectY), mExtendSelection); int scrollX = mSelectX < mScrollX ? -SELECT_CURSOR_OFFSET - : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : mSelectX > maxX - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET : 0; int scrollY = mSelectY < mScrollY ? -SELECT_CURSOR_OFFSET - : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET + : mSelectY > maxY - SELECT_CURSOR_OFFSET ? SELECT_CURSOR_OFFSET : 0; pinScrollBy(scrollX, scrollY, true, 0); Rect select = new Rect(mSelectX, mSelectY, mSelectX + 1, mSelectY + 1); @@ -4152,7 +4227,7 @@ public class WebView extends AbsoluteLayout if (elapsed == 0) { elapsed = TRACKBALL_TIMEOUT; } - float xRate = mTrackballRemainsX * 1000 / elapsed; + float xRate = mTrackballRemainsX * 1000 / elapsed; float yRate = mTrackballRemainsY * 1000 / elapsed; if (mShiftIsPressed) { moveSelection(xRate, yRate); @@ -4162,7 +4237,7 @@ public class WebView extends AbsoluteLayout float ax = Math.abs(xRate); float ay = Math.abs(yRate); float maxA = Math.max(ax, ay); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball elapsed=" + elapsed + " xRate=" + xRate + " yRate=" + yRate @@ -4173,25 +4248,6 @@ public class WebView extends AbsoluteLayout int height = mContentHeight - getViewHeight(); if (width < 0) width = 0; if (height < 0) height = 0; - if (mTouchMode == SCROLL_ZOOM_OUT) { - int oldX = mZoomScrollX; - int oldY = mZoomScrollY; - int maxWH = Math.max(width, height); - mZoomScrollX += scaleTrackballX(xRate, maxWH); - mZoomScrollY += scaleTrackballY(yRate, maxWH); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "doTrackball SCROLL_ZOOM_OUT" - + " mZoomScrollX=" + mZoomScrollX - + " mZoomScrollY=" + mZoomScrollY); - } - mZoomScrollX = Math.min(width, Math.max(0, mZoomScrollX)); - mZoomScrollY = Math.min(height, Math.max(0, mZoomScrollY)); - if (oldX != mZoomScrollX || oldY != mZoomScrollY) { - invalidate(); - } - mTrackballRemainsX = mTrackballRemainsY = 0; - return; - } ax = Math.abs(mTrackballRemainsX * TRACKBALL_MULTIPLIER); ay = Math.abs(mTrackballRemainsY * TRACKBALL_MULTIPLIER); maxA = Math.max(ax, ay); @@ -4199,18 +4255,18 @@ public class WebView extends AbsoluteLayout int oldScrollX = mScrollX; int oldScrollY = mScrollY; if (count > 0) { - int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? - KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : + int selectKeyCode = ax < ay ? mTrackballRemainsY < 0 ? + KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN : mTrackballRemainsX < 0 ? KeyEvent.KEYCODE_DPAD_LEFT : KeyEvent.KEYCODE_DPAD_RIGHT; count = Math.min(count, TRACKBALL_MOVE_COUNT); - if (LOGV_ENABLED) { - Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doTrackball keyCode=" + selectKeyCode + " count=" + count + " mTrackballRemainsX=" + mTrackballRemainsX + " mTrackballRemainsY=" + mTrackballRemainsY); } - if (navHandledKey(selectKeyCode, count, false, time)) { + if (navHandledKey(selectKeyCode, count, false, time, false)) { playSoundEffect(keyCodeToSoundsEffect(selectKeyCode)); } mTrackballRemainsX = mTrackballRemainsY = 0; @@ -4218,12 +4274,12 @@ public class WebView extends AbsoluteLayout if (count >= TRACKBALL_SCROLL_COUNT) { int xMove = scaleTrackballX(xRate, width); int yMove = scaleTrackballY(yRate, height); - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "doTrackball pinScrollBy" + " count=" + count + " xMove=" + xMove + " yMove=" + yMove - + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX) - + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY) + + " mScrollX-oldScrollX=" + (mScrollX-oldScrollX) + + " mScrollY-oldScrollY=" + (mScrollY-oldScrollY) ); } if (Math.abs(mScrollX - oldScrollX) > Math.abs(xMove)) { @@ -4236,24 +4292,28 @@ public class WebView extends AbsoluteLayout pinScrollBy(xMove, yMove, true, 0); } mUserScroll = true; - } - mWebViewCore.sendMessage(EventHub.UNBLOCK_FOCUS); + } + } + + private int computeMaxScrollY() { + int maxContentH = computeVerticalScrollRange() + getTitleHeight(); + return Math.max(maxContentH - getHeight(), getTitleHeight()); } public void flingScroll(int vx, int vy) { int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); - int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); - + int maxY = computeMaxScrollY(); + mScroller.fling(mScrollX, mScrollY, vx, vy, 0, maxX, 0, maxY); invalidate(); } - + private void doFling() { if (mVelocityTracker == null) { return; } int maxX = Math.max(computeHorizontalScrollRange() - getViewWidth(), 0); - int maxY = Math.max(computeVerticalScrollRange() - getViewHeight(), 0); + int maxY = computeMaxScrollY(); mVelocityTracker.computeCurrentVelocity(1000, mMaximumFling); int vx = (int) mVelocityTracker.getXVelocity(); @@ -4266,12 +4326,40 @@ public class WebView extends AbsoluteLayout vx = 0; } } - + if (true /* EMG release: make our fling more like Maps' */) { // maps cuts their velocity in half vx = vx * 3 / 4; vy = vy * 3 / 4; } + if ((maxX == 0 && vy == 0) || (maxY == 0 && vx == 0)) { + WebViewCore.resumeUpdate(mWebViewCore); + return; + } + float currentVelocity = mScroller.getCurrVelocity(); + if (mLastVelocity > 0 && currentVelocity > 0) { + float deltaR = (float) (Math.abs(Math.atan2(mLastVelY, mLastVelX) + - Math.atan2(vy, vx))); + final float circle = (float) (Math.PI) * 2.0f; + if (deltaR > circle * 0.9f || deltaR < circle * 0.1f) { + vx += currentVelocity * mLastVelX / mLastVelocity; + vy += currentVelocity * mLastVelY / mLastVelocity; + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doFling vx= " + vx + " vy=" + vy); + } + } else if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doFling missed " + deltaR / circle); + } + } else if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "doFling start last=" + mLastVelocity + + " current=" + currentVelocity + + " vx=" + vx + " vy=" + vy + + " maxX=" + maxX + " maxY=" + maxY + + " mScrollX=" + mScrollX + " mScrollY=" + mScrollY); + } + mLastVelX = vx; + mLastVelY = vy; + mLastVelocity = (float) Math.hypot(vx, vy); mScroller.fling(mScrollX, mScrollY, -vx, -vy, 0, maxX, 0, maxY); // TODO: duration is calculated based on velocity, if the range is @@ -4280,11 +4368,14 @@ public class WebView extends AbsoluteLayout // resume the webcore update. final int time = mScroller.getDuration(); mPrivateHandler.sendEmptyMessageDelayed(RESUME_WEBCORE_UPDATE, time); + awakenScrollBars(time); invalidate(); } private boolean zoomWithPreview(float scale) { float oldScale = mActualScale; + mInitialScrollX = mScrollX; + mInitialScrollY = mScrollY; // snap to DEFAULT_SCALE if it is close if (scale > (mDefaultScale - 0.05) && scale < (mDefaultScale + 0.05)) { @@ -4299,6 +4390,9 @@ public class WebView extends AbsoluteLayout mInvInitialZoomScale = 1.0f / oldScale; mInvFinalZoomScale = 1.0f / mActualScale; mZoomScale = mActualScale; + if (!mInZoomOverview) { + mLastScale = scale; + } invalidate(); return true; } else { @@ -4327,7 +4421,7 @@ public class WebView extends AbsoluteLayout } if (mZoomControls == null) { mZoomControls = createZoomControls(); - + /* * need to be set to VISIBLE first so that getMeasuredHeight() in * {@link #onSizeChanged()} can return the measured value for proper @@ -4336,7 +4430,7 @@ public class WebView extends AbsoluteLayout mZoomControls.setVisibility(View.VISIBLE); mZoomControlRunnable = new Runnable() { public void run() { - + /* Don't dismiss the controls if the user has * focus on them. Wait and check again later. */ @@ -4374,21 +4468,13 @@ public class WebView extends AbsoluteLayout zoomOut(); } }); - zoomControls.setOnZoomMagnifyClickListener(new OnClickListener() { - public void onClick(View v) { - mPrivateHandler.removeCallbacks(mZoomControlRunnable); - mPrivateHandler.postDelayed(mZoomControlRunnable, - ZOOM_CONTROLS_TIMEOUT); - zoomScrollOut(); - } - }); return zoomControls; } /** * Gets the {@link ZoomButtonsController} which can be used to add * additional buttons to the zoom controls window. - * + * * @return The instance of {@link ZoomButtonsController} used by this class, * or null if it is unavailable. * @hide @@ -4404,7 +4490,18 @@ public class WebView extends AbsoluteLayout public boolean zoomIn() { // TODO: alternatively we can disallow this during draw history mode switchOutDrawHistory(); - return zoomWithPreview(mActualScale * 1.25f); + // Center zooming to the center of the screen. + if (mInZoomOverview) { + // if in overview mode, bring it back to normal mode + mLastTouchX = getViewWidth() * .5f; + mLastTouchY = getViewHeight() * .5f; + doDoubleTap(); + return true; + } else { + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + return zoomWithPreview(mActualScale * 1.25f); + } } /** @@ -4414,7 +4511,18 @@ public class WebView extends AbsoluteLayout public boolean zoomOut() { // TODO: alternatively we can disallow this during draw history mode switchOutDrawHistory(); - return zoomWithPreview(mActualScale * 0.8f); + float scale = mActualScale * 0.8f; + if (scale < (mMinZoomScale + 0.1f) + && mWebViewCore.getSettings().getUseWideViewPort()) { + // when zoom out to min scale, switch to overview mode + doDoubleTap(); + return true; + } else { + // Center zooming to the center of the screen. + mZoomCenterX = getViewWidth() * .5f; + mZoomCenterY = getViewHeight() * .5f; + return zoomWithPreview(scale); + } } private void updateSelection() { @@ -4422,23 +4530,91 @@ public class WebView extends AbsoluteLayout return; } // mLastTouchX and mLastTouchY are the point in the current viewport - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); Rect rect = new Rect(contentX - mNavSlop, contentY - mNavSlop, contentX + mNavSlop, contentY + mNavSlop); - // If we were already focused on a textfield, update its cache. - if (inEditingMode()) { - mTextEntry.updateCachedTextfield(); - } nativeSelectBestAt(rect); } + /** + * Scroll the focused text field/area to match the WebTextView + * @param xPercent New x position of the WebTextView from 0 to 1. + * @param y New y position of the WebTextView in view coordinates + */ + /*package*/ void scrollFocusedTextInput(float xPercent, int y) { + if (!inEditingMode() || mWebViewCore == null) { + return; + } + mWebViewCore.sendMessage(EventHub.SCROLL_TEXT_INPUT, + // Since this position is relative to the top of the text input + // field, we do not need to take the title bar's height into + // consideration. + viewToContentDimension(y), + new Float(xPercent)); + } + + /** + * Set our starting point and time for a drag from the WebTextView. + */ + /*package*/ void initiateTextFieldDrag(float x, float y, long eventTime) { + if (!inEditingMode()) { + return; + } + mLastTouchX = x + (float) (mWebTextView.getLeft() - mScrollX); + mLastTouchY = y + (float) (mWebTextView.getTop() - mScrollY); + mLastTouchTime = eventTime; + if (!mScroller.isFinished()) { + abortAnimation(); + mPrivateHandler.removeMessages(RESUME_WEBCORE_UPDATE); + } + mSnapScrollMode = SNAP_NONE; + mVelocityTracker = VelocityTracker.obtain(); + mTouchMode = TOUCH_DRAG_START_MODE; + } + + /** + * Given a motion event from the WebTextView, set its location to our + * coordinates, and handle the event. + */ + /*package*/ boolean textFieldDrag(MotionEvent event) { + if (!inEditingMode()) { + return false; + } + mDragFromTextInput = true; + event.offsetLocation((float) (mWebTextView.getLeft() - mScrollX), + (float) (mWebTextView.getTop() - mScrollY)); + boolean result = onTouchEvent(event); + mDragFromTextInput = false; + return result; + } + + /** + * Do a touch up from a WebTextView. This will be handled by webkit to + * change the selection. + * @param event MotionEvent in the WebTextView's coordinates. + */ + /*package*/ void touchUpOnTextField(MotionEvent event) { + if (!inEditingMode()) { + return; + } + int x = viewToContentX((int) event.getX() + mWebTextView.getLeft()); + int y = viewToContentY((int) event.getY() + mWebTextView.getTop()); + // In case the soft keyboard has been dismissed, bring it back up. + InputMethodManager.getInstance(getContext()).showSoftInput(mWebTextView, + 0); + if (nativeFocusNodePointer() != nativeCursorNodePointer()) { + nativeMotionUp(x, y, mNavSlop); + } + nativeTextInputMotionUp(x, y); + } + /*package*/ void shortPressOnTextField() { if (inEditingMode()) { - View v = mTextEntry; - int x = viewToContent((v.getLeft() + v.getRight()) >> 1); - int y = viewToContent((v.getTop() + v.getBottom()) >> 1); - nativeMotionUp(x, y, mNavSlop, true); + View v = mWebTextView; + int x = viewToContentX((v.getLeft() + v.getRight()) >> 1); + int y = viewToContentY((v.getTop() + v.getBottom()) >> 1); + nativeTextInputMotionUp(x, y); } } @@ -4448,31 +4624,81 @@ public class WebView extends AbsoluteLayout } switchOutDrawHistory(); // mLastTouchX and mLastTouchY are the point in the current viewport - int contentX = viewToContent((int) mLastTouchX + mScrollX); - int contentY = viewToContent((int) mLastTouchY + mScrollY); - if (nativeMotionUp(contentX, contentY, mNavSlop, true)) { + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); + if (nativeMotionUp(contentX, contentY, mNavSlop)) { if (mLogEvent) { Checkin.updateStats(mContext.getContentResolver(), Checkin.Stats.Tag.BROWSER_SNAP_CENTER, 1, 0.0); } } - if (nativeUpdateFocusNode() && !mFocusNode.mIsTextField - && !mFocusNode.mIsTextArea) { + if (nativeHasCursorNode() && !nativeCursorIsTextInput()) { playSoundEffect(SoundEffectConstants.CLICK); } } + private void doDoubleTap() { + if (mWebViewCore.getSettings().getUseWideViewPort() == false) { + return; + } + mZoomCenterX = mLastTouchX; + mZoomCenterY = mLastTouchY; + mInZoomOverview = !mInZoomOverview; + // remove the zoom control after double tap + WebSettings settings = getSettings(); + if (settings.getBuiltInZoomControls()) { + if (mZoomButtonsController.isVisible()) { + mZoomButtonsController.setVisible(false); + } + } else { + if (mZoomControlRunnable != null) { + mPrivateHandler.removeCallbacks(mZoomControlRunnable); + } + if (mZoomControls != null) { + mZoomControls.hide(); + } + } + settings.setDoubleTapToastCount(0); + if (mInZoomOverview) { + // Force the titlebar fully reveal in overview mode + if (mScrollY < getTitleHeight()) mScrollY = 0; + zoomWithPreview((float) getViewWidth() / mZoomOverviewWidth); + } else { + // mLastTouchX and mLastTouchY are the point in the current viewport + int contentX = viewToContentX((int) mLastTouchX + mScrollX); + int contentY = viewToContentY((int) mLastTouchY + mScrollY); + int left = nativeGetBlockLeftEdge(contentX, contentY, mActualScale); + if (left != NO_LEFTEDGE) { + // add a 5pt padding to the left edge. Re-calculate the zoom + // center so that the new scroll x will be on the left edge. + mZoomCenterX = left < 5 ? 0 : (left - 5) * mLastScale + * mActualScale / (mLastScale - mActualScale); + } + zoomWithPreview(mLastScale); + } + } + // Called by JNI to handle a touch on a node representing an email address, // address, or phone number private void overrideLoading(String url) { mCallbackProxy.uiOverrideUrlLoading(url); } + // called by JNI + private void sendPluginState(int state) { + WebViewCore.PluginStateData psd = new WebViewCore.PluginStateData(); + psd.mFrame = nativeCursorFramePointer(); + psd.mNode = nativeCursorNodePointer(); + psd.mState = state; + mWebViewCore.sendMessage(EventHub.PLUGIN_STATE, psd); + } + @Override public boolean requestFocus(int direction, Rect previouslyFocusedRect) { boolean result = false; if (inEditingMode()) { - result = mTextEntry.requestFocus(direction, previouslyFocusedRect); + result = mWebTextView.requestFocus(direction, + previouslyFocusedRect); } else { result = super.requestFocus(direction, previouslyFocusedRect); if (mWebViewCore.getSettings().getNeedInitialFocus()) { @@ -4496,8 +4722,8 @@ public class WebView extends AbsoluteLayout default: return result; } - if (mNativeClass != 0 && !nativeUpdateFocusNode()) { - navHandledKey(fakeKeyDirection, 1, true, 0); + if (mNativeClass != 0 && !nativeHasCursorNode()) { + navHandledKey(fakeKeyDirection, 1, true, 0, true); } } } @@ -4517,8 +4743,8 @@ public class WebView extends AbsoluteLayout int measuredWidth = widthSize; // Grab the content size from WebViewCore. - int contentHeight = mContentHeight; - int contentWidth = mContentWidth; + int contentHeight = contentToViewDimension(mContentHeight); + int contentWidth = contentToViewDimension(mContentWidth); // Log.d(LOGTAG, "------- measure " + heightMode); @@ -4559,20 +4785,25 @@ public class WebView extends AbsoluteLayout rect.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY()); - int height = getHeight() - getHorizontalScrollbarHeight(); + int height = getViewHeightWithTitle(); int screenTop = mScrollY; int screenBottom = screenTop + height; int scrollYDelta = 0; - if (rect.bottom > screenBottom && rect.top > screenTop) { - if (rect.height() > height) { - scrollYDelta += (rect.top - screenTop); + if (rect.bottom > screenBottom) { + int oneThirdOfScreenHeight = height / 3; + if (rect.height() > 2 * oneThirdOfScreenHeight) { + // If the rectangle is too tall to fit in the bottom two thirds + // of the screen, place it at the top. + scrollYDelta = rect.top - screenTop; } else { - scrollYDelta += (rect.bottom - screenBottom); + // If the rectangle will still fit on screen, we want its + // top to be in the top third of the screen. + scrollYDelta = rect.top - (screenTop + oneThirdOfScreenHeight); } } else if (rect.top < screenTop) { - scrollYDelta -= (screenTop - rect.top); + scrollYDelta = rect.top - screenTop; } int width = getWidth() - getVerticalScrollbarWidth(); @@ -4597,33 +4828,40 @@ public class WebView extends AbsoluteLayout return false; } - + /* package */ void replaceTextfieldText(int oldStart, int oldEnd, String replace, int newStart, int newEnd) { - HashMap arg = new HashMap(); - arg.put("focusData", new WebViewCore.FocusData(mFocusData)); - arg.put("replace", replace); - arg.put("start", new Integer(newStart)); - arg.put("end", new Integer(newEnd)); + WebViewCore.ReplaceTextData arg = new WebViewCore.ReplaceTextData(); + arg.mReplace = replace; + arg.mNewStart = newStart; + arg.mNewEnd = newEnd; mTextGeneration++; + arg.mTextGeneration = mTextGeneration; mWebViewCore.sendMessage(EventHub.REPLACE_TEXT, oldStart, oldEnd, arg); } /* package */ void passToJavaScript(String currentText, KeyEvent event) { - HashMap arg = new HashMap(); - arg.put("focusData", new WebViewCore.FocusData(mFocusData)); - arg.put("event", event); - arg.put("currentText", currentText); + if (nativeCursorWantsKeyEvents() && !nativeCursorMatchesFocus()) { + mWebViewCore.sendMessage(EventHub.CLICK); + if (mWebTextView.mOkayForFocusNotToMatch) { + int select = nativeFocusCandidateIsTextField() ? + nativeFocusCandidateMaxLength() : 0; + setSelection(select, select); + } + } + WebViewCore.JSKeyData arg = new WebViewCore.JSKeyData(); + arg.mEvent = event; + arg.mCurrentText = currentText; // Increase our text generation number, and pass it to webcore thread mTextGeneration++; mWebViewCore.sendMessage(EventHub.PASS_TO_JS, mTextGeneration, 0, arg); // WebKit's document state is not saved until about to leave the page. - // To make sure the host application, like Browser, has the up to date - // document state when it goes to background, we force to save the + // To make sure the host application, like Browser, has the up to date + // document state when it goes to background, we force to save the // document state. mWebViewCore.removeMessages(EventHub.SAVE_DOCUMENT_STATE); mWebViewCore.sendMessageDelayed(EventHub.SAVE_DOCUMENT_STATE, - new WebViewCore.FocusData(mFocusData), 1000); + cursorData(), 1000); } /* package */ WebViewCore getWebViewCore() { @@ -4641,11 +4879,15 @@ public class WebView extends AbsoluteLayout class PrivateHandler extends Handler { @Override public void handleMessage(Message msg) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what - > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, msg.what < REMEMBER_PASSWORD || msg.what + > INVAL_RECT_MSG_ID ? Integer.toString(msg.what) : HandlerDebugString[msg.what - REMEMBER_PASSWORD]); } + if (mWebViewCore == null) { + // after WebView's destroy() is called, skip handling messages. + return; + } switch (msg.what) { case REMEMBER_PASSWORD: { mDatabase.setUsernamePassword( @@ -4662,33 +4904,40 @@ public class WebView extends AbsoluteLayout break; } case SWITCH_TO_SHORTPRESS: { + // if mPreventDrag is not confirmed, treat it as no so that + // it won't block panning the page. + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + mPreventDrag = PREVENT_DRAG_NO; + } if (mTouchMode == TOUCH_INIT_MODE) { mTouchMode = TOUCH_SHORTPRESS_START_MODE; updateSelection(); + } else if (mTouchMode == TOUCH_DOUBLE_TAP_MODE) { + mTouchMode = TOUCH_DONE_MODE; } break; } case SWITCH_TO_LONGPRESS: { - if (!mPreventDrag) { + if (mPreventDrag == PREVENT_DRAG_NO) { mTouchMode = TOUCH_DONE_MODE; performLongClick(); - updateTextEntry(); + rebuildWebTextView(); } break; } - case SWITCH_TO_ENTER: - if (LOGV_ENABLED) Log.v(LOGTAG, "SWITCH_TO_ENTER"); - mTouchMode = TOUCH_DONE_MODE; - onKeyUp(KeyEvent.KEYCODE_ENTER - , new KeyEvent(KeyEvent.ACTION_UP - , KeyEvent.KEYCODE_ENTER)); + case RELEASE_SINGLE_TAP: { + if (mPreventDrag == PREVENT_DRAG_NO) { + mTouchMode = TOUCH_DONE_MODE; + doShortPress(); + } break; + } case SCROLL_BY_MSG_ID: setContentScrollBy(msg.arg1, msg.arg2, (Boolean) msg.obj); break; case SYNC_SCROLL_TO_MSG_ID: if (mUserScroll) { - // if user has scrolled explicitly, don't sync the + // if user has scrolled explicitly, don't sync the // scroll position any more mUserScroll = false; break; @@ -4697,7 +4946,7 @@ public class WebView extends AbsoluteLayout case SCROLL_TO_MSG_ID: if (setContentScrollTo(msg.arg1, msg.arg2)) { // if we can't scroll to the exact position due to pin, - // send a message to WebCore to re-scroll when we get a + // send a message to WebCore to re-scroll when we get a // new picture mUserScroll = false; mWebViewCore.sendMessage(EventHub.SYNC_SCROLL, @@ -4707,24 +4956,58 @@ public class WebView extends AbsoluteLayout case SPAWN_SCROLL_TO_MSG_ID: spawnContentScrollTo(msg.arg1, msg.arg2); break; - case NEW_PICTURE_MSG_ID: + case NEW_PICTURE_MSG_ID: { + WebSettings settings = mWebViewCore.getSettings(); // called for new content - final WebViewCore.DrawData draw = + final int viewWidth = getViewWidth(); + final WebViewCore.DrawData draw = (WebViewCore.DrawData) msg.obj; final Point viewSize = draw.mViewPoint; - if (mZoomScale > 0) { - // use the same logic in sendViewSizeZoom() to make sure - // the mZoomScale has matched the viewSize so that we - // can clear mZoomScale - if (Math.round(getViewWidth() / mZoomScale) == viewSize.x) { - mZoomScale = 0; - mWebViewCore.sendMessage(EventHub.SET_SNAP_ANCHOR, - 0, 0); + boolean useWideViewport = settings.getUseWideViewPort(); + WebViewCore.RestoreState restoreState = draw.mRestoreState; + if (restoreState != null) { + mInZoomOverview = false; + mLastScale = restoreState.mTextWrapScale; + if (restoreState.mMinScale == 0) { + if (restoreState.mMobileSite) { + if (draw.mMinPrefWidth > + Math.max(0, draw.mViewPoint.x)) { + mMinZoomScale = (float) viewWidth + / draw.mMinPrefWidth; + mMinZoomScaleFixed = false; + } else { + mMinZoomScale = restoreState.mDefaultScale; + mMinZoomScaleFixed = true; + } + } else { + mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; + mMinZoomScaleFixed = false; + } + } else { + mMinZoomScale = restoreState.mMinScale; + mMinZoomScaleFixed = true; } - } - if (!mMinZoomScaleFixed) { - mMinZoomScale = (float) getViewWidth() - / Math.max(ZOOM_OUT_WIDTH, draw.mWidthHeight.x); + if (restoreState.mMaxScale == 0) { + mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; + } else { + mMaxZoomScale = restoreState.mMaxScale; + } + setNewZoomScale(mLastScale, false); + setContentScrollTo(restoreState.mScrollX, + restoreState.mScrollY); + if (useWideViewport + && settings.getLoadWithOverviewMode()) { + if (restoreState.mViewScale == 0 + || (restoreState.mMobileSite + && mMinZoomScale < restoreState.mDefaultScale)) { + mInZoomOverview = true; + } + } + // As we are on a new page, remove the WebTextView. This + // is necessary for page loads driven by webkit, and in + // particular when the user was on a password field, so + // the WebTextView was visible. + clearTextEntry(); } // We update the layout (i.e. request a layout from the // view system) if the last view size that we sent to @@ -4732,125 +5015,88 @@ public class WebView extends AbsoluteLayout // received in the fixed dimension. final boolean updateLayout = viewSize.x == mLastWidthSent && viewSize.y == mLastHeightSent; - recordNewContentSize(draw.mWidthHeight.x, - draw.mWidthHeight.y, updateLayout); - if (LOGV_ENABLED) { + recordNewContentSize(draw.mWidthHeight.x, + draw.mWidthHeight.y + + (mFindIsUp ? mFindHeight : 0), updateLayout); + if (DebugFlags.WEB_VIEW) { 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())); + invalidateContentRect(draw.mInvalRegion.getBounds()); if (mPictureListener != null) { mPictureListener.onNewPicture(WebView.this, capturePicture()); } + if (useWideViewport) { + mZoomOverviewWidth = Math.max(draw.mMinPrefWidth, + draw.mViewPoint.x); + } + if (!mMinZoomScaleFixed) { + mMinZoomScale = (float) viewWidth / mZoomOverviewWidth; + } + if (!mDrawHistory && mInZoomOverview) { + // fit the content width to the current view. Ignore + // the rounding error case. + if (Math.abs((viewWidth * mInvActualScale) + - mZoomOverviewWidth) > 1) { + setNewZoomScale((float) viewWidth + / mZoomOverviewWidth, false); + } + } break; + } case WEBCORE_INITIALIZED_MSG_ID: // nativeCreate sets mNativeClass to a non-zero value nativeCreate(msg.arg1); break; case UPDATE_TEXTFIELD_TEXT_MSG_ID: // Make sure that the textfield is currently focused - // and representing the same node as the pointer. - if (inEditingMode() && - mTextEntry.isSameTextField(msg.arg1)) { + // and representing the same node as the pointer. + if (inEditingMode() && + mWebTextView.isSameTextField(msg.arg1)) { if (msg.getData().getBoolean("password")) { - Spannable text = (Spannable) mTextEntry.getText(); + Spannable text = (Spannable) mWebTextView.getText(); int start = Selection.getSelectionStart(text); int end = Selection.getSelectionEnd(text); - mTextEntry.setInPassword(true); + mWebTextView.setInPassword(true); // Restore the selection, which may have been // ruined by setInPassword. - Spannable pword = (Spannable) mTextEntry.getText(); + Spannable pword = + (Spannable) mWebTextView.getText(); Selection.setSelection(pword, start, end); // If the text entry has created more events, ignore // this one. } else if (msg.arg2 == mTextGeneration) { - mTextEntry.setTextAndKeepSelection( + mWebTextView.setTextAndKeepSelection( (String) msg.obj); } } break; - case DID_FIRST_LAYOUT_MSG_ID: - if (mNativeClass == 0) { - break; + case UPDATE_TEXT_SELECTION_MSG_ID: + if (inEditingMode() + && mWebTextView.isSameTextField(msg.arg1) + && msg.arg2 == mTextGeneration) { + WebViewCore.TextSelectionData tData + = (WebViewCore.TextSelectionData) msg.obj; + mWebTextView.setSelectionFromWebKit(tData.mStart, + tData.mEnd); } -// Do not reset the focus or clear the text; the user may have already -// navigated or entered text at this point. The focus should have gotten -// reset, if need be, when the focus cache was built. Similarly, the text -// view should already be torn down and rebuilt if needed. -// nativeResetFocus(); -// clearTextEntry(); - HashMap scaleLimit = (HashMap) msg.obj; - int minScale = (Integer) scaleLimit.get("minScale"); - if (minScale == 0) { - mMinZoomScale = DEFAULT_MIN_ZOOM_SCALE; - mMinZoomScaleFixed = false; - } else { - mMinZoomScale = (float) (minScale / 100.0); - mMinZoomScaleFixed = true; - } - int maxScale = (Integer) scaleLimit.get("maxScale"); - if (maxScale == 0) { - mMaxZoomScale = DEFAULT_MAX_ZOOM_SCALE; - } else { - mMaxZoomScale = (float) (maxScale / 100.0); - } - // If history Picture is drawn, don't update zoomWidth - if (mDrawHistory) { - break; - } - int width = getViewWidth(); - if (width == 0) { - break; - } - int initialScale = msg.arg1; - int viewportWidth = msg.arg2; - // start a new page with DEFAULT_SCALE zoom scale. - float scale = mDefaultScale; - if (mInitialScale > 0) { - scale = mInitialScale / 100.0f; - } else { - if (mWebViewCore.getSettings().getUseWideViewPort()) { - // force viewSizeChanged by setting mLastWidthSent - // to 0 - mLastWidthSent = 0; - } - if (initialScale == 0) { - // if viewportWidth is defined and it is smaller - // than the view width, zoom in to fill the view - if (viewportWidth > 0 && viewportWidth < width) { - scale = (float) width / viewportWidth; - } - } else { - scale = initialScale / 100.0f; - } - } - setNewZoomScale(scale, false); break; - case MARK_NODE_INVALID_ID: - nativeMarkNodeInvalid(msg.arg1); - break; - case NOTIFY_FOCUS_SET_MSG_ID: - if (mNativeClass != 0) { - nativeNotifyFocusSet(inEditingMode()); + case MOVE_OUT_OF_PLUGIN: + if (nativePluginEatsNavKey()) { + navHandledKey(msg.arg1, 1, false, 0, true); } break; case UPDATE_TEXT_ENTRY_MSG_ID: - // this is sent after finishing resize in WebViewCore. Make + // this is sent after finishing resize in WebViewCore. Make // sure the text edit box is still on the screen. - boolean alreadyThere = inEditingMode(); - if (alreadyThere && nativeUpdateFocusNode()) { - FocusNode node = mFocusNode; - if (node.mIsTextField || node.mIsTextArea) { - mTextEntry.bringIntoView(); - } + if (inEditingMode() && nativeCursorIsTextInput()) { + mWebTextView.bringIntoView(); + rebuildWebTextView(); } - updateTextEntry(); break; - case RECOMPUTE_FOCUS_MSG_ID: - if (mNativeClass != 0) { - nativeRecomputeFocus(); - } + case CLEAR_TEXT_ENTRY: + clearTextEntry(); break; case INVAL_RECT_MSG_ID: { Rect r = (Rect)msg.obj; @@ -4863,17 +5109,15 @@ public class WebView extends AbsoluteLayout } break; } - case UPDATE_TEXT_ENTRY_ADAPTER: - HashMap data = (HashMap) msg.obj; - if (mTextEntry.isSameTextField(msg.arg1)) { - AutoCompleteAdapter adapter = - (AutoCompleteAdapter) data.get("adapter"); - mTextEntry.setAdapterCustom(adapter); + case REQUEST_FORM_DATA: + AutoCompleteAdapter adapter = (AutoCompleteAdapter) msg.obj; + if (mWebTextView.isSameTextField(msg.arg1)) { + mWebTextView.setAdapterCustom(adapter); } break; case UPDATE_CLIPBOARD: String str = (String) msg.obj; - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW) { Log.v(LOGTAG, "UPDATE_CLIPBOARD " + str); } try { @@ -4888,12 +5132,12 @@ public class WebView extends AbsoluteLayout WebViewCore.resumeUpdate(mWebViewCore); break; - case LONG_PRESS_ENTER: + case LONG_PRESS_CENTER: // as this is shared by keydown and trackballdown, reset all // the states - mGotEnterDown = false; + mGotCenterDown = false; mTrackballDown = false; - // LONG_PRESS_ENTER is sent as a delayed message. If we + // LONG_PRESS_CENTER 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. @@ -4908,13 +5152,26 @@ public class WebView extends AbsoluteLayout case PREVENT_TOUCH_ID: if (msg.arg1 == MotionEvent.ACTION_DOWN) { - mPreventDrag = msg.arg2 == 1; - if (mPreventDrag) { - mTouchMode = TOUCH_DONE_MODE; + // dont override if mPreventDrag has been set to no due + // to time out + if (mPreventDrag == PREVENT_DRAG_MAYBE_YES) { + mPreventDrag = msg.arg2 == 1 ? PREVENT_DRAG_YES + : PREVENT_DRAG_NO; + if (mPreventDrag == PREVENT_DRAG_YES) { + mTouchMode = TOUCH_DONE_MODE; + } } } break; + case REQUEST_KEYBOARD: + if (msg.arg1 == 0) { + hideSoftKeyboard(); + } else { + displaySoftKeyboard(false); + } + break; + default: super.handleMessage(msg); break; @@ -4924,16 +5181,12 @@ public class WebView extends AbsoluteLayout // Class used to use a dropdown for a <select> element private class InvokeListBox implements Runnable { - // Strings for the labels in the listbox. - private String[] mArray; - // Array representing whether each item is enabled. - private boolean[] mEnableArray; // Whether the listbox allows multiple selection. private boolean mMultiple; // Passed in to a list with multiple selection to tell // which items are selected. private int[] mSelectedArray; - // Passed in to a list with single selection to tell + // Passed in to a list with single selection to tell // where the initial selection is. private int mSelection; @@ -4952,14 +5205,14 @@ public class WebView extends AbsoluteLayout } /** - * Subclass ArrayAdapter so we can disable OptionGroupLabels, + * Subclass ArrayAdapter so we can disable OptionGroupLabels, * and allow filtering. */ private class MyArrayListAdapter extends ArrayAdapter<Container> { public MyArrayListAdapter(Context context, Container[] objects, boolean multiple) { - super(context, + super(context, multiple ? com.android.internal.R.layout.select_dialog_multichoice : - com.android.internal.R.layout.select_dialog_singlechoice, + com.android.internal.R.layout.select_dialog_singlechoice, objects); } @@ -5017,7 +5270,7 @@ public class WebView extends AbsoluteLayout } } - private InvokeListBox(String[] array, boolean[] enabled, int + private InvokeListBox(String[] array, boolean[] enabled, int selection) { mSelection = selection; mMultiple = false; @@ -5080,31 +5333,36 @@ public class WebView extends AbsoluteLayout public void run() { final ListView listView = (ListView) LayoutInflater.from(mContext) .inflate(com.android.internal.R.layout.select_dialog, null); - final MyArrayListAdapter adapter = new + final MyArrayListAdapter adapter = new MyArrayListAdapter(mContext, mContainers, mMultiple); AlertDialog.Builder b = new AlertDialog.Builder(mContext) .setView(listView).setCancelable(true) .setInverseBackgroundForced(true); - + if (mMultiple) { b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { mWebViewCore.sendMessage( - EventHub.LISTBOX_CHOICES, + EventHub.LISTBOX_CHOICES, adapter.getCount(), 0, listView.getCheckedItemPositions()); }}); - b.setNegativeButton(android.R.string.cancel, null); + b.setNegativeButton(android.R.string.cancel, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + mWebViewCore.sendMessage( + EventHub.SINGLE_LISTBOX_CHOICE, -2, 0); + }}); } final AlertDialog dialog = b.create(); listView.setAdapter(adapter); listView.setFocusableInTouchMode(true); // There is a bug (1250103) where the checks in a ListView with // multiple items selected are associated with the positions, not - // the ids, so the items do not properly retain their checks when + // the ids, so the items do not properly retain their checks when // filtered. Do not allow filtering on multiple lists until // that bug is fixed. - + listView.setTextFilterEnabled(!mMultiple); if (mMultiple) { listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); @@ -5167,48 +5425,39 @@ public class WebView extends AbsoluteLayout } // called by JNI - private void sendFinalFocus(int frame, int node, int x, int y) { - WebViewCore.FocusData focusData = new WebViewCore.FocusData(); - focusData.mFrame = frame; - focusData.mNode = node; - focusData.mX = x; - focusData.mY = y; - mWebViewCore.sendMessage(EventHub.SET_FINAL_FOCUS, - EventHub.NO_FOCUS_CHANGE_BLOCK, 0, focusData); + private void sendMoveMouse(int frame, int node, int x, int y) { + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE, + new WebViewCore.CursorData(frame, node, x, y)); } - // called by JNI - private void setFocusData(int moveGeneration, int buildGeneration, - int frame, int node, int x, int y, boolean ignoreNullFocus) { - mFocusData.mMoveGeneration = moveGeneration; - mFocusData.mBuildGeneration = buildGeneration; - mFocusData.mFrame = frame; - mFocusData.mNode = node; - mFocusData.mX = x; - mFocusData.mY = y; - mFocusData.mIgnoreNullFocus = ignoreNullFocus; - } - - // called by JNI - private void sendKitFocus() { - WebViewCore.FocusData focusData = new WebViewCore.FocusData(mFocusData); - mWebViewCore.sendMessage(EventHub.SET_KIT_FOCUS, focusData); + /* + * Send a mouse move event to the webcore thread. + * + * @param removeFocus Pass true if the "mouse" cursor is now over a node + * which wants key events, but it is not the focus. This + * will make the visual appear as though nothing is in + * focus. Remove the WebTextView, if present, and stop + * drawing the blinking caret. + * called by JNI + */ + private void sendMoveMouseIfLatest(boolean removeFocus) { + if (removeFocus) { + clearTextEntry(); + setFocusControllerInactive(); + } + mWebViewCore.sendMessage(EventHub.SET_MOVE_MOUSE_IF_LATEST, + cursorData()); } // called by JNI - private void sendMotionUp(int touchGeneration, int buildGeneration, - int frame, int node, int x, int y, int size, boolean isClick, - boolean retry) { + private void sendMotionUp(int touchGeneration, + int frame, int node, int x, int y) { WebViewCore.TouchUpData touchUpData = new WebViewCore.TouchUpData(); touchUpData.mMoveGeneration = touchGeneration; - touchUpData.mBuildGeneration = buildGeneration; - touchUpData.mSize = size; - touchUpData.mIsClick = isClick; - touchUpData.mRetry = retry; - mFocusData.mFrame = touchUpData.mFrame = frame; - mFocusData.mNode = touchUpData.mNode = node; - mFocusData.mX = touchUpData.mX = x; - mFocusData.mY = touchUpData.mY = y; + touchUpData.mFrame = frame; + touchUpData.mNode = node; + touchUpData.mX = x; + touchUpData.mY = y; mWebViewCore.sendMessage(EventHub.TOUCH_UP, touchUpData); } @@ -5223,7 +5472,7 @@ public class WebView extends AbsoluteLayout width = visRect.width() / 2; } // FIXME the divisor should be retrieved from somewhere - return viewToContent(width); + return viewToContentX(width); } private int getScaledMaxYScroll() { @@ -5238,7 +5487,7 @@ public class WebView extends AbsoluteLayout // FIXME the divisor should be retrieved from somewhere // the closest thing today is hard-coded into ScrollView.java // (from ScrollView.java, line 363) int maxJump = height/2; - return viewToContent(height); + return Math.round(height * mInvActualScale); } /** @@ -5247,56 +5496,72 @@ public class WebView extends AbsoluteLayout private void viewInvalidate() { invalidate(); } - + // return true if the key was handled - private boolean navHandledKey(int keyCode, int count, boolean noScroll - , long time) { + private boolean navHandledKey(int keyCode, int count, boolean noScroll, + long time, boolean ignorePlugin) { if (mNativeClass == 0) { return false; } - mLastFocusTime = time; - mLastFocusBounds = nativeGetFocusRingBounds(); - boolean keyHandled = nativeMoveFocus(keyCode, count, noScroll) == false; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "navHandledKey mLastFocusBounds=" + mLastFocusBounds - + " mLastFocusTime=" + mLastFocusTime + if (ignorePlugin == false && nativePluginEatsNavKey()) { + KeyEvent event = new KeyEvent(time, time, KeyEvent.ACTION_DOWN + , keyCode, count, (mShiftIsPressed ? KeyEvent.META_SHIFT_ON : 0) + | (false ? KeyEvent.META_ALT_ON : 0) // FIXME + | (false ? KeyEvent.META_SYM_ON : 0) // FIXME + , 0, 0, 0); + mWebViewCore.sendMessage(EventHub.KEY_DOWN, event); + mWebViewCore.sendMessage(EventHub.KEY_UP, event); + return true; + } + mLastCursorTime = time; + mLastCursorBounds = nativeGetCursorRingBounds(); + boolean keyHandled + = nativeMoveCursor(keyCode, count, noScroll) == false; + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "navHandledKey mLastCursorBounds=" + mLastCursorBounds + + " mLastCursorTime=" + mLastCursorTime + " handled=" + keyHandled); } if (keyHandled == false || mHeightCanMeasure == false) { return keyHandled; } - Rect contentFocus = nativeGetFocusRingBounds(); - if (contentFocus.isEmpty()) return keyHandled; - Rect viewFocus = contentToView(contentFocus); + Rect contentCursorRingBounds = nativeGetCursorRingBounds(); + if (contentCursorRingBounds.isEmpty()) return keyHandled; + Rect viewCursorRingBounds = contentToViewRect(contentCursorRingBounds); Rect visRect = new Rect(); calcOurVisibleRect(visRect); Rect outset = new Rect(visRect); int maxXScroll = visRect.width() / 2; int maxYScroll = visRect.height() / 2; outset.inset(-maxXScroll, -maxYScroll); - if (Rect.intersects(outset, viewFocus) == false) { + if (Rect.intersects(outset, viewCursorRingBounds) == false) { return keyHandled; } // FIXME: Necessary because ScrollView/ListView do not scroll left/right - int maxH = Math.min(viewFocus.right - visRect.right, maxXScroll); + int maxH = Math.min(viewCursorRingBounds.right - visRect.right, + maxXScroll); if (maxH > 0) { pinScrollBy(maxH, 0, true, 0); } else { - maxH = Math.max(viewFocus.left - visRect.left, -maxXScroll); + maxH = Math.max(viewCursorRingBounds.left - visRect.left, + -maxXScroll); if (maxH < 0) { pinScrollBy(maxH, 0, true, 0); } } - if (mLastFocusBounds.isEmpty()) return keyHandled; - if (mLastFocusBounds.equals(contentFocus)) return keyHandled; - if (LOGV_ENABLED) { - Log.v(LOGTAG, "navHandledKey contentFocus=" + contentFocus); + if (mLastCursorBounds.isEmpty()) return keyHandled; + if (mLastCursorBounds.equals(contentCursorRingBounds)) { + return keyHandled; } - requestRectangleOnScreen(viewFocus); + if (DebugFlags.WEB_VIEW) { + Log.v(LOGTAG, "navHandledKey contentCursorRingBounds=" + + contentCursorRingBounds); + } + requestRectangleOnScreen(viewCursorRingBounds); mUserScroll = true; return keyHandled; } - + /** * Set the background color. It's white by default. Pass * zero to make the view transparent. @@ -5311,7 +5576,7 @@ public class WebView extends AbsoluteLayout nativeDebugDump(); mWebViewCore.sendMessage(EventHub.DUMP_NAVTREE); } - + /** * Update our cache with updatedText. * @param updatedText The new text to put in our cache. @@ -5321,52 +5586,85 @@ public class WebView extends AbsoluteLayout // we recognize that it is up to date. nativeUpdateCachedTextfield(updatedText, mTextGeneration); } - - // Never call this version except by updateCachedTextfield(String) - - // we always want to pass in our generation number. - private native void nativeUpdateCachedTextfield(String updatedText, - int generation); - private native void nativeClearFocus(int x, int y); + + /* package */ native void nativeClearCursor(); private native void nativeCreate(int ptr); + private native int nativeCursorFramePointer(); + private native Rect nativeCursorNodeBounds(); + /* package */ native int nativeCursorNodePointer(); + /* package */ native boolean nativeCursorMatchesFocus(); + private native boolean nativeCursorIntersects(Rect visibleRect); + private native boolean nativeCursorIsAnchor(); + private native boolean nativeCursorIsPlugin(); + private native boolean nativeCursorIsTextInput(); + private native Point nativeCursorPosition(); + private native String nativeCursorText(); + /** + * Returns true if the native cursor node says it wants to handle key events + * (ala plugins). This can only be called if mNativeClass is non-zero! + */ + private native boolean nativeCursorWantsKeyEvents(); private native void nativeDebugDump(); private native void nativeDestroy(); - private native void nativeDrawFocusRing(Canvas content); - private native void nativeDrawSelection(Canvas content - , int x, int y, boolean extendSelection); + private native void nativeDrawCursorRing(Canvas content); + private native void nativeDrawMatches(Canvas canvas); + private native void nativeDrawSelection(Canvas content, float scale, + int offset, int x, int y, boolean extendSelection); private native void nativeDrawSelectionRegion(Canvas content); - private native boolean nativeUpdateFocusNode(); - private native Rect nativeGetFocusRingBounds(); - private native Rect nativeGetNavBounds(); + private native void nativeDumpDisplayTree(String urlOrNull); + private native int nativeFindAll(String findLower, String findUpper); + private native void nativeFindNext(boolean forward); + private native boolean nativeFocusCandidateIsPassword(); + private native boolean nativeFocusCandidateIsRtlText(); + private native boolean nativeFocusCandidateIsTextField(); + private native boolean nativeFocusCandidateIsTextInput(); + private native int nativeFocusCandidateMaxLength(); + /* package */ native String nativeFocusCandidateName(); + private native Rect nativeFocusCandidateNodeBounds(); + /* package */ native int nativeFocusCandidatePointer(); + private native String nativeFocusCandidateText(); + private native int nativeFocusCandidateTextSize(); + /* package */ native int nativeFocusNodePointer(); + private native Rect nativeGetCursorRingBounds(); + private native Region nativeGetSelection(); + private native boolean nativeHasCursorNode(); + private native boolean nativeHasFocusNode(); + private native void nativeHideCursor(); + private native String nativeImageURI(int x, int y); private native void nativeInstrumentReport(); - private native void nativeMarkNodeInvalid(int node); + /* package */ native void nativeMoveCursorToNextTextInput(); // return true if the page has been scrolled - private native boolean nativeMotionUp(int x, int y, int slop, boolean isClick); + private native boolean nativeMotionUp(int x, int y, int slop); // returns false if it handled the key - private native boolean nativeMoveFocus(int keyCode, int count, + private native boolean nativeMoveCursor(int keyCode, int count, boolean noScroll); - private native void nativeNotifyFocusSet(boolean inEditingMode); - private native void nativeRecomputeFocus(); + private native int nativeMoveGeneration(); + private native void nativeMoveSelection(int x, int y, + boolean extendSelection); + private native boolean nativePluginEatsNavKey(); // Like many other of our native methods, you must make sure that // mNativeClass is not null before calling this method. private native void nativeRecordButtons(boolean focused, boolean pressed, boolean invalidate); - private native void nativeResetFocus(); - private native void nativeResetNavClipBounds(); private native void nativeSelectBestAt(Rect rect); private native void nativeSetFindIsDown(); private native void nativeSetFollowedLink(boolean followed); private native void nativeSetHeightCanMeasure(boolean measure); - private native void nativeSetNavBounds(Rect rect); - private native void nativeSetNavClipBounds(Rect rect); - private native String nativeImageURI(int x, int y); + // Returns a value corresponding to CachedFrame::ImeAction + /* package */ native int nativeTextFieldAction(); /** - * 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! + * Perform a click on a currently focused text input. Since it is already + * focused, there is no need to go through the nativeMotionUp code, which + * may change the Cursor. */ - private native boolean nativeFocusNodeWantsKeyEvents(); - private native void nativeMoveSelection(int x, int y - , boolean extendSelection); - private native Region nativeGetSelection(); - - private native void nativeDumpDisplayTree(String urlOrNull); + private native void nativeTextInputMotionUp(int x, int y); + private native int nativeTextGeneration(); + // Never call this version except by updateCachedTextfield(String) - + // we always want to pass in our generation number. + private native void nativeUpdateCachedTextfield(String updatedText, + int generation); + private native void nativeUpdatePluginReceivesEvents(); + // return NO_LEFTEDGE means failure. + private static final int NO_LEFTEDGE = -1; + private native int nativeGetBlockLeftEdge(int x, int y, float scale); } diff --git a/core/java/android/webkit/WebViewClient.java b/core/java/android/webkit/WebViewClient.java index a185779..30dea74 100644 --- a/core/java/android/webkit/WebViewClient.java +++ b/core/java/android/webkit/WebViewClient.java @@ -92,14 +92,46 @@ public class WebViewClient { cancelMsg.sendToTarget(); } + // These ints must match up to the hidden values in EventHandler. + /** Generic error */ + public static final int ERROR_UNKNOWN = -1; + /** Server or proxy hostname lookup failed */ + public static final int ERROR_HOST_LOOKUP = -2; + /** Unsupported authentication scheme (not basic or digest) */ + public static final int ERROR_UNSUPPORTED_AUTH_SCHEME = -3; + /** User authentication failed on server */ + public static final int ERROR_AUTHENTICATION = -4; + /** User authentication failed on proxy */ + public static final int ERROR_PROXY_AUTHENTICATION = -5; + /** Failed to connect to the server */ + public static final int ERROR_CONNECT = -6; + /** Failed to read or write to the server */ + public static final int ERROR_IO = -7; + /** Connection timed out */ + public static final int ERROR_TIMEOUT = -8; + /** Too many redirects */ + public static final int ERROR_REDIRECT_LOOP = -9; + /** Unsupported URI scheme */ + public static final int ERROR_UNSUPPORTED_SCHEME = -10; + /** Failed to perform SSL handshake */ + public static final int ERROR_FAILED_SSL_HANDSHAKE = -11; + /** Malformed URL */ + public static final int ERROR_BAD_URL = -12; + /** Generic file error */ + public static final int ERROR_FILE = -13; + /** File not found */ + public static final int ERROR_FILE_NOT_FOUND = -14; + /** Too many requests during this load */ + public static final int ERROR_TOO_MANY_REQUESTS = -15; + /** - * Report an error to an activity. These errors come up from WebCore, and - * are network errors. - * + * Report an error to the host application. These errors are unrecoverable + * (i.e. the main resource is unavailable). The errorCode parameter + * corresponds to one of the ERROR_* constants. * @param view The WebView that is initiating the callback. - * @param errorCode The HTTP error code. - * @param description A String description. - * @param failingUrl The url that failed. + * @param errorCode The error code corresponding to an ERROR_* value. + * @param description A String describing the error. + * @param failingUrl The url that failed to load. */ public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index a5fa41e..a5a4852 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -17,6 +17,7 @@ package android.webkit; import android.content.Context; +import android.content.Intent; import android.graphics.Canvas; import android.graphics.DrawFilter; import android.graphics.Paint; @@ -29,20 +30,24 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.Process; +import android.provider.Browser; import android.util.Log; import android.util.SparseBooleanArray; import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; import java.util.ArrayList; -import java.util.HashMap; +import java.util.Collection; +import java.util.Map; +import java.util.Set; import junit.framework.Assert; final class WebViewCore { private static final String LOGTAG = "webcore"; - static final boolean DEBUG = false; - static final boolean LOGV_ENABLED = DEBUG; static { // Load libwebcore during static initialization. This happens in the @@ -67,7 +72,8 @@ final class WebViewCore { private int mNativeClass; // The BrowserFrame is an interface to the native Frame component. private BrowserFrame mBrowserFrame; - + // Custom JS interfaces to add during the initialization. + private Map<String, Object> mJavascriptInterfaces; /* * range is from 200 to 10,000. 0 is a special value means device-width. -1 * means undefined. @@ -96,22 +102,38 @@ final class WebViewCore { private int mViewportMaximumScale = 0; private boolean mViewportUserScalable = true; - - private int mRestoredScale = WebView.DEFAULT_SCALE_PERCENT; + + /* + * range is from 70 to 400. + * 0 is a special value means device-dpi. The default scale factor will be + * always 100. + * -1 means undefined. The default scale factor will be + * WebView.DEFAULT_SCALE_PERCENT. + */ + private int mViewportDensityDpi = -1; + + private int mRestoredScale = 0; + private int mRestoredScreenWidthScale = 0; private int mRestoredX = 0; private int mRestoredY = 0; private int mWebkitScrollX = 0; private int mWebkitScrollY = 0; + // If the site doesn't use viewport meta tag to specify the viewport, use + // DEFAULT_VIEWPORT_WIDTH as default viewport width + static final int DEFAULT_VIEWPORT_WIDTH = 800; + // The thread name used to identify the WebCore thread and for use in // debugging other classes that require operation within the WebCore thread. /* package */ static final String THREAD_NAME = "WebViewCoreThread"; - public WebViewCore(Context context, WebView w, CallbackProxy proxy) { + public WebViewCore(Context context, WebView w, CallbackProxy proxy, + Map<String, Object> javascriptInterfaces) { // No need to assign this in the WebCore thread. mCallbackProxy = proxy; mWebView = w; + mJavascriptInterfaces = javascriptInterfaces; // This context object is used to initialize the WebViewCore during // subwindow creation. mContext = context; @@ -143,6 +165,10 @@ final class WebViewCore { // The WebIconDatabase needs to be initialized within the UI thread so // just request the instance here. WebIconDatabase.getInstance(); + // Create the WebStorage singleton and the UI handler + WebStorage.getInstance().createUIHandler(); + // Create the UI handler for GeolocationPermissions + GeolocationPermissions.getInstance().createUIHandler(); // Send a message to initialize the WebViewCore. Message init = sWebCoreHandler.obtainMessage( WebCoreThread.INITIALIZE, this); @@ -157,11 +183,16 @@ final class WebViewCore { * in turn creates a C level FrameView and attaches it to the frame. */ mBrowserFrame = new BrowserFrame(mContext, this, mCallbackProxy, - mSettings); + mSettings, mJavascriptInterfaces); + mJavascriptInterfaces = null; // Sync the native settings and also create the WebCore thread handler. mSettings.syncSettingsAndCreateHandler(mBrowserFrame); // Create the handler and transfer messages for the IconDatabase WebIconDatabase.getInstance().createHandler(); + // Create the handler for WebStorage + WebStorage.getInstance().createHandler(); + // Create the handler for GeolocationPermissions. + GeolocationPermissions.getInstance().createHandler(); // The transferMessages call will transfer all pending messages to the // WebCore thread handler. mEventHub.transferMessages(); @@ -225,6 +256,16 @@ final class WebViewCore { } /** + * Add an error message to the client's console. + * @param message The message to add + * @param lineNumber the line on which the error occurred + * @param sourceID the filename of the source that caused the error. + */ + protected void addMessageToConsole(String message, int lineNumber, String sourceID) { + mCallbackProxy.addMessageToConsole(message, lineNumber, sourceID); + } + + /** * Invoke a javascript alert. * @param message The message displayed in the alert. */ @@ -233,6 +274,80 @@ final class WebViewCore { } /** + * Notify the browser that the origin has exceeded it's database quota. + * @param url The URL that caused the overflow. + * @param databaseIdentifier The identifier of the database. + * @param currentQuota The current quota for the origin. + * @param estimatedSize The estimated size of the database. + */ + protected void exceededDatabaseQuota(String url, + String databaseIdentifier, + long currentQuota, + long estimatedSize) { + // Inform the callback proxy of the quota overflow. Send an object + // that encapsulates a call to the nativeSetDatabaseQuota method to + // awaken the sleeping webcore thread when a decision from the + // client to allow or deny quota is available. + mCallbackProxy.onExceededDatabaseQuota(url, databaseIdentifier, + currentQuota, estimatedSize, getUsedQuota(), + new WebStorage.QuotaUpdater() { + public void updateQuota(long quota) { + nativeSetNewStorageLimit(quota); + } + }); + } + + /** + * Notify the browser that the appcache has exceeded its max size. + * @param spaceNeeded is the amount of disk space that would be needed + * in order for the last appcache operation to succeed. + */ + protected void reachedMaxAppCacheSize(long spaceNeeded) { + mCallbackProxy.onReachedMaxAppCacheSize(spaceNeeded, getUsedQuota(), + new WebStorage.QuotaUpdater() { + public void updateQuota(long quota) { + nativeSetNewStorageLimit(quota); + } + }); + } + + protected void populateVisitedLinks() { + ValueCallback callback = new ValueCallback<String[]>() { + public void onReceiveValue(String[] value) { + sendMessage(EventHub.POPULATE_VISITED_LINKS, (Object)value); + } + }; + mCallbackProxy.getVisitedHistory(callback); + } + + /** + * Shows a prompt to ask the user to set the Geolocation permission state + * for the given origin. + * @param origin The origin for which Geolocation permissions are + * requested. + */ + protected void geolocationPermissionsShowPrompt(String origin) { + mCallbackProxy.onGeolocationPermissionsShowPrompt(origin, + new GeolocationPermissions.Callback() { + public void invoke(String origin, boolean allow, boolean remember) { + GeolocationPermissionsData data = new GeolocationPermissionsData(); + data.mOrigin = origin; + data.mAllow = allow; + data.mRemember = remember; + // Marshall to WebCore thread. + sendMessage(EventHub.GEOLOCATION_PERMISSIONS_PROVIDE, data); + } + }); + } + + /** + * Hides the Geolocation permissions prompt. + */ + protected void geolocationPermissionsHidePrompt() { + mCallbackProxy.onGeolocationPermissionsHidePrompt(); + } + + /** * Invoke a javascript confirm dialog. * @param message The message displayed in the dialog. * @return True if the user confirmed or false if the user cancelled. @@ -277,31 +392,36 @@ final class WebViewCore { // JNI methods //------------------------------------------------------------------------- - static native String nativeFindAddress(String addr); + static native String nativeFindAddress(String addr, boolean caseInsensitive); /** * Empty the picture set. */ private native void nativeClearContent(); - + /** * Create a flat picture from the set of pictures. */ private native void nativeCopyContentToPicture(Picture picture); - + /** * Draw the picture set with a background color. Returns true - * if some individual picture took too long to draw and can be + * 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); - + + /** + * check to see if picture is blank and in progress + */ + private native boolean nativePictureReady(); + /** * Redraw a portion of the picture set. The Point wh returns the * width and height of the overall picture. */ private native boolean nativeRecordContent(Region invalRegion, Point wh); - + /** * Splits slow parts of the picture set. Called from the webkit * thread after nativeDrawContent returns true. @@ -309,9 +429,10 @@ final class WebViewCore { private native void nativeSplitContent(); private native boolean nativeKey(int keyCode, int unichar, - int repeatCount, boolean isShift, boolean isAlt, boolean isDown); + int repeatCount, boolean isShift, boolean isAlt, boolean isSym, + boolean isDown); - private native boolean nativeClick(); + private native void nativeClick(int framePtr, int nodePtr); private native void nativeSendListBoxChoices(boolean[] choices, int size); @@ -326,63 +447,58 @@ final class WebViewCore { should this be called nativeSetViewPortSize? */ private native void nativeSetSize(int width, int height, int screenWidth, - float scale, int realScreenWidth, int screenHeight); + float scale, int realScreenWidth, int screenHeight, + boolean ignoreHeight); private native int nativeGetContentMinPrefWidth(); - + // Start: functions that deal with text editing - private native void nativeReplaceTextfieldText(int frame, int node, int x, - int y, int oldStart, int oldEnd, String replace, int newStart, - int newEnd); + private native void nativeReplaceTextfieldText( + int oldStart, int oldEnd, String replace, int newStart, int newEnd, + int textGeneration); - private native void passToJs(int frame, int node, int x, int y, int gen, + private native void passToJs(int gen, String currentText, int keyCode, int keyValue, boolean down, boolean cap, boolean fn, boolean sym); + private native void nativeSetFocusControllerActive(boolean active); + private native void nativeSaveDocumentState(int frame); - private native void nativeSetFinalFocus(int framePtr, int nodePtr, int x, - int y, boolean block); + private native void nativeMoveMouse(int framePtr, int x, int y); - private native void nativeSetKitFocus(int moveGeneration, - int buildGeneration, int framePtr, int nodePtr, int x, int y, - boolean ignoreNullFocus); + private native void nativeMoveMouseIfLatest(int moveGeneration, + int framePtr, int x, int y); private native String nativeRetrieveHref(int framePtr, int nodePtr); - - private native void nativeTouchUp(int touchGeneration, - int buildGeneration, int framePtr, int nodePtr, int x, int y, - int size, boolean isClick, boolean retry); + + private native void nativeTouchUp(int touchGeneration, + int framePtr, int nodePtr, int x, int y); private native boolean nativeHandleTouchEvent(int action, int x, int y); - private native void nativeUnblockFocus(); - private native void nativeUpdateFrameCache(); - - private native void nativeSetSnapAnchor(int x, int y); - - private native void nativeSnapToAnchor(); - + private native void nativeSetBackgroundColor(int color); - + private native void nativeDumpDomTree(boolean useFile); private native void nativeDumpRenderTree(boolean useFile); private native void nativeDumpNavTree(); - private native void nativeRefreshPlugins(boolean reloadOpenPages); - + private native void nativeSetJsFlags(String flags); + /** * Delete text from start to end in the focused textfield. If there is no - * focus, or if start == end, silently fail. If start and end are out of + * focus, or if start == end, silently fail. If start and end are out of * order, swap them. * @param start Beginning of selection to delete. * @param end End of selection to delete. + * @param textGeneration Text generation number when delete was pressed. */ - private native void nativeDeleteSelection(int frame, int node, int x, int y, - int start, int end); + private native void nativeDeleteSelection(int start, int end, + int textGeneration); /** * Set the selection to (start, end) in the focused textfield. If start and @@ -390,15 +506,39 @@ final class WebViewCore { * @param start Beginning of selection. * @param end End of selection. */ - private native void nativeSetSelection(int frame, int node, int x, int y, - int start, int end); + private native void nativeSetSelection(int start, int end); private native String nativeGetSelection(Region sel); - + // Register a scheme to be treated as local scheme so that it can access // local asset files for resources private native void nativeRegisterURLSchemeAsLocal(String scheme); + /* + * Inform webcore that the user has decided whether to allow or deny new + * quota for the current origin or more space for the app cache, and that + * the main thread should wake up now. + * @param limit Is the new quota for an origin or new app cache max size. + */ + private native void nativeSetNewStorageLimit(long limit); + + private native void nativeUpdatePluginState(int framePtr, int nodePtr, int state); + + /** + * Provide WebCore with a Geolocation permission state for the specified + * origin. + * @param origin The origin for which Geolocation permissions are provided. + * @param allow Whether Geolocation permissions are allowed. + * @param remember Whether this decision should be remembered beyond the + * life of the current page. + */ + private native void nativeGeolocationPermissionsProvide(String origin, boolean allow, boolean remember); + + /** + * Provide WebCore with the previously visted links from the history database + */ + private native void nativeProvideVisitedHistory(String[] history); + // EventHub for processing messages private final EventHub mEventHub; // WebCore thread handler @@ -447,7 +587,7 @@ final class WebViewCore { CacheManager.endCacheTransaction(); CacheManager.startCacheTransaction(); sendMessageDelayed( - obtainMessage(CACHE_TICKER), + obtainMessage(CACHE_TICKER), CACHE_TICKER_INTERVAL); } break; @@ -472,36 +612,64 @@ final class WebViewCore { } } - static class FocusData { - FocusData() {} - FocusData(FocusData d) { - mMoveGeneration = d.mMoveGeneration; - mBuildGeneration = d.mBuildGeneration; - mFrame = d.mFrame; - mNode = d.mNode; - mX = d.mX; - mY = d.mY; - mIgnoreNullFocus = d.mIgnoreNullFocus; + static class BaseUrlData { + String mBaseUrl; + String mData; + String mMimeType; + String mEncoding; + String mFailUrl; + } + + static class CursorData { + CursorData() {} + CursorData(int frame, int node, int x, int y) { + mFrame = frame; + mX = x; + mY = y; } int mMoveGeneration; - int mBuildGeneration; int mFrame; - int mNode; int mX; int mY; - boolean mIgnoreNullFocus; + } + + static class JSInterfaceData { + Object mObject; + String mInterfaceName; + } + + static class JSKeyData { + String mCurrentText; + KeyEvent mEvent; + } + + static class PostUrlData { + String mUrl; + byte[] mPostData; + } + + static class ReplaceTextData { + String mReplace; + int mNewStart; + int mNewEnd; + int mTextGeneration; + } + + static class TextSelectionData { + public TextSelectionData(int start, int end) { + mStart = start; + mEnd = end; + } + int mStart; + int mEnd; } static class TouchUpData { int mMoveGeneration; - int mBuildGeneration; int mFrame; int mNode; int mX; int mY; - int mSize; - boolean mIsClick; - boolean mRetry; } static class TouchEventData { @@ -510,7 +678,21 @@ final class WebViewCore { int mY; } + static class PluginStateData { + int mFrame; + int mNode; + int mState; + } + + static class GeolocationPermissionsData { + String mOrigin; + boolean mAllow; + boolean mRemember; + } + static final String[] HandlerDebugString = { + "UPDATE_FRAME_CACHE_IF_LOADING", // = 98 + "SCROLL_TEXT_INPUT", // = 99 "LOAD_URL", // = 100; "STOP_LOADING", // = 101; "RELOAD", // = 102; @@ -532,33 +714,37 @@ final class WebViewCore { "CLICK", // = 118; "SET_NETWORK_STATE", // = 119; "DOC_HAS_IMAGES", // = 120; - "SET_SNAP_ANCHOR", // = 121; + "121", // = 121; "DELETE_SELECTION", // = 122; "LISTBOX_CHOICES", // = 123; "SINGLE_LISTBOX_CHOICE", // = 124; - "125", + "MESSAGE_RELAY", // = 125; "SET_BACKGROUND_COLOR", // = 126; - "UNBLOCK_FOCUS", // = 127; + "PLUGIN_STATE", // = 127; "SAVE_DOCUMENT_STATE", // = 128; "GET_SELECTION", // = 129; "WEBKIT_DRAW", // = 130; "SYNC_SCROLL", // = 131; - "REFRESH_PLUGINS", // = 132; - // this will replace REFRESH_PLUGINS in the next release - "POST_URL", // = 142; + "POST_URL", // = 132; "SPLIT_PICTURE_SET", // = 133; "CLEAR_CONTENT", // = 134; - "SET_FINAL_FOCUS", // = 135; - "SET_KIT_FOCUS", // = 136; - "REQUEST_FOCUS_HREF", // = 137; + "SET_MOVE_MOUSE", // = 135; + "SET_MOVE_MOUSE_IF_LATEST", // = 136; + "REQUEST_CURSOR_HREF", // = 137; "ADD_JS_INTERFACE", // = 138; "LOAD_DATA", // = 139; "TOUCH_UP", // = 140; "TOUCH_EVENT", // = 141; + "SET_ACTIVE", // = 142; + "ON_PAUSE", // = 143 + "ON_RESUME", // = 144 + "FREE_MEMORY", // = 145 }; class EventHub { // Message Ids + static final int UPDATE_FRAME_CACHE_IF_LOADING = 98; + static final int SCROLL_TEXT_INPUT = 99; static final int LOAD_URL = 100; static final int STOP_LOADING = 101; static final int RELOAD = 102; @@ -580,26 +766,24 @@ final class WebViewCore { static final int CLICK = 118; static final int SET_NETWORK_STATE = 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 MESSAGE_RELAY = 125; static final int SET_BACKGROUND_COLOR = 126; - static final int UNBLOCK_FOCUS = 127; + static final int PLUGIN_STATE = 127; // plugin notifications static final int SAVE_DOCUMENT_STATE = 128; static final int GET_SELECTION = 129; static final int WEBKIT_DRAW = 130; static final int SYNC_SCROLL = 131; - static final int REFRESH_PLUGINS = 132; - // this will replace REFRESH_PLUGINS in the next release - static final int POST_URL = 142; + static final int POST_URL = 132; static final int SPLIT_PICTURE_SET = 133; static final int CLEAR_CONTENT = 134; - + // UI nav messages - static final int SET_FINAL_FOCUS = 135; - static final int SET_KIT_FOCUS = 136; - static final int REQUEST_FOCUS_HREF = 137; + static final int SET_MOVE_MOUSE = 135; + static final int SET_MOVE_MOUSE_IF_LATEST = 136; + static final int REQUEST_CURSOR_HREF = 137; static final int ADD_JS_INTERFACE = 138; static final int LOAD_DATA = 139; @@ -608,6 +792,17 @@ final class WebViewCore { // message used to pass UI touch events to WebCore static final int TOUCH_EVENT = 141; + // Used to tell the focus controller not to draw the blinking cursor, + // based on whether the WebView has focus and whether the WebView's + // cursor matches the webpage's focus. + static final int SET_ACTIVE = 142; + + // lifecycle activities for just this DOM (unlike pauseTimers, which + // is global) + static final int ON_PAUSE = 143; + static final int ON_RESUME = 144; + static final int FREE_MEMORY = 145; + // Network-based messaging static final int CLEAR_SSL_PREF_TABLE = 150; @@ -620,12 +815,14 @@ final class WebViewCore { static final int DUMP_RENDERTREE = 171; static final int DUMP_NAVTREE = 172; + static final int SET_JS_FLAGS = 173; + // Geolocation + static final int GEOLOCATION_PERMISSIONS_PROVIDE = 180; + + static final int POPULATE_VISITED_LINKS = 181; + // private message ids private static final int DESTROY = 200; - - // flag values passed to message SET_FINAL_FOCUS - static final int NO_FOCUS_CHANGE_BLOCK = 0; - static final int BLOCK_FOCUS_CHANGE_UNTIL_KEY_UP = 1; // Private handler for WebCore messages. private Handler mHandler; @@ -654,10 +851,14 @@ final class WebViewCore { mHandler = new Handler() { @Override public void handleMessage(Message msg) { - if (LOGV_ENABLED) { - Log.v(LOGTAG, msg.what < LOAD_URL || msg.what - > TOUCH_EVENT ? Integer.toString(msg.what) - : HandlerDebugString[msg.what - LOAD_URL]); + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, (msg.what < UPDATE_FRAME_CACHE_IF_LOADING + || msg.what + > FREE_MEMORY ? Integer.toString(msg.what) + : HandlerDebugString[msg.what + - UPDATE_FRAME_CACHE_IF_LOADING]) + + " arg1=" + msg.arg1 + " arg2=" + msg.arg2 + + " obj=" + msg.obj); } switch (msg.what) { case WEBKIT_DRAW: @@ -667,9 +868,22 @@ final class WebViewCore { case DESTROY: // Time to take down the world. Cancel all pending // loads and destroy the native view and frame. - mBrowserFrame.destroy(); - mBrowserFrame = null; - mNativeClass = 0; + synchronized (WebViewCore.this) { + mBrowserFrame.destroy(); + mBrowserFrame = null; + mSettings.onDestroyed(); + mNativeClass = 0; + mWebView = null; + } + break; + + case UPDATE_FRAME_CACHE_IF_LOADING: + nativeUpdateFrameCacheIfLoading(); + break; + + case SCROLL_TEXT_INPUT: + nativeScrollFocusedTextInput( + ((Float) msg.obj).floatValue(), msg.arg1); break; case LOAD_URL: @@ -677,15 +891,13 @@ final class WebViewCore { break; case POST_URL: { - HashMap param = (HashMap) msg.obj; - String url = (String) param.get("url"); - byte[] data = (byte[]) param.get("data"); - mBrowserFrame.postUrl(url, data); + PostUrlData param = (PostUrlData) msg.obj; + mBrowserFrame.postUrl(param.mUrl, param.mPostData); break; } case LOAD_DATA: - HashMap loadParams = (HashMap) msg.obj; - String baseUrl = (String) loadParams.get("baseUrl"); + BaseUrlData loadParams = (BaseUrlData) msg.obj; + String baseUrl = loadParams.mBaseUrl; if (baseUrl != null) { int i = baseUrl.indexOf(':'); if (i > 0) { @@ -698,7 +910,7 @@ final class WebViewCore { * 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") && @@ -709,16 +921,16 @@ final class WebViewCore { } } mBrowserFrame.loadData(baseUrl, - (String) loadParams.get("data"), - (String) loadParams.get("mimeType"), - (String) loadParams.get("encoding"), - (String) loadParams.get("failUrl")); + loadParams.mData, + loadParams.mMimeType, + loadParams.mEncoding, + loadParams.mFailUrl); break; case STOP_LOADING: - // If the WebCore has committed the load, but not - // finished the first layout yet, we need to set - // first layout done to trigger the interpreted side sync + // If the WebCore has committed the load, but not + // finished the first layout yet, we need to set + // first layout done to trigger the interpreted side sync // up with native side if (mBrowserFrame.committed() && !mBrowserFrame.firstLayoutDone()) { @@ -741,20 +953,24 @@ final class WebViewCore { break; case CLICK: - nativeClick(); + nativeClick(msg.arg1, msg.arg2); break; - case VIEW_SIZE_CHANGED: - viewSizeChanged(msg.arg1, msg.arg2, - ((Float) msg.obj).floatValue()); + case VIEW_SIZE_CHANGED: { + WebView.ViewSizeData data = + (WebView.ViewSizeData) msg.obj; + viewSizeChanged(data.mWidth, data.mHeight, + data.mTextWrapWidth, data.mScale, + data.mIgnoreHeight); break; - + } case SET_SCROLL_OFFSET: // note: these are in document coordinates // (inv-zoom) - nativeSetScrollOffset(msg.arg1, msg.arg2); + Point pt = (Point) msg.obj; + nativeSetScrollOffset(msg.arg1, pt.x, pt.y); break; - + case SET_GLOBAL_BOUNDS: Rect r = (Rect) msg.obj; nativeSetGlobalBounds(r.left, r.top, r.width(), @@ -765,7 +981,7 @@ final class WebViewCore { // If it is a standard load and the load is not // committed yet, we interpret BACK as RELOAD if (!mBrowserFrame.committed() && msg.arg1 == -1 && - (mBrowserFrame.loadType() == + (mBrowserFrame.loadType() == BrowserFrame.FRAME_LOADTYPE_STANDARD)) { mBrowserFrame.reload(true); } else { @@ -802,6 +1018,24 @@ final class WebViewCore { } break; + case ON_PAUSE: + nativePause(); + break; + + case ON_RESUME: + nativeResume(); + break; + + case FREE_MEMORY: + clearCache(false); + nativeFreeMemory(); + break; + + case PLUGIN_STATE: + PluginStateData psd = (PluginStateData) msg.obj; + nativeUpdatePluginState(psd.mFrame, psd.mNode, psd.mState); + break; + case SET_NETWORK_STATE: if (BrowserFrame.sJavaBridge == null) { throw new IllegalStateException("No WebView " + @@ -812,10 +1046,7 @@ final class WebViewCore { break; case CLEAR_CACHE: - mBrowserFrame.clearCache(); - if (msg.arg1 == 1) { - CacheManager.removeAllCacheFiles(); - } + clearCache(msg.arg1 == 1); break; case CLEAR_HISTORY: @@ -823,29 +1054,21 @@ final class WebViewCore { close(mBrowserFrame.mNativeFrame); break; - case REPLACE_TEXT: - HashMap jMap = (HashMap) msg.obj; - FocusData fData = (FocusData) jMap.get("focusData"); - String replace = (String) jMap.get("replace"); - int newStart = - ((Integer) jMap.get("start")).intValue(); - int newEnd = - ((Integer) jMap.get("end")).intValue(); - nativeReplaceTextfieldText(fData.mFrame, - fData.mNode, fData.mX, fData.mY, msg.arg1, - msg.arg2, replace, newStart, newEnd); + case REPLACE_TEXT: + ReplaceTextData rep = (ReplaceTextData) msg.obj; + nativeReplaceTextfieldText(msg.arg1, msg.arg2, + rep.mReplace, rep.mNewStart, rep.mNewEnd, + rep.mTextGeneration); break; case PASS_TO_JS: { - HashMap jsMap = (HashMap) msg.obj; - FocusData fDat = (FocusData) jsMap.get("focusData"); - KeyEvent evt = (KeyEvent) jsMap.get("event"); + JSKeyData jsData = (JSKeyData) msg.obj; + KeyEvent evt = jsData.mEvent; int keyCode = evt.getKeyCode(); int keyValue = evt.getUnicodeChar(); int generation = msg.arg1; - passToJs(fDat.mFrame, fDat.mNode, fDat.mX, fDat.mY, - generation, - (String) jsMap.get("currentText"), + passToJs(generation, + jsData.mCurrentText, keyCode, keyValue, evt.isDown(), @@ -855,8 +1078,8 @@ final class WebViewCore { } case SAVE_DOCUMENT_STATE: { - FocusData fDat = (FocusData) msg.obj; - nativeSaveDocumentState(fDat.mFrame); + CursorData cDat = (CursorData) msg.obj; + nativeSaveDocumentState(cDat.mFrame); break; } @@ -868,11 +1091,8 @@ final class WebViewCore { case TOUCH_UP: TouchUpData touchUpData = (TouchUpData) msg.obj; nativeTouchUp(touchUpData.mMoveGeneration, - touchUpData.mBuildGeneration, touchUpData.mFrame, touchUpData.mNode, - touchUpData.mX, touchUpData.mY, - touchUpData.mSize, touchUpData.mIsClick, - touchUpData.mRetry); + touchUpData.mX, touchUpData.mY); break; case TOUCH_EVENT: { @@ -885,13 +1105,14 @@ final class WebViewCore { break; } + case SET_ACTIVE: + nativeSetFocusControllerActive(msg.arg1 == 1); + break; + case ADD_JS_INTERFACE: - HashMap map = (HashMap) msg.obj; - Object obj = map.get("object"); - String interfaceName = (String) - map.get("interfaceName"); - mBrowserFrame.addJavascriptInterface(obj, - interfaceName); + JSInterfaceData jsData = (JSInterfaceData) msg.obj; + mBrowserFrame.addJavascriptInterface(jsData.mObject, + jsData.mInterfaceName); break; case REQUEST_EXT_REPRESENTATION: @@ -903,35 +1124,27 @@ final class WebViewCore { mBrowserFrame.documentAsText((Message) msg.obj); break; - case SET_FINAL_FOCUS: - FocusData finalData = (FocusData) msg.obj; - nativeSetFinalFocus(finalData.mFrame, - finalData.mNode, finalData.mX, - finalData.mY, msg.arg1 - != EventHub.NO_FOCUS_CHANGE_BLOCK); + case SET_MOVE_MOUSE: + CursorData cursorData = (CursorData) msg.obj; + nativeMoveMouse(cursorData.mFrame, + cursorData.mX, cursorData.mY); break; - case UNBLOCK_FOCUS: - nativeUnblockFocus(); + case SET_MOVE_MOUSE_IF_LATEST: + CursorData cData = (CursorData) msg.obj; + nativeMoveMouseIfLatest(cData.mMoveGeneration, + cData.mFrame, + cData.mX, cData.mY); break; - case SET_KIT_FOCUS: - FocusData focusData = (FocusData) msg.obj; - nativeSetKitFocus(focusData.mMoveGeneration, - focusData.mBuildGeneration, - focusData.mFrame, focusData.mNode, - focusData.mX, focusData.mY, - focusData.mIgnoreNullFocus); - break; - - case REQUEST_FOCUS_HREF: { + case REQUEST_CURSOR_HREF: { Message hrefMsg = (Message) msg.obj; String res = nativeRetrieveHref(msg.arg1, msg.arg2); hrefMsg.getData().putString("url", res); hrefMsg.sendToTarget(); break; } - + case UPDATE_CACHE_AND_TEXT_ENTRY: nativeUpdateFrameCache(); // FIXME: this should provide a minimal rectangle @@ -948,24 +1161,17 @@ final class WebViewCore { imageResult.sendToTarget(); break; - case SET_SNAP_ANCHOR: - nativeSetSnapAnchor(msg.arg1, msg.arg2); - break; - case DELETE_SELECTION: - FocusData delData = (FocusData) msg.obj; - nativeDeleteSelection(delData.mFrame, - delData.mNode, delData.mX, - delData.mY, msg.arg1, msg.arg2); + TextSelectionData deleteSelectionData + = (TextSelectionData) msg.obj; + nativeDeleteSelection(deleteSelectionData.mStart, + deleteSelectionData.mEnd, msg.arg1); break; case SET_SELECTION: - FocusData selData = (FocusData) msg.obj; - nativeSetSelection(selData.mFrame, - selData.mNode, selData.mX, - selData.mY, msg.arg1, msg.arg2); + nativeSetSelection(msg.arg1, msg.arg2); break; - + case LISTBOX_CHOICES: SparseBooleanArray choices = (SparseBooleanArray) msg.obj; @@ -974,18 +1180,18 @@ final class WebViewCore { for (int c = 0; c < choicesSize; c++) { choicesArray[c] = choices.get(c); } - nativeSendListBoxChoices(choicesArray, + nativeSendListBoxChoices(choicesArray, choicesSize); break; case SINGLE_LISTBOX_CHOICE: nativeSendListBoxChoice(msg.arg1); break; - + case SET_BACKGROUND_COLOR: nativeSetBackgroundColor(msg.arg1); break; - + case GET_SELECTION: String str = nativeGetSelection((Region) msg.obj); Message.obtain(mWebView.mPrivateHandler @@ -1005,26 +1211,43 @@ final class WebViewCore { nativeDumpNavTree(); break; + case SET_JS_FLAGS: + nativeSetJsFlags((String)msg.obj); + break; + + case GEOLOCATION_PERMISSIONS_PROVIDE: + GeolocationPermissionsData data = + (GeolocationPermissionsData) msg.obj; + nativeGeolocationPermissionsProvide(data.mOrigin, + data.mAllow, data.mRemember); + break; + case SYNC_SCROLL: mWebkitScrollX = msg.arg1; mWebkitScrollY = msg.arg2; break; - case REFRESH_PLUGINS: - nativeRefreshPlugins(msg.arg1 != 0); - break; - case SPLIT_PICTURE_SET: nativeSplitContent(); mSplitPictureIsScheduled = false; break; - + case CLEAR_CONTENT: // Clear the view so that onDraw() will draw nothing // but white background // (See public method WebView.clearView) nativeClearContent(); break; + + case MESSAGE_RELAY: + if (msg.obj instanceof Message) { + ((Message) msg.obj).sendToTarget(); + } + break; + + case POPULATE_VISITED_LINKS: + nativeProvideVisitedHistory((String[])msg.obj); + break; } } }; @@ -1066,6 +1289,18 @@ final class WebViewCore { } } + private synchronized boolean hasMessages(int what) { + if (mBlockMessages) { + return false; + } + if (mMessages != null) { + Log.w(LOGTAG, "hasMessages() is not supported in this case."); + return false; + } else { + return mHandler.hasMessages(what); + } + } + private synchronized void sendMessageDelayed(Message msg, long delay) { if (mBlockMessages) { return; @@ -1114,7 +1349,7 @@ final class WebViewCore { //------------------------------------------------------------------------- void stopLoading() { - if (LOGV_ENABLED) Log.v(LOGTAG, "CORE stopLoading"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "CORE stopLoading"); if (mBrowserFrame != null) { mBrowserFrame.stopLoading(); } @@ -1177,11 +1412,23 @@ final class WebViewCore { // We don't want anyone to post a message between removing pending // messages and sending the destroy message. synchronized (mEventHub) { + // RESUME_TIMERS and PAUSE_TIMERS are per process base. They need to + // be preserved even the WebView is destroyed. + // Note: we should not have more than one RESUME_TIMERS/PAUSE_TIMERS + boolean hasResume = mEventHub.hasMessages(EventHub.RESUME_TIMERS); + boolean hasPause = mEventHub.hasMessages(EventHub.PAUSE_TIMERS); mEventHub.removeMessages(); mEventHub.sendMessageAtFrontOfQueue( Message.obtain(null, EventHub.DESTROY)); + if (hasPause) { + mEventHub.sendMessageAtFrontOfQueue( + Message.obtain(null, EventHub.PAUSE_TIMERS)); + } + if (hasResume) { + mEventHub.sendMessageAtFrontOfQueue( + Message.obtain(null, EventHub.RESUME_TIMERS)); + } mEventHub.blockMessages(); - mWebView = null; } } @@ -1189,20 +1436,42 @@ final class WebViewCore { // WebViewCore private methods //------------------------------------------------------------------------- + private void clearCache(boolean includeDiskFiles) { + mBrowserFrame.clearCache(); + if (includeDiskFiles) { + CacheManager.removeAllCacheFiles(); + } + } + private void loadUrl(String url) { - if (LOGV_ENABLED) Log.v(LOGTAG, " CORE loadUrl " + url); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, " CORE loadUrl " + url); mBrowserFrame.loadUrl(url); } private void key(KeyEvent evt, boolean isDown) { - if (LOGV_ENABLED) { + if (DebugFlags.WEB_VIEW_CORE) { Log.v(LOGTAG, "CORE key at " + System.currentTimeMillis() + ", " + evt); } - if (!nativeKey(evt.getKeyCode(), evt.getUnicodeChar(), + int keyCode = evt.getKeyCode(); + if (!nativeKey(keyCode, evt.getUnicodeChar(), evt.getRepeatCount(), evt.isShiftPressed(), evt.isAltPressed(), - isDown)) { + evt.isSymPressed(), + isDown) && keyCode != KeyEvent.KEYCODE_ENTER) { + if (keyCode >= KeyEvent.KEYCODE_DPAD_UP + && keyCode <= KeyEvent.KEYCODE_DPAD_RIGHT) { + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, "key: arrow unused by plugin: " + keyCode); + } + if (mWebView != null && evt.isDown()) { + Message.obtain(mWebView.mPrivateHandler, + WebView.MOVE_OUT_OF_PLUGIN, keyCode).sendToTarget(); + } + return; + } // bubble up the event handling + // but do not bubble up the ENTER key, which would open the search + // bar without any text. mCallbackProxy.onUnhandledKeyEvent(evt); } } @@ -1210,21 +1479,25 @@ final class WebViewCore { // These values are used to avoid requesting a layout based on old values private int mCurrentViewWidth = 0; private int mCurrentViewHeight = 0; + private float mCurrentViewScale = 1.0f; // notify webkit that our virtual view size changed size (after inv-zoom) - private void viewSizeChanged(int w, int h, float scale) { - if (LOGV_ENABLED) Log.v(LOGTAG, "CORE onSizeChanged"); + private void viewSizeChanged(int w, int h, int textwrapWidth, float scale, + boolean ignoreHeight) { + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, "viewSizeChanged w=" + w + "; h=" + h + + "; textwrapWidth=" + textwrapWidth + "; scale=" + scale); + } if (w == 0) { Log.w(LOGTAG, "skip viewSizeChanged as w is 0"); return; } - if (mSettings.getUseWideViewPort() - && (w < mViewportWidth || mViewportWidth == -1)) { - int width = mViewportWidth; + int width = w; + if (mSettings.getUseWideViewPort()) { if (mViewportWidth == -1) { - if (mSettings.getLayoutAlgorithm() == + if (mSettings.getLayoutAlgorithm() == WebSettings.LayoutAlgorithm.NORMAL) { - width = WebView.ZOOM_OUT_WIDTH; + width = DEFAULT_VIEWPORT_WIDTH; } else { /* * if a page's minimum preferred width is wider than the @@ -1238,22 +1511,24 @@ final class WebViewCore { * In the worse case, the native width will be adjusted when * next zoom or screen orientation change happens. */ - width = Math.max(w, nativeGetContentMinPrefWidth()); + width = Math.max(w, Math.max(DEFAULT_VIEWPORT_WIDTH, + nativeGetContentMinPrefWidth())); } + } else { + width = Math.max(w, mViewportWidth); } - nativeSetSize(width, Math.round((float) width * h / w), w, scale, - w, h); - } else { - nativeSetSize(w, h, w, scale, w, h); } + nativeSetSize(width, width == w ? h : Math.round((float) width * h / w), + textwrapWidth, scale, w, h, ignoreHeight); // Remember the current width and height boolean needInvalidate = (mCurrentViewWidth == 0); mCurrentViewWidth = w; mCurrentViewHeight = h; + mCurrentViewScale = scale; if (needInvalidate) { // ensure {@link #webkitDraw} is called as we were blocking in // {@link #contentDraw} when mCurrentViewWidth is 0 - if (LOGV_ENABLED) Log.v(LOGTAG, "viewSizeChanged"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "viewSizeChanged"); contentDraw(); } mEventHub.sendMessage(Message.obtain(null, @@ -1267,42 +1542,84 @@ final class WebViewCore { } } + // Utility method for exceededDatabaseQuota and reachedMaxAppCacheSize + // callbacks. Computes the sum of database quota for all origins. + private long getUsedQuota() { + WebStorage webStorage = WebStorage.getInstance(); + Collection<WebStorage.Origin> origins = webStorage.getOriginsSync(); + + if (origins == null) { + return 0; + } + long usedQuota = 0; + for (WebStorage.Origin website : origins) { + usedQuota += website.getQuota(); + } + return usedQuota; + } + // 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() { + // mRestoreState is set in didFirstLayout(), and reset in the next + // webkitDraw after passing it to the UI thread. + private RestoreState mRestoreState = null; + + static class RestoreState { + float mMinScale; + float mMaxScale; + float mViewScale; + float mTextWrapScale; + float mDefaultScale; + int mScrollX; + int mScrollY; + boolean mMobileSite; + } + + static class DrawData { + DrawData() { mInvalRegion = new Region(); mWidthHeight = new Point(); } - public Region mInvalRegion; - public Point mViewPoint; - public Point mWidthHeight; + Region mInvalRegion; + Point mViewPoint; + Point mWidthHeight; + int mMinPrefWidth; + RestoreState mRestoreState; // only non-null if it is for the first + // picture set after the first layout } - + private void webkitDraw() { mDrawIsScheduled = false; DrawData draw = new DrawData(); - if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw start"); - if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight) + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw start"); + if (nativeRecordContent(draw.mInvalRegion, draw.mWidthHeight) == false) { - if (LOGV_ENABLED) Log.v(LOGTAG, "webkitDraw abort"); + if (DebugFlags.WEB_VIEW_CORE) 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"); + if (mSettings.getUseWideViewPort()) { + draw.mMinPrefWidth = Math.max( + mViewportWidth == -1 ? DEFAULT_VIEWPORT_WIDTH + : (mViewportWidth == 0 ? mCurrentViewWidth + : mViewportWidth), + nativeGetContentMinPrefWidth()); + } + if (mRestoreState != null) { + draw.mRestoreState = mRestoreState; + mRestoreState = null; + } + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "webkitDraw NEW_PICTURE_MSG_ID"); Message.obtain(mWebView.mPrivateHandler, WebView.NEW_PICTURE_MSG_ID, draw).sendToTarget(); if (mWebkitScrollX != 0 || mWebkitScrollY != 0) { @@ -1312,9 +1629,6 @@ final class WebViewCore { mWebkitScrollY).sendToTarget(); mWebkitScrollX = mWebkitScrollY = 0; } - // nativeSnapToAnchor() needs to be called after NEW_PICTURE_MSG_ID - // is sent, so that scroll will be based on the new content size. - nativeSnapToAnchor(); } } @@ -1329,8 +1643,6 @@ final class WebViewCore { final DrawFilter mZoomFilter = new PaintFlagsDrawFilter(ZOOM_BITS, Paint.LINEAR_TEXT_FLAG); - final DrawFilter mScrollFilter = - new PaintFlagsDrawFilter(SCROLL_BITS, 0); /* package */ void drawContentPicture(Canvas canvas, int color, boolean animatingZoom, @@ -1339,7 +1651,7 @@ final class WebViewCore { if (animatingZoom) { df = mZoomFilter; } else if (animatingScroll) { - df = mScrollFilter; + df = null; } canvas.setDrawFilter(df); boolean tookTooLong = nativeDrawContent(canvas, color); @@ -1350,9 +1662,15 @@ final class WebViewCore { } } - /*package*/ Picture copyContentPicture() { + /* package */ synchronized boolean pictureReady() { + return 0 != mNativeClass ? nativePictureReady() : false; + } + + /*package*/ synchronized Picture copyContentPicture() { Picture result = new Picture(); - nativeCopyContentToPicture(result); + if (0 != mNativeClass) { + nativeCopyContentToPicture(result); + } return result; } @@ -1363,9 +1681,9 @@ final class WebViewCore { sWebCoreHandler.sendMessageAtFrontOfQueue(sWebCoreHandler .obtainMessage(WebCoreThread.REDUCE_PRIORITY)); // Note: there is one possible failure mode. If pauseUpdate() is called - // from UI thread while in webcore thread WEBKIT_DRAW is just pulled out - // of the queue and about to be executed. mDrawIsScheduled may be set to - // false in webkitDraw(). So update won't be blocked. But at least the + // from UI thread while in webcore thread WEBKIT_DRAW is just pulled out + // of the queue and about to be executed. mDrawIsScheduled may be set to + // false in webkitDraw(). So update won't be blocked. But at least the // webcore thread priority is still lowered. if (core != null) { synchronized (core) { @@ -1385,7 +1703,7 @@ final class WebViewCore { synchronized (core) { core.mDrawIsScheduled = false; core.mDrawIsPaused = false; - if (LOGV_ENABLED) Log.v(LOGTAG, "resumeUpdate"); + if (DebugFlags.WEB_VIEW_CORE) Log.v(LOGTAG, "resumeUpdate"); core.contentDraw(); } } @@ -1434,7 +1752,7 @@ final class WebViewCore { mEventHub.sendMessage(Message.obtain(null, EventHub.WEBKIT_DRAW)); } } - + // called by JNI private void contentScrollBy(int dx, int dy, boolean animate) { if (!mBrowserFrame.firstLayoutDone()) { @@ -1442,9 +1760,14 @@ final class WebViewCore { return; } if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.SCROLL_BY_MSG_ID, dx, dy, - new Boolean(animate)).sendToTarget(); + Message msg = Message.obtain(mWebView.mPrivateHandler, + WebView.SCROLL_BY_MSG_ID, dx, dy, new Boolean(animate)); + if (mDrawIsScheduled) { + mEventHub.sendMessage(Message.obtain(null, + EventHub.MESSAGE_RELAY, msg)); + } else { + msg.sendToTarget(); + } } } @@ -1461,8 +1784,14 @@ final class WebViewCore { return; } if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.SCROLL_TO_MSG_ID, x, y).sendToTarget(); + Message msg = Message.obtain(mWebView.mPrivateHandler, + WebView.SCROLL_TO_MSG_ID, x, y); + if (mDrawIsScheduled) { + mEventHub.sendMessage(Message.obtain(null, + EventHub.MESSAGE_RELAY, msg)); + } else { + msg.sendToTarget(); + } } } @@ -1479,24 +1808,14 @@ final class WebViewCore { return; } if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.SPAWN_SCROLL_TO_MSG_ID, x, y).sendToTarget(); - } - } - - // called by JNI - private void sendMarkNodeInvalid(int node) { - if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.MARK_NODE_INVALID_ID, node, 0).sendToTarget(); - } - } - - // called by JNI - private void sendNotifyFocusSet() { - if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.NOTIFY_FOCUS_SET_MSG_ID).sendToTarget(); + Message msg = Message.obtain(mWebView.mPrivateHandler, + WebView.SPAWN_SCROLL_TO_MSG_ID, x, y); + if (mDrawIsScheduled) { + mEventHub.sendMessage(Message.obtain(null, + EventHub.MESSAGE_RELAY, msg)); + } else { + msg.sendToTarget(); + } } } @@ -1511,14 +1830,6 @@ final class WebViewCore { contentDraw(); } - // called by JNI - private void sendRecomputeFocus() { - if (mWebView != null) { - Message.obtain(mWebView.mPrivateHandler, - WebView.RECOMPUTE_FOCUS_MSG_ID).sendToTarget(); - } - } - /* Called by JNI. The coordinates are in doc coordinates, so they need to be scaled before they can be used by the view system, which happens in WebView since it (and its thread) know the current scale factor. @@ -1536,109 +1847,185 @@ final class WebViewCore { } private native void setViewportSettingsFromNative(); - + // called by JNI - private void didFirstLayout() { - // Trick to ensure that the Picture has the exact height for the content - // by forcing to layout with 0 height after the page is ready, which is - // indicated by didFirstLayout. This is essential to get rid of the - // white space in the GMail which uses WebView for message view. - if (mWebView != null && mWebView.mHeightCanMeasure) { - mWebView.mLastHeightSent = 0; - // Send a negative scale to indicate that WebCore should reuse the - // current scale - mEventHub.sendMessage(Message.obtain(null, - EventHub.VIEW_SIZE_CHANGED, mWebView.mLastWidthSent, - mWebView.mLastHeightSent, -1.0f)); + private void didFirstLayout(boolean standardLoad) { + if (DebugFlags.WEB_VIEW_CORE) { + Log.v(LOGTAG, "didFirstLayout standardLoad =" + standardLoad); } mBrowserFrame.didFirstLayout(); - // reset the scroll position as it is a new page now - mWebkitScrollX = mWebkitScrollY = 0; + if (mWebView == null) return; + + setupViewport(standardLoad || mRestoredScale > 0); + + // reset the scroll position, the restored offset and scales + mWebkitScrollX = mWebkitScrollY = mRestoredX = mRestoredY + = mRestoredScale = mRestoredScreenWidthScale = 0; + } + + // called by JNI + private void updateViewport() { + // if updateViewport is called before first layout, wait until first + // layout to update the viewport. In the rare case, this is called after + // first layout, force an update as we have just parsed the viewport + // meta tag. + if (mBrowserFrame.firstLayoutDone()) { + setupViewport(true); + } + } + private void setupViewport(boolean updateRestoreState) { // set the viewport settings from WebKit setViewportSettingsFromNative(); - // adjust the default scale to match the density - if (WebView.DEFAULT_SCALE_PERCENT != 100) { - float adjust = (float) WebView.DEFAULT_SCALE_PERCENT / 100.0f; - if (mViewportInitialScale > 0) { - mViewportInitialScale *= adjust; - } - if (mViewportMinimumScale > 0) { - mViewportMinimumScale *= adjust; - } - if (mViewportMaximumScale > 0) { - mViewportMaximumScale *= adjust; + // adjust the default scale to match the densityDpi + float adjust = 1.0f; + if (mViewportDensityDpi == -1) { + if (WebView.DEFAULT_SCALE_PERCENT != 100) { + adjust = WebView.DEFAULT_SCALE_PERCENT / 100.0f; } + } else if (mViewportDensityDpi > 0) { + adjust = (float) mContext.getResources().getDisplayMetrics().densityDpi + / mViewportDensityDpi; + } + int defaultScale = (int) (adjust * 100); + + if (mViewportInitialScale > 0) { + mViewportInitialScale *= adjust; + } + if (mViewportMinimumScale > 0) { + mViewportMinimumScale *= adjust; + } + if (mViewportMaximumScale > 0) { + mViewportMaximumScale *= adjust; } // infer the values if they are not defined. if (mViewportWidth == 0) { if (mViewportInitialScale == 0) { - mViewportInitialScale = WebView.DEFAULT_SCALE_PERCENT; - } - if (mViewportMinimumScale == 0) { - mViewportMinimumScale = WebView.DEFAULT_SCALE_PERCENT; + mViewportInitialScale = defaultScale; } } if (mViewportUserScalable == false) { - mViewportInitialScale = WebView.DEFAULT_SCALE_PERCENT; - mViewportMinimumScale = WebView.DEFAULT_SCALE_PERCENT; - mViewportMaximumScale = WebView.DEFAULT_SCALE_PERCENT; + mViewportInitialScale = defaultScale; + mViewportMinimumScale = defaultScale; + mViewportMaximumScale = defaultScale; } - if (mViewportMinimumScale > mViewportInitialScale) { - if (mViewportInitialScale == 0) { - mViewportInitialScale = mViewportMinimumScale; - } else { - mViewportMinimumScale = mViewportInitialScale; - } + if (mViewportMinimumScale > mViewportInitialScale + && mViewportInitialScale != 0) { + mViewportMinimumScale = mViewportInitialScale; } - if (mViewportMaximumScale > 0) { - if (mViewportMaximumScale < mViewportInitialScale) { - mViewportMaximumScale = mViewportInitialScale; - } else if (mViewportInitialScale == 0) { - mViewportInitialScale = mViewportMaximumScale; - } + if (mViewportMaximumScale > 0 + && mViewportMaximumScale < mViewportInitialScale) { + mViewportMaximumScale = mViewportInitialScale; } - if (mViewportWidth < 0 - && mViewportInitialScale == WebView.DEFAULT_SCALE_PERCENT) { + if (mViewportWidth < 0 && mViewportInitialScale == defaultScale) { mViewportWidth = 0; } - // now notify webview - if (mWebView != null) { - HashMap scaleLimit = new HashMap(); - scaleLimit.put("minScale", mViewportMinimumScale); - scaleLimit.put("maxScale", mViewportMaximumScale); + // if mViewportWidth is 0, it means device-width, always update. + if (mViewportWidth != 0 && !updateRestoreState) return; - if (mRestoredScale > 0) { - Message.obtain(mWebView.mPrivateHandler, - WebView.DID_FIRST_LAYOUT_MSG_ID, mRestoredScale, 0, - scaleLimit).sendToTarget(); - mRestoredScale = 0; + // now notify webview + // webViewWidth refers to the width in the view system + int webViewWidth; + // viewportWidth refers to the width in the document system + int viewportWidth = mCurrentViewWidth; + if (viewportWidth == 0) { + // this may happen when WebView just starts. This is not perfect as + // we call WebView method from WebCore thread. But not perfect + // reference is better than no reference. + webViewWidth = mWebView.getViewWidth(); + viewportWidth = (int) (webViewWidth / adjust); + if (viewportWidth == 0) { + Log.w(LOGTAG, "Can't get the viewWidth after the first layout"); + } + } else { + webViewWidth = Math.round(viewportWidth * mCurrentViewScale); + } + mRestoreState = new RestoreState(); + mRestoreState.mMinScale = mViewportMinimumScale / 100.0f; + mRestoreState.mMaxScale = mViewportMaximumScale / 100.0f; + mRestoreState.mDefaultScale = adjust; + mRestoreState.mScrollX = mRestoredX; + mRestoreState.mScrollY = mRestoredY; + mRestoreState.mMobileSite = (0 == mViewportWidth); + if (mRestoredScale > 0) { + if (mRestoredScreenWidthScale > 0) { + mRestoreState.mTextWrapScale = + mRestoredScreenWidthScale / 100.0f; + // 0 will trigger WebView to turn on zoom overview mode + mRestoreState.mViewScale = 0; } else { - Message.obtain(mWebView.mPrivateHandler, - WebView.DID_FIRST_LAYOUT_MSG_ID, mViewportInitialScale, - mViewportWidth, scaleLimit).sendToTarget(); + mRestoreState.mViewScale = mRestoreState.mTextWrapScale = + mRestoredScale / 100.0f; } + } else { + if (mViewportInitialScale > 0) { + mRestoreState.mViewScale = mRestoreState.mTextWrapScale = + mViewportInitialScale / 100.0f; + } else if (mViewportWidth > 0 && mViewportWidth < webViewWidth) { + mRestoreState.mViewScale = mRestoreState.mTextWrapScale = + (float) webViewWidth / mViewportWidth; + } else { + mRestoreState.mTextWrapScale = adjust; + // 0 will trigger WebView to turn on zoom overview mode + mRestoreState.mViewScale = 0; + } + } - // if no restored offset, move the new page to (0, 0) - Message.obtain(mWebView.mPrivateHandler, WebView.SCROLL_TO_MSG_ID, - mRestoredX, mRestoredY).sendToTarget(); - mRestoredX = mRestoredY = 0; - - // force an early draw for quick feedback after the first layout - if (mCurrentViewWidth != 0) { - synchronized (this) { - if (mDrawIsScheduled) { - mEventHub.removeMessages(EventHub.WEBKIT_DRAW); - } - mDrawIsScheduled = true; - mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null, - EventHub.WEBKIT_DRAW)); - } + if (mWebView.mHeightCanMeasure) { + // Trick to ensure that the Picture has the exact height for the + // content by forcing to layout with 0 height after the page is + // ready, which is indicated by didFirstLayout. This is essential to + // get rid of the white space in the GMail which uses WebView for + // message view. + mWebView.mLastHeightSent = 0; + // Send a negative scale to indicate that WebCore should reuse + // the current scale + WebView.ViewSizeData data = new WebView.ViewSizeData(); + data.mWidth = mWebView.mLastWidthSent; + data.mHeight = 0; + // if mHeightCanMeasure is true, getUseWideViewPort() can't be + // true. It is safe to use mWidth for mTextWrapWidth. + data.mTextWrapWidth = data.mWidth; + data.mScale = -1.0f; + data.mIgnoreHeight = false; + // send VIEW_SIZE_CHANGED to the front of the queue so that we can + // avoid pushing the wrong picture to the WebView side. If there is + // a VIEW_SIZE_CHANGED in the queue, probably from WebView side, + // ignore it as we have a new size. If we leave VIEW_SIZE_CHANGED + // in the queue, as mLastHeightSent has been updated here, we may + // miss the requestLayout in WebView side after the new picture. + mEventHub.removeMessages(EventHub.VIEW_SIZE_CHANGED); + mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null, + EventHub.VIEW_SIZE_CHANGED, data)); + } else if (mSettings.getUseWideViewPort()) { + if (viewportWidth == 0) { + // Trick to ensure VIEW_SIZE_CHANGED will be sent from WebView + // to WebViewCore + mWebView.mLastWidthSent = 0; + } else { + WebView.ViewSizeData data = new WebView.ViewSizeData(); + // mViewScale as 0 means it is in zoom overview mode. So we don't + // know the exact scale. If mRestoredScale is non-zero, use it; + // otherwise just use mTextWrapScale as the initial scale. + data.mScale = mRestoreState.mViewScale == 0 + ? (mRestoredScale > 0 ? mRestoredScale + : mRestoreState.mTextWrapScale) + : mRestoreState.mViewScale; + data.mWidth = Math.round(webViewWidth / data.mScale); + data.mHeight = mCurrentViewHeight * data.mWidth / viewportWidth; + data.mTextWrapWidth = Math.round(webViewWidth + / mRestoreState.mTextWrapScale); + data.mIgnoreHeight = false; + // send VIEW_SIZE_CHANGED to the front of the queue so that we + // can avoid pushing the wrong picture to the WebView side. + mEventHub.removeMessages(EventHub.VIEW_SIZE_CHANGED); + mEventHub.sendMessageAtFrontOfQueue(Message.obtain(null, + EventHub.VIEW_SIZE_CHANGED, data)); } } } @@ -1651,6 +2038,17 @@ final class WebViewCore { } // called by JNI + private void restoreScreenWidthScale(int scale) { + if (!mSettings.getUseWideViewPort()) { + return; + } + + if (mBrowserFrame.firstLayoutDone() == false) { + mRestoredScreenWidthScale = scale; + } + } + + // called by JNI private void needTouchEvents(boolean need) { if (mWebView != null) { Message.obtain(mWebView.mPrivateHandler, @@ -1664,15 +2062,39 @@ final class WebViewCore { String text, int textGeneration) { if (mWebView != null) { Message msg = Message.obtain(mWebView.mPrivateHandler, - WebView.UPDATE_TEXTFIELD_TEXT_MSG_ID, ptr, + WebView.UPDATE_TEXTFIELD_TEXT_MSG_ID, ptr, textGeneration, text); msg.getData().putBoolean("password", changeToPassword); msg.sendToTarget(); } } + // called by JNI + private void updateTextSelection(int pointer, int start, int end, + int textGeneration) { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.UPDATE_TEXT_SELECTION_MSG_ID, pointer, textGeneration, + new TextSelectionData(start, end)).sendToTarget(); + } + } + + // called by JNI + private void clearTextEntry() { + if (mWebView == null) return; + Message.obtain(mWebView.mPrivateHandler, + WebView.CLEAR_TEXT_ENTRY).sendToTarget(); + } + + private native void nativeUpdateFrameCacheIfLoading(); + + /** + * Scroll the focused textfield to (xPercent, y) in document space + */ + private native void nativeScrollFocusedTextInput(float xPercent, int y); + // these must be in document space (i.e. not scaled/zoomed). - private native void nativeSetScrollOffset(int dx, int dy); + private native void nativeSetScrollOffset(int gen, int dx, int dy); private native void nativeSetGlobalBounds(int x, int y, int w, int h); @@ -1690,6 +2112,72 @@ final class WebViewCore { if (mWebView != null) { mWebView.requestListBox(array, enabledArray, selection); } - + + } + + // called by JNI + private void requestKeyboard(boolean showKeyboard) { + if (mWebView != null) { + Message.obtain(mWebView.mPrivateHandler, + WebView.REQUEST_KEYBOARD, showKeyboard ? 1 : 0, 0) + .sendToTarget(); + } + } + + // called by JNI. PluginWidget function to launch an activity and overlays + // the activity with the View provided by the plugin class. + private void startFullScreenPluginActivity(String libName, String clsName, int npp) { + if (mWebView == null) { + return; + } + + String pkgName = PluginManager.getInstance(null).getPluginsAPKName(libName); + if (pkgName == null) { + Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); + return; + } + + Intent intent = new Intent("android.intent.webkit.PLUGIN"); + intent.putExtra(PluginActivity.INTENT_EXTRA_PACKAGE_NAME, pkgName); + intent.putExtra(PluginActivity.INTENT_EXTRA_CLASS_NAME, clsName); + intent.putExtra(PluginActivity.INTENT_EXTRA_NPP_INSTANCE, npp); + mWebView.getContext().startActivity(intent); + } + + // called by JNI. PluginWidget functions for creating an embedded View for + // the surface drawing model. + private ViewManager.ChildView createSurface(String libName, String clsName, + int npp, int x, int y, int width, int height) { + if (mWebView == null) { + return null; + } + + String pkgName = PluginManager.getInstance(null).getPluginsAPKName(libName); + if (pkgName == null) { + Log.w(LOGTAG, "Unable to resolve " + libName + " to a plugin APK"); + return null; + } + + PluginStub stub =PluginUtil.getPluginStub(mWebView.getContext(),pkgName, clsName); + if (stub == null) { + Log.e(LOGTAG, "Unable to find plugin class (" + clsName + + ") in the apk (" + pkgName + ")"); + return null; + } + + View pluginView = stub.getEmbeddedView(npp, mWebView.getContext()); + + ViewManager.ChildView view = mWebView.mViewManager.createView(); + view.mView = pluginView; + view.attachView(x, y, width, height); + return view; + } + + private void destroySurface(ViewManager.ChildView childView) { + childView.removeView(); } + + private native void nativePause(); + private native void nativeResume(); + private native void nativeFreeMemory(); } diff --git a/core/java/android/webkit/WebViewDatabase.java b/core/java/android/webkit/WebViewDatabase.java index 1004e30..6e10811 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 = 9; + private static final int DATABASE_VERSION = 10; // 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 @@ -48,7 +48,10 @@ public class WebViewDatabase { // 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; + // 9 -> 10 Update httpauth table UNIQUE + private static final int CACHE_DATABASE_VERSION = 3; + // 1 -> 2 Add expires String + // 2 -> 3 Add content-disposition private static WebViewDatabase mInstance = null; @@ -107,6 +110,8 @@ public class WebViewDatabase { private static final String CACHE_EXPIRES_COL = "expires"; + private static final String CACHE_EXPIRES_STRING_COL = "expiresstring"; + private static final String CACHE_MIMETYPE_COL = "mimetype"; private static final String CACHE_ENCODING_COL = "encoding"; @@ -117,6 +122,8 @@ public class WebViewDatabase { private static final String CACHE_CONTENTLENGTH_COL = "contentlength"; + private static final String CACHE_CONTENTDISPOSITION_COL = "contentdisposition"; + // column id strings for "password" table private static final String PASSWORD_HOST_COL = "host"; @@ -150,11 +157,13 @@ public class WebViewDatabase { private static int mCacheLastModifyColIndex; private static int mCacheETagColIndex; private static int mCacheExpiresColIndex; + private static int mCacheExpiresStringColIndex; private static int mCacheMimeTypeColIndex; private static int mCacheEncodingColIndex; private static int mCacheHttpStatusColIndex; private static int mCacheLocationColIndex; private static int mCacheContentLengthColIndex; + private static int mCacheContentDispositionColIndex; private static int mCacheTransactionRefcount; @@ -220,6 +229,8 @@ public class WebViewDatabase { .getColumnIndex(CACHE_ETAG_COL); mCacheExpiresColIndex = mCacheInserter .getColumnIndex(CACHE_EXPIRES_COL); + mCacheExpiresStringColIndex = mCacheInserter + .getColumnIndex(CACHE_EXPIRES_STRING_COL); mCacheMimeTypeColIndex = mCacheInserter .getColumnIndex(CACHE_MIMETYPE_COL); mCacheEncodingColIndex = mCacheInserter @@ -230,6 +241,8 @@ public class WebViewDatabase { .getColumnIndex(CACHE_LOCATION_COL); mCacheContentLengthColIndex = mCacheInserter .getColumnIndex(CACHE_CONTENTLENGTH_COL); + mCacheContentDispositionColIndex = mCacheInserter + .getColumnIndex(CACHE_CONTENTDISPOSITION_COL); } } @@ -244,6 +257,20 @@ public class WebViewDatabase { + DATABASE_VERSION + ", which will destroy old data"); } boolean justPasswords = 8 == oldVersion && 9 == DATABASE_VERSION; + boolean justAuth = 9 == oldVersion && 10 == DATABASE_VERSION; + if (justAuth) { + mDatabase.execSQL("DROP TABLE IF EXISTS " + + mTableNames[TABLE_HTTPAUTH_ID]); + mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_HTTPAUTH_ID] + + " (" + ID_COL + " INTEGER PRIMARY KEY, " + + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL + + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " + + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" + + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL + + ") ON CONFLICT REPLACE);"); + return; + } + if (!justPasswords) { mDatabase.execSQL("DROP TABLE IF EXISTS " + mTableNames[TABLE_COOKIES_ID]); @@ -290,8 +317,8 @@ public class WebViewDatabase { + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, " + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE (" - + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL + ", " - + HTTPAUTH_USERNAME_COL + ") ON CONFLICT REPLACE);"); + + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL + + ") ON CONFLICT REPLACE);"); } // passwords mDatabase.execSQL("CREATE TABLE " + mTableNames[TABLE_PASSWORD_ID] @@ -320,11 +347,12 @@ public class WebViewDatabase { + " TEXT, " + CACHE_FILE_PATH_COL + " TEXT, " + CACHE_LAST_MODIFY_COL + " TEXT, " + CACHE_ETAG_COL + " TEXT, " + CACHE_EXPIRES_COL + " INTEGER, " + + CACHE_EXPIRES_STRING_COL + " TEXT, " + CACHE_MIMETYPE_COL + " TEXT, " + CACHE_ENCODING_COL + " TEXT," + CACHE_HTTP_STATUS_COL + " INTEGER, " + CACHE_LOCATION_COL + " TEXT, " + CACHE_CONTENTLENGTH_COL - + " INTEGER, " + " UNIQUE (" + CACHE_URL_COL - + ") ON CONFLICT REPLACE);"); + + " INTEGER, " + CACHE_CONTENTDISPOSITION_COL + " TEXT, " + + " UNIQUE (" + CACHE_URL_COL + ") ON CONFLICT REPLACE);"); mCacheDatabase.execSQL("CREATE INDEX cacheUrlIndex ON cache (" + CACHE_URL_COL + ")"); } @@ -537,8 +565,8 @@ public class WebViewDatabase { } Cursor cursor = mCacheDatabase.rawQuery("SELECT filepath, lastmodify, etag, expires, " - + "mimetype, encoding, httpstatus, location, contentlength " - + "FROM cache WHERE url = ?", + + "expiresstring, mimetype, encoding, httpstatus, location, contentlength, " + + "contentdisposition FROM cache WHERE url = ?", new String[] { url }); try { @@ -548,11 +576,13 @@ public class WebViewDatabase { ret.lastModified = cursor.getString(1); ret.etag = cursor.getString(2); ret.expires = cursor.getLong(3); - ret.mimeType = cursor.getString(4); - ret.encoding = cursor.getString(5); - ret.httpStatusCode = cursor.getInt(6); - ret.location = cursor.getString(7); - ret.contentLength = cursor.getLong(8); + ret.expiresString = cursor.getString(4); + ret.mimeType = cursor.getString(5); + ret.encoding = cursor.getString(6); + ret.httpStatusCode = cursor.getInt(7); + ret.location = cursor.getString(8); + ret.contentLength = cursor.getLong(9); + ret.contentdisposition = cursor.getString(10); return ret; } } finally { @@ -591,11 +621,14 @@ public class WebViewDatabase { mCacheInserter.bind(mCacheLastModifyColIndex, c.lastModified); mCacheInserter.bind(mCacheETagColIndex, c.etag); mCacheInserter.bind(mCacheExpiresColIndex, c.expires); + mCacheInserter.bind(mCacheExpiresStringColIndex, c.expiresString); mCacheInserter.bind(mCacheMimeTypeColIndex, c.mimeType); mCacheInserter.bind(mCacheEncodingColIndex, c.encoding); mCacheInserter.bind(mCacheHttpStatusColIndex, c.httpStatusCode); mCacheInserter.bind(mCacheLocationColIndex, c.location); mCacheInserter.bind(mCacheContentLengthColIndex, c.contentLength); + mCacheInserter.bind(mCacheContentDispositionColIndex, + c.contentdisposition); mCacheInserter.execute(); } diff --git a/core/java/android/webkit/gears/AndroidGpsLocationProvider.java b/core/java/android/webkit/gears/AndroidGpsLocationProvider.java deleted file mode 100644 index 3646042..0000000 --- a/core/java/android/webkit/gears/AndroidGpsLocationProvider.java +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2008, The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.content.Context; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.location.LocationProvider; -import android.os.Bundle; -import android.util.Log; -import android.webkit.WebView; - -/** - * GPS provider implementation for Android. - */ -public final class AndroidGpsLocationProvider implements LocationListener { - /** - * Logging tag - */ - private static final String TAG = "Gears-J-GpsProvider"; - /** - * Our location manager instance. - */ - private LocationManager locationManager; - /** - * The native object ID. - */ - private long nativeObject; - - public AndroidGpsLocationProvider(WebView webview, long object) { - nativeObject = object; - locationManager = (LocationManager) webview.getContext().getSystemService( - Context.LOCATION_SERVICE); - if (locationManager == null) { - Log.e(TAG, - "AndroidGpsLocationProvider: could not get location manager."); - throw new NullPointerException( - "AndroidGpsLocationProvider: locationManager is null."); - } - // Register for location updates. - try { - locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, - this); - } catch (IllegalArgumentException ex) { - Log.e(TAG, - "AndroidLocationGpsProvider: could not register for updates: " + ex); - throw ex; - } catch (SecurityException ex) { - Log.e(TAG, - "AndroidGpsLocationProvider: not allowed to register for update: " - + ex); - throw ex; - } - } - - /** - * Called when the provider is no longer needed. - */ - public void shutdown() { - locationManager.removeUpdates(this); - Log.i(TAG, "GPS provider closed."); - } - - /** - * Called when the location has changed. - * @param location The new location, as a Location object. - */ - public void onLocationChanged(Location location) { - Log.i(TAG, "Location changed: " + location); - nativeLocationChanged(location, nativeObject); - } - - /** - * Called when the provider status changes. - * - * @param provider the name of the location provider associated with this - * update. - * @param status {@link LocationProvider#OUT_OF_SERVICE} if the - * provider is out of service, and this is not expected to change in the - * near future; {@link LocationProvider#TEMPORARILY_UNAVAILABLE} if - * the provider is temporarily unavailable but is expected to be available - * shortly; and {@link LocationProvider#AVAILABLE} if the - * provider is currently available. - * @param extras an optional Bundle which will contain provider specific - * status variables (such as number of satellites). - */ - public void onStatusChanged(String provider, int status, Bundle extras) { - Log.i(TAG, "Provider " + provider + " status changed to " + status); - if (status == LocationProvider.OUT_OF_SERVICE || - status == LocationProvider.TEMPORARILY_UNAVAILABLE) { - nativeProviderError(false, nativeObject); - } - } - - /** - * Called when the provider is enabled. - * - * @param provider the name of the location provider that is now enabled. - */ - public void onProviderEnabled(String provider) { - Log.i(TAG, "Provider " + provider + " enabled."); - // No need to notify the native side. It's enough to start sending - // valid position fixes again. - } - - /** - * Called when the provider is disabled. - * - * @param provider the name of the location provider that is now disabled. - */ - public void onProviderDisabled(String provider) { - Log.i(TAG, "Provider " + provider + " disabled."); - nativeProviderError(true, nativeObject); - } - - /** - * The native method called when a new location is available. - * @param location is the new Location instance to pass to the native side. - * @param nativeObject is a pointer to the corresponding - * AndroidGpsLocationProvider C++ instance. - */ - private native void nativeLocationChanged(Location location, long object); - - /** - * The native method called when there is a GPS provder error. - * @param isDisabled is true when the error signifies the fact that the GPS - * HW is disabled. For other errors, this param is always false. - * @param nativeObject is a pointer to the corresponding - * AndroidGpsLocationProvider C++ instance. - */ - private native void nativeProviderError(boolean isDisabled, long object); -} diff --git a/core/java/android/webkit/gears/AndroidRadioDataProvider.java b/core/java/android/webkit/gears/AndroidRadioDataProvider.java deleted file mode 100644 index 1384042..0000000 --- a/core/java/android/webkit/gears/AndroidRadioDataProvider.java +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2008, The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.content.Context; -import android.telephony.CellLocation; -import android.telephony.ServiceState; -import android.telephony.SignalStrength; -import android.telephony.gsm.GsmCellLocation; -import android.telephony.PhoneStateListener; -import android.telephony.TelephonyManager; -import android.util.Log; -import android.webkit.WebView; - -/** - * Radio data provider implementation for Android. - */ -public final class AndroidRadioDataProvider extends PhoneStateListener { - - /** Logging tag */ - private static final String TAG = "Gears-J-RadioProvider"; - - /** Network types */ - private static final int RADIO_TYPE_UNKNOWN = 0; - private static final int RADIO_TYPE_GSM = 1; - private static final int RADIO_TYPE_WCDMA = 2; - private static final int RADIO_TYPE_CDMA = 3; - private static final int RADIO_TYPE_EVDO = 4; - private static final int RADIO_TYPE_1xRTT = 5; - - /** Simple container for radio data */ - public static final class RadioData { - public int cellId = -1; - public int locationAreaCode = -1; - // TODO: use new SignalStrength instead of asu - public int signalStrength = -1; - public int mobileCountryCode = -1; - public int mobileNetworkCode = -1; - public int homeMobileCountryCode = -1; - public int homeMobileNetworkCode = -1; - public int radioType = RADIO_TYPE_UNKNOWN; - public String carrierName; - - /** - * Constructs radioData object from the given telephony data. - * @param telephonyManager contains the TelephonyManager instance. - * @param cellLocation contains information about the current GSM cell. - * @param signalStrength is the strength of the network signal. - * @param serviceState contains information about the network service. - * @return a new RadioData object populated with the currently - * available network information or null if there isn't - * enough information. - */ - public static RadioData getInstance(TelephonyManager telephonyManager, - CellLocation cellLocation, int signalStrength, - ServiceState serviceState) { - - if (!(cellLocation instanceof GsmCellLocation)) { - // This also covers the case when cellLocation is null. - // When that happens, we do not bother creating a - // RadioData instance. - return null; - } - - RadioData radioData = new RadioData(); - GsmCellLocation gsmCellLocation = (GsmCellLocation) cellLocation; - - // Extract the cell id, LAC, and signal strength. - radioData.cellId = gsmCellLocation.getCid(); - radioData.locationAreaCode = gsmCellLocation.getLac(); - radioData.signalStrength = signalStrength; - - // Extract the home MCC and home MNC. - String operator = telephonyManager.getSimOperator(); - radioData.setMobileCodes(operator, true); - - if (serviceState != null) { - // Extract the carrier name. - radioData.carrierName = serviceState.getOperatorAlphaLong(); - - // Extract the MCC and MNC. - operator = serviceState.getOperatorNumeric(); - radioData.setMobileCodes(operator, false); - } - - // Finally get the radio type. - //TODO We have to edit the parameter for getNetworkType regarding CDMA - int type = telephonyManager.getNetworkType(); - if (type == TelephonyManager.NETWORK_TYPE_UMTS) { - radioData.radioType = RADIO_TYPE_WCDMA; - } else if (type == TelephonyManager.NETWORK_TYPE_GPRS - || type == TelephonyManager.NETWORK_TYPE_EDGE) { - radioData.radioType = RADIO_TYPE_GSM; - } else if (type == TelephonyManager.NETWORK_TYPE_CDMA) { - radioData.radioType = RADIO_TYPE_CDMA; - } else if (type == TelephonyManager.NETWORK_TYPE_EVDO_0) { - radioData.radioType = RADIO_TYPE_EVDO; - } else if (type == TelephonyManager.NETWORK_TYPE_EVDO_A) { - radioData.radioType = RADIO_TYPE_EVDO; - } else if (type == TelephonyManager.NETWORK_TYPE_1xRTT) { - radioData.radioType = RADIO_TYPE_1xRTT; - } - - // Print out what we got. - Log.i(TAG, "Got the following data:"); - Log.i(TAG, "CellId: " + radioData.cellId); - Log.i(TAG, "LAC: " + radioData.locationAreaCode); - Log.i(TAG, "MNC: " + radioData.mobileNetworkCode); - Log.i(TAG, "MCC: " + radioData.mobileCountryCode); - Log.i(TAG, "home MNC: " + radioData.homeMobileNetworkCode); - Log.i(TAG, "home MCC: " + radioData.homeMobileCountryCode); - Log.i(TAG, "Signal strength: " + radioData.signalStrength); - Log.i(TAG, "Carrier: " + radioData.carrierName); - Log.i(TAG, "Network type: " + radioData.radioType); - - return radioData; - } - - private RadioData() {} - - /** - * Parses a string containing a mobile country code and a mobile - * network code and sets the corresponding member variables. - * @param codes is the string to parse. - * @param homeValues flags whether the codes are for the home operator. - */ - private void setMobileCodes(String codes, boolean homeValues) { - if (codes != null) { - try { - // The operator numeric format is 3 digit country code plus 2 or - // 3 digit network code. - int mcc = Integer.parseInt(codes.substring(0, 3)); - int mnc = Integer.parseInt(codes.substring(3)); - if (homeValues) { - homeMobileCountryCode = mcc; - homeMobileNetworkCode = mnc; - } else { - mobileCountryCode = mcc; - mobileNetworkCode = mnc; - } - } catch (IndexOutOfBoundsException ex) { - Log.e( - TAG, - "AndroidRadioDataProvider: Invalid operator numeric data: " + ex); - } catch (NumberFormatException ex) { - Log.e( - TAG, - "AndroidRadioDataProvider: Operator numeric format error: " + ex); - } - } - } - }; - - /** The native object ID */ - private long nativeObject; - - /** The last known cellLocation */ - private CellLocation cellLocation = null; - - /** The last known signal strength */ - // TODO: use new SignalStrength instead of asu - private int signalStrength = -1; - - /** The last known serviceState */ - private ServiceState serviceState = null; - - /** - * Our TelephonyManager instance. - */ - private TelephonyManager telephonyManager; - - /** - * Public constructor. Uses the webview to get the Context object. - */ - public AndroidRadioDataProvider(WebView webview, long object) { - super(); - nativeObject = object; - telephonyManager = (TelephonyManager) webview.getContext().getSystemService( - Context.TELEPHONY_SERVICE); - if (telephonyManager == null) { - Log.e(TAG, - "AndroidRadioDataProvider: could not get tepephony manager."); - throw new NullPointerException( - "AndroidRadioDataProvider: telephonyManager is null."); - } - - // Register for cell id, signal strength and service state changed - // notifications. - telephonyManager.listen(this, PhoneStateListener.LISTEN_CELL_LOCATION - | PhoneStateListener.LISTEN_SIGNAL_STRENGTHS - | PhoneStateListener.LISTEN_SERVICE_STATE); - } - - /** - * Should be called when the provider is no longer needed. - */ - public void shutdown() { - telephonyManager.listen(this, PhoneStateListener.LISTEN_NONE); - Log.i(TAG, "AndroidRadioDataProvider shutdown."); - } - - @Override - public void onServiceStateChanged(ServiceState state) { - serviceState = state; - notifyListeners(); - } - - @Override - public void onSignalStrengthsChanged(SignalStrength ss) { - int gsmSignalStrength = ss.getGsmSignalStrength(); - signalStrength = (gsmSignalStrength == 99 ? -1 : gsmSignalStrength); - notifyListeners(); - } - - @Override - public void onCellLocationChanged(CellLocation location) { - cellLocation = location; - notifyListeners(); - } - - private void notifyListeners() { - RadioData radioData = RadioData.getInstance(telephonyManager, cellLocation, - signalStrength, serviceState); - if (radioData != null) { - onUpdateAvailable(radioData, nativeObject); - } - } - - /** - * The native method called when new radio data is available. - * @param radioData is the RadioData instance to pass to the native side. - * @param nativeObject is a pointer to the corresponding - * AndroidRadioDataProvider C++ instance. - */ - private static native void onUpdateAvailable( - RadioData radioData, long nativeObject); -} diff --git a/core/java/android/webkit/gears/AndroidWifiDataProvider.java b/core/java/android/webkit/gears/AndroidWifiDataProvider.java deleted file mode 100644 index d2850b0..0000000 --- a/core/java/android/webkit/gears/AndroidWifiDataProvider.java +++ /dev/null @@ -1,140 +0,0 @@ -// 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.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"; - /** - * Flag for guarding Log.v() calls. - * Set to true to enable extra debug logging. - */ - private static final boolean LOGV_ENABLED = false; - /** - * 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 (LOGV_ENABLED) { - 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 (LOGV_ENABLED) { - Log.v(TAG, "Wifi scan resulst available"); - } - onUpdateAvailable(mWifiManager.getScanResults(), mNativeObject); - } - } - - /** - * The native method called when new wifi data is available. - * @param scanResults is a list of ScanResults to pass to the native side. - * @param nativeObject is a pointer to the corresponding - * AndroidWifiDataProvider C++ instance. - */ - private static native void onUpdateAvailable( - List<ScanResult> scanResults, long nativeObject); -} diff --git a/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java b/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java deleted file mode 100644 index 74d27ed..0000000 --- a/core/java/android/webkit/gears/ApacheHttpRequestAndroid.java +++ /dev/null @@ -1,1134 +0,0 @@ -// Copyright 2008, The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.net.http.Headers; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -import android.util.Log; -import android.webkit.CacheManager; -import android.webkit.CacheManager.CacheResult; -import android.webkit.CookieManager; - -import java.io.InputStream; -import java.io.OutputStream; -import java.io.IOException; -import java.lang.StringBuilder; -import java.util.Date; -import java.util.Map; -import java.util.HashMap; -import java.util.Iterator; - -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.params.HttpParams; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpProtocolParams; -import org.apache.http.HttpResponse; -import org.apache.http.entity.AbstractHttpEntity; -import org.apache.http.client.*; -import org.apache.http.client.methods.*; -import org.apache.http.impl.client.AbstractHttpClient; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; -import org.apache.http.conn.ssl.StrictHostnameVerifier; -import org.apache.http.impl.cookie.DateUtils; -import org.apache.http.util.CharArrayBuffer; - -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -/** - * Performs the underlying HTTP/HTTPS GET, POST, HEAD, PUT, DELETE requests. - * <p> These are performed synchronously (blocking). The caller should - * ensure that it is in a background thread if asynchronous behavior - * is required. All data is pushed, so there is no need for JNI native - * callbacks. - * <p> This uses Apache's HttpClient framework to perform most - * of the underlying network activity. The Android brower's cache, - * android.webkit.CacheManager, is also used when caching is enabled, - * and updated with new data. The android.webkit.CookieManager is also - * queried and updated as necessary. - * <p> The public interface is designed to be called by native code - * through JNI, and to simplify coding none of the public methods will - * surface a checked exception. Unchecked exceptions may still be - * raised but only if the system is in an ill state, such as out of - * memory. - * <p> TODO: This isn't plumbed into LocalServer yet. Mutually - * dependent on LocalServer - will attach the two together once both - * are submitted. - */ -public final class ApacheHttpRequestAndroid { - /** Debug logging tag. */ - private static final String LOG_TAG = "Gears-J"; - /** Flag for guarding Log.v() calls. */ - private static final boolean LOGV_ENABLED = false; - /** HTTP response header line endings are CR-LF style. */ - private static final String HTTP_LINE_ENDING = "\r\n"; - /** Safe MIME type to use whenever it isn't specified. */ - private static final String DEFAULT_MIME_TYPE = "text/plain"; - /** Case-sensitive header keys */ - public static final String KEY_CONTENT_LENGTH = "Content-Length"; - public static final String KEY_EXPIRES = "Expires"; - public static final String KEY_LAST_MODIFIED = "Last-Modified"; - public static final String KEY_ETAG = "ETag"; - public static final String KEY_LOCATION = "Location"; - public static final String KEY_CONTENT_TYPE = "Content-Type"; - /** Number of bytes to send and receive on the HTTP connection in - * one go. */ - private static final int BUFFER_SIZE = 4096; - - /** The first element of the String[] value in a headers map is the - * unmodified (case-sensitive) key. */ - public static final int HEADERS_MAP_INDEX_KEY = 0; - /** The second element of the String[] value in a headers map is the - * associated value. */ - public static final int HEADERS_MAP_INDEX_VALUE = 1; - - /** Request headers, as key -> value map. */ - // TODO: replace this design by a simpler one (the C++ side has to - // be modified too), where we do not store both the original header - // and the lowercase one. - private Map<String, String[]> mRequestHeaders = - new HashMap<String, String[]>(); - /** Response headers, as a lowercase key -> value map. */ - private Map<String, String[]> mResponseHeaders = - new HashMap<String, String[]>(); - /** The URL used for createCacheResult() */ - private String mCacheResultUrl; - /** CacheResult being saved into, if inserting a new cache entry. */ - private CacheResult mCacheResult; - /** Initialized by initChildThread(). Used to target abort(). */ - private Thread mBridgeThread; - - /** Our HttpClient */ - private AbstractHttpClient mClient; - /** The HttpMethod associated with this request */ - private HttpRequestBase mMethod; - /** The complete response line e.g "HTTP/1.0 200 OK" */ - private String mResponseLine; - /** HTTP body stream, setup after connection. */ - private InputStream mBodyInputStream; - - /** HTTP Response Entity */ - private HttpResponse mResponse; - - /** Post Entity, used to stream the request to the server */ - private StreamEntity mPostEntity = null; - /** Content lenght, mandatory when using POST */ - private long mContentLength; - - /** The request executes in a parallel thread */ - private Thread mHttpThread = null; - /** protect mHttpThread, if interrupt() is called concurrently */ - private Lock mHttpThreadLock = new ReentrantLock(); - /** Flag set to true when the request thread is joined */ - private boolean mConnectionFinished = false; - /** Flag set to true by interrupt() and/or connection errors */ - private boolean mConnectionFailed = false; - /** Lock protecting the access to mConnectionFailed */ - private Lock mConnectionFailedLock = new ReentrantLock(); - - /** Lock on the loop in StreamEntity */ - private Lock mStreamingReadyLock = new ReentrantLock(); - /** Condition variable used to signal the loop is ready... */ - private Condition mStreamingReady = mStreamingReadyLock.newCondition(); - - /** Used to pass around the block of data POSTed */ - private Buffer mBuffer = new Buffer(); - /** Used to signal that the block of data has been written */ - private SignalConsumed mSignal = new SignalConsumed(); - - // inner classes - - /** - * Implements the http request - */ - class Connection implements Runnable { - public void run() { - boolean problem = false; - try { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "REQUEST : " + mMethod.getRequestLine()); - } - mResponse = mClient.execute(mMethod); - if (mResponse != null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "response (status line): " - + mResponse.getStatusLine()); - } - mResponseLine = "" + mResponse.getStatusLine(); - } else { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "problem, response == null"); - } - problem = true; - } - } catch (IOException e) { - Log.e(LOG_TAG, "Connection IO exception ", e); - problem = true; - } catch (RuntimeException e) { - Log.e(LOG_TAG, "Connection runtime exception ", e); - problem = true; - } - - if (!problem) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Request complete (" - + mMethod.getRequestLine() + ")"); - } - } else { - mConnectionFailedLock.lock(); - mConnectionFailed = true; - mConnectionFailedLock.unlock(); - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Request FAILED (" - + mMethod.getRequestLine() + ")"); - } - // We abort the execution in order to shutdown and release - // the underlying connection - mMethod.abort(); - if (mPostEntity != null) { - // If there is a post entity, we need to wake it up from - // a potential deadlock - mPostEntity.signalOutputStream(); - } - } - } - } - - /** - * simple buffer class implementing a producer/consumer model - */ - class Buffer { - private DataPacket mPacket; - private boolean mEmpty = true; - public synchronized void put(DataPacket packet) { - while (!mEmpty) { - try { - wait(); - } catch (InterruptedException e) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "InterruptedException while putting " + - "a DataPacket in the Buffer: " + e); - } - } - } - mPacket = packet; - mEmpty = false; - notify(); - } - public synchronized DataPacket get() { - while (mEmpty) { - try { - wait(); - } catch (InterruptedException e) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "InterruptedException while getting " + - "a DataPacket in the Buffer: " + e); - } - } - } - mEmpty = true; - notify(); - return mPacket; - } - } - - /** - * utility class used to block until the packet is signaled as being - * consumed - */ - class SignalConsumed { - private boolean mConsumed = false; - public synchronized void waitUntilPacketConsumed() { - while (!mConsumed) { - try { - wait(); - } catch (InterruptedException e) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "InterruptedException while waiting " + - "until a DataPacket is consumed: " + e); - } - } - } - mConsumed = false; - notify(); - } - public synchronized void packetConsumed() { - while (mConsumed) { - try { - wait(); - } catch (InterruptedException e) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "InterruptedException while indicating " - + "that the DataPacket has been consumed: " + e); - } - } - } - mConsumed = true; - notify(); - } - } - - /** - * Utility class encapsulating a packet of data - */ - class DataPacket { - private byte[] mContent; - private int mLength; - public DataPacket(byte[] content, int length) { - mContent = content; - mLength = length; - } - public byte[] getBytes() { - return mContent; - } - public int getLength() { - return mLength; - } - } - - /** - * HttpEntity class to write the bytes received by the C++ thread - * on the connection outputstream, in a streaming way. - * This entity is executed in the request thread. - * The writeTo() method is automatically called by the - * HttpPost execution; upon reception, we loop while receiving - * the data packets from the main thread, until completion - * or error. When done, we flush the outputstream. - * The main thread (sendPostData()) also blocks until the - * outputstream is made available (or an error happens) - */ - class StreamEntity implements HttpEntity { - private OutputStream mOutputStream; - - // HttpEntity interface methods - - public boolean isRepeatable() { - return false; - } - - public boolean isChunked() { - return false; - } - - public long getContentLength() { - return mContentLength; - } - - public Header getContentType() { - return null; - } - - public Header getContentEncoding() { - return null; - } - - public InputStream getContent() throws IOException { - return null; - } - - public void writeTo(final OutputStream out) throws IOException { - // We signal that the outputstream is available - mStreamingReadyLock.lock(); - mOutputStream = out; - mStreamingReady.signal(); - mStreamingReadyLock.unlock(); - - // We then loop waiting on messages to process. - boolean finished = false; - while (!finished) { - DataPacket packet = mBuffer.get(); - if (packet == null) { - finished = true; - } else { - write(packet); - } - mSignal.packetConsumed(); - mConnectionFailedLock.lock(); - if (mConnectionFailed) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "stopping loop on error"); - } - finished = true; - } - mConnectionFailedLock.unlock(); - } - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "flushing the outputstream..."); - } - mOutputStream.flush(); - } - - public boolean isStreaming() { - return true; - } - - public void consumeContent() throws IOException { - // Nothing to release - } - - // local methods - - private void write(DataPacket packet) { - try { - if (mOutputStream == null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "NO OUTPUT STREAM !!!"); - } - return; - } - mOutputStream.write(packet.getBytes(), 0, packet.getLength()); - mOutputStream.flush(); - } catch (IOException e) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "exc: " + e); - } - mConnectionFailedLock.lock(); - mConnectionFailed = true; - mConnectionFailedLock.unlock(); - } - } - - public boolean isReady() { - mStreamingReadyLock.lock(); - try { - if (mOutputStream == null) { - mStreamingReady.await(); - } - } catch (InterruptedException e) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "InterruptedException in " - + "StreamEntity::isReady() : ", e); - } - } finally { - mStreamingReadyLock.unlock(); - } - if (mOutputStream == null) { - return false; - } - return true; - } - - public void signalOutputStream() { - mStreamingReadyLock.lock(); - mStreamingReady.signal(); - mStreamingReadyLock.unlock(); - } - } - - /** - * Initialize mBridgeThread using the TLS value of - * Thread.currentThread(). Called on start up of the native child - * thread. - */ - public synchronized void initChildThread() { - mBridgeThread = Thread.currentThread(); - } - - public void setContentLength(long length) { - mContentLength = length; - } - - /** - * Analagous to the native-side HttpRequest::open() function. This - * initializes an underlying HttpClient method, but does - * not go to the wire. On success, this enables a call to send() to - * initiate the transaction. - * - * @param method The HTTP method, e.g GET or POST. - * @param url The URL to open. - * @return True on success with a complete HTTP response. - * False on failure. - */ - public synchronized boolean open(String method, String url) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "open " + method + " " + url); - } - // Create the client - if (mConnectionFailed) { - // interrupt() could have been called even before open() - return false; - } - mClient = new DefaultHttpClient(); - mClient.setHttpRequestRetryHandler( - new DefaultHttpRequestRetryHandler(0, false)); - mBodyInputStream = null; - mResponseLine = null; - mResponseHeaders = null; - mPostEntity = null; - mHttpThread = null; - mConnectionFailed = false; - mConnectionFinished = false; - - // Create the method. We support everything that - // Apache HttpClient supports, apart from TRACE. - if ("GET".equalsIgnoreCase(method)) { - mMethod = new HttpGet(url); - } else if ("POST".equalsIgnoreCase(method)) { - mMethod = new HttpPost(url); - mPostEntity = new StreamEntity(); - ((HttpPost)mMethod).setEntity(mPostEntity); - } else if ("HEAD".equalsIgnoreCase(method)) { - mMethod = new HttpHead(url); - } else if ("PUT".equalsIgnoreCase(method)) { - mMethod = new HttpPut(url); - } else if ("DELETE".equalsIgnoreCase(method)) { - mMethod = new HttpDelete(url); - } else { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Method " + method + " not supported"); - } - return false; - } - HttpParams params = mClient.getParams(); - // We handle the redirections C++-side - HttpClientParams.setRedirecting(params, false); - HttpProtocolParams.setUseExpectContinue(params, false); - return true; - } - - /** - * We use this to start the connection thread (doing the method execute). - * We usually always return true here, as the connection will run its - * course in the thread. - * We only return false if interrupted beforehand -- if a connection - * problem happens, we will thus fail in either sendPostData() or - * parseHeaders(). - */ - public synchronized boolean connectToRemote() { - boolean ret = false; - applyRequestHeaders(); - mConnectionFailedLock.lock(); - if (!mConnectionFailed) { - mHttpThread = new Thread(new Connection()); - mHttpThread.start(); - } - ret = mConnectionFailed; - mConnectionFailedLock.unlock(); - return !ret; - } - - /** - * Get the complete response line of the HTTP request. Only valid on - * completion of the transaction. - * @return The complete HTTP response line, e.g "HTTP/1.0 200 OK". - */ - public synchronized String getResponseLine() { - return mResponseLine; - } - - /** - * Wait for the request thread completion - * (unless already finished) - */ - private void waitUntilConnectionFinished() { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "waitUntilConnectionFinished(" - + mConnectionFinished + ")"); - } - if (!mConnectionFinished) { - if (mHttpThread != null) { - try { - mHttpThread.join(); - mConnectionFinished = true; - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "http thread joined"); - } - } catch (InterruptedException e) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "interrupted: " + e); - } - } - } else { - Log.e(LOG_TAG, ">>> Trying to join on mHttpThread " + - "when it does not exist!"); - } - } - } - - // Headers handling - - /** - * Receive all headers from the server and populate - * mResponseHeaders. - * @return True if headers are successfully received, False on - * connection error. - */ - public synchronized boolean parseHeaders() { - mConnectionFailedLock.lock(); - if (mConnectionFailed) { - mConnectionFailedLock.unlock(); - return false; - } - mConnectionFailedLock.unlock(); - waitUntilConnectionFinished(); - mResponseHeaders = new HashMap<String, String[]>(); - if (mResponse == null) - return false; - - Header[] headers = mResponse.getAllHeaders(); - for (int i = 0; i < headers.length; i++) { - Header header = headers[i]; - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "header " + header.getName() - + " -> " + header.getValue()); - } - setResponseHeader(header.getName(), header.getValue()); - } - - return true; - } - - /** - * Set a header to send with the HTTP request. Will not take effect - * on a transaction already in progress. The key is associated - * case-insensitive, but stored case-sensitive. - * @param name The name of the header, e.g "Set-Cookie". - * @param value The value for this header, e.g "text/html". - */ - public synchronized void setRequestHeader(String name, String value) { - String[] mapValue = { name, value }; - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "setRequestHeader: " + name + " => " + value); - } - if (name.equalsIgnoreCase(KEY_CONTENT_LENGTH)) { - setContentLength(Long.parseLong(value)); - } else { - mRequestHeaders.put(name.toLowerCase(), mapValue); - } - } - - /** - * Returns the value associated with the given request header. - * @param name The name of the request header, non-null, case-insensitive. - * @return The value associated with the request header, or null if - * not set, or error. - */ - public synchronized String getRequestHeader(String name) { - String[] value = mRequestHeaders.get(name.toLowerCase()); - if (value != null) { - return value[HEADERS_MAP_INDEX_VALUE]; - } else { - return null; - } - } - - private void applyRequestHeaders() { - if (mMethod == null) - return; - Iterator<String[]> it = mRequestHeaders.values().iterator(); - while (it.hasNext()) { - // Set the key case-sensitive. - String[] entry = it.next(); - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "apply header " + entry[HEADERS_MAP_INDEX_KEY] + - " => " + entry[HEADERS_MAP_INDEX_VALUE]); - } - mMethod.setHeader(entry[HEADERS_MAP_INDEX_KEY], - entry[HEADERS_MAP_INDEX_VALUE]); - } - } - - /** - * Returns the value associated with the given response header. - * @param name The name of the response header, non-null, case-insensitive. - * @return The value associated with the response header, or null if - * not set or error. - */ - public synchronized String getResponseHeader(String name) { - if (mResponseHeaders != null) { - String[] value = mResponseHeaders.get(name.toLowerCase()); - if (value != null) { - return value[HEADERS_MAP_INDEX_VALUE]; - } else { - return null; - } - } else { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "getResponseHeader() called but " - + "response not received"); - } - return null; - } - } - - /** - * Return all response headers, separated by CR-LF line endings, and - * ending with a trailing blank line. This mimics the format of the - * raw response header up to but not including the body. - * @return A string containing the entire response header. - */ - public synchronized String getAllResponseHeaders() { - if (mResponseHeaders == null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "getAllResponseHeaders() called but " - + "response not received"); - } - return null; - } - StringBuilder result = new StringBuilder(); - Iterator<String[]> it = mResponseHeaders.values().iterator(); - while (it.hasNext()) { - String[] entry = it.next(); - // Output the "key: value" lines. - result.append(entry[HEADERS_MAP_INDEX_KEY]); - result.append(": "); - result.append(entry[HEADERS_MAP_INDEX_VALUE]); - result.append(HTTP_LINE_ENDING); - } - result.append(HTTP_LINE_ENDING); - return result.toString(); - } - - - /** - * Set a response header and associated value. The key is associated - * case-insensitively, but stored case-sensitively. - * @param name Case sensitive request header key. - * @param value The associated value. - */ - private void setResponseHeader(String name, String value) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Set response header " + name + ": " + value); - } - String mapValue[] = { name, value }; - mResponseHeaders.put(name.toLowerCase(), mapValue); - } - - // Cookie handling - - /** - * Get the cookie for the given URL. - * @param url The fully qualified URL. - * @return A string containing the cookie for the URL if it exists, - * or null if not. - */ - public static String getCookieForUrl(String url) { - // Get the cookie for this URL, set as a header - return CookieManager.getInstance().getCookie(url); - } - - /** - * Set the cookie for the given URL. - * @param url The fully qualified URL. - * @param cookie The new cookie value. - * @return A string containing the cookie for the URL if it exists, - * or null if not. - */ - public static void setCookieForUrl(String url, String cookie) { - // Get the cookie for this URL, set as a header - CookieManager.getInstance().setCookie(url, cookie); - } - - // Cache handling - - /** - * Perform a request using LocalServer if possible. Initializes - * class members so that receive() will obtain data from the stream - * provided by the response. - * @param url The fully qualified URL to try in LocalServer. - * @return True if the url was found and is now setup to receive. - * False if not found, with no side-effect. - */ - public synchronized boolean useLocalServerResult(String url) { - UrlInterceptHandlerGears handler = - UrlInterceptHandlerGears.getInstance(); - if (handler == null) { - return false; - } - UrlInterceptHandlerGears.ServiceResponse serviceResponse = - handler.getServiceResponse(url, mRequestHeaders); - if (serviceResponse == null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "No response in LocalServer"); - } - return false; - } - // LocalServer will handle this URL. Initialize stream and - // response. - mBodyInputStream = serviceResponse.getInputStream(); - mResponseLine = serviceResponse.getStatusLine(); - mResponseHeaders = serviceResponse.getResponseHeaders(); - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Got response from LocalServer: " + mResponseLine); - } - return true; - } - - /** - * Perform a request using the cache result if present. Initializes - * class members so that receive() will obtain data from the cache. - * @param url The fully qualified URL to try in the cache. - * @return True is the url was found and is now setup to receive - * from cache. False if not found, with no side-effect. - */ - public synchronized boolean useCacheResult(String url) { - // Try the browser's cache. CacheManager wants a Map<String, String>. - Map<String, String> cacheRequestHeaders = new HashMap<String, String>(); - Iterator<Map.Entry<String, String[]>> it = - mRequestHeaders.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry<String, String[]> entry = it.next(); - cacheRequestHeaders.put( - entry.getKey(), - entry.getValue()[HEADERS_MAP_INDEX_VALUE]); - } - CacheResult mCacheResult = - CacheManager.getCacheFile(url, cacheRequestHeaders); - if (mCacheResult == null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "No CacheResult for " + url); - } - return false; - } - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Got CacheResult from browser cache"); - } - // Check for expiry. -1 is "never", otherwise milliseconds since 1970. - // Can be compared to System.currentTimeMillis(). - long expires = mCacheResult.getExpires(); - if (expires >= 0 && System.currentTimeMillis() >= expires) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "CacheResult expired " - + (System.currentTimeMillis() - expires) - + " milliseconds ago"); - } - // Cache hit has expired. Do not return it. - return false; - } - // Setup the mBodyInputStream to come from the cache. - mBodyInputStream = mCacheResult.getInputStream(); - if (mBodyInputStream == null) { - // Cache result may have gone away. - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "No mBodyInputStream for CacheResult " + url); - } - return false; - } - // Cache hit. Parse headers. - synthesizeHeadersFromCacheResult(mCacheResult); - return true; - } - - /** - * Take the limited set of headers in a CacheResult and synthesize - * response headers. - * @param cacheResult A CacheResult to populate mResponseHeaders with. - */ - private void synthesizeHeadersFromCacheResult(CacheResult cacheResult) { - int statusCode = cacheResult.getHttpStatusCode(); - // The status message is informal, so we can greatly simplify it. - String statusMessage; - if (statusCode >= 200 && statusCode < 300) { - statusMessage = "OK"; - } else if (statusCode >= 300 && statusCode < 400) { - statusMessage = "MOVED"; - } else { - statusMessage = "UNAVAILABLE"; - } - // Synthesize the response line. - mResponseLine = "HTTP/1.1 " + statusCode + " " + statusMessage; - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Synthesized " + mResponseLine); - } - // Synthesize the returned headers from cache. - mResponseHeaders = new HashMap<String, String[]>(); - String contentLength = Long.toString(cacheResult.getContentLength()); - setResponseHeader(KEY_CONTENT_LENGTH, contentLength); - long expires = cacheResult.getExpires(); - if (expires >= 0) { - // "Expires" header is valid and finite. Milliseconds since 1970 - // epoch, formatted as RFC-1123. - String expiresString = DateUtils.formatDate(new Date(expires)); - setResponseHeader(KEY_EXPIRES, expiresString); - } - String lastModified = cacheResult.getLastModified(); - if (lastModified != null) { - // Last modification time of the page. Passed end-to-end, but - // not used by us. - setResponseHeader(KEY_LAST_MODIFIED, lastModified); - } - String eTag = cacheResult.getETag(); - if (eTag != null) { - // Entity tag. A kind of GUID to identify identical resources. - setResponseHeader(KEY_ETAG, eTag); - } - String location = cacheResult.getLocation(); - if (location != null) { - // If valid, refers to the location of a redirect. - setResponseHeader(KEY_LOCATION, location); - } - String mimeType = cacheResult.getMimeType(); - if (mimeType == null) { - // Use a safe default MIME type when none is - // specified. "text/plain" is safe to render in the browser - // window (even if large) and won't be intepreted as anything - // that would cause execution. - mimeType = DEFAULT_MIME_TYPE; - } - String encoding = cacheResult.getEncoding(); - // Encoding may not be specified. No default. - String contentType = mimeType; - if (encoding != null) { - if (encoding.length() > 0) { - contentType += "; charset=" + encoding; - } - } - setResponseHeader(KEY_CONTENT_TYPE, contentType); - } - - /** - * Create a CacheResult for this URL. This enables the repsonse body - * to be sent in calls to appendCacheResult(). - * @param url The fully qualified URL to add to the cache. - * @param responseCode The response code returned for the request, e.g 200. - * @param mimeType The MIME type of the body, e.g "text/plain". - * @param encoding The encoding, e.g "utf-8". Use "" for unknown. - */ - public synchronized boolean createCacheResult( - String url, int responseCode, String mimeType, String encoding) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Making cache entry for " + url); - } - // Take the headers and parse them into a format needed by - // CacheManager. - Headers cacheHeaders = new Headers(); - Iterator<Map.Entry<String, String[]>> it = - mResponseHeaders.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry<String, String[]> entry = it.next(); - // Headers.parseHeader() expects lowercase keys. - String keyValue = entry.getKey() + ": " - + entry.getValue()[HEADERS_MAP_INDEX_VALUE]; - CharArrayBuffer buffer = new CharArrayBuffer(keyValue.length()); - buffer.append(keyValue); - // Parse it into the header container. - cacheHeaders.parseHeader(buffer); - } - mCacheResult = CacheManager.createCacheFile( - url, responseCode, cacheHeaders, mimeType, true); - if (mCacheResult != null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Saving into cache"); - } - mCacheResult.setEncoding(encoding); - mCacheResultUrl = url; - return true; - } else { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Couldn't create mCacheResult"); - } - return false; - } - } - - /** - * Add data from the response body to the CacheResult created with - * createCacheResult(). - * @param data A byte array of the next sequential bytes in the - * response body. - * @param bytes The number of bytes to write from the start of - * the array. - * @return True if all bytes successfully written, false on failure. - */ - public synchronized boolean appendCacheResult(byte[] data, int bytes) { - if (mCacheResult == null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "appendCacheResult() called without a " - + "CacheResult initialized"); - } - return false; - } - try { - mCacheResult.getOutputStream().write(data, 0, bytes); - } catch (IOException ex) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Got IOException writing cache data: " + ex); - } - return false; - } - return true; - } - - /** - * Save the completed CacheResult into the CacheManager. This must - * have been created first with createCacheResult(). - * @return Returns true if the entry has been successfully saved. - */ - public synchronized boolean saveCacheResult() { - if (mCacheResult == null || mCacheResultUrl == null) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Tried to save cache result but " - + "createCacheResult not called"); - } - return false; - } - - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Saving cache result"); - } - CacheManager.saveCacheFile(mCacheResultUrl, mCacheResult); - mCacheResult = null; - mCacheResultUrl = null; - return true; - } - - /** - * Called by the main thread to interrupt the child thread. - * We do not set mConnectionFailed here as we still need the - * ability to receive a null packet for sendPostData(). - */ - public synchronized void abort() { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "ABORT CALLED"); - } - if (mMethod != null) { - mMethod.abort(); - } - } - - /** - * Interrupt a blocking IO operation and wait for the - * thread to complete. - */ - public synchronized void interrupt() { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "INTERRUPT CALLED"); - } - mConnectionFailedLock.lock(); - mConnectionFailed = true; - mConnectionFailedLock.unlock(); - if (mMethod != null) { - mMethod.abort(); - } - if (mHttpThread != null) { - waitUntilConnectionFinished(); - } - } - - /** - * Receive the next sequential bytes of the response body after - * successful connection. This will receive up to the size of the - * provided byte array. If there is no body, this will return 0 - * bytes on the first call after connection. - * @param buf A pre-allocated byte array to receive data into. - * @return The number of bytes from the start of the array which - * have been filled, 0 on EOF, or negative on error. - */ - public synchronized int receive(byte[] buf) { - if (mBodyInputStream == null) { - // If this is the first call, setup the InputStream. This may - // fail if there were headers, but no body returned by the - // server. - try { - if (mResponse != null) { - HttpEntity entity = mResponse.getEntity(); - mBodyInputStream = entity.getContent(); - } - } catch (IOException inputException) { - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Failed to connect InputStream: " - + inputException); - } - // Not unexpected. For example, 404 response return headers, - // and sometimes a body with a detailed error. - } - if (mBodyInputStream == null) { - // No error stream either. Treat as a 0 byte response. - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "No InputStream"); - } - return 0; // EOF. - } - } - int ret; - try { - int got = mBodyInputStream.read(buf); - if (got > 0) { - // Got some bytes, not EOF. - ret = got; - } else { - // EOF. - mBodyInputStream.close(); - ret = 0; - } - } catch (IOException e) { - // An abort() interrupts us by calling close() on our stream. - if (LOGV_ENABLED) { - Log.i(LOG_TAG, "Got IOException in mBodyInputStream.read(): ", e); - } - ret = -1; - } - return ret; - } - - /** - * For POST method requests, send a stream of data provided by the - * native side in repeated callbacks. - * We put the data in mBuffer, and wait until it is consumed - * by the StreamEntity in the request thread. - * @param data A byte array containing the data to sent, or null - * if indicating EOF. - * @param bytes The number of bytes from the start of the array to - * send, or 0 if indicating EOF. - * @return True if all bytes were successfully sent, false on error. - */ - public boolean sendPostData(byte[] data, int bytes) { - mConnectionFailedLock.lock(); - if (mConnectionFailed) { - mConnectionFailedLock.unlock(); - return false; - } - mConnectionFailedLock.unlock(); - if (mPostEntity == null) return false; - - // We block until the outputstream is available - // (or in case of connection error) - if (!mPostEntity.isReady()) return false; - - if (data == null && bytes == 0) { - mBuffer.put(null); - } else { - mBuffer.put(new DataPacket(data, bytes)); - } - mSignal.waitUntilPacketConsumed(); - - mConnectionFailedLock.lock(); - if (mConnectionFailed) { - Log.e(LOG_TAG, "failure"); - mConnectionFailedLock.unlock(); - return false; - } - mConnectionFailedLock.unlock(); - return true; - } - -} diff --git a/core/java/android/webkit/gears/DesktopAndroid.java b/core/java/android/webkit/gears/DesktopAndroid.java deleted file mode 100644 index a7a144b..0000000 --- a/core/java/android/webkit/gears/DesktopAndroid.java +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2008 The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.content.Context; -import android.content.ComponentName; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.provider.Browser; -import android.util.Log; -import android.webkit.WebView; - -/** - * Utility class to create a shortcut on Android - */ -public class DesktopAndroid { - - private static final String TAG = "Gears-J-Desktop"; - private static final String EXTRA_SHORTCUT_DUPLICATE = "duplicate"; - private static final String ACTION_INSTALL_SHORTCUT = - "com.android.launcher.action.INSTALL_SHORTCUT"; - - // Android now enforces a 64x64 limit for the icon - private static int MAX_WIDTH = 64; - private static int MAX_HEIGHT = 64; - - /** - * Small utility function returning a Bitmap object. - * - * @param path the icon path - */ - private static Bitmap getBitmap(String path) { - return BitmapFactory.decodeFile(path); - } - - /** - * Create a shortcut for a webpage. - * - * <p>To set a shortcut on Android, we use the ACTION_INSTALL_SHORTCUT - * from the default Home application. We only have to create an Intent - * containing extra parameters specifying the shortcut. - * <p>Note: the shortcut mechanism is not system wide and depends on the - * Home application; if phone carriers decide to rewrite a Home application - * that does not accept this Intent, no shortcut will be added. - * - * @param webview the webview we are called from - * @param title the shortcut's title - * @param url the shortcut's url - * @param imagePath the local path of the shortcut's icon - */ - public static void setShortcut(WebView webview, String title, - String url, String imagePath) { - Context context = webview.getContext(); - - Intent viewWebPage = new Intent(Intent.ACTION_VIEW); - viewWebPage.setData(Uri.parse(url)); - long urlHash = url.hashCode(); - long uniqueId = (urlHash << 32) | viewWebPage.hashCode(); - viewWebPage.putExtra(Browser.EXTRA_APPLICATION_ID, - Long.toString(uniqueId)); - - Intent intent = new Intent(ACTION_INSTALL_SHORTCUT); - intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, viewWebPage); - intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, title); - - // We disallow the creation of duplicate shortcuts (i.e. same - // url, same title, but different screen position). - intent.putExtra(EXTRA_SHORTCUT_DUPLICATE, false); - - Bitmap bmp = getBitmap(imagePath); - if (bmp != null) { - if ((bmp.getWidth() > MAX_WIDTH) || - (bmp.getHeight() > MAX_HEIGHT)) { - Bitmap scaledBitmap = Bitmap.createScaledBitmap(bmp, - MAX_WIDTH, MAX_HEIGHT, true); - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaledBitmap); - } else { - intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bmp); - } - } else { - // This should not happen as we just downloaded the icon - Log.e(TAG, "icon file <" + imagePath + "> not found"); - } - - context.sendBroadcast(intent); - } - -} diff --git a/core/java/android/webkit/gears/NativeDialog.java b/core/java/android/webkit/gears/NativeDialog.java deleted file mode 100644 index 9e2b375..0000000 --- a/core/java/android/webkit/gears/NativeDialog.java +++ /dev/null @@ -1,142 +0,0 @@ -// 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 deleted file mode 100644 index 2d0cc13..0000000 --- a/core/java/android/webkit/gears/PluginSettings.java +++ /dev/null @@ -1,79 +0,0 @@ -// 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 deleted file mode 100644 index 887afc2..0000000 --- a/core/java/android/webkit/gears/UrlInterceptHandlerGears.java +++ /dev/null @@ -1,417 +0,0 @@ -// Copyright 2008, The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.util.Log; -import android.webkit.CacheManager.CacheResult; -import android.webkit.Plugin; -import android.webkit.PluginData; -import android.webkit.UrlInterceptRegistry; -import android.webkit.UrlInterceptHandler; -import android.webkit.WebView; - -import org.apache.http.util.CharArrayBuffer; - -import java.io.*; -import java.util.*; - -/** - * Services requests to handle URLs coming from the browser or - * HttpRequestAndroid. This registers itself with the - * UrlInterceptRegister in Android so we get a chance to service all - * URLs passing through the browser before anything else. - */ -public class UrlInterceptHandlerGears implements UrlInterceptHandler { - /** Singleton instance. */ - private static UrlInterceptHandlerGears instance; - /** Debug logging tag. */ - private static final String LOG_TAG = "Gears-J"; - /** Buffer size for reading/writing streams. */ - private static final int BUFFER_SIZE = 4096; - /** Enable/disable all logging in this class. */ - private static boolean logEnabled = false; - /** The unmodified (case-sensitive) key in the headers map is the - * same index as used by HttpRequestAndroid. */ - public static final int HEADERS_MAP_INDEX_KEY = - ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_KEY; - /** The associated value in the headers map is the same index as - * used by HttpRequestAndroid. */ - public static final int HEADERS_MAP_INDEX_VALUE = - ApacheHttpRequestAndroid.HEADERS_MAP_INDEX_VALUE; - - /** - * Object passed to the native side, containing information about - * the URL to service. - */ - public static class ServiceRequest { - // The URL being requested. - private String url; - // Request headers. Map of lowercase key to [ unmodified key, value ]. - private Map<String, String[]> requestHeaders; - - /** - * Initialize members on construction. - * @param url The URL being requested. - * @param requestHeaders Headers associated with the request, - * or null if none. - * Map of lowercase key to [ unmodified key, value ]. - */ - public ServiceRequest(String url, Map<String, String[]> requestHeaders) { - this.url = url; - this.requestHeaders = requestHeaders; - } - - /** - * Returns the URL being requested. - * @return The URL being requested. - */ - public String getUrl() { - return url; - } - - /** - * Get the value associated with a request header key, if any. - * @param header The key to find, case insensitive. - * @return The value associated with this header, or null if not found. - */ - public String getRequestHeader(String header) { - if (requestHeaders != null) { - String[] value = requestHeaders.get(header.toLowerCase()); - if (value != null) { - return value[HEADERS_MAP_INDEX_VALUE]; - } else { - return null; - } - } else { - return null; - } - } - } - - /** - * Object returned by the native side, containing information needed - * to pass the entire response back to the browser or - * HttpRequestAndroid. Works from either an in-memory array or a - * file on disk. - */ - public class ServiceResponse { - // The response status code, e.g 200. - private int statusCode; - // The full status line, e.g "HTTP/1.1 200 OK". - private String statusLine; - // All headers associated with the response. Map of lowercase key - // to [ unmodified key, value ]. - private Map<String, String[]> responseHeaders = - new HashMap<String, String[]>(); - // The MIME type, e.g "text/html". - private String mimeType; - // The encoding, e.g "utf-8", or null if none. - private String encoding; - // The stream which contains the body when read(). - private InputStream inputStream; - // The length of the content body. - private long contentLength; - - /** - * Initialize members using an in-memory array to return the body. - * @param statusCode The response status code, e.g 200. - * @param statusLine The full status line, e.g "HTTP/1.1 200 OK". - * @param mimeType The MIME type, e.g "text/html". - * @param encoding Encoding, e.g "utf-8" or null if none. - * @param body The response body as a byte array, non-empty. - */ - void setResultArray( - int statusCode, - String statusLine, - String mimeType, - String encoding, - byte[] body) { - this.statusCode = statusCode; - this.statusLine = statusLine; - this.mimeType = mimeType; - this.encoding = encoding; - // Setup a stream to read out of the byte array. - this.contentLength = body.length; - this.inputStream = new ByteArrayInputStream(body); - } - - /** - * Initialize members using a file on disk to return the body. - * @param statusCode The response status code, e.g 200. - * @param statusLine The full status line, e.g "HTTP/1.1 200 OK". - * @param mimeType The MIME type, e.g "text/html". - * @param encoding Encoding, e.g "utf-8" or null if none. - * @param path Full path to the file containing the body. - * @return True if the file is successfully setup to stream, - * false on error such as file not found. - */ - boolean setResultFile( - int statusCode, - String statusLine, - String mimeType, - String encoding, - String path) { - this.statusCode = statusCode; - this.statusLine = statusLine; - this.mimeType = mimeType; - this.encoding = encoding; - try { - // Setup a stream to read out of a file on disk. - File file = new File(path); - this.contentLength = file.length(); - this.inputStream = new FileInputStream(file); - return true; - } catch (java.io.FileNotFoundException ex) { - log("File not found: " + path); - return false; - } - } - - /** - * Set a response header, adding its settings to the header members. - * @param key The case sensitive key for the response header, - * e.g "Set-Cookie". - * @param value The value associated with this key, e.g "cookie1234". - */ - public void setResponseHeader(String key, String value) { - // The map value contains the unmodified key (not lowercase). - String[] mapValue = { key, value }; - responseHeaders.put(key.toLowerCase(), mapValue); - } - - /** - * Return the "Content-Type" header possibly supplied by a - * previous setResponseHeader(). - * @return The "Content-Type" value, or null if not present. - */ - public String getContentType() { - // The map keys are lowercase. - String[] value = responseHeaders.get("content-type"); - if (value != null) { - return value[HEADERS_MAP_INDEX_VALUE]; - } else { - return null; - } - } - - /** - * Returns the HTTP status code for the response, supplied in - * setResultArray() or setResultFile(). - * @return The HTTP statue code, e.g 200. - */ - public int getStatusCode() { - return statusCode; - } - - /** - * Returns the full HTTP status line for the response, supplied in - * setResultArray() or setResultFile(). - * @return The HTTP statue line, e.g "HTTP/1.1 200 OK". - */ - public String getStatusLine() { - return statusLine; - } - - /** - * Get all response headers supplied in calls in - * setResponseHeader(). - * @return A Map<String, String[]> containing all headers. - */ - public Map<String, String[]> getResponseHeaders() { - return responseHeaders; - } - - /** - * Returns the MIME type for the response, supplied in - * setResultArray() or setResultFile(). - * @return The MIME type, e.g "text/html". - */ - public String getMimeType() { - return mimeType; - } - - /** - * Returns the encoding for the response, supplied in - * setResultArray() or setResultFile(), or null if none. - * @return The encoding, e.g "utf-8", or null if none. - */ - public String getEncoding() { - return encoding; - } - - /** - * Returns the InputStream setup by setResultArray() or - * setResultFile() to allow reading data either from memory or - * disk. - * @return The InputStream containing the response body. - */ - public InputStream getInputStream() { - return inputStream; - } - - /** - * @return The length of the response body. - */ - public long getContentLength() { - return contentLength; - } - } - - /** - * Construct and initialize the singleton instance. - */ - public UrlInterceptHandlerGears() { - if (instance != null) { - Log.e(LOG_TAG, "UrlInterceptHandlerGears singleton already constructed"); - throw new RuntimeException(); - } - instance = this; - } - - /** - * Turn on/off logging in this class. - * @param on Logging enable state. - */ - public static void enableLogging(boolean on) { - logEnabled = on; - } - - /** - * Get the singleton instance. - * @return The singleton instance. - */ - public static UrlInterceptHandlerGears getInstance() { - return instance; - } - - /** - * Register the singleton instance with the browser's interception - * mechanism. - */ - public synchronized void register() { - UrlInterceptRegistry.registerHandler(this); - } - - /** - * Unregister the singleton instance from the browser's interception - * mechanism. - */ - public synchronized void unregister() { - UrlInterceptRegistry.unregisterHandler(this); - } - - /** - * Given an URL, returns the CacheResult which contains the - * surrogate response for the request, or null if the handler is - * not interested. - * - * @param url URL string. - * @param headers The headers associated with the request. May be null. - * @return The CacheResult containing the surrogate response. - * @deprecated Use PluginData getPluginData(String url, - * Map<String, String> headers); instead - */ - @Deprecated - public CacheResult service(String url, Map<String, String> headers) { - throw new UnsupportedOperationException("unimplemented"); - } - - /** - * Given an URL, returns a PluginData instance which contains the - * response for the request. This implements the UrlInterceptHandler - * interface. - * - * @param url The fully qualified URL being requested. - * @param requestHeaders The request headers for this URL. - * @return a PluginData object. - */ - public PluginData getPluginData(String url, Map<String, String> requestHeaders) { - // Thankfully the browser does call us with case-sensitive - // headers. We just need to map it case-insensitive. - Map<String, String[]> lowercaseRequestHeaders = - new HashMap<String, String[]>(); - Iterator<Map.Entry<String, String>> requestHeadersIt = - requestHeaders.entrySet().iterator(); - while (requestHeadersIt.hasNext()) { - Map.Entry<String, String> entry = requestHeadersIt.next(); - String key = entry.getKey(); - String mapValue[] = { key, entry.getValue() }; - lowercaseRequestHeaders.put(key.toLowerCase(), mapValue); - } - ServiceResponse response = getServiceResponse(url, lowercaseRequestHeaders); - if (response == null) { - // No result for this URL. - return null; - } - return new PluginData(response.getInputStream(), - response.getContentLength(), - response.getResponseHeaders(), - response.getStatusCode()); - } - - /** - * Given an URL, returns a CacheResult and headers which contain the - * response for the request. - * - * @param url The fully qualified URL being requested. - * @param requestHeaders The request headers for this URL. - * @return If a response can be crafted, a ServiceResponse is - * created which contains all response headers and an InputStream - * attached to the body. If there is no response, null is returned. - */ - public ServiceResponse getServiceResponse(String url, - Map<String, String[]> requestHeaders) { - if (!url.startsWith("http://") && !url.startsWith("https://")) { - // Don't know how to service non-HTTP URLs - return null; - } - // Call the native handler to craft a response for this URL. - return nativeService(new ServiceRequest(url, requestHeaders)); - } - - /** - * Convenience debug function. Calls the Android logging - * mechanism. logEnabled is not a constant, so if the string - * evaluation is potentially expensive, the caller also needs to - * check it. - * @param str String to log to the Android console. - */ - private void log(String str) { - if (logEnabled) { - Log.i(LOG_TAG, str); - } - } - - /** - * Native method which handles the bulk of the request in LocalServer. - * @param request A ServiceRequest object containing information about - * the request. - * @return If serviced, a ServiceResponse object containing all the - * information to provide a response for the URL, or null - * if no response available for this URL. - */ - private native static ServiceResponse nativeService(ServiceRequest request); -} diff --git a/core/java/android/webkit/gears/VersionExtractor.java b/core/java/android/webkit/gears/VersionExtractor.java deleted file mode 100644 index 172dacb..0000000 --- a/core/java/android/webkit/gears/VersionExtractor.java +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2008, The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.util.Log; -import java.io.IOException; -import java.io.StringReader; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.FactoryConfigurationError; -import javax.xml.parsers.ParserConfigurationException; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.w3c.dom.Document; -import org.w3c.dom.DOMException; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -/** - * A class that can extract the Gears version and upgrade URL from an - * xml document. - */ -public final class VersionExtractor { - - /** - * Logging tag - */ - private static final String TAG = "Gears-J-VersionExtractor"; - /** - * XML element names. - */ - private static final String VERSION = "em:version"; - private static final String URL = "em:updateLink"; - - /** - * Parses the input xml string and invokes the native - * setVersionAndUrl method. - * @param xml is the XML string to parse. - * @return true if the extraction is successful and false otherwise. - */ - public static boolean extract(String xml, long nativeObject) { - try { - // Build the builders. - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - - // Create the document. - Document doc = builder.parse(new InputSource(new StringReader(xml))); - - // Look for the version and url elements and get their text - // contents. - String version = extractText(doc, VERSION); - String url = extractText(doc, URL); - - // If we have both, let the native side know. - if (version != null && url != null) { - setVersionAndUrl(version, url, nativeObject); - return true; - } - - return false; - - } catch (FactoryConfigurationError ex) { - Log.e(TAG, "Could not create the DocumentBuilderFactory " + ex); - } catch (ParserConfigurationException ex) { - Log.e(TAG, "Could not create the DocumentBuilder " + ex); - } catch (SAXException ex) { - Log.e(TAG, "Could not parse the xml " + ex); - } catch (IOException ex) { - Log.e(TAG, "Could not read the xml " + ex); - } - - return false; - } - - /** - * Extracts the text content of the first element with the given name. - * @param doc is the Document where the element is searched for. - * @param elementName is name of the element to searched for. - * @return the text content of the element or null if no such - * element is found. - */ - private static String extractText(Document doc, String elementName) { - String text = null; - NodeList node_list = doc.getElementsByTagName(elementName); - - if (node_list.getLength() > 0) { - // We are only interested in the first node. Normally there - // should not be more than one anyway. - Node node = node_list.item(0); - - // Iterate through the text children. - NodeList child_list = node.getChildNodes(); - - try { - for (int i = 0; i < child_list.getLength(); ++i) { - Node child = child_list.item(i); - if (child.getNodeType() == Node.TEXT_NODE) { - if (text == null) { - text = new String(); - } - text += child.getNodeValue(); - } - } - } catch (DOMException ex) { - Log.e(TAG, "getNodeValue() failed " + ex); - } - } - - if (text != null) { - text = text.trim(); - } - - return text; - } - - /** - * Native method used to send the version and url back to the C++ - * side. - */ - private static native void setVersionAndUrl( - String version, String url, long nativeObject); -} diff --git a/core/java/android/webkit/gears/ZipInflater.java b/core/java/android/webkit/gears/ZipInflater.java deleted file mode 100644 index f6b6be5..0000000 --- a/core/java/android/webkit/gears/ZipInflater.java +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2008, The Android Open Source Project -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// 2. Redistributions in binary form must reproduce the above copyright notice, -// this list of conditions and the following disclaimer in the documentation -// and/or other materials provided with the distribution. -// 3. Neither the name of Google Inc. nor the names of its contributors may be -// used to endorse or promote products derived from this software without -// specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -package android.webkit.gears; - -import android.os.StatFs; -import android.util.Log; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Enumeration; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; - - -/** - * A class that can inflate a zip archive. - */ -public final class ZipInflater { - /** - * Logging tag - */ - private static final String TAG = "Gears-J-ZipInflater"; - - /** - * The size of the buffer used to read from the archive. - */ - private static final int BUFFER_SIZE_BYTES = 32 * 1024; // 32 KB. - /** - * The path navigation component (i.e. "../"). - */ - private static final String PATH_NAVIGATION_COMPONENT = ".." + File.separator; - /** - * The root of the data partition. - */ - private static final String DATA_PARTITION_ROOT = "/data"; - - /** - * We need two be able to store two versions of gears in parallel: - * - the zipped version - * - the unzipped version, which will be loaded next time the browser is started. - * We are conservative and do not attempt to unpack unless there enough free - * space on the device to store 4 times the new Gears size. - */ - private static final long SIZE_MULTIPLIER = 4; - - /** - * Unzips the archive with the given name. - * @param filename is the name of the zip to inflate. - * @param path is the path where the zip should be unpacked. It must contain - * a trailing separator, or the extraction will fail. - * @return true if the extraction is successful and false otherwise. - */ - public static boolean inflate(String filename, String path) { - Log.i(TAG, "Extracting " + filename + " to " + path); - - // Check that the path ends with a separator. - if (!path.endsWith(File.separator)) { - Log.e(TAG, "Path missing trailing separator: " + path); - return false; - } - - boolean result = false; - - // Use a ZipFile to get an enumeration of the entries and - // calculate the overall uncompressed size of the archive. Also - // check for existing files or directories that have the same - // name as the entries in the archive. Also check for invalid - // entry names (e.g names that attempt to navigate to the - // parent directory). - ZipInputStream zipStream = null; - long uncompressedSize = 0; - try { - ZipFile zipFile = new ZipFile(filename); - try { - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = (ZipEntry) entries.nextElement(); - uncompressedSize += entry.getSize(); - // Check against entry names that may attempt to navigate - // out of the destination directory. - if (entry.getName().indexOf(PATH_NAVIGATION_COMPONENT) >= 0) { - throw new IOException("Illegal entry name: " + entry.getName()); - } - - // Check against entries with the same name as pre-existing files or - // directories. - File file = new File(path + entry.getName()); - if (file.exists()) { - // A file or directory with the same name already exist. - // This must not happen, so we treat this as an error. - throw new IOException( - "A file or directory with the same name already exists."); - } - } - } finally { - zipFile.close(); - } - - Log.i(TAG, "Determined uncompressed size: " + uncompressedSize); - // Check we have enough space to unpack this archive. - if (freeSpace() <= uncompressedSize * SIZE_MULTIPLIER) { - throw new IOException("Not enough space to unpack this archive."); - } - - zipStream = new ZipInputStream( - new BufferedInputStream(new FileInputStream(filename))); - ZipEntry entry; - int counter; - byte buffer[] = new byte[BUFFER_SIZE_BYTES]; - - // Iterate through the entries and write each of them to a file. - while ((entry = zipStream.getNextEntry()) != null) { - File file = new File(path + entry.getName()); - if (entry.isDirectory()) { - // If the entry denotes a directory, we need to create a - // directory with the same name. - file.mkdirs(); - } else { - CRC32 checksum = new CRC32(); - BufferedOutputStream output = new BufferedOutputStream( - new FileOutputStream(file), - BUFFER_SIZE_BYTES); - try { - // Read the entry and write it to the file. - while ((counter = zipStream.read(buffer, 0, BUFFER_SIZE_BYTES)) != - -1) { - output.write(buffer, 0, counter); - checksum.update(buffer, 0, counter); - } - output.flush(); - } finally { - output.close(); - } - - if (checksum.getValue() != entry.getCrc()) { - throw new IOException( - "Integrity check failed for: " + entry.getName()); - } - } - zipStream.closeEntry(); - } - - result = true; - - } catch (FileNotFoundException ex) { - Log.e(TAG, "The zip file could not be found. " + ex); - } catch (IOException ex) { - Log.e(TAG, "Could not read or write an entry. " + ex); - } catch(IllegalArgumentException ex) { - Log.e(TAG, "Could not create the BufferedOutputStream. " + ex); - } finally { - if (zipStream != null) { - try { - zipStream.close(); - } catch (IOException ex) { - // Ignored. - } - } - // Discard any exceptions and return the result to the native side. - return result; - } - } - - private static final long freeSpace() { - StatFs data_partition = new StatFs(DATA_PARTITION_ROOT); - long freeSpace = data_partition.getAvailableBlocks() * - data_partition.getBlockSize(); - Log.i(TAG, "Free space on the data partition: " + freeSpace); - return freeSpace; - } -} diff --git a/core/java/android/webkit/gears/package.html b/core/java/android/webkit/gears/package.html deleted file mode 100644 index db6f78b..0000000 --- a/core/java/android/webkit/gears/package.html +++ /dev/null @@ -1,3 +0,0 @@ -<body> -{@hide} -</body>
\ No newline at end of file |