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