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