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