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