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.os.Bundle; 20 import android.util.Log; 21 import android.webkit.WebView; 22 23 import java.util.ArrayList; 24 import java.util.HashMap; 25 import java.util.List; 26 import java.util.Vector; 27 28 class TabControl { 29 // Log Tag 30 private static final String LOGTAG = "TabControl"; 31 32 // next Tab ID, starting at 1 33 private static long sNextId = 1; 34 35 private static final String POSITIONS = "positions"; 36 private static final String CURRENT = "current"; 37 38 public static interface OnThumbnailUpdatedListener { 39 void onThumbnailUpdated(Tab t); 40 } 41 42 // Maximum number of tabs. 43 private int mMaxTabs; 44 // Private array of WebViews that are used as tabs. 45 private ArrayList<Tab> mTabs; 46 // Queue of most recently viewed tabs. 47 private ArrayList<Tab> mTabQueue; 48 // Current position in mTabs. 49 private int mCurrentTab = -1; 50 // the main browser controller 51 private final Controller mController; 52 53 private OnThumbnailUpdatedListener mOnThumbnailUpdatedListener; 54 55 /** 56 * Construct a new TabControl object 57 */ 58 TabControl(Controller controller) { 59 mController = controller; 60 mMaxTabs = mController.getMaxTabs(); 61 mTabs = new ArrayList<Tab>(mMaxTabs); 62 mTabQueue = new ArrayList<Tab>(mMaxTabs); 63 } 64 65 synchronized static long getNextId() { 66 return sNextId++; 67 } 68 69 /** 70 * Return the current tab's main WebView. This will always return the main 71 * WebView for a given tab and not a subwindow. 72 * @return The current tab's WebView. 73 */ 74 WebView getCurrentWebView() { 75 Tab t = getTab(mCurrentTab); 76 if (t == null) { 77 return null; 78 } 79 return t.getWebView(); 80 } 81 82 /** 83 * Return the current tab's top-level WebView. This can return a subwindow 84 * if one exists. 85 * @return The top-level WebView of the current tab. 86 */ 87 WebView getCurrentTopWebView() { 88 Tab t = getTab(mCurrentTab); 89 if (t == null) { 90 return null; 91 } 92 return t.getTopWindow(); 93 } 94 95 /** 96 * Return the current tab's subwindow if it exists. 97 * @return The subwindow of the current tab or null if it doesn't exist. 98 */ 99 WebView getCurrentSubWindow() { 100 Tab t = getTab(mCurrentTab); 101 if (t == null) { 102 return null; 103 } 104 return t.getSubWebView(); 105 } 106 107 /** 108 * return the list of tabs 109 */ 110 List<Tab> getTabs() { 111 return mTabs; 112 } 113 114 /** 115 * Return the tab at the specified position. 116 * @return The Tab for the specified position or null if the tab does not 117 * exist. 118 */ 119 Tab getTab(int position) { 120 if (position >= 0 && position < mTabs.size()) { 121 return mTabs.get(position); 122 } 123 return null; 124 } 125 126 /** 127 * Return the current tab. 128 * @return The current tab. 129 */ 130 Tab getCurrentTab() { 131 return getTab(mCurrentTab); 132 } 133 134 /** 135 * Return the current tab position. 136 * @return The current tab position 137 */ 138 int getCurrentPosition() { 139 return mCurrentTab; 140 } 141 142 /** 143 * Given a Tab, find it's position 144 * @param Tab to find 145 * @return position of Tab or -1 if not found 146 */ 147 int getTabPosition(Tab tab) { 148 if (tab == null) { 149 return -1; 150 } 151 return mTabs.indexOf(tab); 152 } 153 154 boolean canCreateNewTab() { 155 return mMaxTabs > mTabs.size(); 156 } 157 158 /** 159 * Returns true if there are any incognito tabs open. 160 * @return True when any incognito tabs are open, false otherwise. 161 */ 162 boolean hasAnyOpenIncognitoTabs() { 163 for (Tab tab : mTabs) { 164 if (tab.getWebView() != null 165 && tab.getWebView().isPrivateBrowsingEnabled()) { 166 return true; 167 } 168 } 169 return false; 170 } 171 172 void addPreloadedTab(Tab tab) { 173 for (Tab current : mTabs) { 174 if (current != null && current.getId() == tab.getId()) { 175 throw new IllegalStateException("Tab with id " + tab.getId() + " already exists: " 176 + current.toString()); 177 } 178 } 179 mTabs.add(tab); 180 tab.setController(mController); 181 mController.onSetWebView(tab, tab.getWebView()); 182 tab.putInBackground(); 183 } 184 185 /** 186 * Create a new tab. 187 * @return The newly createTab or null if we have reached the maximum 188 * number of open tabs. 189 */ 190 Tab createNewTab(boolean privateBrowsing) { 191 return createNewTab(null, privateBrowsing); 192 } 193 194 Tab createNewTab(Bundle state, boolean privateBrowsing) { 195 int size = mTabs.size(); 196 // Return false if we have maxed out on tabs 197 if (!canCreateNewTab()) { 198 return null; 199 } 200 201 final WebView w = createNewWebView(privateBrowsing); 202 203 // Create a new tab and add it to the tab list 204 Tab t = new Tab(mController, w, state); 205 mTabs.add(t); 206 // Initially put the tab in the background. 207 t.putInBackground(); 208 return t; 209 } 210 211 /** 212 * Create a new tab with default values for closeOnExit(false), 213 * appId(null), url(null), and privateBrowsing(false). 214 */ 215 Tab createNewTab() { 216 return createNewTab(false); 217 } 218 219 SnapshotTab createSnapshotTab(long snapshotId) { 220 SnapshotTab t = new SnapshotTab(mController, snapshotId); 221 mTabs.add(t); 222 return t; 223 } 224 225 /** 226 * Remove the parent child relationships from all tabs. 227 */ 228 void removeParentChildRelationShips() { 229 for (Tab tab : mTabs) { 230 tab.removeFromTree(); 231 } 232 } 233 234 /** 235 * Remove the tab from the list. If the tab is the current tab shown, the 236 * last created tab will be shown. 237 * @param t The tab to be removed. 238 */ 239 boolean removeTab(Tab t) { 240 if (t == null) { 241 return false; 242 } 243 244 // Grab the current tab before modifying the list. 245 Tab current = getCurrentTab(); 246 247 // Remove t from our list of tabs. 248 mTabs.remove(t); 249 250 // Put the tab in the background only if it is the current one. 251 if (current == t) { 252 t.putInBackground(); 253 mCurrentTab = -1; 254 } else { 255 // If a tab that is earlier in the list gets removed, the current 256 // index no longer points to the correct tab. 257 mCurrentTab = getTabPosition(current); 258 } 259 260 // destroy the tab 261 t.destroy(); 262 // clear it's references to parent and children 263 t.removeFromTree(); 264 265 // Remove it from the queue of viewed tabs. 266 mTabQueue.remove(t); 267 return true; 268 } 269 270 /** 271 * Destroy all the tabs and subwindows 272 */ 273 void destroy() { 274 for (Tab t : mTabs) { 275 t.destroy(); 276 } 277 mTabs.clear(); 278 mTabQueue.clear(); 279 } 280 281 /** 282 * Returns the number of tabs created. 283 * @return The number of tabs created. 284 */ 285 int getTabCount() { 286 return mTabs.size(); 287 } 288 289 /** 290 * save the tab state: 291 * current position 292 * position sorted array of tab ids 293 * for each tab id, save the tab state 294 * @param outState 295 * @param saveImages 296 */ 297 void saveState(Bundle outState) { 298 final int numTabs = getTabCount(); 299 if (numTabs == 0) { 300 return; 301 } 302 long[] ids = new long[numTabs]; 303 int i = 0; 304 for (Tab tab : mTabs) { 305 Bundle tabState = tab.saveState(); 306 if (tabState != null) { 307 ids[i++] = tab.getId(); 308 String key = Long.toString(tab.getId()); 309 if (outState.containsKey(key)) { 310 // Dump the tab state for debugging purposes 311 for (Tab dt : mTabs) { 312 Log.e(LOGTAG, dt.toString()); 313 } 314 throw new IllegalStateException( 315 "Error saving state, duplicate tab ids!"); 316 } 317 outState.putBundle(key, tabState); 318 } else { 319 ids[i++] = -1; 320 // Since we won't be restoring the thumbnail, delete it 321 tab.deleteThumbnail(); 322 } 323 } 324 if (!outState.isEmpty()) { 325 outState.putLongArray(POSITIONS, ids); 326 Tab current = getCurrentTab(); 327 long cid = -1; 328 if (current != null) { 329 cid = current.getId(); 330 } 331 outState.putLong(CURRENT, cid); 332 } 333 } 334 335 /** 336 * Check if the state can be restored. If the state can be restored, the 337 * current tab id is returned. This can be passed to restoreState below 338 * in order to restore the correct tab. Otherwise, -1 is returned and the 339 * state cannot be restored. 340 */ 341 long canRestoreState(Bundle inState, boolean restoreIncognitoTabs) { 342 final long[] ids = (inState == null) ? null : inState.getLongArray(POSITIONS); 343 if (ids == null) { 344 return -1; 345 } 346 final long oldcurrent = inState.getLong(CURRENT); 347 long current = -1; 348 if (restoreIncognitoTabs || (hasState(oldcurrent, inState) && !isIncognito(oldcurrent, inState))) { 349 current = oldcurrent; 350 } else { 351 // pick first non incognito tab 352 for (long id : ids) { 353 if (hasState(id, inState) && !isIncognito(id, inState)) { 354 current = id; 355 break; 356 } 357 } 358 } 359 return current; 360 } 361 362 private boolean hasState(long id, Bundle state) { 363 if (id == -1) return false; 364 Bundle tab = state.getBundle(Long.toString(id)); 365 return ((tab != null) && !tab.isEmpty()); 366 } 367 368 private boolean isIncognito(long id, Bundle state) { 369 Bundle tabstate = state.getBundle(Long.toString(id)); 370 if ((tabstate != null) && !tabstate.isEmpty()) { 371 return tabstate.getBoolean(Tab.INCOGNITO); 372 } 373 return false; 374 } 375 376 /** 377 * Restore the state of all the tabs. 378 * @param currentId The tab id to restore. 379 * @param inState The saved state of all the tabs. 380 * @param restoreIncognitoTabs Restoring private browsing tabs 381 * @param restoreAll All webviews get restored, not just the current tab 382 * (this does not override handling of incognito tabs) 383 */ 384 void restoreState(Bundle inState, long currentId, 385 boolean restoreIncognitoTabs, boolean restoreAll) { 386 if (currentId == -1) { 387 return; 388 } 389 long[] ids = inState.getLongArray(POSITIONS); 390 long maxId = -Long.MAX_VALUE; 391 HashMap<Long, Tab> tabMap = new HashMap<Long, Tab>(); 392 for (long id : ids) { 393 if (id > maxId) { 394 maxId = id; 395 } 396 final String idkey = Long.toString(id); 397 Bundle state = inState.getBundle(idkey); 398 if (state == null || state.isEmpty()) { 399 // Skip tab 400 continue; 401 } else if (!restoreIncognitoTabs 402 && state.getBoolean(Tab.INCOGNITO)) { 403 // ignore tab 404 } else if (id == currentId || restoreAll) { 405 Tab t = createNewTab(state, false); 406 if (t == null) { 407 // We could "break" at this point, but we want 408 // sNextId to be set correctly. 409 continue; 410 } 411 tabMap.put(id, t); 412 // Me must set the current tab before restoring the state 413 // so that all the client classes are set. 414 if (id == currentId) { 415 setCurrentTab(t); 416 } 417 } else { 418 // Create a new tab and don't restore the state yet, add it 419 // to the tab list 420 Tab t = new Tab(mController, state); 421 tabMap.put(id, t); 422 mTabs.add(t); 423 // added the tab to the front as they are not current 424 mTabQueue.add(0, t); 425 } 426 } 427 428 // make sure that there is no id overlap between the restored 429 // and new tabs 430 sNextId = maxId + 1; 431 432 if (mCurrentTab == -1) { 433 if (getTabCount() > 0) { 434 setCurrentTab(getTab(0)); 435 } 436 } 437 // restore parent/child relationships 438 for (long id : ids) { 439 final Tab tab = tabMap.get(id); 440 final Bundle b = inState.getBundle(Long.toString(id)); 441 if ((b != null) && (tab != null)) { 442 final long parentId = b.getLong(Tab.PARENTTAB, -1); 443 if (parentId != -1) { 444 final Tab parent = tabMap.get(parentId); 445 if (parent != null) { 446 parent.addChildTab(tab); 447 } 448 } 449 } 450 } 451 } 452 453 /** 454 * Free the memory in this order, 1) free the background tabs; 2) free the 455 * WebView cache; 456 */ 457 void freeMemory() { 458 if (getTabCount() == 0) return; 459 460 // free the least frequently used background tabs 461 Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab()); 462 if (tabs.size() > 0) { 463 Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser"); 464 for (Tab t : tabs) { 465 // store the WebView's state. 466 t.saveState(); 467 // destroy the tab 468 t.destroy(); 469 } 470 return; 471 } 472 473 // free the WebView's unused memory (this includes the cache) 474 Log.w(LOGTAG, "Free WebView's unused memory and cache"); 475 WebView view = getCurrentWebView(); 476 if (view != null) { 477 view.freeMemory(); 478 } 479 } 480 481 private Vector<Tab> getHalfLeastUsedTabs(Tab current) { 482 Vector<Tab> tabsToGo = new Vector<Tab>(); 483 484 // Don't do anything if we only have 1 tab or if the current tab is 485 // null. 486 if (getTabCount() == 1 || current == null) { 487 return tabsToGo; 488 } 489 490 if (mTabQueue.size() == 0) { 491 return tabsToGo; 492 } 493 494 // Rip through the queue starting at the beginning and tear down half of 495 // available tabs which are not the current tab or the parent of the 496 // current tab. 497 int openTabCount = 0; 498 for (Tab t : mTabQueue) { 499 if (t != null && t.getWebView() != null) { 500 openTabCount++; 501 if (t != current && t != current.getParent()) { 502 tabsToGo.add(t); 503 } 504 } 505 } 506 507 openTabCount /= 2; 508 if (tabsToGo.size() > openTabCount) { 509 tabsToGo.setSize(openTabCount); 510 } 511 512 return tabsToGo; 513 } 514 515 Tab getLeastUsedTab(Tab current) { 516 if (getTabCount() == 1 || current == null) { 517 return null; 518 } 519 if (mTabQueue.size() == 0) { 520 return null; 521 } 522 // find a tab which is not the current tab or the parent of the 523 // current tab 524 for (Tab t : mTabQueue) { 525 if (t != null && t.getWebView() != null) { 526 if (t != current && t != current.getParent()) { 527 return t; 528 } 529 } 530 } 531 return null; 532 } 533 534 /** 535 * Show the tab that contains the given WebView. 536 * @param view The WebView used to find the tab. 537 */ 538 Tab getTabFromView(WebView view) { 539 for (Tab t : mTabs) { 540 if (t.getSubWebView() == view || t.getWebView() == view) { 541 return t; 542 } 543 } 544 return null; 545 } 546 547 /** 548 * Return the tab with the matching application id. 549 * @param id The application identifier. 550 */ 551 Tab getTabFromAppId(String id) { 552 if (id == null) { 553 return null; 554 } 555 for (Tab t : mTabs) { 556 if (id.equals(t.getAppId())) { 557 return t; 558 } 559 } 560 return null; 561 } 562 563 /** 564 * Stop loading in all opened WebView including subWindows. 565 */ 566 void stopAllLoading() { 567 for (Tab t : mTabs) { 568 final WebView webview = t.getWebView(); 569 if (webview != null) { 570 webview.stopLoading(); 571 } 572 final WebView subview = t.getSubWebView(); 573 if (subview != null) { 574 subview.stopLoading(); 575 } 576 } 577 } 578 579 // This method checks if a tab matches the given url. 580 private boolean tabMatchesUrl(Tab t, String url) { 581 return url.equals(t.getUrl()) || url.equals(t.getOriginalUrl()); 582 } 583 584 /** 585 * Return the tab that matches the given url. 586 * @param url The url to search for. 587 */ 588 Tab findTabWithUrl(String url) { 589 if (url == null) { 590 return null; 591 } 592 // Check the current tab first. 593 Tab currentTab = getCurrentTab(); 594 if (currentTab != null && tabMatchesUrl(currentTab, url)) { 595 return currentTab; 596 } 597 // Now check all the rest. 598 for (Tab tab : mTabs) { 599 if (tabMatchesUrl(tab, url)) { 600 return tab; 601 } 602 } 603 return null; 604 } 605 606 /** 607 * Recreate the main WebView of the given tab. 608 */ 609 void recreateWebView(Tab t) { 610 final WebView w = t.getWebView(); 611 if (w != null) { 612 t.destroy(); 613 } 614 // Create a new WebView. If this tab is the current tab, we need to put 615 // back all the clients so force it to be the current tab. 616 t.setWebView(createNewWebView(), false); 617 if (getCurrentTab() == t) { 618 setCurrentTab(t, true); 619 } 620 } 621 622 /** 623 * Creates a new WebView and registers it with the global settings. 624 */ 625 private WebView createNewWebView() { 626 return createNewWebView(false); 627 } 628 629 /** 630 * Creates a new WebView and registers it with the global settings. 631 * @param privateBrowsing When true, enables private browsing in the new 632 * WebView. 633 */ 634 private WebView createNewWebView(boolean privateBrowsing) { 635 return mController.getWebViewFactory().createWebView(privateBrowsing); 636 } 637 638 /** 639 * Put the current tab in the background and set newTab as the current tab. 640 * @param newTab The new tab. If newTab is null, the current tab is not 641 * set. 642 */ 643 boolean setCurrentTab(Tab newTab) { 644 return setCurrentTab(newTab, false); 645 } 646 647 /** 648 * If force is true, this method skips the check for newTab == current. 649 */ 650 private boolean setCurrentTab(Tab newTab, boolean force) { 651 Tab current = getTab(mCurrentTab); 652 if (current == newTab && !force) { 653 return true; 654 } 655 if (current != null) { 656 current.putInBackground(); 657 mCurrentTab = -1; 658 } 659 if (newTab == null) { 660 return false; 661 } 662 663 // Move the newTab to the end of the queue 664 int index = mTabQueue.indexOf(newTab); 665 if (index != -1) { 666 mTabQueue.remove(index); 667 } 668 mTabQueue.add(newTab); 669 670 // Display the new current tab 671 mCurrentTab = mTabs.indexOf(newTab); 672 WebView mainView = newTab.getWebView(); 673 boolean needRestore = !newTab.isSnapshot() && (mainView == null); 674 if (needRestore) { 675 // Same work as in createNewTab() except don't do new Tab() 676 mainView = createNewWebView(); 677 newTab.setWebView(mainView); 678 } 679 newTab.putInForeground(); 680 return true; 681 } 682 683 public void setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener) { 684 mOnThumbnailUpdatedListener = listener; 685 for (Tab t : mTabs) { 686 WebView web = t.getWebView(); 687 if (web != null) { 688 web.setPictureListener(listener != null ? t : null); 689 } 690 } 691 } 692 693 public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() { 694 return mOnThumbnailUpdatedListener; 695 } 696 697 } 698