| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.webkit; |
| |
| import android.app.ActivityManager; |
| import android.content.ComponentCallbacks; |
| import android.content.Context; |
| import android.content.res.AssetManager; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.content.res.Resources.NotFoundException; |
| import android.graphics.Bitmap; |
| import android.net.ParseException; |
| import android.net.Uri; |
| import android.net.WebAddress; |
| import android.net.http.ErrorStrings; |
| import android.net.http.SslCertificate; |
| import android.net.http.SslError; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.util.Log; |
| import android.util.TypedValue; |
| import android.view.Surface; |
| import android.view.ViewRootImpl; |
| import android.view.WindowManager; |
| |
| import junit.framework.Assert; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.ref.WeakReference; |
| import java.net.URLEncoder; |
| import java.nio.charset.Charsets; |
| import java.security.PrivateKey; |
| import java.security.cert.CertificateEncodingException; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.harmony.security.provider.cert.X509CertImpl; |
| import org.apache.harmony.xnet.provider.jsse.OpenSSLDSAPrivateKey; |
| import org.apache.harmony.xnet.provider.jsse.OpenSSLRSAPrivateKey; |
| |
| class BrowserFrame extends Handler { |
| |
| private static final String LOGTAG = "webkit"; |
| |
| /** |
| * Cap the number of LoadListeners that will be instantiated, so |
| * we don't blow the GREF count. Attempting to queue more than |
| * this many requests will prompt an error() callback on the |
| * request's LoadListener |
| */ |
| private final static int MAX_OUTSTANDING_REQUESTS = 300; |
| |
| private final CallbackProxy mCallbackProxy; |
| private final WebSettingsClassic mSettings; |
| private final Context mContext; |
| private final WebViewDatabaseClassic mDatabase; |
| private final WebViewCore mWebViewCore; |
| /* package */ boolean mLoadInitFromJava; |
| private int mLoadType; |
| private boolean mFirstLayoutDone = true; |
| private boolean mCommitted = true; |
| // Flag for blocking messages. This is used during destroy() so |
| // that if the UI thread posts any messages after the message |
| // queue has been cleared,they are ignored. |
| private boolean mBlockMessages = false; |
| private int mOrientation = -1; |
| |
| // Is this frame the main frame? |
| private boolean mIsMainFrame; |
| |
| // Javascript interface object |
| private class JSObject { |
| Object object; |
| boolean requireAnnotation; |
| |
| public JSObject(Object object, boolean requireAnnotation) { |
| this.object = object; |
| this.requireAnnotation = requireAnnotation; |
| } |
| } |
| |
| // Attached Javascript interfaces |
| private Map<String, JSObject> mJavaScriptObjects; |
| private Set<Object> mRemovedJavaScriptObjects; |
| |
| // Key store handler when Chromium HTTP stack is used. |
| private KeyStoreHandler mKeyStoreHandler = null; |
| |
| // message ids |
| // a message posted when a frame loading is completed |
| static final int FRAME_COMPLETED = 1001; |
| // orientation change message |
| static final int ORIENTATION_CHANGED = 1002; |
| // a message posted when the user decides the policy |
| static final int POLICY_FUNCTION = 1003; |
| |
| // Note: need to keep these in sync with FrameLoaderTypes.h in native |
| static final int FRAME_LOADTYPE_STANDARD = 0; |
| static final int FRAME_LOADTYPE_BACK = 1; |
| static final int FRAME_LOADTYPE_FORWARD = 2; |
| static final int FRAME_LOADTYPE_INDEXEDBACKFORWARD = 3; |
| static final int FRAME_LOADTYPE_RELOAD = 4; |
| static final int FRAME_LOADTYPE_RELOADALLOWINGSTALEDATA = 5; |
| static final int FRAME_LOADTYPE_SAME = 6; |
| static final int FRAME_LOADTYPE_REDIRECT = 7; |
| static final int FRAME_LOADTYPE_REPLACE = 8; |
| |
| // A progress threshold to switch from history Picture to live Picture |
| private static final int TRANSITION_SWITCH_THRESHOLD = 75; |
| |
| // This is a field accessed by native code as well as package classes. |
| /*package*/ int mNativeFrame; |
| |
| // Static instance of a JWebCoreJavaBridge to handle timer and cookie |
| // requests from WebCore. |
| static JWebCoreJavaBridge sJavaBridge; |
| |
| private static class ConfigCallback implements ComponentCallbacks { |
| private final ArrayList<WeakReference<Handler>> mHandlers = |
| new ArrayList<WeakReference<Handler>>(); |
| private final WindowManager mWindowManager; |
| |
| ConfigCallback(WindowManager wm) { |
| mWindowManager = wm; |
| } |
| |
| public synchronized void addHandler(Handler h) { |
| // No need to ever remove a Handler. If the BrowserFrame is |
| // destroyed, it will be collected and the WeakReference set to |
| // null. If it happens to still be around during a configuration |
| // change, the message will be ignored. |
| mHandlers.add(new WeakReference<Handler>(h)); |
| } |
| |
| public void onConfigurationChanged(Configuration newConfig) { |
| if (mHandlers.size() == 0) { |
| return; |
| } |
| int orientation = |
| mWindowManager.getDefaultDisplay().getOrientation(); |
| switch (orientation) { |
| case Surface.ROTATION_90: |
| orientation = 90; |
| break; |
| case Surface.ROTATION_180: |
| orientation = 180; |
| break; |
| case Surface.ROTATION_270: |
| orientation = -90; |
| break; |
| case Surface.ROTATION_0: |
| orientation = 0; |
| break; |
| default: |
| break; |
| } |
| synchronized (this) { |
| // Create a list of handlers to remove. Go ahead and make it |
| // the same size to avoid resizing. |
| ArrayList<WeakReference> handlersToRemove = |
| new ArrayList<WeakReference>(mHandlers.size()); |
| for (WeakReference<Handler> wh : mHandlers) { |
| Handler h = wh.get(); |
| if (h != null) { |
| h.sendMessage(h.obtainMessage(ORIENTATION_CHANGED, |
| orientation, 0)); |
| } else { |
| handlersToRemove.add(wh); |
| } |
| } |
| // Now remove all the null references. |
| for (WeakReference weak : handlersToRemove) { |
| mHandlers.remove(weak); |
| } |
| } |
| } |
| |
| public void onLowMemory() {} |
| } |
| static ConfigCallback sConfigCallback; |
| |
| /** |
| * Create a new BrowserFrame to be used in an application. |
| * @param context An application context to use when retrieving assets. |
| * @param w A WebViewCore used as the view for this frame. |
| * @param proxy A CallbackProxy for posting messages to the UI thread and |
| * querying a client for information. |
| * @param settings A WebSettings object that holds all settings. |
| * XXX: Called by WebCore thread. |
| */ |
| public BrowserFrame(Context context, WebViewCore w, CallbackProxy proxy, |
| WebSettingsClassic settings, Map<String, Object> javascriptInterfaces) { |
| |
| Context appContext = context.getApplicationContext(); |
| |
| // Create a global JWebCoreJavaBridge to handle timers and |
| // cookies in the WebCore thread. |
| if (sJavaBridge == null) { |
| sJavaBridge = new JWebCoreJavaBridge(); |
| // set WebCore native cache size |
| 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(appContext); |
| // create CookieSyncManager with current Context |
| CookieSyncManager.createInstance(appContext); |
| // create PluginManager with current Context |
| PluginManager.getInstance(appContext); |
| } |
| |
| if (sConfigCallback == null) { |
| sConfigCallback = new ConfigCallback( |
| (WindowManager) appContext.getSystemService( |
| Context.WINDOW_SERVICE)); |
| ViewRootImpl.addConfigCallback(sConfigCallback); |
| } |
| sConfigCallback.addHandler(this); |
| |
| mJavaScriptObjects = new HashMap<String, JSObject>(); |
| addJavaScriptObjects(javascriptInterfaces); |
| mRemovedJavaScriptObjects = new HashSet<Object>(); |
| |
| mSettings = settings; |
| mContext = context; |
| mCallbackProxy = proxy; |
| mDatabase = WebViewDatabaseClassic.getInstance(appContext); |
| mWebViewCore = w; |
| |
| AssetManager am = context.getAssets(); |
| nativeCreateFrame(w, am, proxy.getBackForwardList()); |
| |
| if (DebugFlags.BROWSER_FRAME) { |
| Log.v(LOGTAG, "BrowserFrame constructor: this=" + this); |
| } |
| } |
| |
| /** |
| * Load a url from the network or the filesystem into the main frame. |
| * Following the same behaviour as Safari, javascript: URLs are not passed |
| * to the main frame, instead they are evaluated immediately. |
| * @param url The url to load. |
| * @param extraHeaders The extra headers sent with this url. This should not |
| * include the common headers like "user-agent". If it does, it |
| * will be replaced by the intrinsic value of the WebView. |
| */ |
| public void loadUrl(String url, Map<String, String> extraHeaders) { |
| mLoadInitFromJava = true; |
| if (URLUtil.isJavaScriptUrl(url)) { |
| // strip off the scheme and evaluate the string |
| stringByEvaluatingJavaScriptFromString( |
| url.substring("javascript:".length())); |
| } else { |
| nativeLoadUrl(url, extraHeaders); |
| } |
| mLoadInitFromJava = false; |
| } |
| |
| /** |
| * Load a url with "POST" method from the network into the main frame. |
| * @param url The url to load. |
| * @param data The data for POST request. |
| */ |
| public void postUrl(String url, byte[] data) { |
| mLoadInitFromJava = true; |
| nativePostUrl(url, data); |
| mLoadInitFromJava = false; |
| } |
| |
| /** |
| * Load the content as if it was loaded by the provided base URL. The |
| * historyUrl is used as the history entry for the load data. |
| * |
| * @param baseUrl Base URL used to resolve relative paths in the content |
| * @param data Content to render in the browser |
| * @param mimeType Mimetype of the data being passed in |
| * @param encoding Character set encoding of the provided data. |
| * @param historyUrl URL to use as the history entry. |
| */ |
| public void loadData(String baseUrl, String data, String mimeType, |
| String encoding, String historyUrl) { |
| mLoadInitFromJava = true; |
| if (historyUrl == null || historyUrl.length() == 0) { |
| historyUrl = "about:blank"; |
| } |
| if (data == null) { |
| data = ""; |
| } |
| |
| // Setup defaults for missing values. These defaults where taken from |
| // WebKit's WebFrame.mm |
| if (baseUrl == null || baseUrl.length() == 0) { |
| baseUrl = "about:blank"; |
| } |
| if (mimeType == null || mimeType.length() == 0) { |
| mimeType = "text/html"; |
| } |
| nativeLoadData(baseUrl, data, mimeType, encoding, historyUrl); |
| mLoadInitFromJava = false; |
| } |
| |
| /** |
| * Saves the contents of the frame as a web archive. |
| * |
| * @param basename The filename where the archive should be placed. |
| * @param autoname If false, takes filename to be a file. If true, filename |
| * is assumed to be a directory in which a filename will be |
| * chosen according to the url of the current page. |
| */ |
| /* package */ String saveWebArchive(String basename, boolean autoname) { |
| return nativeSaveWebArchive(basename, autoname); |
| } |
| |
| /** |
| * Go back or forward the number of steps given. |
| * @param steps A negative or positive number indicating the direction |
| * and number of steps to move. |
| */ |
| public void goBackOrForward(int steps) { |
| mLoadInitFromJava = true; |
| nativeGoBackOrForward(steps); |
| mLoadInitFromJava = false; |
| } |
| |
| /** |
| * native callback |
| * Report an error to an activity. |
| * @param errorCode The HTTP error code. |
| * @param description Optional human-readable description. If no description |
| * is given, we'll use a standard localized error message. |
| * @param failingUrl The URL that was being loaded when the error occurred. |
| * TODO: Report all errors including resource errors but include some kind |
| * of domain identifier. Change errorCode to an enum for a cleaner |
| * interface. |
| */ |
| private void reportError(int errorCode, String description, String failingUrl) { |
| // As this is called for the main resource and loading will be stopped |
| // after, reset the state variables. |
| resetLoadingStates(); |
| if (description == null || description.isEmpty()) { |
| description = ErrorStrings.getString(errorCode, mContext); |
| } |
| mCallbackProxy.onReceivedError(errorCode, description, failingUrl); |
| } |
| |
| private void resetLoadingStates() { |
| mCommitted = true; |
| mFirstLayoutDone = true; |
| } |
| |
| /* package */boolean committed() { |
| return mCommitted; |
| } |
| |
| /* package */boolean firstLayoutDone() { |
| return mFirstLayoutDone; |
| } |
| |
| /* package */int loadType() { |
| return mLoadType; |
| } |
| |
| /* package */void didFirstLayout() { |
| if (!mFirstLayoutDone) { |
| mFirstLayoutDone = true; |
| // ensure {@link WebViewCore#webkitDraw} is called as we were |
| // blocking the update in {@link #loadStarted} |
| mWebViewCore.contentDraw(); |
| } |
| } |
| |
| /** |
| * native callback |
| * Indicates the beginning of a new load. |
| * This method will be called once for the main frame. |
| */ |
| private void loadStarted(String url, Bitmap favicon, int loadType, |
| boolean isMainFrame) { |
| mIsMainFrame = isMainFrame; |
| |
| if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { |
| mLoadType = loadType; |
| |
| if (isMainFrame) { |
| // Call onPageStarted for main frames. |
| mCallbackProxy.onPageStarted(url, favicon); |
| // as didFirstLayout() is only called for the main frame, reset |
| // mFirstLayoutDone only for the main frames |
| mFirstLayoutDone = false; |
| mCommitted = false; |
| // remove pending draw to block update until mFirstLayoutDone is |
| // set to true in didFirstLayout() |
| mWebViewCore.clearContent(); |
| mWebViewCore.removeMessages(WebViewCore.EventHub.WEBKIT_DRAW); |
| } |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| private void saveFormData(HashMap<String, String> data) { |
| if (mSettings.getSaveFormData()) { |
| final WebHistoryItem h = mCallbackProxy.getBackForwardList() |
| .getCurrentItem(); |
| if (h != null) { |
| String url = WebTextView.urlForAutoCompleteData(h.getUrl()); |
| if (url != null) { |
| mDatabase.setFormData(url, data); |
| } |
| } |
| } |
| } |
| |
| @SuppressWarnings("unused") |
| private boolean shouldSaveFormData() { |
| if (mSettings.getSaveFormData()) { |
| final WebHistoryItem h = mCallbackProxy.getBackForwardList() |
| .getCurrentItem(); |
| return h != null && h.getUrl() != null; |
| } |
| return false; |
| } |
| |
| /** |
| * native callback |
| * Indicates the WebKit has committed to the new load |
| */ |
| private void transitionToCommitted(int loadType, boolean isMainFrame) { |
| // loadType is not used yet |
| if (isMainFrame) { |
| mCommitted = true; |
| mWebViewCore.getWebViewClassic().mViewManager.postResetStateAll(); |
| } |
| } |
| |
| /** |
| * native callback |
| * <p> |
| * Indicates the end of a new load. |
| * This method will be called once for the main frame. |
| */ |
| private void loadFinished(String url, int loadType, boolean isMainFrame) { |
| // mIsMainFrame and isMainFrame are better be equal!!! |
| |
| if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) { |
| if (isMainFrame) { |
| resetLoadingStates(); |
| mCallbackProxy.switchOutDrawHistory(); |
| mCallbackProxy.onPageFinished(url); |
| } |
| } |
| } |
| |
| /** |
| * Destroy all native components of the BrowserFrame. |
| */ |
| public void destroy() { |
| nativeDestroyFrame(); |
| mBlockMessages = true; |
| removeCallbacksAndMessages(null); |
| } |
| |
| /** |
| * Handle messages posted to us. |
| * @param msg The message to handle. |
| */ |
| @Override |
| public void handleMessage(Message msg) { |
| if (mBlockMessages) { |
| return; |
| } |
| switch (msg.what) { |
| case FRAME_COMPLETED: { |
| if (mSettings.getSavePassword() && hasPasswordField()) { |
| WebHistoryItem item = mCallbackProxy.getBackForwardList() |
| .getCurrentItem(); |
| if (item != null) { |
| WebAddress uri = new WebAddress(item.getUrl()); |
| String schemePlusHost = uri.getScheme() + uri.getHost(); |
| String[] up = mDatabase.getUsernamePassword( |
| schemePlusHost); |
| if (up != null && up[0] != null) { |
| setUsernamePassword(up[0], up[1]); |
| } |
| } |
| } |
| break; |
| } |
| |
| case POLICY_FUNCTION: { |
| nativeCallPolicyFunction(msg.arg1, msg.arg2); |
| break; |
| } |
| |
| case ORIENTATION_CHANGED: { |
| if (mOrientation != msg.arg1) { |
| mOrientation = msg.arg1; |
| nativeOrientationChanged(msg.arg1); |
| } |
| break; |
| } |
| |
| default: |
| break; |
| } |
| } |
| |
| /** |
| * Punch-through for WebCore to set the document |
| * title. Inform the Activity of the new title. |
| * @param title The new title of the document. |
| */ |
| private void setTitle(String title) { |
| // FIXME: The activity must call getTitle (a native method) to get the |
| // title. We should try and cache the title if we can also keep it in |
| // sync with the document. |
| mCallbackProxy.onReceivedTitle(title); |
| } |
| |
| /** |
| * Retrieves the render tree of this frame and puts it as the object for |
| * the message and sends the message. |
| * @param callback the message to use to send the render tree |
| */ |
| public void externalRepresentation(Message callback) { |
| callback.obj = externalRepresentation();; |
| callback.sendToTarget(); |
| } |
| |
| /** |
| * Return the render tree as a string |
| */ |
| private native String externalRepresentation(); |
| |
| /** |
| * Retrieves the visual text of the frames, puts it as the object for |
| * the message and sends the message. |
| * @param callback the message to use to send the visual text |
| */ |
| public void documentAsText(Message callback) { |
| StringBuilder text = new StringBuilder(); |
| if (callback.arg1 != 0) { |
| // Dump top frame as text. |
| text.append(documentAsText()); |
| } |
| if (callback.arg2 != 0) { |
| // Dump child frames as text. |
| text.append(childFramesAsText()); |
| } |
| callback.obj = text.toString(); |
| callback.sendToTarget(); |
| } |
| |
| /** |
| * Return the text drawn on the screen as a string |
| */ |
| private native String documentAsText(); |
| |
| /** |
| * Return the text drawn on the child frames as a string |
| */ |
| private native String childFramesAsText(); |
| |
| /* |
| * This method is called by WebCore to inform the frame that |
| * the Javascript window object has been cleared. |
| * We should re-attach any attached js interfaces. |
| */ |
| private void windowObjectCleared(int nativeFramePointer) { |
| Iterator<String> iter = mJavaScriptObjects.keySet().iterator(); |
| while (iter.hasNext()) { |
| String interfaceName = iter.next(); |
| JSObject jsobject = mJavaScriptObjects.get(interfaceName); |
| if (jsobject != null && jsobject.object != null) { |
| nativeAddJavascriptInterface(nativeFramePointer, |
| jsobject.object, interfaceName, jsobject.requireAnnotation); |
| } |
| } |
| mRemovedJavaScriptObjects.clear(); |
| } |
| |
| /* |
| * Add javascript objects to the internal list of objects. The default behavior |
| * is to allow access to inherited methods (no annotation needed). This is only |
| * used when js objects are passed through a constructor (via a hidden constructor). |
| * |
| * @TODO change the default behavior to be compatible with the public addjavascriptinterface |
| */ |
| private void addJavaScriptObjects(Map<String, Object> javascriptInterfaces) { |
| |
| // TODO in a separate CL provide logic to enable annotations for API level JB_MR1 and above. |
| if (javascriptInterfaces == null) return; |
| Iterator<String> iter = javascriptInterfaces.keySet().iterator(); |
| while (iter.hasNext()) { |
| String interfaceName = iter.next(); |
| Object object = javascriptInterfaces.get(interfaceName); |
| if (object != null) { |
| mJavaScriptObjects.put(interfaceName, new JSObject(object, false)); |
| } |
| } |
| } |
| |
| /** |
| * This method is called by WebCore to check whether application |
| * wants to hijack url loading |
| */ |
| public boolean handleUrl(String url) { |
| if (mLoadInitFromJava == true) { |
| return false; |
| } |
| if (mCallbackProxy.shouldOverrideUrlLoading(url)) { |
| // if the url is hijacked, reset the state of the BrowserFrame |
| didFirstLayout(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| public void addJavascriptInterface(Object obj, String interfaceName, |
| boolean requireAnnotation) { |
| assert obj != null; |
| removeJavascriptInterface(interfaceName); |
| mJavaScriptObjects.put(interfaceName, new JSObject(obj, requireAnnotation)); |
| } |
| |
| public void removeJavascriptInterface(String interfaceName) { |
| // We keep a reference to the removed object because the native side holds only a weak |
| // reference and we need to allow the object to continue to be used until the page has been |
| // navigated. |
| if (mJavaScriptObjects.containsKey(interfaceName)) { |
| mRemovedJavaScriptObjects.add(mJavaScriptObjects.remove(interfaceName)); |
| } |
| } |
| |
| /** |
| * Called by JNI. Given a URI, find the associated file and return its size |
| * @param uri A String representing the URI of the desired file. |
| * @return int The size of the given file. |
| */ |
| private int getFileSize(String uri) { |
| int size = 0; |
| try { |
| InputStream stream = mContext.getContentResolver() |
| .openInputStream(Uri.parse(uri)); |
| size = stream.available(); |
| stream.close(); |
| } catch (Exception e) {} |
| return size; |
| } |
| |
| /** |
| * Called by JNI. Given a URI, a buffer, and an offset into the buffer, |
| * copy the resource into buffer. |
| * @param uri A String representing the URI of the desired file. |
| * @param buffer The byte array to copy the data into. |
| * @param offset The offet into buffer to place the data. |
| * @param expectedSize The size that the buffer has allocated for this file. |
| * @return int The size of the given file, or zero if it fails. |
| */ |
| private int getFile(String uri, byte[] buffer, int offset, |
| int expectedSize) { |
| int size = 0; |
| try { |
| InputStream stream = mContext.getContentResolver() |
| .openInputStream(Uri.parse(uri)); |
| size = stream.available(); |
| if (size <= expectedSize && buffer != null |
| && buffer.length - offset >= size) { |
| stream.read(buffer, offset, size); |
| } else { |
| size = 0; |
| } |
| stream.close(); |
| } catch (java.io.FileNotFoundException e) { |
| Log.e(LOGTAG, "FileNotFoundException:" + e); |
| size = 0; |
| } catch (java.io.IOException e2) { |
| Log.e(LOGTAG, "IOException: " + e2); |
| size = 0; |
| } |
| return size; |
| } |
| |
| /** |
| * Get the InputStream for an Android resource |
| * There are three different kinds of android resources: |
| * - file:///android_res |
| * - file:///android_asset |
| * - content:// |
| * @param url The url to load. |
| * @return An InputStream to the android resource |
| */ |
| private InputStream inputStreamForAndroidResource(String url) { |
| final String ANDROID_ASSET = URLUtil.ASSET_BASE; |
| final String ANDROID_RESOURCE = URLUtil.RESOURCE_BASE; |
| final String ANDROID_CONTENT = URLUtil.CONTENT_BASE; |
| |
| if (url.startsWith(ANDROID_RESOURCE)) { |
| url = url.replaceFirst(ANDROID_RESOURCE, ""); |
| if (url == null || url.length() == 0) { |
| Log.e(LOGTAG, "url has length 0 " + url); |
| return null; |
| } |
| int slash = url.indexOf('/'); |
| int dot = url.indexOf('.', slash); |
| if (slash == -1 || dot == -1) { |
| Log.e(LOGTAG, "Incorrect res path: " + url); |
| return null; |
| } |
| String subClassName = url.substring(0, slash); |
| String fieldName = url.substring(slash + 1, dot); |
| String errorMsg = null; |
| try { |
| final Class<?> d = mContext.getApplicationContext() |
| .getClassLoader().loadClass( |
| mContext.getPackageName() + ".R$" |
| + subClassName); |
| final java.lang.reflect.Field field = d.getField(fieldName); |
| final int id = field.getInt(null); |
| TypedValue value = new TypedValue(); |
| mContext.getResources().getValue(id, value, true); |
| if (value.type == TypedValue.TYPE_STRING) { |
| return mContext.getAssets().openNonAsset( |
| value.assetCookie, value.string.toString(), |
| AssetManager.ACCESS_STREAMING); |
| } else { |
| // Old stack only supports TYPE_STRING for res files |
| Log.e(LOGTAG, "not of type string: " + url); |
| return null; |
| } |
| } catch (Exception e) { |
| Log.e(LOGTAG, "Exception: " + url); |
| return null; |
| } |
| } else if (url.startsWith(ANDROID_ASSET)) { |
| url = url.replaceFirst(ANDROID_ASSET, ""); |
| try { |
| AssetManager assets = mContext.getAssets(); |
| Uri uri = Uri.parse(url); |
| return assets.open(uri.getPath(), AssetManager.ACCESS_STREAMING); |
| } catch (IOException e) { |
| return null; |
| } |
| } else if (mSettings.getAllowContentAccess() && |
| url.startsWith(ANDROID_CONTENT)) { |
| try { |
| // Strip off MIME type. If we don't do this, we can fail to |
| // load Gmail attachments, because the URL being loaded doesn't |
| // exactly match the URL we have permission to read. |
| int mimeIndex = url.lastIndexOf('?'); |
| if (mimeIndex != -1) { |
| url = url.substring(0, mimeIndex); |
| } |
| Uri uri = Uri.parse(url); |
| return mContext.getContentResolver().openInputStream(uri); |
| } catch (Exception e) { |
| Log.e(LOGTAG, "Exception: " + url); |
| return null; |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * If this looks like a POST request (form submission) containing a username |
| * and password, give the user the option of saving them. Will either do |
| * nothing, or block until the UI interaction is complete. |
| * |
| * Called directly by WebKit. |
| * |
| * @param postData The data about to be sent as the body of a POST request. |
| * @param username The username entered by the user (sniffed from the DOM). |
| * @param password The password entered by the user (sniffed from the DOM). |
| */ |
| private void maybeSavePassword( |
| byte[] postData, String username, String password) { |
| if (postData == null |
| || username == null || username.isEmpty() |
| || password == null || password.isEmpty()) { |
| return; // No password to save. |
| } |
| |
| if (!mSettings.getSavePassword()) { |
| return; // User doesn't want to save passwords. |
| } |
| |
| try { |
| if (DebugFlags.BROWSER_FRAME) { |
| Assert.assertNotNull(mCallbackProxy.getBackForwardList() |
| .getCurrentItem()); |
| } |
| WebAddress uri = new WebAddress(mCallbackProxy |
| .getBackForwardList().getCurrentItem().getUrl()); |
| String schemePlusHost = uri.getScheme() + uri.getHost(); |
| // Check to see if the username & password appear in |
| // the post data (there could be another form on the |
| // page and that was posted instead. |
| String postString = new String(postData); |
| if (postString.contains(URLEncoder.encode(username)) && |
| postString.contains(URLEncoder.encode(password))) { |
| String[] saved = mDatabase.getUsernamePassword( |
| schemePlusHost); |
| if (saved != null) { |
| // null username implies that user has chosen not to |
| // save password |
| if (saved[0] != null) { |
| // non-null username implies that user has |
| // chosen to save password, so update the |
| // recorded password |
| mDatabase.setUsernamePassword(schemePlusHost, username, password); |
| } |
| } else { |
| // CallbackProxy will handle creating the resume |
| // message |
| mCallbackProxy.onSavePassword(schemePlusHost, username, |
| password, null); |
| } |
| } |
| } catch (ParseException ex) { |
| // if it is bad uri, don't save its password |
| } |
| } |
| |
| // Called by jni from the chrome network stack. |
| private WebResourceResponse shouldInterceptRequest(String url) { |
| InputStream androidResource = inputStreamForAndroidResource(url); |
| if (androidResource != null) { |
| return new WebResourceResponse(null, null, androidResource); |
| } |
| |
| // Note that we check this after looking for an android_asset or |
| // android_res URL, as we allow those even if file access is disabled. |
| if (!mSettings.getAllowFileAccess() && url.startsWith("file://")) { |
| return new WebResourceResponse(null, null, null); |
| } |
| |
| WebResourceResponse response = mCallbackProxy.shouldInterceptRequest(url); |
| if (response == null && "browser:incognito".equals(url)) { |
| try { |
| Resources res = mContext.getResources(); |
| InputStream ins = res.openRawResource( |
| com.android.internal.R.raw.incognito_mode_start_page); |
| response = new WebResourceResponse("text/html", "utf8", ins); |
| } catch (NotFoundException ex) { |
| // This shouldn't happen, but try and gracefully handle it jic |
| Log.w(LOGTAG, "Failed opening raw.incognito_mode_start_page", ex); |
| } |
| } |
| return response; |
| } |
| |
| /** |
| * Set the progress for the browser activity. Called by native code. |
| * Uses a delay so it does not happen too often. |
| * @param newProgress An int between zero and one hundred representing |
| * the current progress percentage of loading the page. |
| */ |
| private void setProgress(int newProgress) { |
| mCallbackProxy.onProgressChanged(newProgress); |
| if (newProgress == 100) { |
| sendMessageDelayed(obtainMessage(FRAME_COMPLETED), 100); |
| } |
| // FIXME: Need to figure out a better way to switch out of the history |
| // drawing mode. Maybe we can somehow compare the history picture with |
| // the current picture, and switch when it contains more content. |
| if (mFirstLayoutDone && newProgress > TRANSITION_SWITCH_THRESHOLD) { |
| mCallbackProxy.switchOutDrawHistory(); |
| } |
| } |
| |
| /** |
| * Send the icon to the activity for display. |
| * @param icon A Bitmap representing a page's favicon. |
| */ |
| private void didReceiveIcon(Bitmap icon) { |
| mCallbackProxy.onReceivedIcon(icon); |
| } |
| |
| // 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. |
| */ |
| private BrowserFrame createWindow(boolean dialog, boolean userGesture) { |
| return mCallbackProxy.createWindow(dialog, userGesture); |
| } |
| |
| /** |
| * Try to focus this WebView. |
| */ |
| private void requestFocus() { |
| mCallbackProxy.onRequestFocus(); |
| } |
| |
| /** |
| * Close this frame and window. |
| */ |
| private void closeWindow(WebViewCore w) { |
| mCallbackProxy.onCloseWindow(w.getWebViewClassic()); |
| } |
| |
| // XXX: Must match PolicyAction in FrameLoaderTypes.h in webcore |
| static final int POLICY_USE = 0; |
| static final int POLICY_IGNORE = 2; |
| |
| private void decidePolicyForFormResubmission(int policyFunction) { |
| Message dontResend = obtainMessage(POLICY_FUNCTION, policyFunction, |
| POLICY_IGNORE); |
| Message resend = obtainMessage(POLICY_FUNCTION, policyFunction, |
| POLICY_USE); |
| mCallbackProxy.onFormResubmission(dontResend, resend); |
| } |
| |
| /** |
| * Tell the activity to update its global history. |
| */ |
| private void updateVisitedHistory(String url, boolean isReload) { |
| mCallbackProxy.doUpdateVisitedHistory(url, isReload); |
| } |
| |
| /** |
| * Get the CallbackProxy for sending messages to the UI thread. |
| */ |
| /* package */ CallbackProxy getCallbackProxy() { |
| return mCallbackProxy; |
| } |
| |
| /** |
| * Returns the User Agent used by this frame |
| */ |
| String getUserAgentString() { |
| return mSettings.getUserAgentString(); |
| } |
| |
| // These ids need to be in sync with enum rawResId in PlatformBridge.h |
| private static final int NODOMAIN = 1; |
| private static final int LOADERROR = 2; |
| /* package */ static final int DRAWABLEDIR = 3; |
| private static final int FILE_UPLOAD_LABEL = 4; |
| private static final int RESET_LABEL = 5; |
| private static final int SUBMIT_LABEL = 6; |
| private static final int FILE_UPLOAD_NO_FILE_CHOSEN = 7; |
| |
| private String getRawResFilename(int id) { |
| return getRawResFilename(id, mContext); |
| } |
| /* package */ static String getRawResFilename(int id, Context context) { |
| int resid; |
| switch (id) { |
| case NODOMAIN: |
| resid = com.android.internal.R.raw.nodomain; |
| break; |
| |
| case LOADERROR: |
| 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; |
| |
| case FILE_UPLOAD_LABEL: |
| return context.getResources().getString( |
| com.android.internal.R.string.upload_file); |
| |
| case RESET_LABEL: |
| return context.getResources().getString( |
| com.android.internal.R.string.reset); |
| |
| case SUBMIT_LABEL: |
| return context.getResources().getString( |
| com.android.internal.R.string.submit); |
| |
| case FILE_UPLOAD_NO_FILE_CHOSEN: |
| return context.getResources().getString( |
| com.android.internal.R.string.no_file_chosen); |
| |
| default: |
| Log.e(LOGTAG, "getRawResFilename got incompatible resource ID"); |
| return ""; |
| } |
| TypedValue value = new TypedValue(); |
| context.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; |
| } |
| |
| /** |
| * Called by JNI when the native HTTP stack gets an authentication request. |
| * |
| * We delegate the request to CallbackProxy, and route its response to |
| * {@link #nativeAuthenticationProceed(int, String, String)} or |
| * {@link #nativeAuthenticationCancel(int)}. |
| * |
| * We don't care what thread the callback is invoked on. All threading is |
| * handled on the C++ side, because the WebKit thread may be blocked on a |
| * synchronous call and unable to pump our MessageQueue. |
| */ |
| private void didReceiveAuthenticationChallenge( |
| final int handle, String host, String realm, final boolean useCachedCredentials, |
| final boolean suppressDialog) { |
| |
| HttpAuthHandler handler = new HttpAuthHandler() { |
| |
| @Override |
| public boolean useHttpAuthUsernamePassword() { |
| return useCachedCredentials; |
| } |
| |
| @Override |
| public void proceed(String username, String password) { |
| nativeAuthenticationProceed(handle, username, password); |
| } |
| |
| @Override |
| public void cancel() { |
| nativeAuthenticationCancel(handle); |
| } |
| |
| @Override |
| public boolean suppressDialog() { |
| return suppressDialog; |
| } |
| }; |
| mCallbackProxy.onReceivedHttpAuthRequest(handler, host, realm); |
| } |
| |
| /** |
| * Called by JNI when the Chromium HTTP stack gets an invalid certificate chain. |
| * |
| * We delegate the request to CallbackProxy, and route its response to |
| * {@link #nativeSslCertErrorProceed(int)} or |
| * {@link #nativeSslCertErrorCancel(int, int)}. |
| */ |
| private void reportSslCertError(final int handle, final int certError, byte certDER[], |
| String url) { |
| final SslError sslError; |
| try { |
| X509Certificate cert = new X509CertImpl(certDER); |
| SslCertificate sslCert = new SslCertificate(cert); |
| sslError = SslError.SslErrorFromChromiumErrorCode(certError, sslCert, url); |
| } catch (IOException e) { |
| // Can't get the certificate, not much to do. |
| Log.e(LOGTAG, "Can't get the certificate from WebKit, canceling"); |
| nativeSslCertErrorCancel(handle, certError); |
| return; |
| } |
| |
| if (SslCertLookupTable.getInstance().isAllowed(sslError)) { |
| nativeSslCertErrorProceed(handle); |
| mCallbackProxy.onProceededAfterSslError(sslError); |
| return; |
| } |
| |
| SslErrorHandler handler = new SslErrorHandler() { |
| @Override |
| public void proceed() { |
| SslCertLookupTable.getInstance().setIsAllowed(sslError); |
| post(new Runnable() { |
| public void run() { |
| nativeSslCertErrorProceed(handle); |
| } |
| }); |
| } |
| @Override |
| public void cancel() { |
| post(new Runnable() { |
| public void run() { |
| nativeSslCertErrorCancel(handle, certError); |
| } |
| }); |
| } |
| }; |
| mCallbackProxy.onReceivedSslError(handler, sslError); |
| } |
| |
| /** |
| * Called by JNI when the native HTTPS stack gets a client |
| * certificate request. |
| * |
| * We delegate the request to CallbackProxy, and route its response to |
| * {@link #nativeSslClientCert(int, X509Certificate)}. |
| */ |
| private void requestClientCert(int handle, String hostAndPort) { |
| SslClientCertLookupTable table = SslClientCertLookupTable.getInstance(); |
| if (table.IsAllowed(hostAndPort)) { |
| // previously allowed |
| PrivateKey pkey = table.PrivateKey(hostAndPort); |
| if (pkey instanceof OpenSSLRSAPrivateKey) { |
| nativeSslClientCert(handle, |
| ((OpenSSLRSAPrivateKey)pkey).getPkeyContext(), |
| table.CertificateChain(hostAndPort)); |
| } else if (pkey instanceof OpenSSLDSAPrivateKey) { |
| nativeSslClientCert(handle, |
| ((OpenSSLDSAPrivateKey)pkey).getPkeyContext(), |
| table.CertificateChain(hostAndPort)); |
| } else { |
| nativeSslClientCert(handle, |
| pkey.getEncoded(), |
| table.CertificateChain(hostAndPort)); |
| } |
| } else if (table.IsDenied(hostAndPort)) { |
| // previously denied |
| nativeSslClientCert(handle, 0, null); |
| } else { |
| // previously ignored or new |
| mCallbackProxy.onReceivedClientCertRequest( |
| new ClientCertRequestHandler(this, handle, hostAndPort, table), hostAndPort); |
| } |
| } |
| |
| /** |
| * Called by JNI when the native HTTP stack needs to download a file. |
| * |
| * We delegate the request to CallbackProxy, which owns the current app's |
| * DownloadListener. |
| */ |
| private void downloadStart(String url, String userAgent, |
| String contentDisposition, String mimeType, String referer, long contentLength) { |
| // This will only work if the url ends with the filename |
| if (mimeType.isEmpty()) { |
| try { |
| String extension = url.substring(url.lastIndexOf('.') + 1); |
| mimeType = libcore.net.MimeUtils.guessMimeTypeFromExtension(extension); |
| // MimeUtils might return null, not sure if downloadmanager is happy with that |
| if (mimeType == null) |
| mimeType = ""; |
| } catch(IndexOutOfBoundsException exception) { |
| // mimeType string end with a '.', not much to do |
| } |
| } |
| mimeType = MimeTypeMap.getSingleton().remapGenericMimeType( |
| mimeType, url, contentDisposition); |
| |
| if (CertTool.getCertType(mimeType) != null) { |
| mKeyStoreHandler = new KeyStoreHandler(mimeType); |
| } else { |
| mCallbackProxy.onDownloadStart(url, userAgent, |
| contentDisposition, mimeType, referer, contentLength); |
| } |
| } |
| |
| /** |
| * Called by JNI for Chrome HTTP stack when the Java side needs to access the data. |
| */ |
| private void didReceiveData(byte data[], int size) { |
| if (mKeyStoreHandler != null) mKeyStoreHandler.didReceiveData(data, size); |
| } |
| |
| private void didFinishLoading() { |
| if (mKeyStoreHandler != null) { |
| mKeyStoreHandler.installCert(mContext); |
| mKeyStoreHandler = null; |
| } |
| } |
| |
| /** |
| * Called by JNI when we recieve a certificate for the page's main resource. |
| * Used by the Chromium HTTP stack only. |
| */ |
| private void setCertificate(byte cert_der[]) { |
| try { |
| X509Certificate cert = new X509CertImpl(cert_der); |
| mCallbackProxy.onReceivedCertificate(new SslCertificate(cert)); |
| } catch (IOException e) { |
| // Can't get the certificate, not much to do. |
| Log.e(LOGTAG, "Can't get the certificate from WebKit, canceling"); |
| return; |
| } |
| } |
| |
| /** |
| * Called by JNI when processing the X-Auto-Login header. |
| */ |
| private void autoLogin(String realm, String account, String args) { |
| mCallbackProxy.onReceivedLoginRequest(realm, account, args); |
| } |
| |
| //========================================================================== |
| // native functions |
| //========================================================================== |
| |
| /** |
| * Create a new native frame for a given WebView |
| * @param w A WebView that the frame draws into. |
| * @param am AssetManager to use to get assets. |
| * @param list The native side will add and remove items from this list as |
| * the native list changes. |
| */ |
| private native void nativeCreateFrame(WebViewCore w, AssetManager am, |
| WebBackForwardList list); |
| |
| /** |
| * Destroy the native frame. |
| */ |
| public native void nativeDestroyFrame(); |
| |
| private native void nativeCallPolicyFunction(int policyFunction, |
| int decision); |
| |
| /** |
| * Reload the current main frame. |
| */ |
| public native void reload(boolean allowStale); |
| |
| /** |
| * Go back or forward the number of steps given. |
| * @param steps A negative or positive number indicating the direction |
| * and number of steps to move. |
| */ |
| private native void nativeGoBackOrForward(int steps); |
| |
| /** |
| * stringByEvaluatingJavaScriptFromString will execute the |
| * JS passed in in the context of this browser frame. |
| * @param script A javascript string to execute |
| * |
| * @return string result of execution or null |
| */ |
| public native String stringByEvaluatingJavaScriptFromString(String script); |
| |
| /** |
| * Add a javascript interface to the main frame. |
| */ |
| private native void nativeAddJavascriptInterface(int nativeFramePointer, |
| Object obj, String interfaceName, boolean requireAnnotation); |
| |
| public native void clearCache(); |
| |
| /** |
| * Returns false if the url is bad. |
| */ |
| private native void nativeLoadUrl(String url, Map<String, String> headers); |
| |
| private native void nativePostUrl(String url, byte[] postData); |
| |
| private native void nativeLoadData(String baseUrl, String data, |
| String mimeType, String encoding, String historyUrl); |
| |
| /** |
| * Stop loading the current page. |
| */ |
| public void stopLoading() { |
| if (mIsMainFrame) { |
| resetLoadingStates(); |
| } |
| nativeStopLoading(); |
| } |
| |
| private native void nativeStopLoading(); |
| |
| /** |
| * Return true if the document has images. |
| */ |
| public native boolean documentHasImages(); |
| |
| /** |
| * @return TRUE if there is a password field in the current frame |
| */ |
| private native boolean hasPasswordField(); |
| |
| /** |
| * Get username and password in the current frame. If found, String[0] is |
| * username and String[1] is password. Otherwise return NULL. |
| * @return String[] |
| */ |
| private native String[] getUsernamePassword(); |
| |
| /** |
| * Set username and password to the proper fields in the current frame |
| * @param username |
| * @param password |
| */ |
| private native void setUsernamePassword(String username, String password); |
| |
| private native String nativeSaveWebArchive(String basename, boolean autoname); |
| |
| private native void nativeOrientationChanged(int orientation); |
| |
| private native void nativeAuthenticationProceed(int handle, String username, String password); |
| private native void nativeAuthenticationCancel(int handle); |
| |
| private native void nativeSslCertErrorProceed(int handle); |
| private native void nativeSslCertErrorCancel(int handle, int certError); |
| |
| native void nativeSslClientCert(int handle, |
| int ctx, |
| byte[][] asn1DerEncodedCertificateChain); |
| |
| native void nativeSslClientCert(int handle, |
| byte[] pkey, |
| byte[][] asn1DerEncodedCertificateChain); |
| |
| /** |
| * Returns true when the contents of the frame is an RTL or vertical-rl |
| * page. This is used for determining whether a frame should be initially |
| * scrolled right-most as opposed to left-most. |
| * @return true when the frame should be initially scrolled right-most |
| * based on the text direction and writing mode. |
| */ |
| /* package */ boolean getShouldStartScrolledRight() { |
| return nativeGetShouldStartScrolledRight(mNativeFrame); |
| } |
| |
| private native boolean nativeGetShouldStartScrolledRight(int nativeBrowserFrame); |
| } |