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.graphics.Bitmap; 20 import android.graphics.BitmapFactory; 21 import android.graphics.BitmapShader; 22 import android.graphics.Paint; 23 import android.graphics.Shader; 24 import android.os.Bundle; 25 import android.util.Log; 26 import android.view.View; 27 import android.webkit.WebBackForwardList; 28 import android.webkit.WebView; 29 30 import java.io.File; 31 import java.util.ArrayList; 32 import java.util.Vector; 33 34 class TabControl { 35 // Log Tag 36 private static final String LOGTAG = "TabControl"; 37 // Maximum number of tabs. 38 private static final int MAX_TABS = 8; 39 // Private array of WebViews that are used as tabs. 40 private ArrayList<Tab> mTabs = new ArrayList<Tab>(MAX_TABS); 41 // Queue of most recently viewed tabs. 42 private ArrayList<Tab> mTabQueue = new ArrayList<Tab>(MAX_TABS); 43 // Current position in mTabs. 44 private int mCurrentTab = -1; 45 // A private instance of BrowserActivity to interface with when adding and 46 // switching between tabs. 47 private final BrowserActivity mActivity; 48 // Directory to store thumbnails for each WebView. 49 private final File mThumbnailDir; 50 51 /** 52 * Construct a new TabControl object that interfaces with the given 53 * BrowserActivity instance. 54 * @param activity A BrowserActivity instance that TabControl will interface 55 * with. 56 */ 57 TabControl(BrowserActivity activity) { 58 mActivity = activity; 59 mThumbnailDir = activity.getDir("thumbnails", 0); 60 } 61 62 File getThumbnailDir() { 63 return mThumbnailDir; 64 } 65 66 BrowserActivity getBrowserActivity() { 67 return mActivity; 68 } 69 70 /** 71 * Return the current tab's main WebView. This will always return the main 72 * WebView for a given tab and not a subwindow. 73 * @return The current tab's WebView. 74 */ 75 WebView getCurrentWebView() { 76 Tab t = getTab(mCurrentTab); 77 if (t == null) { 78 return null; 79 } 80 return t.getWebView(); 81 } 82 83 /** 84 * Return the current tab's top-level WebView. This can return a subwindow 85 * if one exists. 86 * @return The top-level WebView of the current tab. 87 */ 88 WebView getCurrentTopWebView() { 89 Tab t = getTab(mCurrentTab); 90 if (t == null) { 91 return null; 92 } 93 return t.getTopWindow(); 94 } 95 96 /** 97 * Return the current tab's subwindow if it exists. 98 * @return The subwindow of the current tab or null if it doesn't exist. 99 */ 100 WebView getCurrentSubWindow() { 101 Tab t = getTab(mCurrentTab); 102 if (t == null) { 103 return null; 104 } 105 return t.getSubWebView(); 106 } 107 108 /** 109 * Return the tab at the specified index. 110 * @return The Tab for the specified index or null if the tab does not 111 * exist. 112 */ 113 Tab getTab(int index) { 114 if (index >= 0 && index < mTabs.size()) { 115 return mTabs.get(index); 116 } 117 return null; 118 } 119 120 /** 121 * Return the current tab. 122 * @return The current tab. 123 */ 124 Tab getCurrentTab() { 125 return getTab(mCurrentTab); 126 } 127 128 /** 129 * Return the current tab index. 130 * @return The current tab index 131 */ 132 int getCurrentIndex() { 133 return mCurrentTab; 134 } 135 136 /** 137 * Given a Tab, find it's index 138 * @param Tab to find 139 * @return index of Tab or -1 if not found 140 */ 141 int getTabIndex(Tab tab) { 142 if (tab == null) { 143 return -1; 144 } 145 return mTabs.indexOf(tab); 146 } 147 148 boolean canCreateNewTab() { 149 return MAX_TABS != mTabs.size(); 150 } 151 152 /** 153 * Create a new tab. 154 * @return The newly createTab or null if we have reached the maximum 155 * number of open tabs. 156 */ 157 Tab createNewTab(boolean closeOnExit, String appId, String url) { 158 int size = mTabs.size(); 159 // Return false if we have maxed out on tabs 160 if (MAX_TABS == size) { 161 return null; 162 } 163 final WebView w = createNewWebView(); 164 165 // Create a new tab and add it to the tab list 166 Tab t = new Tab(mActivity, w, closeOnExit, appId, url); 167 mTabs.add(t); 168 // Initially put the tab in the background. 169 t.putInBackground(); 170 return t; 171 } 172 173 /** 174 * Create a new tab with default values for closeOnExit(false), 175 * appId(null), and url(null). 176 */ 177 Tab createNewTab() { 178 return createNewTab(false, null, null); 179 } 180 181 /** 182 * Remove the parent child relationships from all tabs. 183 */ 184 void removeParentChildRelationShips() { 185 for (Tab tab : mTabs) { 186 tab.removeFromTree(); 187 } 188 } 189 190 /** 191 * Remove the tab from the list. If the tab is the current tab shown, the 192 * last created tab will be shown. 193 * @param t The tab to be removed. 194 */ 195 boolean removeTab(Tab t) { 196 if (t == null) { 197 return false; 198 } 199 200 // Grab the current tab before modifying the list. 201 Tab current = getCurrentTab(); 202 203 // Remove t from our list of tabs. 204 mTabs.remove(t); 205 206 // Put the tab in the background only if it is the current one. 207 if (current == t) { 208 t.putInBackground(); 209 mCurrentTab = -1; 210 } else { 211 // If a tab that is earlier in the list gets removed, the current 212 // index no longer points to the correct tab. 213 mCurrentTab = getTabIndex(current); 214 } 215 216 // destroy the tab 217 t.destroy(); 218 // clear it's references to parent and children 219 t.removeFromTree(); 220 221 // The tab indices have shifted, update all the saved state so we point 222 // to the correct index. 223 for (Tab tab : mTabs) { 224 Vector<Tab> children = tab.getChildTabs(); 225 if (children != null) { 226 for (Tab child : children) { 227 child.setParentTab(tab); 228 } 229 } 230 } 231 232 // This tab may have been pushed in to the background and then closed. 233 // If the saved state contains a picture file, delete the file. 234 Bundle savedState = t.getSavedState(); 235 if (savedState != null) { 236 if (savedState.containsKey(Tab.CURRPICTURE)) { 237 new File(savedState.getString(Tab.CURRPICTURE)).delete(); 238 } 239 } 240 241 // Remove it from the queue of viewed tabs. 242 mTabQueue.remove(t); 243 return true; 244 } 245 246 /** 247 * Destroy all the tabs and subwindows 248 */ 249 void destroy() { 250 for (Tab t : mTabs) { 251 t.destroy(); 252 } 253 mTabs.clear(); 254 mTabQueue.clear(); 255 } 256 257 /** 258 * Returns the number of tabs created. 259 * @return The number of tabs created. 260 */ 261 int getTabCount() { 262 return mTabs.size(); 263 } 264 265 266 /** 267 * Save the state of all the Tabs. 268 * @param outState The Bundle to save the state to. 269 */ 270 void saveState(Bundle outState) { 271 final int numTabs = getTabCount(); 272 outState.putInt(Tab.NUMTABS, numTabs); 273 final int index = getCurrentIndex(); 274 outState.putInt(Tab.CURRTAB, (index >= 0 && index < numTabs) ? index : 0); 275 for (int i = 0; i < numTabs; i++) { 276 final Tab t = getTab(i); 277 if (t.saveState()) { 278 outState.putBundle(Tab.WEBVIEW + i, t.getSavedState()); 279 } 280 } 281 } 282 283 /** 284 * Restore the state of all the tabs. 285 * @param inState The saved state of all the tabs. 286 * @return True if there were previous tabs that were restored. False if 287 * there was no saved state or restoring the state failed. 288 */ 289 boolean restoreState(Bundle inState) { 290 final int numTabs = (inState == null) 291 ? -1 : inState.getInt(Tab.NUMTABS, -1); 292 if (numTabs == -1) { 293 return false; 294 } else { 295 final int currentTab = inState.getInt(Tab.CURRTAB, -1); 296 for (int i = 0; i < numTabs; i++) { 297 if (i == currentTab) { 298 Tab t = createNewTab(); 299 // Me must set the current tab before restoring the state 300 // so that all the client classes are set. 301 setCurrentTab(t); 302 if (!t.restoreState(inState.getBundle(Tab.WEBVIEW + i))) { 303 Log.w(LOGTAG, "Fail in restoreState, load home page."); 304 t.getWebView().loadUrl(BrowserSettings.getInstance() 305 .getHomePage()); 306 } 307 } else { 308 // Create a new tab and don't restore the state yet, add it 309 // to the tab list 310 Tab t = new Tab(mActivity, null, false, null, null); 311 Bundle state = inState.getBundle(Tab.WEBVIEW + i); 312 if (state != null) { 313 t.setSavedState(state); 314 t.populatePickerDataFromSavedState(); 315 // Need to maintain the app id and original url so we 316 // can possibly reuse this tab. 317 t.setAppId(state.getString(Tab.APPID)); 318 t.setOriginalUrl(state.getString(Tab.ORIGINALURL)); 319 } 320 mTabs.add(t); 321 // added the tab to the front as they are not current 322 mTabQueue.add(0, t); 323 } 324 } 325 // Rebuild the tree of tabs. Do this after all tabs have been 326 // created/restored so that the parent tab exists. 327 for (int i = 0; i < numTabs; i++) { 328 final Bundle b = inState.getBundle(Tab.WEBVIEW + i); 329 final Tab t = getTab(i); 330 if (b != null && t != null) { 331 final int parentIndex = b.getInt(Tab.PARENTTAB, -1); 332 if (parentIndex != -1) { 333 final Tab parent = getTab(parentIndex); 334 if (parent != null) { 335 parent.addChildTab(t); 336 } 337 } 338 } 339 } 340 } 341 return true; 342 } 343 344 /** 345 * Free the memory in this order, 1) free the background tabs; 2) free the 346 * WebView cache; 347 */ 348 void freeMemory() { 349 if (getTabCount() == 0) return; 350 351 // free the least frequently used background tabs 352 Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab()); 353 if (tabs.size() > 0) { 354 Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser"); 355 for (Tab t : tabs) { 356 // store the WebView's state. 357 t.saveState(); 358 // destroy the tab 359 t.destroy(); 360 } 361 return; 362 } 363 364 // free the WebView's unused memory (this includes the cache) 365 Log.w(LOGTAG, "Free WebView's unused memory and cache"); 366 WebView view = getCurrentWebView(); 367 if (view != null) { 368 view.freeMemory(); 369 } 370 } 371 372 private Vector<Tab> getHalfLeastUsedTabs(Tab current) { 373 Vector<Tab> tabsToGo = new Vector<Tab>(); 374 375 // Don't do anything if we only have 1 tab or if the current tab is 376 // null. 377 if (getTabCount() == 1 || current == null) { 378 return tabsToGo; 379 } 380 381 if (mTabQueue.size() == 0) { 382 return tabsToGo; 383 } 384 385 // Rip through the queue starting at the beginning and tear down half of 386 // available tabs which are not the current tab or the parent of the 387 // current tab. 388 int openTabCount = 0; 389 for (Tab t : mTabQueue) { 390 if (t != null && t.getWebView() != null) { 391 openTabCount++; 392 if (t != current && t != current.getParentTab()) { 393 tabsToGo.add(t); 394 } 395 } 396 } 397 398 openTabCount /= 2; 399 if (tabsToGo.size() > openTabCount) { 400 tabsToGo.setSize(openTabCount); 401 } 402 403 return tabsToGo; 404 } 405 406 /** 407 * Show the tab that contains the given WebView. 408 * @param view The WebView used to find the tab. 409 */ 410 Tab getTabFromView(WebView view) { 411 final int size = getTabCount(); 412 for (int i = 0; i < size; i++) { 413 final Tab t = getTab(i); 414 if (t.getSubWebView() == view || t.getWebView() == view) { 415 return t; 416 } 417 } 418 return null; 419 } 420 421 /** 422 * Return the tab with the matching application id. 423 * @param id The application identifier. 424 */ 425 Tab getTabFromId(String id) { 426 if (id == null) { 427 return null; 428 } 429 final int size = getTabCount(); 430 for (int i = 0; i < size; i++) { 431 final Tab t = getTab(i); 432 if (id.equals(t.getAppId())) { 433 return t; 434 } 435 } 436 return null; 437 } 438 439 /** 440 * Stop loading in all opened WebView including subWindows. 441 */ 442 void stopAllLoading() { 443 final int size = getTabCount(); 444 for (int i = 0; i < size; i++) { 445 final Tab t = getTab(i); 446 final WebView webview = t.getWebView(); 447 if (webview != null) { 448 webview.stopLoading(); 449 } 450 final WebView subview = t.getSubWebView(); 451 if (subview != null) { 452 webview.stopLoading(); 453 } 454 } 455 } 456 457 // This method checks if a non-app tab (one created within the browser) 458 // matches the given url. 459 private boolean tabMatchesUrl(Tab t, String url) { 460 if (t.getAppId() != null) { 461 return false; 462 } 463 WebView webview = t.getWebView(); 464 if (webview == null) { 465 return false; 466 } else if (url.equals(webview.getUrl()) 467 || url.equals(webview.getOriginalUrl())) { 468 return true; 469 } 470 return false; 471 } 472 473 /** 474 * Return the tab that has no app id associated with it and the url of the 475 * tab matches the given url. 476 * @param url The url to search for. 477 */ 478 Tab findUnusedTabWithUrl(String url) { 479 if (url == null) { 480 return null; 481 } 482 // Check the current tab first. 483 Tab t = getCurrentTab(); 484 if (t != null && tabMatchesUrl(t, url)) { 485 return t; 486 } 487 // Now check all the rest. 488 final int size = getTabCount(); 489 for (int i = 0; i < size; i++) { 490 t = getTab(i); 491 if (tabMatchesUrl(t, url)) { 492 return t; 493 } 494 } 495 return null; 496 } 497 498 /** 499 * Recreate the main WebView of the given tab. Returns true if the WebView 500 * requires a load, whether it was due to the fact that it was deleted, or 501 * it is because it was a voice search. 502 */ 503 boolean recreateWebView(Tab t, BrowserActivity.UrlData urlData) { 504 final String url = urlData.mUrl; 505 final WebView w = t.getWebView(); 506 if (w != null) { 507 if (url != null && url.equals(t.getOriginalUrl()) 508 // Treat a voice intent as though it is a different URL, 509 // since it most likely is. 510 && urlData.mVoiceIntent == null) { 511 // The original url matches the current url. Just go back to the 512 // first history item so we can load it faster than if we 513 // rebuilt the WebView. 514 final WebBackForwardList list = w.copyBackForwardList(); 515 if (list != null) { 516 w.goBackOrForward(-list.getCurrentIndex()); 517 w.clearHistory(); // maintains the current page. 518 return false; 519 } 520 } 521 t.destroy(); 522 } 523 // Create a new WebView. If this tab is the current tab, we need to put 524 // back all the clients so force it to be the current tab. 525 t.setWebView(createNewWebView()); 526 if (getCurrentTab() == t) { 527 setCurrentTab(t, true); 528 } 529 // Clear the saved state and picker data 530 t.setSavedState(null); 531 t.clearPickerData(); 532 // Save the new url in order to avoid deleting the WebView. 533 t.setOriginalUrl(url); 534 return true; 535 } 536 537 /** 538 * Creates a new WebView and registers it with the global settings. 539 */ 540 private WebView createNewWebView() { 541 // Create a new WebView 542 WebView w = new WebView(mActivity); 543 w.setScrollbarFadingEnabled(true); 544 w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); 545 w.setMapTrackballToArrowKeys(false); // use trackball directly 546 // Enable the built-in zoom 547 w.getSettings().setBuiltInZoomControls(true); 548 // Add this WebView to the settings observer list and update the 549 // settings 550 final BrowserSettings s = BrowserSettings.getInstance(); 551 s.addObserver(w.getSettings()).update(s, null); 552 553 // pick a default 554 if (false) { 555 MeshTracker mt = new MeshTracker(2); 556 Paint paint = new Paint(); 557 Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(), 558 R.drawable.pattern_carbon_fiber_dark); 559 paint.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT, 560 Shader.TileMode.REPEAT)); 561 mt.setBGPaint(paint); 562 w.setDragTracker(mt); 563 } 564 return w; 565 } 566 567 /** 568 * Put the current tab in the background and set newTab as the current tab. 569 * @param newTab The new tab. If newTab is null, the current tab is not 570 * set. 571 */ 572 boolean setCurrentTab(Tab newTab) { 573 return setCurrentTab(newTab, false); 574 } 575 576 void pauseCurrentTab() { 577 Tab t = getCurrentTab(); 578 if (t != null) { 579 t.pause(); 580 } 581 } 582 583 void resumeCurrentTab() { 584 Tab t = getCurrentTab(); 585 if (t != null) { 586 t.resume(); 587 } 588 } 589 590 /** 591 * If force is true, this method skips the check for newTab == current. 592 */ 593 private boolean setCurrentTab(Tab newTab, boolean force) { 594 Tab current = getTab(mCurrentTab); 595 if (current == newTab && !force) { 596 return true; 597 } 598 if (current != null) { 599 current.putInBackground(); 600 mCurrentTab = -1; 601 } 602 if (newTab == null) { 603 return false; 604 } 605 606 // Move the newTab to the end of the queue 607 int index = mTabQueue.indexOf(newTab); 608 if (index != -1) { 609 mTabQueue.remove(index); 610 } 611 mTabQueue.add(newTab); 612 613 // Display the new current tab 614 mCurrentTab = mTabs.indexOf(newTab); 615 WebView mainView = newTab.getWebView(); 616 boolean needRestore = (mainView == null); 617 if (needRestore) { 618 // Same work as in createNewTab() except don't do new Tab() 619 mainView = createNewWebView(); 620 newTab.setWebView(mainView); 621 } 622 newTab.putInForeground(); 623 if (needRestore) { 624 // Have to finish setCurrentTab work before calling restoreState 625 if (!newTab.restoreState(newTab.getSavedState())) { 626 mainView.loadUrl(BrowserSettings.getInstance().getHomePage()); 627 } 628 } 629 return true; 630 } 631 } 632