blob: 0e93453babe738dc77d7f546e3be7ac2dbbc3876 [file] [log] [blame]
The Android Open Source Project0c908882009-03-03 19:32:16 -08001/*
2 * Copyright (C) 2007 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 com.android.browser;
18
19import android.content.Context;
Patrick Scott2ed6edb2009-04-22 10:07:45 -040020import android.graphics.Picture;
The Android Open Source Project0c908882009-03-03 19:32:16 -080021import android.net.http.SslError;
22import android.os.Bundle;
23import android.os.Message;
The Android Open Source Project0c908882009-03-03 19:32:16 -080024import android.util.Log;
25import android.view.Gravity;
26import android.view.LayoutInflater;
27import android.view.View;
28import android.view.ViewGroup;
29import android.view.View.OnClickListener;
30import android.webkit.HttpAuthHandler;
31import android.webkit.JsPromptResult;
32import android.webkit.JsResult;
33import android.webkit.SslErrorHandler;
34import android.webkit.WebBackForwardList;
35import android.webkit.WebChromeClient;
36import android.webkit.WebHistoryItem;
37import android.webkit.WebView;
38import android.webkit.WebViewClient;
39import android.widget.FrameLayout;
40import android.widget.ImageButton;
41
42import java.io.File;
Patrick Scott2ed6edb2009-04-22 10:07:45 -040043import java.io.FileInputStream;
The Android Open Source Project0c908882009-03-03 19:32:16 -080044import java.util.ArrayList;
45import java.util.Vector;
46
47class TabControl {
48 // Log Tag
49 private static final String LOGTAG = "TabControl";
50 // Maximum number of tabs.
51 static final int MAX_TABS = 8;
52 // Static instance of an empty callback.
53 private static final WebViewClient mEmptyClient =
54 new WebViewClient();
55 // Instance of BackgroundChromeClient for background tabs.
56 private final BackgroundChromeClient mBackgroundChromeClient =
57 new BackgroundChromeClient();
58 // Private array of WebViews that are used as tabs.
59 private ArrayList<Tab> mTabs = new ArrayList<Tab>(MAX_TABS);
60 // Queue of most recently viewed tabs.
61 private ArrayList<Tab> mTabQueue = new ArrayList<Tab>(MAX_TABS);
62 // Current position in mTabs.
63 private int mCurrentTab = -1;
64 // A private instance of BrowserActivity to interface with when adding and
65 // switching between tabs.
66 private final BrowserActivity mActivity;
67 // Inflation service for making subwindows.
68 private final LayoutInflater mInflateService;
69 // Subclass of WebViewClient used in subwindows to notify the main
70 // WebViewClient of certain WebView activities.
71 private class SubWindowClient extends WebViewClient {
72 // The main WebViewClient.
73 private final WebViewClient mClient;
74
75 SubWindowClient(WebViewClient client) {
76 mClient = client;
77 }
78 @Override
79 public void doUpdateVisitedHistory(WebView view, String url,
80 boolean isReload) {
81 mClient.doUpdateVisitedHistory(view, url, isReload);
82 }
83 @Override
84 public boolean shouldOverrideUrlLoading(WebView view, String url) {
85 return mClient.shouldOverrideUrlLoading(view, url);
86 }
87 @Override
88 public void onReceivedSslError(WebView view, SslErrorHandler handler,
89 SslError error) {
90 mClient.onReceivedSslError(view, handler, error);
91 }
92 @Override
93 public void onReceivedHttpAuthRequest(WebView view,
94 HttpAuthHandler handler, String host, String realm) {
95 mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
96 }
97 @Override
98 public void onFormResubmission(WebView view, Message dontResend,
99 Message resend) {
100 mClient.onFormResubmission(view, dontResend, resend);
101 }
102 @Override
103 public void onReceivedError(WebView view, int errorCode,
104 String description, String failingUrl) {
105 mClient.onReceivedError(view, errorCode, description, failingUrl);
106 }
107 }
108 // Subclass of WebChromeClient to display javascript dialogs.
109 private class SubWindowChromeClient extends WebChromeClient {
110 // This subwindow's tab.
111 private final Tab mTab;
112 // The main WebChromeClient.
113 private final WebChromeClient mClient;
114
115 SubWindowChromeClient(Tab t, WebChromeClient client) {
116 mTab = t;
117 mClient = client;
118 }
119 @Override
120 public void onProgressChanged(WebView view, int newProgress) {
121 mClient.onProgressChanged(view, newProgress);
122 }
123 @Override
124 public boolean onCreateWindow(WebView view, boolean dialog,
125 boolean userGesture, android.os.Message resultMsg) {
126 return mClient.onCreateWindow(view, dialog, userGesture, resultMsg);
127 }
128 @Override
129 public void onCloseWindow(WebView window) {
Dave Bort31a6d1c2009-04-13 15:56:49 -0700130 if (Browser.DEBUG && window != mTab.mSubView) {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800131 throw new AssertionError("Can't close the window");
132 }
133 mActivity.dismissSubWindow(mTab);
134 }
135 }
136 // Background WebChromeClient for focusing tabs
137 private class BackgroundChromeClient extends WebChromeClient {
138 @Override
139 public void onRequestFocus(WebView view) {
140 Tab t = getTabFromView(view);
141 if (t != getCurrentTab()) {
142 mActivity.showTab(t);
143 }
144 }
145 }
146
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400147 // Extra saved information for displaying the tab in the picker.
148 public static class PickerData {
149 String mUrl;
150 String mTitle;
151 float mScale;
152 int mScrollX;
153 int mScrollY;
154 int mWidth;
155 Picture mPicture;
156 // This can be null. When a new picture comes in, this view should be
157 // invalidated to show the new picture.
158 FakeWebView mFakeWebView;
159 }
160
The Android Open Source Project0c908882009-03-03 19:32:16 -0800161 /**
162 * Private class for maintaining Tabs with a main WebView and a subwindow.
163 */
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400164 public class Tab implements WebView.PictureListener {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800165 // Main WebView
166 private WebView mMainView;
167 // Subwindow WebView
168 private WebView mSubView;
169 // Subwindow container
170 private View mSubViewContainer;
171 // Subwindow callback
172 private SubWindowClient mSubViewClient;
173 // Subwindow chrome callback
174 private SubWindowChromeClient mSubViewChromeClient;
175 // Saved bundle for when we are running low on memory. It contains the
176 // information needed to restore the WebView if the user goes back to
177 // the tab.
178 private Bundle mSavedState;
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400179 // Data used when displaying the tab in the picker.
180 private PickerData mPickerData;
181
The Android Open Source Project0c908882009-03-03 19:32:16 -0800182 // Parent Tab. This is the Tab that created this Tab, or null
183 // if the Tab was created by the UI
184 private Tab mParentTab;
185 // Tab that constructed by this Tab. This is used when this
186 // Tab is destroyed, it clears all mParentTab values in the
187 // children.
188 private Vector<Tab> mChildTabs;
189
190 private Boolean mCloseOnExit;
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700191 // Application identifier used to find tabs that another application
192 // wants to reuse.
193 private String mAppId;
194 // Keep the original url around to avoid killing the old WebView if the
195 // url has not changed.
196 private String mOriginalUrl;
The Android Open Source Project0c908882009-03-03 19:32:16 -0800197
198 // Construct a new tab
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700199 private Tab(WebView w, boolean closeOnExit, String appId, String url) {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800200 mMainView = w;
201 mCloseOnExit = closeOnExit;
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700202 mAppId = appId;
203 mOriginalUrl = url;
The Android Open Source Project0c908882009-03-03 19:32:16 -0800204 }
205
206 /**
207 * Return the top window of this tab; either the subwindow if it is not
208 * null or the main window.
209 * @return The top window of this tab.
210 */
211 public WebView getTopWindow() {
212 if (mSubView != null) {
213 return mSubView;
214 }
215 return mMainView;
216 }
217
218 /**
219 * Return the main window of this tab. Note: if a tab is freed in the
220 * background, this can return null. It is only guaranteed to be
221 * non-null for the current tab.
222 * @return The main WebView of this tab.
223 */
224 public WebView getWebView() {
225 return mMainView;
226 }
227
228 /**
229 * Return the subwindow of this tab or null if there is no subwindow.
230 * @return The subwindow of this tab or null.
231 */
232 public WebView getSubWebView() {
233 return mSubView;
234 }
235
236 /**
237 * Return the subwindow container of this tab or null if there is no
238 * subwindow.
239 * @return The subwindow's container View.
240 */
241 public View getSubWebViewContainer() {
242 return mSubViewContainer;
243 }
244
245 /**
246 * Get the url of this tab. Valid after calling populatePickerData, but
247 * before calling wipePickerData, or if the webview has been destroyed.
248 *
249 * @return The WebView's url or null.
250 */
251 public String getUrl() {
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400252 return mPickerData.mUrl;
The Android Open Source Project0c908882009-03-03 19:32:16 -0800253 }
254
255 /**
256 * Get the title of this tab. Valid after calling populatePickerData,
257 * but before calling wipePickerData, or if the webview has been
258 * destroyed. If the url has no title, use the url instead.
259 *
260 * @return The WebView's title (or url) or null.
261 */
262 public String getTitle() {
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400263 return mPickerData.mTitle;
264 }
265
266 /**
267 * Returns the picker data.
268 */
269 public PickerData getPickerData() {
270 return mPickerData;
The Android Open Source Project0c908882009-03-03 19:32:16 -0800271 }
272
273 private void setParentTab(Tab parent) {
274 mParentTab = parent;
275 // This tab may have been freed due to low memory. If that is the
276 // case, the parent tab index is already saved. If we are changing
277 // that index (most likely due to removing the parent tab) we must
278 // update the parent tab index in the saved Bundle.
279 if (mSavedState != null) {
280 if (parent == null) {
281 mSavedState.remove(PARENTTAB);
282 } else {
283 mSavedState.putInt(PARENTTAB, getTabIndex(parent));
284 }
285 }
286 }
287
288 /**
289 * When a Tab is created through the content of another Tab, then
290 * we associate the Tabs.
291 * @param child the Tab that was created from this Tab
292 */
293 public void addChildTab(Tab child) {
294 if (mChildTabs == null) {
295 mChildTabs = new Vector<Tab>();
296 }
297 mChildTabs.add(child);
298 child.setParentTab(this);
299 }
300
301 private void removeFromTree() {
302 // detach the children
303 if (mChildTabs != null) {
304 for(Tab t : mChildTabs) {
305 t.setParentTab(null);
306 }
307 }
308
309 // Find myself in my parent list
310 if (mParentTab != null) {
311 mParentTab.mChildTabs.remove(this);
312 }
313 }
314
315 /**
316 * If this Tab was created through another Tab, then this method
317 * returns that Tab.
318 * @return the Tab parent or null
319 */
320 public Tab getParentTab() {
321 return mParentTab;
322 }
323
324 /**
325 * Return whether this tab should be closed when it is backing out of
326 * the first page.
327 * @return TRUE if this tab should be closed when exit.
328 */
329 public boolean closeOnExit() {
330 return mCloseOnExit;
331 }
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400332
333 public void onNewPicture(WebView view, Picture p) {
334 if (mPickerData == null) {
335 return;
336 }
337
338 mPickerData.mPicture = p;
339 // Tell the FakeWebView to redraw.
340 if (mPickerData.mFakeWebView != null) {
341 mPickerData.mFakeWebView.invalidate();
342 }
343 }
The Android Open Source Project0c908882009-03-03 19:32:16 -0800344 };
345
346 // Directory to store thumbnails for each WebView.
347 private final File mThumbnailDir;
348
349 /**
350 * Construct a new TabControl object that interfaces with the given
351 * BrowserActivity instance.
352 * @param activity A BrowserActivity instance that TabControl will interface
353 * with.
354 */
355 TabControl(BrowserActivity activity) {
356 mActivity = activity;
357 mInflateService =
358 ((LayoutInflater) activity.getSystemService(
359 Context.LAYOUT_INFLATER_SERVICE));
360 mThumbnailDir = activity.getDir("thumbnails", 0);
361 }
362
363 File getThumbnailDir() {
364 return mThumbnailDir;
365 }
366
367 BrowserActivity getBrowserActivity() {
368 return mActivity;
369 }
370
371 /**
372 * Return the current tab's main WebView. This will always return the main
373 * WebView for a given tab and not a subwindow.
374 * @return The current tab's WebView.
375 */
376 WebView getCurrentWebView() {
377 Tab t = getTab(mCurrentTab);
378 if (t == null) {
379 return null;
380 }
381 return t.mMainView;
382 }
383
384 /**
385 * Return the current tab's top-level WebView. This can return a subwindow
386 * if one exists.
387 * @return The top-level WebView of the current tab.
388 */
389 WebView getCurrentTopWebView() {
390 Tab t = getTab(mCurrentTab);
391 if (t == null) {
392 return null;
393 }
394 return t.mSubView != null ? t.mSubView : t.mMainView;
395 }
396
397 /**
398 * Return the current tab's subwindow if it exists.
399 * @return The subwindow of the current tab or null if it doesn't exist.
400 */
401 WebView getCurrentSubWindow() {
402 Tab t = getTab(mCurrentTab);
403 if (t == null) {
404 return null;
405 }
406 return t.mSubView;
407 }
408
409 /**
410 * Return the tab at the specified index.
411 * @return The Tab for the specified index or null if the tab does not
412 * exist.
413 */
414 Tab getTab(int index) {
415 if (index >= 0 && index < mTabs.size()) {
416 return mTabs.get(index);
417 }
418 return null;
419 }
420
421 /**
422 * Return the current tab.
423 * @return The current tab.
424 */
425 Tab getCurrentTab() {
426 return getTab(mCurrentTab);
427 }
428
429 /**
430 * Return the current tab index.
431 * @return The current tab index
432 */
433 int getCurrentIndex() {
434 return mCurrentTab;
435 }
436
437 /**
438 * Given a Tab, find it's index
439 * @param Tab to find
440 * @return index of Tab or -1 if not found
441 */
442 int getTabIndex(Tab tab) {
443 return mTabs.indexOf(tab);
444 }
445
446 /**
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700447 * Create a new tab.
The Android Open Source Project0c908882009-03-03 19:32:16 -0800448 * @return The newly createTab or null if we have reached the maximum
449 * number of open tabs.
450 */
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700451 Tab createNewTab(boolean closeOnExit, String appId, String url) {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800452 int size = mTabs.size();
453 // Return false if we have maxed out on tabs
454 if (MAX_TABS == size) {
455 return null;
456 }
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700457 final WebView w = createNewWebView();
The Android Open Source Project0c908882009-03-03 19:32:16 -0800458 // Create a new tab and add it to the tab list
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700459 Tab t = new Tab(w, closeOnExit, appId, url);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800460 mTabs.add(t);
The Android Open Source Project4e5f5872009-03-09 11:52:14 -0700461 // Initially put the tab in the background.
462 putTabInBackground(t);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800463 return t;
464 }
465
466 /**
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700467 * Create a new tab with default values for closeOnExit(false),
468 * appId(null), and url(null).
469 */
470 Tab createNewTab() {
471 return createNewTab(false, null, null);
472 }
473
474 /**
The Android Open Source Project0c908882009-03-03 19:32:16 -0800475 * Remove the tab from the list. If the tab is the current tab shown, the
476 * last created tab will be shown.
477 * @param t The tab to be removed.
478 */
479 boolean removeTab(Tab t) {
480 if (t == null) {
481 return false;
482 }
483 // Only remove the tab if it is the current one.
484 if (getCurrentTab() == t) {
485 putTabInBackground(t);
486 }
487
488 // Only destroy the WebView if it still exists.
489 if (t.mMainView != null) {
490 // Take down the sub window.
491 dismissSubWindow(t);
492 // Remove the WebView's settings from the BrowserSettings list of
493 // observers.
494 BrowserSettings.getInstance().deleteObserver(
495 t.mMainView.getSettings());
496 // Destroy the main view and subview
497 t.mMainView.destroy();
498 t.mMainView = null;
499 }
500 // clear it's references to parent and children
501 t.removeFromTree();
502
503 // Remove it from our list of tabs.
504 mTabs.remove(t);
505
506 // The tab indices have shifted, update all the saved state so we point
507 // to the correct index.
508 for (Tab tab : mTabs) {
509 if (tab.mChildTabs != null) {
510 for (Tab child : tab.mChildTabs) {
511 child.setParentTab(tab);
512 }
513 }
514 }
515
516
517 // This tab may have been pushed in to the background and then closed.
518 // If the saved state contains a picture file, delete the file.
519 if (t.mSavedState != null) {
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400520 if (t.mSavedState.containsKey(CURRPICTURE)) {
521 new File(t.mSavedState.getString(CURRPICTURE)).delete();
The Android Open Source Project0c908882009-03-03 19:32:16 -0800522 }
523 }
524
525 // Remove it from the queue of viewed tabs.
526 mTabQueue.remove(t);
527 mCurrentTab = -1;
528 return true;
529 }
530
531 /**
532 * Clear the back/forward list for all the current tabs.
533 */
534 void clearHistory() {
535 int size = getTabCount();
536 for (int i = 0; i < size; i++) {
537 Tab t = mTabs.get(i);
538 // TODO: if a tab is freed due to low memory, its history is not
539 // cleared here.
540 if (t.mMainView != null) {
541 t.mMainView.clearHistory();
542 }
543 if (t.mSubView != null) {
544 t.mSubView.clearHistory();
545 }
546 }
547 }
548
549 /**
550 * Destroy all the tabs and subwindows
551 */
552 void destroy() {
553 BrowserSettings s = BrowserSettings.getInstance();
554 for (Tab t : mTabs) {
555 if (t.mMainView != null) {
556 dismissSubWindow(t);
557 s.deleteObserver(t.mMainView.getSettings());
558 t.mMainView.destroy();
559 t.mMainView = null;
560 }
561 }
562 mTabs.clear();
563 mTabQueue.clear();
564 }
565
566 /**
567 * Returns the number of tabs created.
568 * @return The number of tabs created.
569 */
570 int getTabCount() {
571 return mTabs.size();
572 }
573
574 // Used for saving and restoring each Tab
575 private static final String WEBVIEW = "webview";
576 private static final String NUMTABS = "numTabs";
577 private static final String CURRTAB = "currentTab";
578 private static final String CURRURL = "currentUrl";
579 private static final String CURRTITLE = "currentTitle";
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400580 private static final String CURRWIDTH = "currentWidth";
581 private static final String CURRPICTURE = "currentPicture";
The Android Open Source Project0c908882009-03-03 19:32:16 -0800582 private static final String CLOSEONEXIT = "closeonexit";
583 private static final String PARENTTAB = "parentTab";
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700584 private static final String APPID = "appid";
585 private static final String ORIGINALURL = "originalUrl";
The Android Open Source Project0c908882009-03-03 19:32:16 -0800586
587 /**
588 * Save the state of all the Tabs.
589 * @param outState The Bundle to save the state to.
590 */
591 void saveState(Bundle outState) {
592 final int numTabs = getTabCount();
593 outState.putInt(NUMTABS, numTabs);
594 final int index = getCurrentIndex();
595 outState.putInt(CURRTAB, (index >= 0 && index < numTabs) ? index : 0);
596 for (int i = 0; i < numTabs; i++) {
597 final Tab t = getTab(i);
598 if (saveState(t)) {
599 outState.putBundle(WEBVIEW + i, t.mSavedState);
600 }
601 }
602 }
603
604 /**
605 * Restore the state of all the tabs.
606 * @param inState The saved state of all the tabs.
607 * @return True if there were previous tabs that were restored. False if
608 * there was no saved state or restoring the state failed.
609 */
610 boolean restoreState(Bundle inState) {
611 final int numTabs = (inState == null)
612 ? -1 : inState.getInt(NUMTABS, -1);
613 if (numTabs == -1) {
614 return false;
615 } else {
616 final int currentTab = inState.getInt(CURRTAB, -1);
617 for (int i = 0; i < numTabs; i++) {
618 if (i == currentTab) {
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700619 Tab t = createNewTab();
The Android Open Source Project0c908882009-03-03 19:32:16 -0800620 // Me must set the current tab before restoring the state
621 // so that all the client classes are set.
622 setCurrentTab(t);
623 if (!restoreState(inState.getBundle(WEBVIEW + i), t)) {
624 Log.w(LOGTAG, "Fail in restoreState, load home page.");
625 t.mMainView.loadUrl(BrowserSettings.getInstance()
626 .getHomePage());
627 }
628 } else {
629 // Create a new tab and don't restore the state yet, add it
630 // to the tab list
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700631 Tab t = new Tab(null, false, null, null);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800632 t.mSavedState = inState.getBundle(WEBVIEW + i);
633 if (t.mSavedState != null) {
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400634 populatePickerDataFromSavedState(t);
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700635 // Need to maintain the app id and original url so we
636 // can possibly reuse this tab.
637 t.mAppId = t.mSavedState.getString(APPID);
638 t.mOriginalUrl = t.mSavedState.getString(ORIGINALURL);
The Android Open Source Project0c908882009-03-03 19:32:16 -0800639 }
640 mTabs.add(t);
641 mTabQueue.add(t);
642 }
643 }
644 // Rebuild the tree of tabs. Do this after all tabs have been
645 // created/restored so that the parent tab exists.
646 for (int i = 0; i < numTabs; i++) {
647 final Bundle b = inState.getBundle(WEBVIEW + i);
648 final Tab t = getTab(i);
649 if (b != null && t != null) {
650 final int parentIndex = b.getInt(PARENTTAB, -1);
651 if (parentIndex != -1) {
652 final Tab parent = getTab(parentIndex);
653 if (parent != null) {
654 parent.addChildTab(t);
655 }
656 }
657 }
658 }
659 }
660 return true;
661 }
662
663 /**
664 * Free the memory in this order, 1) free the background tab; 2) free the
665 * WebView cache;
666 */
667 void freeMemory() {
668 // free the least frequently used background tab
669 Tab t = getLeastUsedTab();
670 if (t != null) {
671 Log.w(LOGTAG, "Free a tab in the browser");
672 freeTab(t);
673 // force a gc
674 System.gc();
675 return;
676 }
677
678 // free the WebView cache
679 Log.w(LOGTAG, "Free WebView cache");
680 WebView view = getCurrentWebView();
681 if (view != null) {
682 view.clearCache(false);
683 }
684 // force a gc
685 System.gc();
686 }
687
688 private Tab getLeastUsedTab() {
689 // Don't do anything if we only have 1 tab.
690 if (getTabCount() == 1) {
691 return null;
692 }
693
694 // Rip through the queue starting at the beginning and teardown the
695 // next available tab.
696 Tab t = null;
697 int i = 0;
698 final int queueSize = mTabQueue.size();
699 if (queueSize == 0) {
700 return null;
701 }
702 do {
703 t = mTabQueue.get(i++);
704 } while (i < queueSize && t != null && t.mMainView == null);
705
706 // Don't do anything if the last remaining tab is the current one.
707 if (t == getCurrentTab()) {
708 return null;
709 }
710
711 return t;
712 }
713
714 private void freeTab(Tab t) {
715 // Store the WebView's state.
716 saveState(t);
717
718 // Tear down the tab.
719 dismissSubWindow(t);
720 // Remove the WebView's settings from the BrowserSettings list of
721 // observers.
722 BrowserSettings.getInstance().deleteObserver(t.mMainView.getSettings());
723 t.mMainView.destroy();
724 t.mMainView = null;
725 }
726
727 /**
728 * Create a new subwindow unless a subwindow already exists.
729 * @return True if a new subwindow was created. False if one already exists.
730 */
731 void createSubWindow() {
732 Tab t = getTab(mCurrentTab);
733 if (t != null && t.mSubView == null) {
734 final View v = mInflateService.inflate(R.layout.browser_subwindow, null);
735 final WebView w = (WebView) v.findViewById(R.id.webview);
736 w.setMapTrackballToArrowKeys(false); // use trackball directly
737 final SubWindowClient subClient =
738 new SubWindowClient(mActivity.getWebViewClient());
739 final SubWindowChromeClient subChromeClient =
740 new SubWindowChromeClient(t,
741 mActivity.getWebChromeClient());
742 w.setWebViewClient(subClient);
743 w.setWebChromeClient(subChromeClient);
744 w.setDownloadListener(mActivity);
745 w.setOnCreateContextMenuListener(mActivity);
746 final BrowserSettings s = BrowserSettings.getInstance();
747 s.addObserver(w.getSettings()).update(s, null);
748 t.mSubView = w;
749 t.mSubViewClient = subClient;
750 t.mSubViewChromeClient = subChromeClient;
751 // FIXME: I really hate having to know the name of the view
752 // containing the webview.
753 t.mSubViewContainer = v.findViewById(R.id.subwindow_container);
754 final ImageButton cancel =
755 (ImageButton) v.findViewById(R.id.subwindow_close);
756 cancel.setOnClickListener(new OnClickListener() {
757 public void onClick(View v) {
758 subChromeClient.onCloseWindow(w);
759 }
760 });
761 }
762 }
763
764 /**
765 * Show the tab that contains the given WebView.
766 * @param view The WebView used to find the tab.
767 */
768 Tab getTabFromView(WebView view) {
769 final int size = getTabCount();
770 for (int i = 0; i < size; i++) {
771 final Tab t = getTab(i);
772 if (t.mSubView == view || t.mMainView == view) {
773 return t;
774 }
775 }
776 return null;
777 }
778
779 /**
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700780 * Return the tab with the matching application id.
781 * @param id The application identifier.
782 */
783 Tab getTabFromId(String id) {
784 if (id == null) {
785 return null;
786 }
787 final int size = getTabCount();
788 for (int i = 0; i < size; i++) {
789 final Tab t = getTab(i);
790 if (id.equals(t.mAppId)) {
791 return t;
792 }
793 }
794 return null;
795 }
796
797 /**
798 * Recreate the main WebView of the given tab. Returns true if the WebView
799 * was deleted.
800 */
801 boolean recreateWebView(Tab t, String url) {
802 final WebView w = t.mMainView;
803 if (w != null) {
804 if (url != null && url.equals(t.mOriginalUrl)) {
805 // The original url matches the current url. Just go back to the
806 // first history item so we can load it faster than if we
807 // rebuilt the WebView.
808 final WebBackForwardList list = w.copyBackForwardList();
809 if (list != null) {
810 w.goBackOrForward(-list.getCurrentIndex());
811 w.clearHistory(); // maintains the current page.
812 return false;
813 }
814 }
815 // Remove the settings object from the global settings and destroy
816 // the WebView.
817 BrowserSettings.getInstance().deleteObserver(
818 t.mMainView.getSettings());
819 t.mMainView.destroy();
820 }
821 // Create a new WebView. If this tab is the current tab, we need to put
822 // back all the clients so force it to be the current tab.
823 t.mMainView = createNewWebView();
824 if (getCurrentTab() == t) {
825 setCurrentTab(t, true);
826 }
827 // Clear the saved state except for the app id and close-on-exit
828 // values.
829 t.mSavedState = null;
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400830 t.mPickerData = null;
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700831 // Save the new url in order to avoid deleting the WebView.
832 t.mOriginalUrl = url;
833 return true;
834 }
835
836 /**
837 * Creates a new WebView and registers it with the global settings.
838 */
839 private WebView createNewWebView() {
840 // Create a new WebView
841 WebView w = new WebView(mActivity);
842 w.setMapTrackballToArrowKeys(false); // use trackball directly
The Android Open Source Projecta3c0aab2009-03-18 17:39:48 -0700843 // Enable the built-in zoom
844 w.getSettings().setBuiltInZoomControls(true);
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700845 // Add this WebView to the settings observer list and update the
846 // settings
847 final BrowserSettings s = BrowserSettings.getInstance();
848 s.addObserver(w.getSettings()).update(s, null);
849 return w;
850 }
851
852 /**
The Android Open Source Project0c908882009-03-03 19:32:16 -0800853 * Put the current tab in the background and set newTab as the current tab.
854 * @param newTab The new tab. If newTab is null, the current tab is not
855 * set.
856 */
857 boolean setCurrentTab(Tab newTab) {
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700858 return setCurrentTab(newTab, false);
859 }
860
861 /**
862 * If force is true, this method skips the check for newTab == current.
863 */
864 private boolean setCurrentTab(Tab newTab, boolean force) {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800865 Tab current = getTab(mCurrentTab);
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700866 if (current == newTab && !force) {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800867 return true;
868 }
869 if (current != null) {
870 // Remove the current WebView and the container of the subwindow
871 putTabInBackground(current);
872 }
873
874 if (newTab == null) {
875 return false;
876 }
877
878 // Move the newTab to the end of the queue
879 int index = mTabQueue.indexOf(newTab);
880 if (index != -1) {
881 mTabQueue.remove(index);
882 }
883 mTabQueue.add(newTab);
884
885 WebView mainView;
886 WebView subView;
887
888 // Display the new current tab
889 mCurrentTab = mTabs.indexOf(newTab);
890 mainView = newTab.mMainView;
891 boolean needRestore = (mainView == null);
892 if (needRestore) {
893 // Same work as in createNewTab() except don't do new Tab()
The Android Open Source Projectf59ec872009-03-13 13:04:24 -0700894 newTab.mMainView = mainView = createNewWebView();
The Android Open Source Project0c908882009-03-03 19:32:16 -0800895 }
896 mainView.setWebViewClient(mActivity.getWebViewClient());
897 mainView.setWebChromeClient(mActivity.getWebChromeClient());
898 mainView.setOnCreateContextMenuListener(mActivity);
899 mainView.setDownloadListener(mActivity);
900 // Add the subwindow if it exists
901 if (newTab.mSubViewContainer != null) {
902 subView = newTab.mSubView;
903 subView.setWebViewClient(newTab.mSubViewClient);
904 subView.setWebChromeClient(newTab.mSubViewChromeClient);
905 subView.setOnCreateContextMenuListener(mActivity);
906 subView.setDownloadListener(mActivity);
907 }
908 if (needRestore) {
909 // Have to finish setCurrentTab work before calling restoreState
910 if (!restoreState(newTab.mSavedState, newTab)) {
911 mainView.loadUrl(BrowserSettings.getInstance().getHomePage());
912 }
913 }
914 return true;
915 }
916
917 /*
918 * Put the tab in the background using all the empty/background clients.
919 */
920 private void putTabInBackground(Tab t) {
921 WebView mainView = t.mMainView;
922 // Set an empty callback so that default actions are not triggered.
923 mainView.setWebViewClient(mEmptyClient);
924 mainView.setWebChromeClient(mBackgroundChromeClient);
925 mainView.setOnCreateContextMenuListener(null);
926 // Leave the DownloadManager attached so that downloads can start in
927 // a non-active window. This can happen when going to a site that does
928 // a redirect after a period of time. The user could have switched to
929 // another tab while waiting for the download to start.
930 mainView.setDownloadListener(mActivity);
931 WebView subView = t.mSubView;
932 if (subView != null) {
933 // Set an empty callback so that default actions are not triggered.
934 subView.setWebViewClient(mEmptyClient);
935 subView.setWebChromeClient(mBackgroundChromeClient);
936 subView.setOnCreateContextMenuListener(null);
937 subView.setDownloadListener(mActivity);
938 }
939 }
940
941 /*
942 * Dismiss the subwindow for the given tab.
943 */
944 void dismissSubWindow(Tab t) {
945 if (t != null && t.mSubView != null) {
946 BrowserSettings.getInstance().deleteObserver(
947 t.mSubView.getSettings());
948 t.mSubView.destroy();
949 t.mSubView = null;
950 t.mSubViewContainer = null;
951 }
952 }
953
954 /**
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400955 * Ensure that Tab t has data to display in the tab picker.
The Android Open Source Project0c908882009-03-03 19:32:16 -0800956 * @param t Tab to populate.
957 */
958 /* package */ void populatePickerData(Tab t) {
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400959 if (t == null) {
The Android Open Source Project0c908882009-03-03 19:32:16 -0800960 return;
961 }
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400962
963 // mMainView == null indicates that the tab has been freed.
964 if (t.mMainView == null) {
965 populatePickerDataFromSavedState(t);
966 return;
967 }
968
The Android Open Source Project0c908882009-03-03 19:32:16 -0800969 // FIXME: The only place we cared about subwindow was for
970 // bookmarking (i.e. not when saving state). Was this deliberate?
971 final WebBackForwardList list = t.mMainView.copyBackForwardList();
972 final WebHistoryItem item =
973 list != null ? list.getCurrentItem() : null;
974 populatePickerData(t, item);
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400975
976 // This method is only called during the tab picker creation. At this
977 // point we need to listen for new pictures since the WebView is still
978 // active.
979 final WebView w = t.getTopWindow();
980 w.setPictureListener(t);
981 // Capture the picture here instead of populatePickerData since it can
982 // be called when saving the state of a tab.
983 t.mPickerData.mPicture = w.capturePicture();
The Android Open Source Project0c908882009-03-03 19:32:16 -0800984 }
985
Patrick Scott2ed6edb2009-04-22 10:07:45 -0400986 // Create the PickerData and populate it using the saved state of the tab.
987 private void populatePickerDataFromSavedState(Tab t) {
988 if (t.mSavedState == null) {
989 return;
990 }
991
992 final PickerData data = new PickerData();
993 final Bundle state = t.mSavedState;
994 data.mUrl = state.getString(CURRURL);
995 data.mTitle = state.getString(CURRTITLE);
996 data.mWidth = state.getInt(CURRWIDTH, 0);
997 // XXX: These keys are from WebView.savePicture so if they change, this
998 // will break.
999 data.mScale = state.getFloat("scale", 1.0f);
1000 data.mScrollX = state.getInt("scrollX", 0);
1001 data.mScrollY = state.getInt("scrollY", 0);
1002
1003 if (state.containsKey(CURRPICTURE)) {
1004 final File f = new File(t.mSavedState.getString(CURRPICTURE));
1005 try {
1006 final FileInputStream in = new FileInputStream(f);
1007 data.mPicture = Picture.createFromStream(in);
1008 in.close();
1009 } catch (Exception ex) {
1010 // Ignore any problems with inflating the picture. We just
1011 // won't draw anything.
The Android Open Source Project0c908882009-03-03 19:32:16 -08001012 }
1013 }
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001014
1015 // Set the tab's picker data.
1016 t.mPickerData = data;
1017 }
1018
1019 // Populate the picker data using the given history item and the current
1020 // top WebView.
1021 private void populatePickerData(Tab t, WebHistoryItem item) {
1022 final PickerData data = new PickerData();
1023 if (item != null) {
1024 data.mUrl = item.getUrl();
1025 data.mTitle = item.getTitle();
1026 if (data.mTitle == null) {
1027 data.mTitle = data.mUrl;
1028 }
1029 }
1030 // We want to display the top window in the tab picker but use the url
1031 // and title of the main window.
1032 final WebView w = t.getTopWindow();
1033 data.mWidth = w.getWidth();
1034 data.mScale = w.getScale();
1035 data.mScrollX = w.getScrollX();
1036 data.mScrollY = w.getScrollY();
1037 t.mPickerData = data;
The Android Open Source Project0c908882009-03-03 19:32:16 -08001038 }
1039
1040 /**
1041 * Clean up the data for all tabs.
1042 */
1043 /* package */ void wipeAllPickerData() {
1044 int size = getTabCount();
1045 for (int i = 0; i < size; i++) {
1046 final Tab t = getTab(i);
1047 if (t != null && t.mSavedState == null) {
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001048 t.mPickerData = null;
1049 }
1050 if (t.mMainView != null) {
1051 // Clear the picture listeners.
1052 t.mMainView.setPictureListener(null);
1053 if (t.mSubView != null) {
1054 t.mSubView.setPictureListener(null);
1055 }
The Android Open Source Project0c908882009-03-03 19:32:16 -08001056 }
1057 }
1058 }
1059
1060 /*
1061 * Save the state for an individual tab.
1062 */
1063 private boolean saveState(Tab t) {
1064 if (t != null) {
1065 final WebView w = t.mMainView;
1066 // If the WebView is null it means we ran low on memory and we
1067 // already stored the saved state in mSavedState.
1068 if (w == null) {
1069 return true;
1070 }
1071 final Bundle b = new Bundle();
1072 final WebBackForwardList list = w.saveState(b);
1073 if (list != null) {
1074 final File f = new File(mThumbnailDir, w.hashCode()
1075 + "_pic.save");
1076 if (w.savePicture(b, f)) {
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001077 b.putString(CURRPICTURE, f.getPath());
The Android Open Source Project0c908882009-03-03 19:32:16 -08001078 }
1079 }
1080
1081 // Store some extra info for displaying the tab in the picker.
1082 final WebHistoryItem item =
1083 list != null ? list.getCurrentItem() : null;
1084 populatePickerData(t, item);
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001085
1086 // XXX: WebView.savePicture stores the scale and scroll positions
1087 // in the bundle so we don't have to do it here.
1088 final PickerData data = t.mPickerData;
1089 if (data.mUrl != null) {
1090 b.putString(CURRURL, data.mUrl);
The Android Open Source Project0c908882009-03-03 19:32:16 -08001091 }
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001092 if (data.mTitle != null) {
1093 b.putString(CURRTITLE, data.mTitle);
The Android Open Source Project0c908882009-03-03 19:32:16 -08001094 }
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001095 b.putInt(CURRWIDTH, data.mWidth);
The Android Open Source Project0c908882009-03-03 19:32:16 -08001096 b.putBoolean(CLOSEONEXIT, t.mCloseOnExit);
The Android Open Source Projectf59ec872009-03-13 13:04:24 -07001097 if (t.mAppId != null) {
1098 b.putString(APPID, t.mAppId);
1099 }
1100 if (t.mOriginalUrl != null) {
1101 b.putString(ORIGINALURL, t.mOriginalUrl);
1102 }
The Android Open Source Project0c908882009-03-03 19:32:16 -08001103
1104 // Remember the parent tab so the relationship can be restored.
1105 if (t.mParentTab != null) {
1106 b.putInt(PARENTTAB, getTabIndex(t.mParentTab));
1107 }
1108
1109 // Remember the saved state.
1110 t.mSavedState = b;
1111 return true;
1112 }
1113 return false;
1114 }
1115
1116 /*
1117 * Restore the state of the tab.
1118 */
1119 private boolean restoreState(Bundle b, Tab t) {
1120 if (b == null) {
1121 return false;
1122 }
The Android Open Source Projectf59ec872009-03-13 13:04:24 -07001123 // Restore the internal state even if the WebView fails to restore.
1124 // This will maintain the app id, original url and close-on-exit values.
1125 t.mSavedState = null;
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001126 t.mPickerData = null;
The Android Open Source Projectf59ec872009-03-13 13:04:24 -07001127 t.mCloseOnExit = b.getBoolean(CLOSEONEXIT);
1128 t.mAppId = b.getString(APPID);
1129 t.mOriginalUrl = b.getString(ORIGINALURL);
1130
The Android Open Source Project0c908882009-03-03 19:32:16 -08001131 final WebView w = t.mMainView;
1132 final WebBackForwardList list = w.restoreState(b);
1133 if (list == null) {
1134 return false;
1135 }
Patrick Scott2ed6edb2009-04-22 10:07:45 -04001136 if (b.containsKey(CURRPICTURE)) {
1137 final File f = new File(b.getString(CURRPICTURE));
The Android Open Source Project0c908882009-03-03 19:32:16 -08001138 w.restorePicture(b, f);
1139 f.delete();
1140 }
The Android Open Source Project0c908882009-03-03 19:32:16 -08001141 return true;
1142 }
1143}