BrowserFrame.java revision d6662b5eb7ec7446215346c7212899b11f5b77a4
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.webkit;
18
19import android.app.ActivityManager;
20import android.content.ComponentCallbacks;
21import android.content.Context;
22import android.content.res.AssetManager;
23import android.content.res.Configuration;
24import android.content.res.Resources;
25import android.content.res.Resources.NotFoundException;
26import android.graphics.Bitmap;
27import android.net.ParseException;
28import android.net.Uri;
29import android.net.WebAddress;
30import android.net.http.ErrorStrings;
31import android.net.http.SslCertificate;
32import android.net.http.SslError;
33import android.os.Handler;
34import android.os.Message;
35import android.util.Log;
36import android.util.TypedValue;
37import android.view.Surface;
38import android.view.ViewRoot;
39import android.view.WindowManager;
40
41import junit.framework.Assert;
42
43import java.io.IOException;
44import java.io.InputStream;
45import java.lang.ref.WeakReference;
46import java.net.URLEncoder;
47import java.security.cert.X509Certificate;
48import java.util.ArrayList;
49import java.util.Date;
50import java.util.HashMap;
51import java.util.Map;
52import java.util.Iterator;
53
54import org.apache.harmony.security.provider.cert.X509CertImpl;
55
56class BrowserFrame extends Handler {
57
58    private static final String LOGTAG = "webkit";
59
60    /**
61     * Cap the number of LoadListeners that will be instantiated, so
62     * we don't blow the GREF count.  Attempting to queue more than
63     * this many requests will prompt an error() callback on the
64     * request's LoadListener
65     */
66    private final static int MAX_OUTSTANDING_REQUESTS = 300;
67
68    private final CallbackProxy mCallbackProxy;
69    private final WebSettings mSettings;
70    private final Context mContext;
71    private final WebViewDatabase mDatabase;
72    private final WebViewCore mWebViewCore;
73    /* package */ boolean mLoadInitFromJava;
74    private int mLoadType;
75    private boolean mFirstLayoutDone = true;
76    private boolean mCommitted = true;
77    // Flag for blocking messages. This is used during destroy() so
78    // that if the UI thread posts any messages after the message
79    // queue has been cleared,they are ignored.
80    private boolean mBlockMessages = false;
81    private int mOrientation = -1;
82
83    // Is this frame the main frame?
84    private boolean mIsMainFrame;
85
86    // Attached Javascript interfaces
87    private Map<String, Object> mJSInterfaceMap;
88
89    // Key store handler when Chromium HTTP stack is used.
90    private KeyStoreHandler mKeyStoreHandler = null;
91
92    // message ids
93    // a message posted when a frame loading is completed
94    static final int FRAME_COMPLETED = 1001;
95    // orientation change message
96    static final int ORIENTATION_CHANGED = 1002;
97    // a message posted when the user decides the policy
98    static final int POLICY_FUNCTION = 1003;
99
100    // Note: need to keep these in sync with FrameLoaderTypes.h in native
101    static final int FRAME_LOADTYPE_STANDARD = 0;
102    static final int FRAME_LOADTYPE_BACK = 1;
103    static final int FRAME_LOADTYPE_FORWARD = 2;
104    static final int FRAME_LOADTYPE_INDEXEDBACKFORWARD = 3;
105    static final int FRAME_LOADTYPE_RELOAD = 4;
106    static final int FRAME_LOADTYPE_RELOADALLOWINGSTALEDATA = 5;
107    static final int FRAME_LOADTYPE_SAME = 6;
108    static final int FRAME_LOADTYPE_REDIRECT = 7;
109    static final int FRAME_LOADTYPE_REPLACE = 8;
110
111    // A progress threshold to switch from history Picture to live Picture
112    private static final int TRANSITION_SWITCH_THRESHOLD = 75;
113
114    // This is a field accessed by native code as well as package classes.
115    /*package*/ int mNativeFrame;
116
117    // Static instance of a JWebCoreJavaBridge to handle timer and cookie
118    // requests from WebCore.
119    static JWebCoreJavaBridge sJavaBridge;
120
121    private static class ConfigCallback implements ComponentCallbacks {
122        private final ArrayList<WeakReference<Handler>> mHandlers =
123                new ArrayList<WeakReference<Handler>>();
124        private final WindowManager mWindowManager;
125
126        ConfigCallback(WindowManager wm) {
127            mWindowManager = wm;
128        }
129
130        public synchronized void addHandler(Handler h) {
131            // No need to ever remove a Handler. If the BrowserFrame is
132            // destroyed, it will be collected and the WeakReference set to
133            // null. If it happens to still be around during a configuration
134            // change, the message will be ignored.
135            mHandlers.add(new WeakReference<Handler>(h));
136        }
137
138        public void onConfigurationChanged(Configuration newConfig) {
139            if (mHandlers.size() == 0) {
140                return;
141            }
142            int orientation =
143                    mWindowManager.getDefaultDisplay().getOrientation();
144            switch (orientation) {
145                case Surface.ROTATION_90:
146                    orientation = 90;
147                    break;
148                case Surface.ROTATION_180:
149                    orientation = 180;
150                    break;
151                case Surface.ROTATION_270:
152                    orientation = -90;
153                    break;
154                case Surface.ROTATION_0:
155                    orientation = 0;
156                    break;
157                default:
158                    break;
159            }
160            synchronized (this) {
161                // Create a list of handlers to remove. Go ahead and make it
162                // the same size to avoid resizing.
163                ArrayList<WeakReference> handlersToRemove =
164                        new ArrayList<WeakReference>(mHandlers.size());
165                for (WeakReference<Handler> wh : mHandlers) {
166                    Handler h = wh.get();
167                    if (h != null) {
168                        h.sendMessage(h.obtainMessage(ORIENTATION_CHANGED,
169                                    orientation, 0));
170                    } else {
171                        handlersToRemove.add(wh);
172                    }
173                }
174                // Now remove all the null references.
175                for (WeakReference weak : handlersToRemove) {
176                    mHandlers.remove(weak);
177                }
178            }
179        }
180
181        public void onLowMemory() {}
182    }
183    static ConfigCallback sConfigCallback;
184
185    /**
186     * Create a new BrowserFrame to be used in an application.
187     * @param context An application context to use when retrieving assets.
188     * @param w A WebViewCore used as the view for this frame.
189     * @param proxy A CallbackProxy for posting messages to the UI thread and
190     *              querying a client for information.
191     * @param settings A WebSettings object that holds all settings.
192     * XXX: Called by WebCore thread.
193     */
194    public BrowserFrame(Context context, WebViewCore w, CallbackProxy proxy,
195            WebSettings settings, Map<String, Object> javascriptInterfaces) {
196
197        Context appContext = context.getApplicationContext();
198
199        // Create a global JWebCoreJavaBridge to handle timers and
200        // cookies in the WebCore thread.
201        if (sJavaBridge == null) {
202            sJavaBridge = new JWebCoreJavaBridge();
203            // set WebCore native cache size
204            ActivityManager am = (ActivityManager) context
205                    .getSystemService(Context.ACTIVITY_SERVICE);
206            if (am.getMemoryClass() > 16) {
207                sJavaBridge.setCacheSize(8 * 1024 * 1024);
208            } else {
209                sJavaBridge.setCacheSize(4 * 1024 * 1024);
210            }
211            // initialize CacheManager
212            CacheManager.init(appContext);
213            // create CookieSyncManager with current Context
214            CookieSyncManager.createInstance(appContext);
215            // create PluginManager with current Context
216            PluginManager.getInstance(appContext);
217        }
218
219        if (sConfigCallback == null) {
220            sConfigCallback = new ConfigCallback(
221                    (WindowManager) appContext.getSystemService(
222                            Context.WINDOW_SERVICE));
223            ViewRoot.addConfigCallback(sConfigCallback);
224        }
225        sConfigCallback.addHandler(this);
226
227        mJSInterfaceMap = javascriptInterfaces;
228
229        mSettings = settings;
230        mContext = context;
231        mCallbackProxy = proxy;
232        mDatabase = WebViewDatabase.getInstance(appContext);
233        mWebViewCore = w;
234
235        AssetManager am = context.getAssets();
236        nativeCreateFrame(w, am, proxy.getBackForwardList());
237
238        if (DebugFlags.BROWSER_FRAME) {
239            Log.v(LOGTAG, "BrowserFrame constructor: this=" + this);
240        }
241    }
242
243    /**
244     * Load a url from the network or the filesystem into the main frame.
245     * Following the same behaviour as Safari, javascript: URLs are not passed
246     * to the main frame, instead they are evaluated immediately.
247     * @param url The url to load.
248     * @param extraHeaders The extra headers sent with this url. This should not
249     *            include the common headers like "user-agent". If it does, it
250     *            will be replaced by the intrinsic value of the WebView.
251     */
252    public void loadUrl(String url, Map<String, String> extraHeaders) {
253        mLoadInitFromJava = true;
254        if (URLUtil.isJavaScriptUrl(url)) {
255            // strip off the scheme and evaluate the string
256            stringByEvaluatingJavaScriptFromString(
257                    url.substring("javascript:".length()));
258        } else {
259            nativeLoadUrl(url, extraHeaders);
260        }
261        mLoadInitFromJava = false;
262    }
263
264    /**
265     * Load a url with "POST" method from the network into the main frame.
266     * @param url The url to load.
267     * @param data The data for POST request.
268     */
269    public void postUrl(String url, byte[] data) {
270        mLoadInitFromJava = true;
271        nativePostUrl(url, data);
272        mLoadInitFromJava = false;
273    }
274
275    /**
276     * Load the content as if it was loaded by the provided base URL. The
277     * historyUrl is used as the history entry for the load data.
278     *
279     * @param baseUrl Base URL used to resolve relative paths in the content
280     * @param data Content to render in the browser
281     * @param mimeType Mimetype of the data being passed in
282     * @param encoding Character set encoding of the provided data.
283     * @param historyUrl URL to use as the history entry.
284     */
285    public void loadData(String baseUrl, String data, String mimeType,
286            String encoding, String historyUrl) {
287        mLoadInitFromJava = true;
288        if (historyUrl == null || historyUrl.length() == 0) {
289            historyUrl = "about:blank";
290        }
291        if (data == null) {
292            data = "";
293        }
294
295        // Setup defaults for missing values. These defaults where taken from
296        // WebKit's WebFrame.mm
297        if (baseUrl == null || baseUrl.length() == 0) {
298            baseUrl = "about:blank";
299        }
300        if (mimeType == null || mimeType.length() == 0) {
301            mimeType = "text/html";
302        }
303        nativeLoadData(baseUrl, data, mimeType, encoding, historyUrl);
304        mLoadInitFromJava = false;
305    }
306
307    /**
308     * Saves the contents of the frame as a web archive.
309     *
310     * @param basename The filename where the archive should be placed.
311     * @param autoname If false, takes filename to be a file. If true, filename
312     *                 is assumed to be a directory in which a filename will be
313     *                 chosen according to the url of the current page.
314     */
315    /* package */ String saveWebArchive(String basename, boolean autoname) {
316        return nativeSaveWebArchive(basename, autoname);
317    }
318
319    /**
320     * Go back or forward the number of steps given.
321     * @param steps A negative or positive number indicating the direction
322     *              and number of steps to move.
323     */
324    public void goBackOrForward(int steps) {
325        mLoadInitFromJava = true;
326        nativeGoBackOrForward(steps);
327        mLoadInitFromJava = false;
328    }
329
330    /**
331     * native callback
332     * Report an error to an activity.
333     * @param errorCode The HTTP error code.
334     * @param description Optional human-readable description. If no description
335     *     is given, we'll use a standard localized error message.
336     * @param failingUrl The URL that was being loaded when the error occurred.
337     * TODO: Report all errors including resource errors but include some kind
338     * of domain identifier. Change errorCode to an enum for a cleaner
339     * interface.
340     */
341    private void reportError(int errorCode, String description, String failingUrl) {
342        // As this is called for the main resource and loading will be stopped
343        // after, reset the state variables.
344        resetLoadingStates();
345        if (description == null || description.isEmpty()) {
346            description = ErrorStrings.getString(errorCode, mContext);
347        }
348        mCallbackProxy.onReceivedError(errorCode, description, failingUrl);
349    }
350
351    private void resetLoadingStates() {
352        mCommitted = true;
353        mFirstLayoutDone = true;
354    }
355
356    /* package */boolean committed() {
357        return mCommitted;
358    }
359
360    /* package */boolean firstLayoutDone() {
361        return mFirstLayoutDone;
362    }
363
364    /* package */int loadType() {
365        return mLoadType;
366    }
367
368    /* package */void didFirstLayout() {
369        if (!mFirstLayoutDone) {
370            mFirstLayoutDone = true;
371            // ensure {@link WebViewCore#webkitDraw} is called as we were
372            // blocking the update in {@link #loadStarted}
373            mWebViewCore.contentDraw();
374        }
375    }
376
377    /**
378     * native callback
379     * Indicates the beginning of a new load.
380     * This method will be called once for the main frame.
381     */
382    private void loadStarted(String url, Bitmap favicon, int loadType,
383            boolean isMainFrame) {
384        mIsMainFrame = isMainFrame;
385
386        if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) {
387            mLoadType = loadType;
388
389            if (isMainFrame) {
390                // Call onPageStarted for main frames.
391                mCallbackProxy.onPageStarted(url, favicon);
392                // as didFirstLayout() is only called for the main frame, reset
393                // mFirstLayoutDone only for the main frames
394                mFirstLayoutDone = false;
395                mCommitted = false;
396                // remove pending draw to block update until mFirstLayoutDone is
397                // set to true in didFirstLayout()
398                mWebViewCore.removeMessages(WebViewCore.EventHub.WEBKIT_DRAW);
399            }
400        }
401    }
402
403    @SuppressWarnings("unused")
404    private void saveFormData(HashMap<String, String> data) {
405        if (mSettings.getSaveFormData()) {
406            final WebHistoryItem h = mCallbackProxy.getBackForwardList()
407                    .getCurrentItem();
408            if (h != null) {
409                String currentUrl = h.getUrl();
410                if (currentUrl != null) {
411                    mDatabase.setFormData(currentUrl, data);
412                }
413            }
414        }
415    }
416
417    @SuppressWarnings("unused")
418    private boolean shouldSaveFormData() {
419        if (mSettings.getSaveFormData()) {
420            final WebHistoryItem h = mCallbackProxy.getBackForwardList()
421                    .getCurrentItem();
422            return h != null && h.getUrl() != null;
423        }
424        return false;
425    }
426
427    /**
428     * native callback
429     * Indicates the WebKit has committed to the new load
430     */
431    private void transitionToCommitted(int loadType, boolean isMainFrame) {
432        // loadType is not used yet
433        if (isMainFrame) {
434            mCommitted = true;
435            mWebViewCore.getWebView().mViewManager.postResetStateAll();
436        }
437    }
438
439    /**
440     * native callback
441     * <p>
442     * Indicates the end of a new load.
443     * This method will be called once for the main frame.
444     */
445    private void loadFinished(String url, int loadType, boolean isMainFrame) {
446        // mIsMainFrame and isMainFrame are better be equal!!!
447
448        if (isMainFrame || loadType == FRAME_LOADTYPE_STANDARD) {
449            if (isMainFrame) {
450                resetLoadingStates();
451                mCallbackProxy.switchOutDrawHistory();
452                mCallbackProxy.onPageFinished(url);
453            }
454        }
455    }
456
457    /**
458     * We have received an SSL certificate for the main top-level page.
459     *
460     * !!!Called from the network thread!!!
461     */
462    void certificate(SslCertificate certificate) {
463        if (mIsMainFrame) {
464            // we want to make this call even if the certificate is null
465            // (ie, the site is not secure)
466            mCallbackProxy.onReceivedCertificate(certificate);
467        }
468    }
469
470    /**
471     * Destroy all native components of the BrowserFrame.
472     */
473    public void destroy() {
474        nativeDestroyFrame();
475        mBlockMessages = true;
476        removeCallbacksAndMessages(null);
477    }
478
479    /**
480     * Handle messages posted to us.
481     * @param msg The message to handle.
482     */
483    @Override
484    public void handleMessage(Message msg) {
485        if (mBlockMessages) {
486            return;
487        }
488        switch (msg.what) {
489            case FRAME_COMPLETED: {
490                if (mSettings.getSavePassword() && hasPasswordField()) {
491                    WebHistoryItem item = mCallbackProxy.getBackForwardList()
492                            .getCurrentItem();
493                    if (item != null) {
494                        WebAddress uri = new WebAddress(item.getUrl());
495                        String schemePlusHost = uri.getScheme() + uri.getHost();
496                        String[] up =
497                                mDatabase.getUsernamePassword(schemePlusHost);
498                        if (up != null && up[0] != null) {
499                            setUsernamePassword(up[0], up[1]);
500                        }
501                    }
502                }
503                if (!JniUtil.useChromiumHttpStack()) {
504                    WebViewWorker.getHandler().sendEmptyMessage(
505                            WebViewWorker.MSG_TRIM_CACHE);
506                }
507                break;
508            }
509
510            case POLICY_FUNCTION: {
511                nativeCallPolicyFunction(msg.arg1, msg.arg2);
512                break;
513            }
514
515            case ORIENTATION_CHANGED: {
516                if (mOrientation != msg.arg1) {
517                    mOrientation = msg.arg1;
518                    nativeOrientationChanged(msg.arg1);
519                }
520                break;
521            }
522
523            default:
524                break;
525        }
526    }
527
528    /**
529     * Punch-through for WebCore to set the document
530     * title. Inform the Activity of the new title.
531     * @param title The new title of the document.
532     */
533    private void setTitle(String title) {
534        // FIXME: The activity must call getTitle (a native method) to get the
535        // title. We should try and cache the title if we can also keep it in
536        // sync with the document.
537        mCallbackProxy.onReceivedTitle(title);
538    }
539
540    /**
541     * Retrieves the render tree of this frame and puts it as the object for
542     * the message and sends the message.
543     * @param callback the message to use to send the render tree
544     */
545    public void externalRepresentation(Message callback) {
546        callback.obj = externalRepresentation();;
547        callback.sendToTarget();
548    }
549
550    /**
551     * Return the render tree as a string
552     */
553    private native String externalRepresentation();
554
555    /**
556     * Retrieves the visual text of the frames, puts it as the object for
557     * the message and sends the message.
558     * @param callback the message to use to send the visual text
559     */
560    public void documentAsText(Message callback) {
561        StringBuilder text = new StringBuilder();
562        if (callback.arg1 != 0) {
563            // Dump top frame as text.
564            text.append(documentAsText());
565        }
566        if (callback.arg2 != 0) {
567            // Dump child frames as text.
568            text.append(childFramesAsText());
569        }
570        callback.obj = text.toString();
571        callback.sendToTarget();
572    }
573
574    /**
575     * Return the text drawn on the screen as a string
576     */
577    private native String documentAsText();
578
579    /**
580     * Return the text drawn on the child frames as a string
581     */
582    private native String childFramesAsText();
583
584    /*
585     * This method is called by WebCore to inform the frame that
586     * the Javascript window object has been cleared.
587     * We should re-attach any attached js interfaces.
588     */
589    private void windowObjectCleared(int nativeFramePointer) {
590        if (mJSInterfaceMap != null) {
591            Iterator iter = mJSInterfaceMap.keySet().iterator();
592            while (iter.hasNext())  {
593                String interfaceName = (String) iter.next();
594                Object object = mJSInterfaceMap.get(interfaceName);
595                if (object != null) {
596                    nativeAddJavascriptInterface(nativeFramePointer,
597                            mJSInterfaceMap.get(interfaceName), interfaceName);
598                }
599            }
600        }
601    }
602
603    /**
604     * This method is called by WebCore to check whether application
605     * wants to hijack url loading
606     */
607    public boolean handleUrl(String url) {
608        if (mLoadInitFromJava == true) {
609            return false;
610        }
611        if (mCallbackProxy.shouldOverrideUrlLoading(url)) {
612            // if the url is hijacked, reset the state of the BrowserFrame
613            didFirstLayout();
614            return true;
615        } else {
616            return false;
617        }
618    }
619
620    public void addJavascriptInterface(Object obj, String interfaceName) {
621        assert obj != null;
622        removeJavascriptInterface(interfaceName);
623        if (mJSInterfaceMap == null) {
624            mJSInterfaceMap = new HashMap<String, Object>();
625        }
626        mJSInterfaceMap.put(interfaceName, obj);
627    }
628
629    public void removeJavascriptInterface(String interfaceName) {
630        if (mJSInterfaceMap == null) {
631            return;
632        }
633        if (mJSInterfaceMap.containsKey(interfaceName)) {
634            mJSInterfaceMap.remove(interfaceName);
635        }
636    }
637
638    /**
639     * Called by JNI.  Given a URI, find the associated file and return its size
640     * @param uri A String representing the URI of the desired file.
641     * @return int The size of the given file.
642     */
643    private int getFileSize(String uri) {
644        int size = 0;
645        try {
646            InputStream stream = mContext.getContentResolver()
647                            .openInputStream(Uri.parse(uri));
648            size = stream.available();
649            stream.close();
650        } catch (Exception e) {}
651        return size;
652    }
653
654    /**
655     * Called by JNI.  Given a URI, a buffer, and an offset into the buffer,
656     * copy the resource into buffer.
657     * @param uri A String representing the URI of the desired file.
658     * @param buffer The byte array to copy the data into.
659     * @param offset The offet into buffer to place the data.
660     * @param expectedSize The size that the buffer has allocated for this file.
661     * @return int The size of the given file, or zero if it fails.
662     */
663    private int getFile(String uri, byte[] buffer, int offset,
664            int expectedSize) {
665        int size = 0;
666        try {
667            InputStream stream = mContext.getContentResolver()
668                            .openInputStream(Uri.parse(uri));
669            size = stream.available();
670            if (size <= expectedSize && buffer != null
671                    && buffer.length - offset >= size) {
672                stream.read(buffer, offset, size);
673            } else {
674                size = 0;
675            }
676            stream.close();
677        } catch (java.io.FileNotFoundException e) {
678            Log.e(LOGTAG, "FileNotFoundException:" + e);
679            size = 0;
680        } catch (java.io.IOException e2) {
681            Log.e(LOGTAG, "IOException: " + e2);
682            size = 0;
683        }
684        return size;
685    }
686
687    /**
688     * Get the InputStream for an Android resource
689     * There are three different kinds of android resources:
690     * - file:///android_res
691     * - file:///android_asset
692     * - content://
693     * @param url The url to load.
694     * @return An InputStream to the android resource
695     */
696    private InputStream inputStreamForAndroidResource(String url) {
697        // This list needs to be kept in sync with the list in
698        // external/webkit/WebKit/android/WebCoreSupport/WebUrlLoaderClient.cpp
699        final String ANDROID_ASSET = "file:///android_asset/";
700        final String ANDROID_RESOURCE = "file:///android_res/";
701        final String ANDROID_CONTENT = "content:";
702
703        // file:///android_res
704        if (url.startsWith(ANDROID_RESOURCE)) {
705            url = url.replaceFirst(ANDROID_RESOURCE, "");
706            if (url == null || url.length() == 0) {
707                Log.e(LOGTAG, "url has length 0 " + url);
708                return null;
709            }
710            int slash = url.indexOf('/');
711            int dot = url.indexOf('.', slash);
712            if (slash == -1 || dot == -1) {
713                Log.e(LOGTAG, "Incorrect res path: " + url);
714                return null;
715            }
716            String subClassName = url.substring(0, slash);
717            String fieldName = url.substring(slash + 1, dot);
718            String errorMsg = null;
719            try {
720                final Class<?> d = mContext.getApplicationContext()
721                        .getClassLoader().loadClass(
722                                mContext.getPackageName() + ".R$"
723                                        + subClassName);
724                final java.lang.reflect.Field field = d.getField(fieldName);
725                final int id = field.getInt(null);
726                TypedValue value = new TypedValue();
727                mContext.getResources().getValue(id, value, true);
728                if (value.type == TypedValue.TYPE_STRING) {
729                    return mContext.getAssets().openNonAsset(
730                            value.assetCookie, value.string.toString(),
731                            AssetManager.ACCESS_STREAMING);
732                } else {
733                    // Old stack only supports TYPE_STRING for res files
734                    Log.e(LOGTAG, "not of type string: " + url);
735                    return null;
736                }
737            } catch (Exception e) {
738                Log.e(LOGTAG, "Exception: " + url);
739                return null;
740            }
741
742        // file:///android_asset
743        } else if (url.startsWith(ANDROID_ASSET)) {
744            url = url.replaceFirst(ANDROID_ASSET, "");
745            try {
746                AssetManager assets = mContext.getAssets();
747                return assets.open(url, AssetManager.ACCESS_STREAMING);
748            } catch (IOException e) {
749                return null;
750            }
751
752        // content://
753        } else if (mSettings.getAllowContentAccess() &&
754                   url.startsWith(ANDROID_CONTENT)) {
755            try {
756                // Strip off mimetype, for compatibility with ContentLoader.java
757                // If we don't do this, we can fail to load Gmail attachments,
758                // because the URL being loaded doesn't exactly match the URL we
759                // have permission to read.
760                int mimeIndex = url.lastIndexOf('?');
761                if (mimeIndex != -1) {
762                    url = url.substring(0, mimeIndex);
763                }
764                Uri uri = Uri.parse(url);
765                return mContext.getContentResolver().openInputStream(uri);
766            } catch (Exception e) {
767                Log.e(LOGTAG, "Exception: " + url);
768                return null;
769            }
770        } else {
771            return null;
772        }
773    }
774
775    /**
776     * Start loading a resource.
777     * @param loaderHandle The native ResourceLoader that is the target of the
778     *                     data.
779     * @param url The url to load.
780     * @param method The http method.
781     * @param headers The http headers.
782     * @param postData If the method is "POST" postData is sent as the request
783     *                 body. Is null when empty.
784     * @param postDataIdentifier If the post data contained form this is the form identifier, otherwise it is 0.
785     * @param cacheMode The cache mode to use when loading this resource. See WebSettings.setCacheMode
786     * @param mainResource True if the this resource is the main request, not a supporting resource
787     * @param userGesture
788     * @param synchronous True if the load is synchronous.
789     * @return A newly created LoadListener object.
790     */
791    private LoadListener startLoadingResource(int loaderHandle,
792                                              String url,
793                                              String method,
794                                              HashMap headers,
795                                              byte[] postData,
796                                              long postDataIdentifier,
797                                              int cacheMode,
798                                              boolean mainResource,
799                                              boolean userGesture,
800                                              boolean synchronous,
801                                              String username,
802                                              String password) {
803        PerfChecker checker = new PerfChecker();
804
805        if (mSettings.getCacheMode() != WebSettings.LOAD_DEFAULT) {
806            cacheMode = mSettings.getCacheMode();
807        }
808
809        if (method.equals("POST")) {
810            // Don't use the cache on POSTs when issuing a normal POST
811            // request.
812            if (cacheMode == WebSettings.LOAD_NORMAL) {
813                cacheMode = WebSettings.LOAD_NO_CACHE;
814            }
815            String[] ret = getUsernamePassword();
816            if (ret != null) {
817                String domUsername = ret[0];
818                String domPassword = ret[1];
819                maybeSavePassword(postData, domUsername, domPassword);
820            }
821        }
822
823        // is this resource the main-frame top-level page?
824        boolean isMainFramePage = mIsMainFrame;
825
826        if (DebugFlags.BROWSER_FRAME) {
827            Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method="
828                    + method + ", postData=" + postData + ", isMainFramePage="
829                    + isMainFramePage + ", mainResource=" + mainResource
830                    + ", userGesture=" + userGesture);
831        }
832
833        // Create a LoadListener
834        LoadListener loadListener = LoadListener.getLoadListener(mContext,
835                this, url, loaderHandle, synchronous, isMainFramePage,
836                mainResource, userGesture, postDataIdentifier, username, password);
837
838        if (LoadListener.getNativeLoaderCount() > MAX_OUTSTANDING_REQUESTS) {
839            // send an error message, so that loadListener can be deleted
840            // after this is returned. This is important as LoadListener's
841            // nativeError will remove the request from its DocLoader's request
842            // list. But the set up is not done until this method is returned.
843            loadListener.error(
844                    android.net.http.EventHandler.ERROR, mContext.getString(
845                            com.android.internal.R.string.httpErrorTooManyRequests));
846            return loadListener;
847        }
848
849        // Note that we are intentionally skipping
850        // inputStreamForAndroidResource.  This is so that FrameLoader will use
851        // the various StreamLoader classes to handle assets.
852        FrameLoader loader = new FrameLoader(loadListener, mSettings, method,
853                mCallbackProxy.shouldInterceptRequest(url));
854        loader.setHeaders(headers);
855        loader.setPostData(postData);
856        // Set the load mode to the mode used for the current page.
857        // If WebKit wants validation, go to network directly.
858        loader.setCacheMode(headers.containsKey("If-Modified-Since")
859                || headers.containsKey("If-None-Match") ?
860                        WebSettings.LOAD_NO_CACHE : cacheMode);
861        // Set referrer to current URL?
862        if (!loader.executeLoad()) {
863            checker.responseAlert("startLoadingResource fail");
864        }
865        checker.responseAlert("startLoadingResource succeed");
866
867        return !synchronous ? loadListener : null;
868    }
869
870    /**
871     * If this looks like a POST request (form submission) containing a username
872     * and password, give the user the option of saving them. Will either do
873     * nothing, or block until the UI interaction is complete.
874     *
875     * Called by startLoadingResource when using the Apache HTTP stack.
876     * Called directly by WebKit when using the Chrome HTTP stack.
877     *
878     * @param postData The data about to be sent as the body of a POST request.
879     * @param username The username entered by the user (sniffed from the DOM).
880     * @param password The password entered by the user (sniffed from the DOM).
881     */
882    private void maybeSavePassword(
883            byte[] postData, String username, String password) {
884        if (postData == null
885                || username == null || username.isEmpty()
886                || password == null || password.isEmpty()) {
887            return; // No password to save.
888        }
889
890        if (!mSettings.getSavePassword()) {
891            return; // User doesn't want to save passwords.
892        }
893
894        try {
895            if (DebugFlags.BROWSER_FRAME) {
896                Assert.assertNotNull(mCallbackProxy.getBackForwardList()
897                        .getCurrentItem());
898            }
899            WebAddress uri = new WebAddress(mCallbackProxy
900                    .getBackForwardList().getCurrentItem().getUrl());
901            String schemePlusHost = uri.getScheme() + uri.getHost();
902            // Check to see if the username & password appear in
903            // the post data (there could be another form on the
904            // page and that was posted instead.
905            String postString = new String(postData);
906            if (postString.contains(URLEncoder.encode(username)) &&
907                    postString.contains(URLEncoder.encode(password))) {
908                String[] saved = mDatabase.getUsernamePassword(
909                        schemePlusHost);
910                if (saved != null) {
911                    // null username implies that user has chosen not to
912                    // save password
913                    if (saved[0] != null) {
914                        // non-null username implies that user has
915                        // chosen to save password, so update the
916                        // recorded password
917                        mDatabase.setUsernamePassword(
918                                schemePlusHost, username, password);
919                    }
920                } else {
921                    // CallbackProxy will handle creating the resume
922                    // message
923                    mCallbackProxy.onSavePassword(schemePlusHost, username,
924                            password, null);
925                }
926            }
927        } catch (ParseException ex) {
928            // if it is bad uri, don't save its password
929        }
930    }
931
932    // Called by jni from the chrome network stack.
933    private WebResourceResponse shouldInterceptRequest(String url) {
934        InputStream androidResource = inputStreamForAndroidResource(url);
935        if (androidResource != null) {
936            return new WebResourceResponse(null, null, androidResource);
937        }
938        WebResourceResponse response = mCallbackProxy.shouldInterceptRequest(url);
939        if (response == null && "browser:incognito".equals(url)) {
940            try {
941                Resources res = mContext.getResources();
942                InputStream ins = res.openRawResource(
943                        com.android.internal.R.raw.incognito_mode_start_page);
944                response = new WebResourceResponse("text/html", "utf8", ins);
945            } catch (NotFoundException ex) {
946                // This shouldn't happen, but try and gracefully handle it jic
947                Log.w(LOGTAG, "Failed opening raw.incognito_mode_start_page", ex);
948            }
949        }
950        return response;
951    }
952
953    /**
954     * Set the progress for the browser activity.  Called by native code.
955     * Uses a delay so it does not happen too often.
956     * @param newProgress An int between zero and one hundred representing
957     *                    the current progress percentage of loading the page.
958     */
959    private void setProgress(int newProgress) {
960        mCallbackProxy.onProgressChanged(newProgress);
961        if (newProgress == 100) {
962            sendMessageDelayed(obtainMessage(FRAME_COMPLETED), 100);
963        }
964        // FIXME: Need to figure out a better way to switch out of the history
965        // drawing mode. Maybe we can somehow compare the history picture with
966        // the current picture, and switch when it contains more content.
967        if (mFirstLayoutDone && newProgress > TRANSITION_SWITCH_THRESHOLD) {
968            mCallbackProxy.switchOutDrawHistory();
969        }
970    }
971
972    /**
973     * Send the icon to the activity for display.
974     * @param icon A Bitmap representing a page's favicon.
975     */
976    private void didReceiveIcon(Bitmap icon) {
977        mCallbackProxy.onReceivedIcon(icon);
978    }
979
980    // Called by JNI when an apple-touch-icon attribute was found.
981    private void didReceiveTouchIconUrl(String url, boolean precomposed) {
982        mCallbackProxy.onReceivedTouchIconUrl(url, precomposed);
983    }
984
985    /**
986     * Request a new window from the client.
987     * @return The BrowserFrame object stored in the new WebView.
988     */
989    private BrowserFrame createWindow(boolean dialog, boolean userGesture) {
990        return mCallbackProxy.createWindow(dialog, userGesture);
991    }
992
993    /**
994     * Try to focus this WebView.
995     */
996    private void requestFocus() {
997        mCallbackProxy.onRequestFocus();
998    }
999
1000    /**
1001     * Close this frame and window.
1002     */
1003    private void closeWindow(WebViewCore w) {
1004        mCallbackProxy.onCloseWindow(w.getWebView());
1005    }
1006
1007    // XXX: Must match PolicyAction in FrameLoaderTypes.h in webcore
1008    static final int POLICY_USE = 0;
1009    static final int POLICY_IGNORE = 2;
1010
1011    private void decidePolicyForFormResubmission(int policyFunction) {
1012        Message dontResend = obtainMessage(POLICY_FUNCTION, policyFunction,
1013                POLICY_IGNORE);
1014        Message resend = obtainMessage(POLICY_FUNCTION, policyFunction,
1015                POLICY_USE);
1016        mCallbackProxy.onFormResubmission(dontResend, resend);
1017    }
1018
1019    /**
1020     * Tell the activity to update its global history.
1021     */
1022    private void updateVisitedHistory(String url, boolean isReload) {
1023        mCallbackProxy.doUpdateVisitedHistory(url, isReload);
1024    }
1025
1026    /**
1027     * Get the CallbackProxy for sending messages to the UI thread.
1028     */
1029    /* package */ CallbackProxy getCallbackProxy() {
1030        return mCallbackProxy;
1031    }
1032
1033    /**
1034     * Returns the User Agent used by this frame
1035     */
1036    String getUserAgentString() {
1037        return mSettings.getUserAgentString();
1038    }
1039
1040    // These ids need to be in sync with enum rawResId in PlatformBridge.h
1041    private static final int NODOMAIN = 1;
1042    private static final int LOADERROR = 2;
1043    private static final int DRAWABLEDIR = 3;
1044    private static final int FILE_UPLOAD_LABEL = 4;
1045    private static final int RESET_LABEL = 5;
1046    private static final int SUBMIT_LABEL = 6;
1047    private static final int FILE_UPLOAD_NO_FILE_CHOSEN = 7;
1048
1049    String getRawResFilename(int id) {
1050        int resid;
1051        switch (id) {
1052            case NODOMAIN:
1053                resid = com.android.internal.R.raw.nodomain;
1054                break;
1055
1056            case LOADERROR:
1057                resid = com.android.internal.R.raw.loaderror;
1058                break;
1059
1060            case DRAWABLEDIR:
1061                // use one known resource to find the drawable directory
1062                resid = com.android.internal.R.drawable.btn_check_off;
1063                break;
1064
1065            case FILE_UPLOAD_LABEL:
1066                return mContext.getResources().getString(
1067                        com.android.internal.R.string.upload_file);
1068
1069            case RESET_LABEL:
1070                return mContext.getResources().getString(
1071                        com.android.internal.R.string.reset);
1072
1073            case SUBMIT_LABEL:
1074                return mContext.getResources().getString(
1075                        com.android.internal.R.string.submit);
1076
1077            case FILE_UPLOAD_NO_FILE_CHOSEN:
1078                return mContext.getResources().getString(
1079                        com.android.internal.R.string.no_file_chosen);
1080
1081            default:
1082                Log.e(LOGTAG, "getRawResFilename got incompatible resource ID");
1083                return "";
1084        }
1085        TypedValue value = new TypedValue();
1086        mContext.getResources().getValue(resid, value, true);
1087        if (id == DRAWABLEDIR) {
1088            String path = value.string.toString();
1089            int index = path.lastIndexOf('/');
1090            if (index < 0) {
1091                Log.e(LOGTAG, "Can't find drawable directory.");
1092                return "";
1093            }
1094            return path.substring(0, index + 1);
1095        }
1096        return value.string.toString();
1097    }
1098
1099    private float density() {
1100        return mContext.getResources().getDisplayMetrics().density;
1101    }
1102
1103    /**
1104     * Called by JNI when the native HTTP stack gets an authentication request.
1105     *
1106     * We delegate the request to CallbackProxy, and route its response to
1107     * {@link #nativeAuthenticationProceed(int, String, String)} or
1108     * {@link #nativeAuthenticationCancel(int)}.
1109     *
1110     * We don't care what thread the callback is invoked on. All threading is
1111     * handled on the C++ side, because the WebKit thread may be blocked on a
1112     * synchronous call and unable to pump our MessageQueue.
1113     */
1114    private void didReceiveAuthenticationChallenge(
1115            final int handle, String host, String realm, final boolean useCachedCredentials) {
1116
1117        HttpAuthHandler handler = new HttpAuthHandler() {
1118
1119            @Override
1120            public boolean useHttpAuthUsernamePassword() {
1121                return useCachedCredentials;
1122            }
1123
1124            @Override
1125            public void proceed(String username, String password) {
1126                nativeAuthenticationProceed(handle, username, password);
1127            }
1128
1129            @Override
1130            public void cancel() {
1131                nativeAuthenticationCancel(handle);
1132            }
1133        };
1134        mCallbackProxy.onReceivedHttpAuthRequest(handler, host, realm);
1135    }
1136
1137    /**
1138     * Called by JNI when the native HTTP(S) stack gets an invalid cert chain.
1139     *
1140     * We delegate the request to CallbackProxy, and route its response to
1141     * {@link #nativeSslCertErrorProceed(int)} or
1142     * {@link #nativeSslCertErrorCancel(int, int)}.
1143     */
1144    private void reportSslCertError(final int handle, final int cert_error, byte cert_der[]) {
1145        final SslError ssl_error;
1146        try {
1147            X509Certificate cert = new X509CertImpl(cert_der);
1148            ssl_error = new SslError(cert_error, cert);
1149        } catch (IOException e) {
1150            // Can't get the certificate, not much to do.
1151            Log.e(LOGTAG, "Can't get the certificate from WebKit, canceling");
1152            nativeSslCertErrorCancel(handle, cert_error);
1153            return;
1154        }
1155
1156        SslErrorHandler handler = new SslErrorHandler() {
1157
1158            @Override
1159            public void proceed() {
1160                SslCertLookupTable.getInstance().Allow(ssl_error);
1161                nativeSslCertErrorProceed(handle);
1162            }
1163
1164            @Override
1165            public void cancel() {
1166                SslCertLookupTable.getInstance().Deny(ssl_error);
1167                nativeSslCertErrorCancel(handle, cert_error);
1168            }
1169        };
1170
1171        if (SslCertLookupTable.getInstance().IsAllowed(ssl_error)) {
1172            nativeSslCertErrorProceed(handle);
1173        } else {
1174            mCallbackProxy.onReceivedSslError(handler, ssl_error);
1175        }
1176    }
1177
1178    /**
1179     * Called by JNI when the native HTTP stack needs to download a file.
1180     *
1181     * We delegate the request to CallbackProxy, which owns the current app's
1182     * DownloadListener.
1183     */
1184    private void downloadStart(String url, String userAgent,
1185            String contentDisposition, String mimeType, long contentLength) {
1186        // This will only work if the url ends with the filename
1187        if (mimeType.isEmpty()) {
1188            try {
1189                String extension = url.substring(url.lastIndexOf('.') + 1);
1190                mimeType = libcore.net.MimeUtils.guessMimeTypeFromExtension(extension);
1191                // MimeUtils might return null, not sure if downloadmanager is happy with that
1192                if (mimeType == null)
1193                    mimeType = "";
1194            } catch(IndexOutOfBoundsException exception) {
1195                // mimeType string end with a '.', not much to do
1196            }
1197        }
1198        mimeType = MimeTypeMap.getSingleton().remapGenericMimeType(
1199                mimeType, url, contentDisposition);
1200
1201        if (CertTool.getCertType(mimeType) != null) {
1202            mKeyStoreHandler = new KeyStoreHandler(mimeType);
1203        } else {
1204            mCallbackProxy.onDownloadStart(url, userAgent,
1205                contentDisposition, mimeType, contentLength);
1206        }
1207    }
1208
1209    /**
1210     * Called by JNI for Chrome HTTP stack when the Java side needs to access the data.
1211     */
1212    private void didReceiveData(byte data[], int size) {
1213        if (mKeyStoreHandler != null) mKeyStoreHandler.didReceiveData(data, size);
1214    }
1215
1216    private void didFinishLoading() {
1217      if (mKeyStoreHandler != null) {
1218          mKeyStoreHandler.installCert(mContext);
1219          mKeyStoreHandler = null;
1220      }
1221    }
1222
1223    /**
1224     * Called by JNI when we load a page over SSL.
1225     */
1226    private void setCertificate(byte cert_der[]) {
1227        try {
1228            X509Certificate cert = new X509CertImpl(cert_der);
1229            mCallbackProxy.onReceivedCertificate(new SslCertificate(cert));
1230        } catch (IOException e) {
1231            // Can't get the certificate, not much to do.
1232            Log.e(LOGTAG, "Can't get the certificate from WebKit, canceling");
1233            return;
1234        }
1235    }
1236
1237    //==========================================================================
1238    // native functions
1239    //==========================================================================
1240
1241    /**
1242     * Create a new native frame for a given WebView
1243     * @param w     A WebView that the frame draws into.
1244     * @param am    AssetManager to use to get assets.
1245     * @param list  The native side will add and remove items from this list as
1246     *              the native list changes.
1247     */
1248    private native void nativeCreateFrame(WebViewCore w, AssetManager am,
1249            WebBackForwardList list);
1250
1251    /**
1252     * Destroy the native frame.
1253     */
1254    public native void nativeDestroyFrame();
1255
1256    private native void nativeCallPolicyFunction(int policyFunction,
1257            int decision);
1258
1259    /**
1260     * Reload the current main frame.
1261     */
1262    public native void reload(boolean allowStale);
1263
1264    /**
1265     * Go back or forward the number of steps given.
1266     * @param steps A negative or positive number indicating the direction
1267     *              and number of steps to move.
1268     */
1269    private native void nativeGoBackOrForward(int steps);
1270
1271    /**
1272     * stringByEvaluatingJavaScriptFromString will execute the
1273     * JS passed in in the context of this browser frame.
1274     * @param script A javascript string to execute
1275     *
1276     * @return string result of execution or null
1277     */
1278    public native String stringByEvaluatingJavaScriptFromString(String script);
1279
1280    /**
1281     * Add a javascript interface to the main frame.
1282     */
1283    private native void nativeAddJavascriptInterface(int nativeFramePointer,
1284            Object obj, String interfaceName);
1285
1286    /**
1287     * Enable or disable the native cache.
1288     */
1289    /* FIXME: The native cache is always on for now until we have a better
1290     * solution for our 2 caches. */
1291    private native void setCacheDisabled(boolean disabled);
1292
1293    public native boolean cacheDisabled();
1294
1295    public native void clearCache();
1296
1297    /**
1298     * Returns false if the url is bad.
1299     */
1300    private native void nativeLoadUrl(String url, Map<String, String> headers);
1301
1302    private native void nativePostUrl(String url, byte[] postData);
1303
1304    private native void nativeLoadData(String baseUrl, String data,
1305            String mimeType, String encoding, String historyUrl);
1306
1307    /**
1308     * Stop loading the current page.
1309     */
1310    public void stopLoading() {
1311        if (mIsMainFrame) {
1312            resetLoadingStates();
1313        }
1314        nativeStopLoading();
1315    }
1316
1317    private native void nativeStopLoading();
1318
1319    /**
1320     * Return true if the document has images.
1321     */
1322    public native boolean documentHasImages();
1323
1324    /**
1325     * @return TRUE if there is a password field in the current frame
1326     */
1327    private native boolean hasPasswordField();
1328
1329    /**
1330     * Get username and password in the current frame. If found, String[0] is
1331     * username and String[1] is password. Otherwise return NULL.
1332     * @return String[]
1333     */
1334    private native String[] getUsernamePassword();
1335
1336    /**
1337     * Set username and password to the proper fields in the current frame
1338     * @param username
1339     * @param password
1340     */
1341    private native void setUsernamePassword(String username, String password);
1342
1343    private native String nativeSaveWebArchive(String basename, boolean autoname);
1344
1345    private native void nativeOrientationChanged(int orientation);
1346
1347    private native void nativeAuthenticationProceed(int handle, String username, String password);
1348    private native void nativeAuthenticationCancel(int handle);
1349
1350    private native void nativeSslCertErrorProceed(int handle);
1351    private native void nativeSslCertErrorCancel(int handle, int cert_error);
1352}
1353