1 /* 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 17 package com.android.browser; 18 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.Picture; 22 import android.net.http.SslError; 23 import android.os.Bundle; 24 import android.os.Message; 25 import android.util.Log; 26 import android.view.Gravity; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.View.OnClickListener; 31 import android.webkit.HttpAuthHandler; 32 import android.webkit.JsPromptResult; 33 import android.webkit.JsResult; 34 import android.webkit.SslErrorHandler; 35 import android.webkit.WebBackForwardList; 36 import android.webkit.WebChromeClient; 37 import android.webkit.WebHistoryItem; 38 import android.webkit.WebView; 39 import android.webkit.WebViewClient; 40 import android.widget.FrameLayout; 41 import android.widget.ImageButton; 42 import android.widget.LinearLayout; 43 44 import java.io.File; 45 import java.io.FileInputStream; 46 import java.util.ArrayList; 47 import java.util.Vector; 48 49 class TabControl { 50 // Log Tag 51 private static final String LOGTAG = "TabControl"; 52 // Maximum number of tabs. 53 static final int MAX_TABS = 8; 54 // Static instance of an empty callback. 55 private static final WebViewClient mEmptyClient = 56 new WebViewClient(); 57 // Instance of BackgroundChromeClient for background tabs. 58 private final BackgroundChromeClient mBackgroundChromeClient = 59 new BackgroundChromeClient(); 60 // Private array of WebViews that are used as tabs. 61 private ArrayList<Tab> mTabs = new ArrayList<Tab>(MAX_TABS); 62 // Queue of most recently viewed tabs. 63 private ArrayList<Tab> mTabQueue = new ArrayList<Tab>(MAX_TABS); 64 // Current position in mTabs. 65 private int mCurrentTab = -1; 66 // A private instance of BrowserActivity to interface with when adding and 67 // switching between tabs. 68 private final BrowserActivity mActivity; 69 // Inflation service for making subwindows. 70 private final LayoutInflater mInflateService; 71 // Subclass of WebViewClient used in subwindows to notify the main 72 // WebViewClient of certain WebView activities. 73 private static class SubWindowClient extends WebViewClient { 74 // The main WebViewClient. 75 private final WebViewClient mClient; 76 SubWindowClient(WebViewClient client)77 SubWindowClient(WebViewClient client) { 78 mClient = client; 79 } 80 @Override doUpdateVisitedHistory(WebView view, String url, boolean isReload)81 public void doUpdateVisitedHistory(WebView view, String url, 82 boolean isReload) { 83 mClient.doUpdateVisitedHistory(view, url, isReload); 84 } 85 @Override shouldOverrideUrlLoading(WebView view, String url)86 public boolean shouldOverrideUrlLoading(WebView view, String url) { 87 return mClient.shouldOverrideUrlLoading(view, url); 88 } 89 @Override onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)90 public void onReceivedSslError(WebView view, SslErrorHandler handler, 91 SslError error) { 92 mClient.onReceivedSslError(view, handler, error); 93 } 94 @Override onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)95 public void onReceivedHttpAuthRequest(WebView view, 96 HttpAuthHandler handler, String host, String realm) { 97 mClient.onReceivedHttpAuthRequest(view, handler, host, realm); 98 } 99 @Override onFormResubmission(WebView view, Message dontResend, Message resend)100 public void onFormResubmission(WebView view, Message dontResend, 101 Message resend) { 102 mClient.onFormResubmission(view, dontResend, resend); 103 } 104 @Override onReceivedError(WebView view, int errorCode, String description, String failingUrl)105 public void onReceivedError(WebView view, int errorCode, 106 String description, String failingUrl) { 107 mClient.onReceivedError(view, errorCode, description, failingUrl); 108 } 109 @Override shouldOverrideKeyEvent(WebView view, android.view.KeyEvent event)110 public boolean shouldOverrideKeyEvent(WebView view, 111 android.view.KeyEvent event) { 112 return mClient.shouldOverrideKeyEvent(view, event); 113 } 114 @Override onUnhandledKeyEvent(WebView view, android.view.KeyEvent event)115 public void onUnhandledKeyEvent(WebView view, 116 android.view.KeyEvent event) { 117 mClient.onUnhandledKeyEvent(view, event); 118 } 119 } 120 // Subclass of WebChromeClient to display javascript dialogs. 121 private class SubWindowChromeClient extends WebChromeClient { 122 // This subwindow's tab. 123 private final Tab mTab; 124 // The main WebChromeClient. 125 private final WebChromeClient mClient; 126 SubWindowChromeClient(Tab t, WebChromeClient client)127 SubWindowChromeClient(Tab t, WebChromeClient client) { 128 mTab = t; 129 mClient = client; 130 } 131 @Override onProgressChanged(WebView view, int newProgress)132 public void onProgressChanged(WebView view, int newProgress) { 133 mClient.onProgressChanged(view, newProgress); 134 } 135 @Override onCreateWindow(WebView view, boolean dialog, boolean userGesture, android.os.Message resultMsg)136 public boolean onCreateWindow(WebView view, boolean dialog, 137 boolean userGesture, android.os.Message resultMsg) { 138 return mClient.onCreateWindow(view, dialog, userGesture, resultMsg); 139 } 140 @Override onCloseWindow(WebView window)141 public void onCloseWindow(WebView window) { 142 if (Browser.DEBUG && window != mTab.mSubView) { 143 throw new AssertionError("Can't close the window"); 144 } 145 mActivity.dismissSubWindow(mTab); 146 } 147 } 148 // Background WebChromeClient for focusing tabs 149 private class BackgroundChromeClient extends WebChromeClient { 150 @Override onRequestFocus(WebView view)151 public void onRequestFocus(WebView view) { 152 Tab t = getTabFromView(view); 153 if (t != getCurrentTab()) { 154 mActivity.switchToTab(getTabIndex(t)); 155 } 156 } 157 } 158 159 // Extra saved information for displaying the tab in the picker. 160 public static class PickerData { 161 String mUrl; 162 String mTitle; 163 Bitmap mFavicon; 164 float mScale; 165 int mScrollX; 166 int mScrollY; 167 } 168 169 /** 170 * Private class for maintaining Tabs with a main WebView and a subwindow. 171 */ 172 public class Tab { 173 // The Geolocation permissions prompt 174 private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt; 175 private View mContainer; 176 // Main WebView 177 private WebView mMainView; 178 // Subwindow WebView 179 private WebView mSubView; 180 // Subwindow container 181 private View mSubViewContainer; 182 // Subwindow callback 183 private SubWindowClient mSubViewClient; 184 // Subwindow chrome callback 185 private SubWindowChromeClient mSubViewChromeClient; 186 // Saved bundle for when we are running low on memory. It contains the 187 // information needed to restore the WebView if the user goes back to 188 // the tab. 189 private Bundle mSavedState; 190 // Data used when displaying the tab in the picker. 191 private PickerData mPickerData; 192 193 // Parent Tab. This is the Tab that created this Tab, or null 194 // if the Tab was created by the UI 195 private Tab mParentTab; 196 // Tab that constructed by this Tab. This is used when this 197 // Tab is destroyed, it clears all mParentTab values in the 198 // children. 199 private Vector<Tab> mChildTabs; 200 201 private Boolean mCloseOnExit; 202 // Application identifier used to find tabs that another application 203 // wants to reuse. 204 private String mAppId; 205 // Keep the original url around to avoid killing the old WebView if the 206 // url has not changed. 207 private String mOriginalUrl; 208 209 private ErrorConsoleView mErrorConsole; 210 // the lock icon type and previous lock icon type for the tab 211 private int mSavedLockIconType; 212 private int mSavedPrevLockIconType; 213 214 // Construct a new tab Tab(WebView w, boolean closeOnExit, String appId, String url, Context context)215 private Tab(WebView w, boolean closeOnExit, String appId, String url, Context context) { 216 mCloseOnExit = closeOnExit; 217 mAppId = appId; 218 mOriginalUrl = url; 219 mSavedLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; 220 mSavedPrevLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; 221 222 // The tab consists of a container view, which contains the main 223 // WebView, as well as any other UI elements associated with the tab. 224 LayoutInflater factory = LayoutInflater.from(context); 225 mContainer = factory.inflate(R.layout.tab, null); 226 227 mGeolocationPermissionsPrompt = 228 (GeolocationPermissionsPrompt) mContainer.findViewById( 229 R.id.geolocation_permissions_prompt); 230 231 setWebView(w); 232 } 233 234 /** 235 * Sets the WebView for this tab, correctly removing the old WebView 236 * from the container view. 237 */ setWebView(WebView w)238 public void setWebView(WebView w) { 239 if (mMainView == w) { 240 return; 241 } 242 // If the WebView is changing, the page will be reloaded, so any ongoing Geolocation 243 // permission requests are void. 244 mGeolocationPermissionsPrompt.hide(); 245 246 // Just remove the old one. 247 FrameLayout wrapper = 248 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 249 wrapper.removeView(mMainView); 250 mMainView = w; 251 } 252 253 /** 254 * This method attaches both the WebView and any sub window to the 255 * given content view. 256 */ attachTabToContentView(ViewGroup content)257 public void attachTabToContentView(ViewGroup content) { 258 if (mMainView == null) { 259 return; 260 } 261 262 // Attach the WebView to the container and then attach the 263 // container to the content view. 264 FrameLayout wrapper = 265 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 266 wrapper.addView(mMainView); 267 content.addView(mContainer, BrowserActivity.COVER_SCREEN_PARAMS); 268 attachSubWindow(content); 269 } 270 271 /** 272 * Remove the WebView and any sub window from the given content view. 273 */ removeTabFromContentView(ViewGroup content)274 public void removeTabFromContentView(ViewGroup content) { 275 if (mMainView == null) { 276 return; 277 } 278 279 // Remove the container from the content and then remove the 280 // WebView from the container. This will trigger a focus change 281 // needed by WebView. 282 FrameLayout wrapper = 283 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 284 wrapper.removeView(mMainView); 285 content.removeView(mContainer); 286 removeSubWindow(content); 287 } 288 289 /** 290 * Attach the sub window to the content view. 291 */ attachSubWindow(ViewGroup content)292 public void attachSubWindow(ViewGroup content) { 293 if (mSubView != null) { 294 content.addView(mSubViewContainer, 295 BrowserActivity.COVER_SCREEN_PARAMS); 296 } 297 } 298 299 /** 300 * Remove the sub window from the content view. 301 */ removeSubWindow(ViewGroup content)302 public void removeSubWindow(ViewGroup content) { 303 if (mSubView != null) { 304 content.removeView(mSubViewContainer); 305 } 306 } 307 308 /** 309 * Return the top window of this tab; either the subwindow if it is not 310 * null or the main window. 311 * @return The top window of this tab. 312 */ getTopWindow()313 public WebView getTopWindow() { 314 if (mSubView != null) { 315 return mSubView; 316 } 317 return mMainView; 318 } 319 320 /** 321 * Return the main window of this tab. Note: if a tab is freed in the 322 * background, this can return null. It is only guaranteed to be 323 * non-null for the current tab. 324 * @return The main WebView of this tab. 325 */ getWebView()326 public WebView getWebView() { 327 return mMainView; 328 } 329 330 /** 331 * @return The geolocation permissions prompt for this tab. 332 */ getGeolocationPermissionsPrompt()333 public GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() { 334 return mGeolocationPermissionsPrompt; 335 } 336 337 /** 338 * Return the subwindow of this tab or null if there is no subwindow. 339 * @return The subwindow of this tab or null. 340 */ getSubWebView()341 public WebView getSubWebView() { 342 return mSubView; 343 } 344 345 /** 346 * Get the url of this tab. Valid after calling populatePickerData, but 347 * before calling wipePickerData, or if the webview has been destroyed. 348 * 349 * @return The WebView's url or null. 350 */ getUrl()351 public String getUrl() { 352 if (mPickerData != null) { 353 return mPickerData.mUrl; 354 } 355 return null; 356 } 357 358 /** 359 * Get the title of this tab. Valid after calling populatePickerData, 360 * but before calling wipePickerData, or if the webview has been 361 * destroyed. If the url has no title, use the url instead. 362 * 363 * @return The WebView's title (or url) or null. 364 */ getTitle()365 public String getTitle() { 366 if (mPickerData != null) { 367 return mPickerData.mTitle; 368 } 369 return null; 370 } 371 getFavicon()372 public Bitmap getFavicon() { 373 if (mPickerData != null) { 374 return mPickerData.mFavicon; 375 } 376 return null; 377 } 378 setParentTab(Tab parent)379 private void setParentTab(Tab parent) { 380 mParentTab = parent; 381 // This tab may have been freed due to low memory. If that is the 382 // case, the parent tab index is already saved. If we are changing 383 // that index (most likely due to removing the parent tab) we must 384 // update the parent tab index in the saved Bundle. 385 if (mSavedState != null) { 386 if (parent == null) { 387 mSavedState.remove(PARENTTAB); 388 } else { 389 mSavedState.putInt(PARENTTAB, getTabIndex(parent)); 390 } 391 } 392 } 393 394 /** 395 * When a Tab is created through the content of another Tab, then 396 * we associate the Tabs. 397 * @param child the Tab that was created from this Tab 398 */ addChildTab(Tab child)399 public void addChildTab(Tab child) { 400 if (mChildTabs == null) { 401 mChildTabs = new Vector<Tab>(); 402 } 403 mChildTabs.add(child); 404 child.setParentTab(this); 405 } 406 removeFromTree()407 private void removeFromTree() { 408 // detach the children 409 if (mChildTabs != null) { 410 for(Tab t : mChildTabs) { 411 t.setParentTab(null); 412 } 413 } 414 415 // Find myself in my parent list 416 if (mParentTab != null) { 417 mParentTab.mChildTabs.remove(this); 418 } 419 } 420 421 /** 422 * If this Tab was created through another Tab, then this method 423 * returns that Tab. 424 * @return the Tab parent or null 425 */ getParentTab()426 public Tab getParentTab() { 427 return mParentTab; 428 } 429 430 /** 431 * Return whether this tab should be closed when it is backing out of 432 * the first page. 433 * @return TRUE if this tab should be closed when exit. 434 */ closeOnExit()435 public boolean closeOnExit() { 436 return mCloseOnExit; 437 } 438 setLockIconType(int type)439 void setLockIconType(int type) { 440 mSavedLockIconType = type; 441 } 442 getLockIconType()443 int getLockIconType() { 444 return mSavedLockIconType; 445 } 446 setPrevLockIconType(int type)447 void setPrevLockIconType(int type) { 448 mSavedPrevLockIconType = type; 449 } 450 getPrevLockIconType()451 int getPrevLockIconType() { 452 return mSavedPrevLockIconType; 453 } 454 }; 455 456 // Directory to store thumbnails for each WebView. 457 private final File mThumbnailDir; 458 459 /** 460 * Construct a new TabControl object that interfaces with the given 461 * BrowserActivity instance. 462 * @param activity A BrowserActivity instance that TabControl will interface 463 * with. 464 */ TabControl(BrowserActivity activity)465 TabControl(BrowserActivity activity) { 466 mActivity = activity; 467 mInflateService = 468 ((LayoutInflater) activity.getSystemService( 469 Context.LAYOUT_INFLATER_SERVICE)); 470 mThumbnailDir = activity.getDir("thumbnails", 0); 471 } 472 getThumbnailDir()473 File getThumbnailDir() { 474 return mThumbnailDir; 475 } 476 getBrowserActivity()477 BrowserActivity getBrowserActivity() { 478 return mActivity; 479 } 480 481 /** 482 * Return the current tab's main WebView. This will always return the main 483 * WebView for a given tab and not a subwindow. 484 * @return The current tab's WebView. 485 */ getCurrentWebView()486 WebView getCurrentWebView() { 487 Tab t = getTab(mCurrentTab); 488 if (t == null) { 489 return null; 490 } 491 return t.mMainView; 492 } 493 494 /** 495 * Return the current tab's error console. Creates the console if createIfNEcessary 496 * is true and we haven't already created the console. 497 * @param createIfNecessary Flag to indicate if the console should be created if it has 498 * not been already. 499 * @return The current tab's error console, or null if one has not been created and 500 * createIfNecessary is false. 501 */ getCurrentErrorConsole(boolean createIfNecessary)502 ErrorConsoleView getCurrentErrorConsole(boolean createIfNecessary) { 503 Tab t = getTab(mCurrentTab); 504 if (t == null) { 505 return null; 506 } 507 508 if (createIfNecessary && t.mErrorConsole == null) { 509 t.mErrorConsole = new ErrorConsoleView(mActivity); 510 t.mErrorConsole.setWebView(t.mMainView); 511 } 512 513 return t.mErrorConsole; 514 } 515 516 /** 517 * Return the current tab's top-level WebView. This can return a subwindow 518 * if one exists. 519 * @return The top-level WebView of the current tab. 520 */ getCurrentTopWebView()521 WebView getCurrentTopWebView() { 522 Tab t = getTab(mCurrentTab); 523 if (t == null) { 524 return null; 525 } 526 return t.mSubView != null ? t.mSubView : t.mMainView; 527 } 528 529 /** 530 * Return the current tab's subwindow if it exists. 531 * @return The subwindow of the current tab or null if it doesn't exist. 532 */ getCurrentSubWindow()533 WebView getCurrentSubWindow() { 534 Tab t = getTab(mCurrentTab); 535 if (t == null) { 536 return null; 537 } 538 return t.mSubView; 539 } 540 541 /** 542 * Return the tab at the specified index. 543 * @return The Tab for the specified index or null if the tab does not 544 * exist. 545 */ getTab(int index)546 Tab getTab(int index) { 547 if (index >= 0 && index < mTabs.size()) { 548 return mTabs.get(index); 549 } 550 return null; 551 } 552 553 /** 554 * Return the current tab. 555 * @return The current tab. 556 */ getCurrentTab()557 Tab getCurrentTab() { 558 return getTab(mCurrentTab); 559 } 560 561 /** 562 * Return the current tab index. 563 * @return The current tab index 564 */ getCurrentIndex()565 int getCurrentIndex() { 566 return mCurrentTab; 567 } 568 569 /** 570 * Given a Tab, find it's index 571 * @param Tab to find 572 * @return index of Tab or -1 if not found 573 */ getTabIndex(Tab tab)574 int getTabIndex(Tab tab) { 575 if (tab == null) { 576 return -1; 577 } 578 return mTabs.indexOf(tab); 579 } 580 581 /** 582 * Create a new tab. 583 * @return The newly createTab or null if we have reached the maximum 584 * number of open tabs. 585 */ createNewTab(boolean closeOnExit, String appId, String url)586 Tab createNewTab(boolean closeOnExit, String appId, String url) { 587 int size = mTabs.size(); 588 // Return false if we have maxed out on tabs 589 if (MAX_TABS == size) { 590 return null; 591 } 592 final WebView w = createNewWebView(); 593 594 // Create a new tab and add it to the tab list 595 Tab t = new Tab(w, closeOnExit, appId, url, mActivity); 596 mTabs.add(t); 597 // Initially put the tab in the background. 598 putTabInBackground(t); 599 return t; 600 } 601 602 /** 603 * Create a new tab with default values for closeOnExit(false), 604 * appId(null), and url(null). 605 */ createNewTab()606 Tab createNewTab() { 607 return createNewTab(false, null, null); 608 } 609 610 /** 611 * Remove the tab from the list. If the tab is the current tab shown, the 612 * last created tab will be shown. 613 * @param t The tab to be removed. 614 */ removeTab(Tab t)615 boolean removeTab(Tab t) { 616 if (t == null) { 617 return false; 618 } 619 // Only remove the tab if it is the current one. 620 if (getCurrentTab() == t) { 621 putTabInBackground(t); 622 } 623 624 // Only destroy the WebView if it still exists. 625 if (t.mMainView != null) { 626 // Take down the sub window. 627 dismissSubWindow(t); 628 // Remove the WebView's settings from the BrowserSettings list of 629 // observers. 630 BrowserSettings.getInstance().deleteObserver( 631 t.mMainView.getSettings()); 632 WebView w = t.mMainView; 633 t.setWebView(null); 634 // Destroy the main view 635 w.destroy(); 636 } 637 // clear it's references to parent and children 638 t.removeFromTree(); 639 640 // Remove it from our list of tabs. 641 mTabs.remove(t); 642 643 // The tab indices have shifted, update all the saved state so we point 644 // to the correct index. 645 for (Tab tab : mTabs) { 646 if (tab.mChildTabs != null) { 647 for (Tab child : tab.mChildTabs) { 648 child.setParentTab(tab); 649 } 650 } 651 } 652 653 654 // This tab may have been pushed in to the background and then closed. 655 // If the saved state contains a picture file, delete the file. 656 if (t.mSavedState != null) { 657 if (t.mSavedState.containsKey(CURRPICTURE)) { 658 new File(t.mSavedState.getString(CURRPICTURE)).delete(); 659 } 660 } 661 662 // Remove it from the queue of viewed tabs. 663 mTabQueue.remove(t); 664 mCurrentTab = -1; 665 return true; 666 } 667 668 /** 669 * Clear the back/forward list for all the current tabs. 670 */ clearHistory()671 void clearHistory() { 672 int size = getTabCount(); 673 for (int i = 0; i < size; i++) { 674 Tab t = mTabs.get(i); 675 // TODO: if a tab is freed due to low memory, its history is not 676 // cleared here. 677 if (t.mMainView != null) { 678 t.mMainView.clearHistory(); 679 } 680 if (t.mSubView != null) { 681 t.mSubView.clearHistory(); 682 } 683 } 684 } 685 686 /** 687 * Destroy all the tabs and subwindows 688 */ destroy()689 void destroy() { 690 BrowserSettings s = BrowserSettings.getInstance(); 691 for (Tab t : mTabs) { 692 if (t.mMainView != null) { 693 dismissSubWindow(t); 694 s.deleteObserver(t.mMainView.getSettings()); 695 WebView w = t.mMainView; 696 t.setWebView(null); 697 w.destroy(); 698 } 699 } 700 mTabs.clear(); 701 mTabQueue.clear(); 702 } 703 704 /** 705 * Returns the number of tabs created. 706 * @return The number of tabs created. 707 */ getTabCount()708 int getTabCount() { 709 return mTabs.size(); 710 } 711 712 // Used for saving and restoring each Tab 713 private static final String WEBVIEW = "webview"; 714 private static final String NUMTABS = "numTabs"; 715 private static final String CURRTAB = "currentTab"; 716 private static final String CURRURL = "currentUrl"; 717 private static final String CURRTITLE = "currentTitle"; 718 private static final String CURRPICTURE = "currentPicture"; 719 private static final String CLOSEONEXIT = "closeonexit"; 720 private static final String PARENTTAB = "parentTab"; 721 private static final String APPID = "appid"; 722 private static final String ORIGINALURL = "originalUrl"; 723 724 /** 725 * Save the state of all the Tabs. 726 * @param outState The Bundle to save the state to. 727 */ saveState(Bundle outState)728 void saveState(Bundle outState) { 729 final int numTabs = getTabCount(); 730 outState.putInt(NUMTABS, numTabs); 731 final int index = getCurrentIndex(); 732 outState.putInt(CURRTAB, (index >= 0 && index < numTabs) ? index : 0); 733 for (int i = 0; i < numTabs; i++) { 734 final Tab t = getTab(i); 735 if (saveState(t)) { 736 outState.putBundle(WEBVIEW + i, t.mSavedState); 737 } 738 } 739 } 740 741 /** 742 * Restore the state of all the tabs. 743 * @param inState The saved state of all the tabs. 744 * @return True if there were previous tabs that were restored. False if 745 * there was no saved state or restoring the state failed. 746 */ restoreState(Bundle inState)747 boolean restoreState(Bundle inState) { 748 final int numTabs = (inState == null) 749 ? -1 : inState.getInt(NUMTABS, -1); 750 if (numTabs == -1) { 751 return false; 752 } else { 753 final int currentTab = inState.getInt(CURRTAB, -1); 754 for (int i = 0; i < numTabs; i++) { 755 if (i == currentTab) { 756 Tab t = createNewTab(); 757 // Me must set the current tab before restoring the state 758 // so that all the client classes are set. 759 setCurrentTab(t); 760 if (!restoreState(inState.getBundle(WEBVIEW + i), t)) { 761 Log.w(LOGTAG, "Fail in restoreState, load home page."); 762 t.mMainView.loadUrl(BrowserSettings.getInstance() 763 .getHomePage()); 764 } 765 } else { 766 // Create a new tab and don't restore the state yet, add it 767 // to the tab list 768 Tab t = new Tab(null, false, null, null, mActivity); 769 t.mSavedState = inState.getBundle(WEBVIEW + i); 770 if (t.mSavedState != null) { 771 populatePickerDataFromSavedState(t); 772 // Need to maintain the app id and original url so we 773 // can possibly reuse this tab. 774 t.mAppId = t.mSavedState.getString(APPID); 775 t.mOriginalUrl = t.mSavedState.getString(ORIGINALURL); 776 } 777 mTabs.add(t); 778 mTabQueue.add(t); 779 } 780 } 781 // Rebuild the tree of tabs. Do this after all tabs have been 782 // created/restored so that the parent tab exists. 783 for (int i = 0; i < numTabs; i++) { 784 final Bundle b = inState.getBundle(WEBVIEW + i); 785 final Tab t = getTab(i); 786 if (b != null && t != null) { 787 final int parentIndex = b.getInt(PARENTTAB, -1); 788 if (parentIndex != -1) { 789 final Tab parent = getTab(parentIndex); 790 if (parent != null) { 791 parent.addChildTab(t); 792 } 793 } 794 } 795 } 796 } 797 return true; 798 } 799 800 /** 801 * Free the memory in this order, 1) free the background tab; 2) free the 802 * WebView cache; 803 */ freeMemory()804 void freeMemory() { 805 if (getTabCount() == 0) return; 806 807 // free the least frequently used background tab 808 Tab t = getLeastUsedTab(getCurrentTab()); 809 if (t != null) { 810 Log.w(LOGTAG, "Free a tab in the browser"); 811 freeTab(t); 812 // force a gc 813 System.gc(); 814 return; 815 } 816 817 // free the WebView's unused memory (this includes the cache) 818 Log.w(LOGTAG, "Free WebView's unused memory and cache"); 819 WebView view = getCurrentWebView(); 820 if (view != null) { 821 view.freeMemory(); 822 } 823 // force a gc 824 System.gc(); 825 } 826 getLeastUsedTab(Tab current)827 private Tab getLeastUsedTab(Tab current) { 828 // Don't do anything if we only have 1 tab or if the current tab is 829 // null. 830 if (getTabCount() == 1 || current == null) { 831 return null; 832 } 833 834 // Rip through the queue starting at the beginning and teardown the 835 // next available tab. 836 Tab t = null; 837 int i = 0; 838 final int queueSize = mTabQueue.size(); 839 if (queueSize == 0) { 840 return null; 841 } 842 do { 843 t = mTabQueue.get(i++); 844 } while (i < queueSize 845 && ((t != null && t.mMainView == null) 846 || t == current.mParentTab)); 847 848 // Don't do anything if the last remaining tab is the current one or if 849 // the last tab has been freed already. 850 if (t == current || t.mMainView == null) { 851 return null; 852 } 853 854 return t; 855 } 856 freeTab(Tab t)857 private void freeTab(Tab t) { 858 // Store the WebView's state. 859 saveState(t); 860 861 // Tear down the tab. 862 dismissSubWindow(t); 863 // Remove the WebView's settings from the BrowserSettings list of 864 // observers. 865 BrowserSettings.getInstance().deleteObserver(t.mMainView.getSettings()); 866 WebView w = t.mMainView; 867 t.setWebView(null); 868 w.destroy(); 869 } 870 871 /** 872 * Create a new subwindow unless a subwindow already exists. 873 * @return True if a new subwindow was created. False if one already exists. 874 */ createSubWindow()875 void createSubWindow() { 876 Tab t = getTab(mCurrentTab); 877 if (t != null && t.mSubView == null) { 878 final View v = mInflateService.inflate(R.layout.browser_subwindow, null); 879 final WebView w = (WebView) v.findViewById(R.id.webview); 880 w.setMapTrackballToArrowKeys(false); // use trackball directly 881 final SubWindowClient subClient = 882 new SubWindowClient(mActivity.getWebViewClient()); 883 final SubWindowChromeClient subChromeClient = 884 new SubWindowChromeClient(t, 885 mActivity.getWebChromeClient()); 886 w.setWebViewClient(subClient); 887 w.setWebChromeClient(subChromeClient); 888 w.setDownloadListener(mActivity); 889 w.setOnCreateContextMenuListener(mActivity); 890 final BrowserSettings s = BrowserSettings.getInstance(); 891 s.addObserver(w.getSettings()).update(s, null); 892 t.mSubView = w; 893 t.mSubViewClient = subClient; 894 t.mSubViewChromeClient = subChromeClient; 895 // FIXME: I really hate having to know the name of the view 896 // containing the webview. 897 t.mSubViewContainer = v.findViewById(R.id.subwindow_container); 898 final ImageButton cancel = 899 (ImageButton) v.findViewById(R.id.subwindow_close); 900 cancel.setOnClickListener(new OnClickListener() { 901 public void onClick(View v) { 902 subChromeClient.onCloseWindow(w); 903 } 904 }); 905 } 906 } 907 908 /** 909 * Show the tab that contains the given WebView. 910 * @param view The WebView used to find the tab. 911 */ getTabFromView(WebView view)912 Tab getTabFromView(WebView view) { 913 final int size = getTabCount(); 914 for (int i = 0; i < size; i++) { 915 final Tab t = getTab(i); 916 if (t.mSubView == view || t.mMainView == view) { 917 return t; 918 } 919 } 920 return null; 921 } 922 923 /** 924 * Return the tab with the matching application id. 925 * @param id The application identifier. 926 */ getTabFromId(String id)927 Tab getTabFromId(String id) { 928 if (id == null) { 929 return null; 930 } 931 final int size = getTabCount(); 932 for (int i = 0; i < size; i++) { 933 final Tab t = getTab(i); 934 if (id.equals(t.mAppId)) { 935 return t; 936 } 937 } 938 return null; 939 } 940 941 // This method checks if a non-app tab (one created within the browser) 942 // matches the given url. tabMatchesUrl(Tab t, String url)943 private boolean tabMatchesUrl(Tab t, String url) { 944 if (t.mAppId != null) { 945 return false; 946 } else if (t.mMainView == null) { 947 return false; 948 } else if (url.equals(t.mMainView.getUrl()) || 949 url.equals(t.mMainView.getOriginalUrl())) { 950 return true; 951 } 952 return false; 953 } 954 955 /** 956 * Return the tab that has no app id associated with it and the url of the 957 * tab matches the given url. 958 * @param url The url to search for. 959 */ findUnusedTabWithUrl(String url)960 Tab findUnusedTabWithUrl(String url) { 961 if (url == null) { 962 return null; 963 } 964 // Check the current tab first. 965 Tab t = getCurrentTab(); 966 if (t != null && tabMatchesUrl(t, url)) { 967 return t; 968 } 969 // Now check all the rest. 970 final int size = getTabCount(); 971 for (int i = 0; i < size; i++) { 972 t = getTab(i); 973 if (tabMatchesUrl(t, url)) { 974 return t; 975 } 976 } 977 return null; 978 } 979 980 /** 981 * Recreate the main WebView of the given tab. Returns true if the WebView 982 * was deleted. 983 */ recreateWebView(Tab t, String url)984 boolean recreateWebView(Tab t, String url) { 985 final WebView w = t.mMainView; 986 if (w != null) { 987 if (url != null && url.equals(t.mOriginalUrl)) { 988 // The original url matches the current url. Just go back to the 989 // first history item so we can load it faster than if we 990 // rebuilt the WebView. 991 final WebBackForwardList list = w.copyBackForwardList(); 992 if (list != null) { 993 w.goBackOrForward(-list.getCurrentIndex()); 994 w.clearHistory(); // maintains the current page. 995 return false; 996 } 997 } 998 // Remove the settings object from the global settings and destroy 999 // the WebView. 1000 BrowserSettings.getInstance().deleteObserver( 1001 t.mMainView.getSettings()); 1002 t.mMainView.destroy(); 1003 } 1004 // Create a new WebView. If this tab is the current tab, we need to put 1005 // back all the clients so force it to be the current tab. 1006 t.setWebView(createNewWebView()); 1007 if (getCurrentTab() == t) { 1008 setCurrentTab(t, true); 1009 } 1010 // Clear the saved state except for the app id and close-on-exit 1011 // values. 1012 t.mSavedState = null; 1013 t.mPickerData = null; 1014 // Save the new url in order to avoid deleting the WebView. 1015 t.mOriginalUrl = url; 1016 return true; 1017 } 1018 1019 /** 1020 * Creates a new WebView and registers it with the global settings. 1021 */ createNewWebView()1022 private WebView createNewWebView() { 1023 // Create a new WebView 1024 WebView w = new WebView(mActivity); 1025 w.setScrollbarFadingEnabled(true); 1026 w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); 1027 w.setMapTrackballToArrowKeys(false); // use trackball directly 1028 // Enable the built-in zoom 1029 w.getSettings().setBuiltInZoomControls(true); 1030 // Add this WebView to the settings observer list and update the 1031 // settings 1032 final BrowserSettings s = BrowserSettings.getInstance(); 1033 s.addObserver(w.getSettings()).update(s, null); 1034 return w; 1035 } 1036 1037 /** 1038 * Put the current tab in the background and set newTab as the current tab. 1039 * @param newTab The new tab. If newTab is null, the current tab is not 1040 * set. 1041 */ setCurrentTab(Tab newTab)1042 boolean setCurrentTab(Tab newTab) { 1043 return setCurrentTab(newTab, false); 1044 } 1045 pauseCurrentTab()1046 /*package*/ void pauseCurrentTab() { 1047 Tab t = getCurrentTab(); 1048 if (t != null) { 1049 t.mMainView.onPause(); 1050 if (t.mSubView != null) { 1051 t.mSubView.onPause(); 1052 } 1053 } 1054 } 1055 resumeCurrentTab()1056 /*package*/ void resumeCurrentTab() { 1057 Tab t = getCurrentTab(); 1058 if (t != null) { 1059 t.mMainView.onResume(); 1060 if (t.mSubView != null) { 1061 t.mSubView.onResume(); 1062 } 1063 } 1064 } 1065 putViewInForeground(WebView v, WebViewClient vc, WebChromeClient cc)1066 private void putViewInForeground(WebView v, WebViewClient vc, 1067 WebChromeClient cc) { 1068 v.setWebViewClient(vc); 1069 v.setWebChromeClient(cc); 1070 v.setOnCreateContextMenuListener(mActivity); 1071 v.setDownloadListener(mActivity); 1072 v.onResume(); 1073 } 1074 putViewInBackground(WebView v)1075 private void putViewInBackground(WebView v) { 1076 // Set an empty callback so that default actions are not triggered. 1077 v.setWebViewClient(mEmptyClient); 1078 v.setWebChromeClient(mBackgroundChromeClient); 1079 v.setOnCreateContextMenuListener(null); 1080 // Leave the DownloadManager attached so that downloads can start in 1081 // a non-active window. This can happen when going to a site that does 1082 // a redirect after a period of time. The user could have switched to 1083 // another tab while waiting for the download to start. 1084 v.setDownloadListener(mActivity); 1085 v.onPause(); 1086 } 1087 1088 /** 1089 * If force is true, this method skips the check for newTab == current. 1090 */ setCurrentTab(Tab newTab, boolean force)1091 private boolean setCurrentTab(Tab newTab, boolean force) { 1092 Tab current = getTab(mCurrentTab); 1093 if (current == newTab && !force) { 1094 return true; 1095 } 1096 if (current != null) { 1097 // Remove the current WebView and the container of the subwindow 1098 putTabInBackground(current); 1099 } 1100 1101 if (newTab == null) { 1102 return false; 1103 } 1104 1105 // Move the newTab to the end of the queue 1106 int index = mTabQueue.indexOf(newTab); 1107 if (index != -1) { 1108 mTabQueue.remove(index); 1109 } 1110 mTabQueue.add(newTab); 1111 1112 WebView mainView; 1113 1114 // Display the new current tab 1115 mCurrentTab = mTabs.indexOf(newTab); 1116 mainView = newTab.mMainView; 1117 boolean needRestore = (mainView == null); 1118 if (needRestore) { 1119 // Same work as in createNewTab() except don't do new Tab() 1120 mainView = createNewWebView(); 1121 newTab.setWebView(mainView); 1122 } 1123 putViewInForeground(mainView, mActivity.getWebViewClient(), 1124 mActivity.getWebChromeClient()); 1125 // Add the subwindow if it exists 1126 if (newTab.mSubViewContainer != null) { 1127 putViewInForeground(newTab.mSubView, newTab.mSubViewClient, 1128 newTab.mSubViewChromeClient); 1129 } 1130 if (needRestore) { 1131 // Have to finish setCurrentTab work before calling restoreState 1132 if (!restoreState(newTab.mSavedState, newTab)) { 1133 mainView.loadUrl(BrowserSettings.getInstance().getHomePage()); 1134 } 1135 } 1136 return true; 1137 } 1138 1139 /* 1140 * Put the tab in the background using all the empty/background clients. 1141 */ putTabInBackground(Tab t)1142 private void putTabInBackground(Tab t) { 1143 putViewInBackground(t.mMainView); 1144 if (t.mSubView != null) { 1145 putViewInBackground(t.mSubView); 1146 } 1147 } 1148 1149 /* 1150 * Dismiss the subwindow for the given tab. 1151 */ dismissSubWindow(Tab t)1152 void dismissSubWindow(Tab t) { 1153 if (t != null && t.mSubView != null) { 1154 BrowserSettings.getInstance().deleteObserver( 1155 t.mSubView.getSettings()); 1156 t.mSubView.destroy(); 1157 t.mSubView = null; 1158 t.mSubViewContainer = null; 1159 } 1160 } 1161 1162 /** 1163 * Ensure that Tab t has data to display in the tab picker. 1164 * @param t Tab to populate. 1165 */ populatePickerData(Tab t)1166 /* package */ void populatePickerData(Tab t) { 1167 if (t == null) { 1168 return; 1169 } 1170 1171 // mMainView == null indicates that the tab has been freed. 1172 if (t.mMainView == null) { 1173 populatePickerDataFromSavedState(t); 1174 return; 1175 } 1176 1177 // FIXME: The only place we cared about subwindow was for 1178 // bookmarking (i.e. not when saving state). Was this deliberate? 1179 final WebBackForwardList list = t.mMainView.copyBackForwardList(); 1180 final WebHistoryItem item = 1181 list != null ? list.getCurrentItem() : null; 1182 populatePickerData(t, item); 1183 } 1184 1185 // Create the PickerData and populate it using the saved state of the tab. populatePickerDataFromSavedState(Tab t)1186 private void populatePickerDataFromSavedState(Tab t) { 1187 if (t.mSavedState == null) { 1188 return; 1189 } 1190 1191 final PickerData data = new PickerData(); 1192 final Bundle state = t.mSavedState; 1193 data.mUrl = state.getString(CURRURL); 1194 data.mTitle = state.getString(CURRTITLE); 1195 // XXX: These keys are from WebView.savePicture so if they change, this 1196 // will break. 1197 data.mScale = state.getFloat("scale", 1.0f); 1198 data.mScrollX = state.getInt("scrollX", 0); 1199 data.mScrollY = state.getInt("scrollY", 0); 1200 1201 // Set the tab's picker data. 1202 t.mPickerData = data; 1203 } 1204 1205 // Populate the picker data using the given history item and the current 1206 // top WebView. populatePickerData(Tab t, WebHistoryItem item)1207 private void populatePickerData(Tab t, WebHistoryItem item) { 1208 final PickerData data = new PickerData(); 1209 if (item != null) { 1210 data.mUrl = item.getUrl(); 1211 data.mTitle = item.getTitle(); 1212 data.mFavicon = item.getFavicon(); 1213 if (data.mTitle == null) { 1214 data.mTitle = data.mUrl; 1215 } 1216 } 1217 // We want to display the top window in the tab picker but use the url 1218 // and title of the main window. 1219 final WebView w = t.getTopWindow(); 1220 data.mScale = w.getScale(); 1221 data.mScrollX = w.getScrollX(); 1222 data.mScrollY = w.getScrollY(); 1223 1224 t.mPickerData = data; 1225 } 1226 1227 /** 1228 * Clean up the data for all tabs. 1229 */ wipeAllPickerData()1230 /* package */ void wipeAllPickerData() { 1231 int size = getTabCount(); 1232 for (int i = 0; i < size; i++) { 1233 final Tab t = getTab(i); 1234 if (t != null && t.mSavedState == null) { 1235 t.mPickerData = null; 1236 } 1237 } 1238 } 1239 1240 /* 1241 * Save the state for an individual tab. 1242 */ saveState(Tab t)1243 private boolean saveState(Tab t) { 1244 if (t != null) { 1245 final WebView w = t.mMainView; 1246 // If the WebView is null it means we ran low on memory and we 1247 // already stored the saved state in mSavedState. 1248 if (w == null) { 1249 return true; 1250 } 1251 final Bundle b = new Bundle(); 1252 final WebBackForwardList list = w.saveState(b); 1253 if (list != null) { 1254 final File f = new File(mThumbnailDir, w.hashCode() 1255 + "_pic.save"); 1256 if (w.savePicture(b, f)) { 1257 b.putString(CURRPICTURE, f.getPath()); 1258 } 1259 } 1260 1261 // Store some extra info for displaying the tab in the picker. 1262 final WebHistoryItem item = 1263 list != null ? list.getCurrentItem() : null; 1264 populatePickerData(t, item); 1265 1266 // XXX: WebView.savePicture stores the scale and scroll positions 1267 // in the bundle so we don't have to do it here. 1268 final PickerData data = t.mPickerData; 1269 if (data.mUrl != null) { 1270 b.putString(CURRURL, data.mUrl); 1271 } 1272 if (data.mTitle != null) { 1273 b.putString(CURRTITLE, data.mTitle); 1274 } 1275 b.putBoolean(CLOSEONEXIT, t.mCloseOnExit); 1276 if (t.mAppId != null) { 1277 b.putString(APPID, t.mAppId); 1278 } 1279 if (t.mOriginalUrl != null) { 1280 b.putString(ORIGINALURL, t.mOriginalUrl); 1281 } 1282 1283 // Remember the parent tab so the relationship can be restored. 1284 if (t.mParentTab != null) { 1285 b.putInt(PARENTTAB, getTabIndex(t.mParentTab)); 1286 } 1287 1288 // Remember the saved state. 1289 t.mSavedState = b; 1290 return true; 1291 } 1292 return false; 1293 } 1294 1295 /* 1296 * Restore the state of the tab. 1297 */ restoreState(Bundle b, Tab t)1298 private boolean restoreState(Bundle b, Tab t) { 1299 if (b == null) { 1300 return false; 1301 } 1302 // Restore the internal state even if the WebView fails to restore. 1303 // This will maintain the app id, original url and close-on-exit values. 1304 t.mSavedState = null; 1305 t.mPickerData = null; 1306 t.mCloseOnExit = b.getBoolean(CLOSEONEXIT); 1307 t.mAppId = b.getString(APPID); 1308 t.mOriginalUrl = b.getString(ORIGINALURL); 1309 1310 final WebView w = t.mMainView; 1311 final WebBackForwardList list = w.restoreState(b); 1312 if (list == null) { 1313 return false; 1314 } 1315 if (b.containsKey(CURRPICTURE)) { 1316 final File f = new File(b.getString(CURRPICTURE)); 1317 w.restorePicture(b, f); 1318 f.delete(); 1319 } 1320 return true; 1321 } 1322 } 1323