BrowserFrame.java revision bf52c0ea10482ad761e4fbc8ce07e9517b8541f6
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.ViewRootImpl;
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            ViewRootImpl.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 url = WebTextView.urlForAutoCompleteData(h.getUrl());
425                if (url != null) {
426                    mDatabase.setFormData(url, 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    void certificate(SslCertificate certificate) {
476        if (mIsMainFrame) {
477            // we want to make this call even if the certificate is null
478            // (ie, the site is not secure)
479            mCallbackProxy.onReceivedCertificate(certificate);
480        }
481    }
482
483    /**
484     * Destroy all native components of the BrowserFrame.
485     */
486    public void destroy() {
487        nativeDestroyFrame();
488        mBlockMessages = true;
489        removeCallbacksAndMessages(null);
490    }
491
492    /**
493     * Handle messages posted to us.
494     * @param msg The message to handle.
495     */
496    @Override
497    public void handleMessage(Message msg) {
498        if (mBlockMessages) {
499            return;
500        }
501        switch (msg.what) {
502            case FRAME_COMPLETED: {
503                if (mSettings.getSavePassword() && hasPasswordField()) {
504                    WebHistoryItem item = mCallbackProxy.getBackForwardList()
505                            .getCurrentItem();
506                    if (item != null) {
507                        WebAddress uri = new WebAddress(item.getUrl());
508                        String schemePlusHost = uri.getScheme() + uri.getHost();
509                        String[] up =
510                                mDatabase.getUsernamePassword(schemePlusHost);
511                        if (up != null && up[0] != null) {
512                            setUsernamePassword(up[0], up[1]);
513                        }
514                    }
515                }
516                if (!JniUtil.useChromiumHttpStack()) {
517                    WebViewWorker.getHandler().sendEmptyMessage(
518                            WebViewWorker.MSG_TRIM_CACHE);
519                }
520                break;
521            }
522
523            case POLICY_FUNCTION: {
524                nativeCallPolicyFunction(msg.arg1, msg.arg2);
525                break;
526            }
527
528            case ORIENTATION_CHANGED: {
529                if (mOrientation != msg.arg1) {
530                    mOrientation = msg.arg1;
531                    nativeOrientationChanged(msg.arg1);
532                }
533                break;
534            }
535
536            default:
537                break;
538        }
539    }
540
541    /**
542     * Punch-through for WebCore to set the document
543     * title. Inform the Activity of the new title.
544     * @param title The new title of the document.
545     */
546    private void setTitle(String title) {
547        // FIXME: The activity must call getTitle (a native method) to get the
548        // title. We should try and cache the title if we can also keep it in
549        // sync with the document.
550        mCallbackProxy.onReceivedTitle(title);
551    }
552
553    /**
554     * Retrieves the render tree of this frame and puts it as the object for
555     * the message and sends the message.
556     * @param callback the message to use to send the render tree
557     */
558    public void externalRepresentation(Message callback) {
559        callback.obj = externalRepresentation();;
560        callback.sendToTarget();
561    }
562
563    /**
564     * Return the render tree as a string
565     */
566    private native String externalRepresentation();
567
568    /**
569     * Retrieves the visual text of the frames, puts it as the object for
570     * the message and sends the message.
571     * @param callback the message to use to send the visual text
572     */
573    public void documentAsText(Message callback) {
574        StringBuilder text = new StringBuilder();
575        if (callback.arg1 != 0) {
576            // Dump top frame as text.
577            text.append(documentAsText());
578        }
579        if (callback.arg2 != 0) {
580            // Dump child frames as text.
581            text.append(childFramesAsText());
582        }
583        callback.obj = text.toString();
584        callback.sendToTarget();
585    }
586
587    /**
588     * Return the text drawn on the screen as a string
589     */
590    private native String documentAsText();
591
592    /**
593     * Return the text drawn on the child frames as a string
594     */
595    private native String childFramesAsText();
596
597    /*
598     * This method is called by WebCore to inform the frame that
599     * the Javascript window object has been cleared.
600     * We should re-attach any attached js interfaces.
601     */
602    private void windowObjectCleared(int nativeFramePointer) {
603        Iterator<String> iter = mJavaScriptObjects.keySet().iterator();
604        while (iter.hasNext())  {
605            String interfaceName = iter.next();
606            Object object = mJavaScriptObjects.get(interfaceName);
607            if (object != null) {
608                nativeAddJavascriptInterface(nativeFramePointer,
609                        mJavaScriptObjects.get(interfaceName), interfaceName);
610            }
611        }
612        mRemovedJavaScriptObjects.clear();
613
614        stringByEvaluatingJavaScriptFromString(SearchBoxImpl.JS_BRIDGE);
615    }
616
617    /**
618     * This method is called by WebCore to check whether application
619     * wants to hijack url loading
620     */
621    public boolean handleUrl(String url) {
622        if (mLoadInitFromJava == true) {
623            return false;
624        }
625        if (mCallbackProxy.shouldOverrideUrlLoading(url)) {
626            // if the url is hijacked, reset the state of the BrowserFrame
627            didFirstLayout();
628            return true;
629        } else {
630            return false;
631        }
632    }
633
634    public void addJavascriptInterface(Object obj, String interfaceName) {
635        assert obj != null;
636        removeJavascriptInterface(interfaceName);
637
638        mJavaScriptObjects.put(interfaceName, obj);
639    }
640
641    public void removeJavascriptInterface(String interfaceName) {
642        // We keep a reference to the removed object because the native side holds only a weak
643        // reference and we need to allow the object to continue to be used until the page has been
644        // navigated.
645        if (mJavaScriptObjects.containsKey(interfaceName)) {
646            mRemovedJavaScriptObjects.add(mJavaScriptObjects.remove(interfaceName));
647        }
648    }
649
650    /**
651     * Called by JNI.  Given a URI, find the associated file and return its size
652     * @param uri A String representing the URI of the desired file.
653     * @return int The size of the given file.
654     */
655    private int getFileSize(String uri) {
656        int size = 0;
657        try {
658            InputStream stream = mContext.getContentResolver()
659                            .openInputStream(Uri.parse(uri));
660            size = stream.available();
661            stream.close();
662        } catch (Exception e) {}
663        return size;
664    }
665
666    /**
667     * Called by JNI.  Given a URI, a buffer, and an offset into the buffer,
668     * copy the resource into buffer.
669     * @param uri A String representing the URI of the desired file.
670     * @param buffer The byte array to copy the data into.
671     * @param offset The offet into buffer to place the data.
672     * @param expectedSize The size that the buffer has allocated for this file.
673     * @return int The size of the given file, or zero if it fails.
674     */
675    private int getFile(String uri, byte[] buffer, int offset,
676            int expectedSize) {
677        int size = 0;
678        try {
679            InputStream stream = mContext.getContentResolver()
680                            .openInputStream(Uri.parse(uri));
681            size = stream.available();
682            if (size <= expectedSize && buffer != null
683                    && buffer.length - offset >= size) {
684                stream.read(buffer, offset, size);
685            } else {
686                size = 0;
687            }
688            stream.close();
689        } catch (java.io.FileNotFoundException e) {
690            Log.e(LOGTAG, "FileNotFoundException:" + e);
691            size = 0;
692        } catch (java.io.IOException e2) {
693            Log.e(LOGTAG, "IOException: " + e2);
694            size = 0;
695        }
696        return size;
697    }
698
699    /**
700     * Get the InputStream for an Android resource
701     * There are three different kinds of android resources:
702     * - file:///android_res
703     * - file:///android_asset
704     * - content://
705     * @param url The url to load.
706     * @return An InputStream to the android resource
707     */
708    private InputStream inputStreamForAndroidResource(String url) {
709        // This list needs to be kept in sync with the list in
710        // external/webkit/WebKit/android/WebCoreSupport/WebUrlLoaderClient.cpp
711        final String ANDROID_ASSET = "file:///android_asset/";
712        final String ANDROID_RESOURCE = "file:///android_res/";
713        final String ANDROID_CONTENT = "content:";
714
715        // file:///android_res
716        if (url.startsWith(ANDROID_RESOURCE)) {
717            url = url.replaceFirst(ANDROID_RESOURCE, "");
718            if (url == null || url.length() == 0) {
719                Log.e(LOGTAG, "url has length 0 " + url);
720                return null;
721            }
722            int slash = url.indexOf('/');
723            int dot = url.indexOf('.', slash);
724            if (slash == -1 || dot == -1) {
725                Log.e(LOGTAG, "Incorrect res path: " + url);
726                return null;
727            }
728            String subClassName = url.substring(0, slash);
729            String fieldName = url.substring(slash + 1, dot);
730            String errorMsg = null;
731            try {
732                final Class<?> d = mContext.getApplicationContext()
733                        .getClassLoader().loadClass(
734                                mContext.getPackageName() + ".R$"
735                                        + subClassName);
736                final java.lang.reflect.Field field = d.getField(fieldName);
737                final int id = field.getInt(null);
738                TypedValue value = new TypedValue();
739                mContext.getResources().getValue(id, value, true);
740                if (value.type == TypedValue.TYPE_STRING) {
741                    return mContext.getAssets().openNonAsset(
742                            value.assetCookie, value.string.toString(),
743                            AssetManager.ACCESS_STREAMING);
744                } else {
745                    // Old stack only supports TYPE_STRING for res files
746                    Log.e(LOGTAG, "not of type string: " + url);
747                    return null;
748                }
749            } catch (Exception e) {
750                Log.e(LOGTAG, "Exception: " + url);
751                return null;
752            }
753
754        // file:///android_asset
755        } else if (url.startsWith(ANDROID_ASSET)) {
756            url = url.replaceFirst(ANDROID_ASSET, "");
757            try {
758                AssetManager assets = mContext.getAssets();
759                return assets.open(url, AssetManager.ACCESS_STREAMING);
760            } catch (IOException e) {
761                return null;
762            }
763
764        // content://
765        } else if (mSettings.getAllowContentAccess() &&
766                   url.startsWith(ANDROID_CONTENT)) {
767            try {
768                // Strip off mimetype, for compatibility with ContentLoader.java
769                // If we don't do this, we can fail to load Gmail attachments,
770                // because the URL being loaded doesn't exactly match the URL we
771                // have permission to read.
772                int mimeIndex = url.lastIndexOf('?');
773                if (mimeIndex != -1) {
774                    url = url.substring(0, mimeIndex);
775                }
776                Uri uri = Uri.parse(url);
777                return mContext.getContentResolver().openInputStream(uri);
778            } catch (Exception e) {
779                Log.e(LOGTAG, "Exception: " + url);
780                return null;
781            }
782        } else {
783            return null;
784        }
785    }
786
787    /**
788     * Start loading a resource.
789     * @param loaderHandle The native ResourceLoader that is the target of the
790     *                     data.
791     * @param url The url to load.
792     * @param method The http method.
793     * @param headers The http headers.
794     * @param postData If the method is "POST" postData is sent as the request
795     *                 body. Is null when empty.
796     * @param postDataIdentifier If the post data contained form this is the form identifier, otherwise it is 0.
797     * @param cacheMode The cache mode to use when loading this resource. See WebSettings.setCacheMode
798     * @param mainResource True if the this resource is the main request, not a supporting resource
799     * @param userGesture
800     * @param synchronous True if the load is synchronous.
801     * @return A newly created LoadListener object.
802     */
803    private LoadListener startLoadingResource(int loaderHandle,
804                                              String url,
805                                              String method,
806                                              HashMap headers,
807                                              byte[] postData,
808                                              long postDataIdentifier,
809                                              int cacheMode,
810                                              boolean mainResource,
811                                              boolean userGesture,
812                                              boolean synchronous,
813                                              String username,
814                                              String password) {
815        PerfChecker checker = new PerfChecker();
816
817        if (mSettings.getCacheMode() != WebSettings.LOAD_DEFAULT) {
818            cacheMode = mSettings.getCacheMode();
819        }
820
821        if (method.equals("POST")) {
822            // Don't use the cache on POSTs when issuing a normal POST
823            // request.
824            if (cacheMode == WebSettings.LOAD_NORMAL) {
825                cacheMode = WebSettings.LOAD_NO_CACHE;
826            }
827            String[] ret = getUsernamePassword();
828            if (ret != null) {
829                String domUsername = ret[0];
830                String domPassword = ret[1];
831                maybeSavePassword(postData, domUsername, domPassword);
832            }
833        }
834
835        // is this resource the main-frame top-level page?
836        boolean isMainFramePage = mIsMainFrame;
837
838        if (DebugFlags.BROWSER_FRAME) {
839            Log.v(LOGTAG, "startLoadingResource: url=" + url + ", method="
840                    + method + ", postData=" + postData + ", isMainFramePage="
841                    + isMainFramePage + ", mainResource=" + mainResource
842                    + ", userGesture=" + userGesture);
843        }
844
845        // Create a LoadListener
846        LoadListener loadListener = LoadListener.getLoadListener(mContext,
847                this, url, loaderHandle, synchronous, isMainFramePage,
848                mainResource, userGesture, postDataIdentifier, username, password);
849
850        if (LoadListener.getNativeLoaderCount() > MAX_OUTSTANDING_REQUESTS) {
851            // send an error message, so that loadListener can be deleted
852            // after this is returned. This is important as LoadListener's
853            // nativeError will remove the request from its DocLoader's request
854            // list. But the set up is not done until this method is returned.
855            loadListener.error(
856                    android.net.http.EventHandler.ERROR, mContext.getString(
857                            com.android.internal.R.string.httpErrorTooManyRequests));
858            return loadListener;
859        }
860
861        // Note that we are intentionally skipping
862        // inputStreamForAndroidResource.  This is so that FrameLoader will use
863        // the various StreamLoader classes to handle assets.
864        FrameLoader loader = new FrameLoader(loadListener, mSettings, method,
865                mCallbackProxy.shouldInterceptRequest(url));
866        loader.setHeaders(headers);
867        loader.setPostData(postData);
868        // Set the load mode to the mode used for the current page.
869        // If WebKit wants validation, go to network directly.
870        loader.setCacheMode(headers.containsKey("If-Modified-Since")
871                || headers.containsKey("If-None-Match") ?
872                        WebSettings.LOAD_NO_CACHE : cacheMode);
873        // Set referrer to current URL?
874        if (!loader.executeLoad()) {
875            checker.responseAlert("startLoadingResource fail");
876        }
877        checker.responseAlert("startLoadingResource succeed");
878
879        return !synchronous ? loadListener : null;
880    }
881
882    /**
883     * If this looks like a POST request (form submission) containing a username
884     * and password, give the user the option of saving them. Will either do
885     * nothing, or block until the UI interaction is complete.
886     *
887     * Called by startLoadingResource when using the Apache HTTP stack.
888     * Called directly by WebKit when using the Chrome HTTP stack.
889     *
890     * @param postData The data about to be sent as the body of a POST request.
891     * @param username The username entered by the user (sniffed from the DOM).
892     * @param password The password entered by the user (sniffed from the DOM).
893     */
894    private void maybeSavePassword(
895            byte[] postData, String username, String password) {
896        if (postData == null
897                || username == null || username.isEmpty()
898                || password == null || password.isEmpty()) {
899            return; // No password to save.
900        }
901
902        if (!mSettings.getSavePassword()) {
903            return; // User doesn't want to save passwords.
904        }
905
906        try {
907            if (DebugFlags.BROWSER_FRAME) {
908                Assert.assertNotNull(mCallbackProxy.getBackForwardList()
909                        .getCurrentItem());
910            }
911            WebAddress uri = new WebAddress(mCallbackProxy
912                    .getBackForwardList().getCurrentItem().getUrl());
913            String schemePlusHost = uri.getScheme() + uri.getHost();
914            // Check to see if the username & password appear in
915            // the post data (there could be another form on the
916            // page and that was posted instead.
917            String postString = new String(postData);
918            if (postString.contains(URLEncoder.encode(username)) &&
919                    postString.contains(URLEncoder.encode(password))) {
920                String[] saved = mDatabase.getUsernamePassword(
921                        schemePlusHost);
922                if (saved != null) {
923                    // null username implies that user has chosen not to
924                    // save password
925                    if (saved[0] != null) {
926                        // non-null username implies that user has
927                        // chosen to save password, so update the
928                        // recorded password
929                        mDatabase.setUsernamePassword(
930                                schemePlusHost, username, password);
931                    }
932                } else {
933                    // CallbackProxy will handle creating the resume
934                    // message
935                    mCallbackProxy.onSavePassword(schemePlusHost, username,
936                            password, null);
937                }
938            }
939        } catch (ParseException ex) {
940            // if it is bad uri, don't save its password
941        }
942    }
943
944    // Called by jni from the chrome network stack.
945    private WebResourceResponse shouldInterceptRequest(String url) {
946        InputStream androidResource = inputStreamForAndroidResource(url);
947        if (androidResource != null) {
948            return new WebResourceResponse(null, null, androidResource);
949        }
950        WebResourceResponse response = mCallbackProxy.shouldInterceptRequest(url);
951        if (response == null && "browser:incognito".equals(url)) {
952            try {
953                Resources res = mContext.getResources();
954                InputStream ins = res.openRawResource(
955                        com.android.internal.R.raw.incognito_mode_start_page);
956                response = new WebResourceResponse("text/html", "utf8", ins);
957            } catch (NotFoundException ex) {
958                // This shouldn't happen, but try and gracefully handle it jic
959                Log.w(LOGTAG, "Failed opening raw.incognito_mode_start_page", ex);
960            }
961        }
962        return response;
963    }
964
965    /**
966     * Set the progress for the browser activity.  Called by native code.
967     * Uses a delay so it does not happen too often.
968     * @param newProgress An int between zero and one hundred representing
969     *                    the current progress percentage of loading the page.
970     */
971    private void setProgress(int newProgress) {
972        mCallbackProxy.onProgressChanged(newProgress);
973        if (newProgress == 100) {
974            sendMessageDelayed(obtainMessage(FRAME_COMPLETED), 100);
975        }
976        // FIXME: Need to figure out a better way to switch out of the history
977        // drawing mode. Maybe we can somehow compare the history picture with
978        // the current picture, and switch when it contains more content.
979        if (mFirstLayoutDone && newProgress > TRANSITION_SWITCH_THRESHOLD) {
980            mCallbackProxy.switchOutDrawHistory();
981        }
982    }
983
984    /**
985     * Send the icon to the activity for display.
986     * @param icon A Bitmap representing a page's favicon.
987     */
988    private void didReceiveIcon(Bitmap icon) {
989        mCallbackProxy.onReceivedIcon(icon);
990    }
991
992    // Called by JNI when an apple-touch-icon attribute was found.
993    private void didReceiveTouchIconUrl(String url, boolean precomposed) {
994        mCallbackProxy.onReceivedTouchIconUrl(url, precomposed);
995    }
996
997    /**
998     * Request a new window from the client.
999     * @return The BrowserFrame object stored in the new WebView.
1000     */
1001    private BrowserFrame createWindow(boolean dialog, boolean userGesture) {
1002        return mCallbackProxy.createWindow(dialog, userGesture);
1003    }
1004
1005    /**
1006     * Try to focus this WebView.
1007     */
1008    private void requestFocus() {
1009        mCallbackProxy.onRequestFocus();
1010    }
1011
1012    /**
1013     * Close this frame and window.
1014     */
1015    private void closeWindow(WebViewCore w) {
1016        mCallbackProxy.onCloseWindow(w.getWebView());
1017    }
1018
1019    // XXX: Must match PolicyAction in FrameLoaderTypes.h in webcore
1020    static final int POLICY_USE = 0;
1021    static final int POLICY_IGNORE = 2;
1022
1023    private void decidePolicyForFormResubmission(int policyFunction) {
1024        Message dontResend = obtainMessage(POLICY_FUNCTION, policyFunction,
1025                POLICY_IGNORE);
1026        Message resend = obtainMessage(POLICY_FUNCTION, policyFunction,
1027                POLICY_USE);
1028        mCallbackProxy.onFormResubmission(dontResend, resend);
1029    }
1030
1031    /**
1032     * Tell the activity to update its global history.
1033     */
1034    private void updateVisitedHistory(String url, boolean isReload) {
1035        mCallbackProxy.doUpdateVisitedHistory(url, isReload);
1036    }
1037
1038    /**
1039     * Get the CallbackProxy for sending messages to the UI thread.
1040     */
1041    /* package */ CallbackProxy getCallbackProxy() {
1042        return mCallbackProxy;
1043    }
1044
1045    /**
1046     * Returns the User Agent used by this frame
1047     */
1048    String getUserAgentString() {
1049        return mSettings.getUserAgentString();
1050    }
1051
1052    // These ids need to be in sync with enum rawResId in PlatformBridge.h
1053    private static final int NODOMAIN = 1;
1054    private static final int LOADERROR = 2;
1055    /* package */ static final int DRAWABLEDIR = 3;
1056    private static final int FILE_UPLOAD_LABEL = 4;
1057    private static final int RESET_LABEL = 5;
1058    private static final int SUBMIT_LABEL = 6;
1059    private static final int FILE_UPLOAD_NO_FILE_CHOSEN = 7;
1060
1061    private String getRawResFilename(int id) {
1062        return getRawResFilename(id, mContext);
1063    }
1064    /* package */ static String getRawResFilename(int id, Context context) {
1065        int resid;
1066        switch (id) {
1067            case NODOMAIN:
1068                resid = com.android.internal.R.raw.nodomain;
1069                break;
1070
1071            case LOADERROR:
1072                resid = com.android.internal.R.raw.loaderror;
1073                break;
1074
1075            case DRAWABLEDIR:
1076                // use one known resource to find the drawable directory
1077                resid = com.android.internal.R.drawable.btn_check_off;
1078                break;
1079
1080            case FILE_UPLOAD_LABEL:
1081                return context.getResources().getString(
1082                        com.android.internal.R.string.upload_file);
1083
1084            case RESET_LABEL:
1085                return context.getResources().getString(
1086                        com.android.internal.R.string.reset);
1087
1088            case SUBMIT_LABEL:
1089                return context.getResources().getString(
1090                        com.android.internal.R.string.submit);
1091
1092            case FILE_UPLOAD_NO_FILE_CHOSEN:
1093                return context.getResources().getString(
1094                        com.android.internal.R.string.no_file_chosen);
1095
1096            default:
1097                Log.e(LOGTAG, "getRawResFilename got incompatible resource ID");
1098                return "";
1099        }
1100        TypedValue value = new TypedValue();
1101        context.getResources().getValue(resid, value, true);
1102        if (id == DRAWABLEDIR) {
1103            String path = value.string.toString();
1104            int index = path.lastIndexOf('/');
1105            if (index < 0) {
1106                Log.e(LOGTAG, "Can't find drawable directory.");
1107                return "";
1108            }
1109            return path.substring(0, index + 1);
1110        }
1111        return value.string.toString();
1112    }
1113
1114    private float density() {
1115        return mContext.getResources().getDisplayMetrics().density;
1116    }
1117
1118    /**
1119     * Called by JNI when the native HTTP stack gets an authentication request.
1120     *
1121     * We delegate the request to CallbackProxy, and route its response to
1122     * {@link #nativeAuthenticationProceed(int, String, String)} or
1123     * {@link #nativeAuthenticationCancel(int)}.
1124     *
1125     * We don't care what thread the callback is invoked on. All threading is
1126     * handled on the C++ side, because the WebKit thread may be blocked on a
1127     * synchronous call and unable to pump our MessageQueue.
1128     */
1129    private void didReceiveAuthenticationChallenge(
1130            final int handle, String host, String realm, final boolean useCachedCredentials,
1131            final boolean suppressDialog) {
1132
1133        HttpAuthHandler handler = new HttpAuthHandler() {
1134
1135            @Override
1136            public boolean useHttpAuthUsernamePassword() {
1137                return useCachedCredentials;
1138            }
1139
1140            @Override
1141            public void proceed(String username, String password) {
1142                nativeAuthenticationProceed(handle, username, password);
1143            }
1144
1145            @Override
1146            public void cancel() {
1147                nativeAuthenticationCancel(handle);
1148            }
1149
1150            @Override
1151            public boolean suppressDialog() {
1152                return suppressDialog;
1153            }
1154        };
1155        mCallbackProxy.onReceivedHttpAuthRequest(handler, host, realm);
1156    }
1157
1158    /**
1159     * Called by JNI when the Chromium HTTP stack gets an invalid certificate chain.
1160     *
1161     * We delegate the request to CallbackProxy, and route its response to
1162     * {@link #nativeSslCertErrorProceed(int)} or
1163     * {@link #nativeSslCertErrorCancel(int, int)}.
1164     */
1165    private void reportSslCertError(final int handle, final int certError, byte certDER[],
1166            String url) {
1167        final SslError sslError;
1168        try {
1169            X509Certificate cert = new X509CertImpl(certDER);
1170            SslCertificate sslCert = new SslCertificate(cert);
1171            sslError = SslError.SslErrorFromChromiumErrorCode(certError, sslCert, url);
1172        } catch (IOException e) {
1173            // Can't get the certificate, not much to do.
1174            Log.e(LOGTAG, "Can't get the certificate from WebKit, canceling");
1175            nativeSslCertErrorCancel(handle, certError);
1176            return;
1177        }
1178
1179        if (SslCertLookupTable.getInstance().isAllowed(sslError)) {
1180            nativeSslCertErrorProceed(handle);
1181            return;
1182        }
1183
1184        SslErrorHandler handler = new SslErrorHandler() {
1185            @Override
1186            public void proceed() {
1187                SslCertLookupTable.getInstance().setIsAllowed(sslError);
1188                nativeSslCertErrorProceed(handle);
1189            }
1190            @Override
1191            public void cancel() {
1192                nativeSslCertErrorCancel(handle, certError);
1193            }
1194        };
1195        mCallbackProxy.onReceivedSslError(handler, sslError);
1196    }
1197
1198    /**
1199     * Called by JNI when the native HTTPS stack gets a client
1200     * certificate request.
1201     *
1202     * We delegate the request to CallbackProxy, and route its response to
1203     * {@link #nativeSslClientCert(int, X509Certificate)}.
1204     */
1205    private void requestClientCert(int handle, byte[] host_and_port_bytes) {
1206        String host_and_port = new String(host_and_port_bytes, Charsets.UTF_8);
1207        SslClientCertLookupTable table = SslClientCertLookupTable.getInstance();
1208        if (table.IsAllowed(host_and_port)) {
1209            // previously allowed
1210            nativeSslClientCert(handle,
1211                                table.PrivateKey(host_and_port),
1212                                table.CertificateChain(host_and_port));
1213        } else if (table.IsDenied(host_and_port)) {
1214            // previously denied
1215            nativeSslClientCert(handle, null, null);
1216        } else {
1217            // previously ignored or new
1218            mCallbackProxy.onReceivedClientCertRequest(
1219                    new ClientCertRequestHandler(this, handle, host_and_port, table),
1220                    host_and_port);
1221        }
1222    }
1223
1224    /**
1225     * Called by JNI when the native HTTP stack needs to download a file.
1226     *
1227     * We delegate the request to CallbackProxy, which owns the current app's
1228     * DownloadListener.
1229     */
1230    private void downloadStart(String url, String userAgent,
1231            String contentDisposition, String mimeType, long contentLength) {
1232        // This will only work if the url ends with the filename
1233        if (mimeType.isEmpty()) {
1234            try {
1235                String extension = url.substring(url.lastIndexOf('.') + 1);
1236                mimeType = libcore.net.MimeUtils.guessMimeTypeFromExtension(extension);
1237                // MimeUtils might return null, not sure if downloadmanager is happy with that
1238                if (mimeType == null)
1239                    mimeType = "";
1240            } catch(IndexOutOfBoundsException exception) {
1241                // mimeType string end with a '.', not much to do
1242            }
1243        }
1244        mimeType = MimeTypeMap.getSingleton().remapGenericMimeType(
1245                mimeType, url, contentDisposition);
1246
1247        if (CertTool.getCertType(mimeType) != null) {
1248            mKeyStoreHandler = new KeyStoreHandler(mimeType);
1249        } else {
1250            mCallbackProxy.onDownloadStart(url, userAgent,
1251                contentDisposition, mimeType, contentLength);
1252        }
1253    }
1254
1255    /**
1256     * Called by JNI for Chrome HTTP stack when the Java side needs to access the data.
1257     */
1258    private void didReceiveData(byte data[], int size) {
1259        if (mKeyStoreHandler != null) mKeyStoreHandler.didReceiveData(data, size);
1260    }
1261
1262    private void didFinishLoading() {
1263      if (mKeyStoreHandler != null) {
1264          mKeyStoreHandler.installCert(mContext);
1265          mKeyStoreHandler = null;
1266      }
1267    }
1268
1269    /**
1270     * Called by JNI when we load a page over SSL.
1271     */
1272    private void setCertificate(byte cert_der[]) {
1273        try {
1274            X509Certificate cert = new X509CertImpl(cert_der);
1275            mCallbackProxy.onReceivedCertificate(new SslCertificate(cert));
1276        } catch (IOException e) {
1277            // Can't get the certificate, not much to do.
1278            Log.e(LOGTAG, "Can't get the certificate from WebKit, canceling");
1279            return;
1280        }
1281    }
1282
1283    /*package*/ SearchBox getSearchBox() {
1284        return mSearchBox;
1285    }
1286
1287    /**
1288     * Called by JNI when processing the X-Auto-Login header.
1289     */
1290    private void autoLogin(String realm, String account, String args) {
1291        mCallbackProxy.onReceivedLoginRequest(realm, account, args);
1292    }
1293
1294    //==========================================================================
1295    // native functions
1296    //==========================================================================
1297
1298    /**
1299     * Create a new native frame for a given WebView
1300     * @param w     A WebView that the frame draws into.
1301     * @param am    AssetManager to use to get assets.
1302     * @param list  The native side will add and remove items from this list as
1303     *              the native list changes.
1304     */
1305    private native void nativeCreateFrame(WebViewCore w, AssetManager am,
1306            WebBackForwardList list);
1307
1308    /**
1309     * Destroy the native frame.
1310     */
1311    public native void nativeDestroyFrame();
1312
1313    private native void nativeCallPolicyFunction(int policyFunction,
1314            int decision);
1315
1316    /**
1317     * Reload the current main frame.
1318     */
1319    public native void reload(boolean allowStale);
1320
1321    /**
1322     * Go back or forward the number of steps given.
1323     * @param steps A negative or positive number indicating the direction
1324     *              and number of steps to move.
1325     */
1326    private native void nativeGoBackOrForward(int steps);
1327
1328    /**
1329     * stringByEvaluatingJavaScriptFromString will execute the
1330     * JS passed in in the context of this browser frame.
1331     * @param script A javascript string to execute
1332     *
1333     * @return string result of execution or null
1334     */
1335    public native String stringByEvaluatingJavaScriptFromString(String script);
1336
1337    /**
1338     * Add a javascript interface to the main frame.
1339     */
1340    private native void nativeAddJavascriptInterface(int nativeFramePointer,
1341            Object obj, String interfaceName);
1342
1343    /**
1344     * Enable or disable the native cache.
1345     */
1346    /* FIXME: The native cache is always on for now until we have a better
1347     * solution for our 2 caches. */
1348    private native void setCacheDisabled(boolean disabled);
1349
1350    public native boolean cacheDisabled();
1351
1352    public native void clearCache();
1353
1354    /**
1355     * Returns false if the url is bad.
1356     */
1357    private native void nativeLoadUrl(String url, Map<String, String> headers);
1358
1359    private native void nativePostUrl(String url, byte[] postData);
1360
1361    private native void nativeLoadData(String baseUrl, String data,
1362            String mimeType, String encoding, String historyUrl);
1363
1364    /**
1365     * Stop loading the current page.
1366     */
1367    public void stopLoading() {
1368        if (mIsMainFrame) {
1369            resetLoadingStates();
1370        }
1371        nativeStopLoading();
1372    }
1373
1374    private native void nativeStopLoading();
1375
1376    /**
1377     * Return true if the document has images.
1378     */
1379    public native boolean documentHasImages();
1380
1381    /**
1382     * @return TRUE if there is a password field in the current frame
1383     */
1384    private native boolean hasPasswordField();
1385
1386    /**
1387     * Get username and password in the current frame. If found, String[0] is
1388     * username and String[1] is password. Otherwise return NULL.
1389     * @return String[]
1390     */
1391    private native String[] getUsernamePassword();
1392
1393    /**
1394     * Set username and password to the proper fields in the current frame
1395     * @param username
1396     * @param password
1397     */
1398    private native void setUsernamePassword(String username, String password);
1399
1400    private native String nativeSaveWebArchive(String basename, boolean autoname);
1401
1402    private native void nativeOrientationChanged(int orientation);
1403
1404    private native void nativeAuthenticationProceed(int handle, String username, String password);
1405    private native void nativeAuthenticationCancel(int handle);
1406
1407    private native void nativeSslCertErrorProceed(int handle);
1408    private native void nativeSslCertErrorCancel(int handle, int certError);
1409
1410    native void nativeSslClientCert(int handle,
1411                                    byte[] pkcs8EncodedPrivateKey,
1412                                    byte[][] asn1DerEncodedCertificateChain);
1413}
1414