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 /** 220 * Remove the parent child relationships from all tabs. 221 */ 222 void removeParentChildRelationShips() { 223 for (Tab tab : mTabs) { 224 tab.removeFromTree(); 225 } 226 } 227 228 /** 229 * Remove the tab from the list. If the tab is the current tab shown, the 230 * last created tab will be shown. 231 * @param t The tab to be removed. 232 */ 233 boolean removeTab(Tab t) { 234 if (t == null) { 235 return false; 236 } 237 238 // Grab the current tab before modifying the list. 239 Tab current = getCurrentTab(); 240 241 // Remove t from our list of tabs. 242 mTabs.remove(t); 243 244 // Put the tab in the background only if it is the current one. 245 if (current == t) { 246 t.putInBackground(); 247 mCurrentTab = -1; 248 } else { 249 // If a tab that is earlier in the list gets removed, the current 250 // index no longer points to the correct tab. 251 mCurrentTab = getTabPosition(current); 252 } 253 254 // destroy the tab 255 t.destroy(); 256 // clear it's references to parent and children 257 t.removeFromTree(); 258 259 // Remove it from the queue of viewed tabs. 260 mTabQueue.remove(t); 261 return true; 262 } 263 264 /** 265 * Destroy all the tabs and subwindows 266 */ 267 void destroy() { 268 for (Tab t : mTabs) { 269 t.destroy(); 270 } 271 mTabs.clear(); 272 mTabQueue.clear(); 273 } 274 275 /** 276 * Returns the number of tabs created. 277 * @return The number of tabs created. 278 */ 279 int getTabCount() { 280 return mTabs.size(); 281 } 282 283 /** 284 * save the tab state: 285 * current position 286 * position sorted array of tab ids 287 * for each tab id, save the tab state 288 * @param outState 289 * @param saveImages 290 */ 291 void saveState(Bundle outState) { 292 final int numTabs = getTabCount(); 293 if (numTabs == 0) { 294 return; 295 } 296 long[] ids = new long[numTabs]; 297 int i = 0; 298 for (Tab tab : mTabs) { 299 Bundle tabState = tab.saveState(); 300 if (tabState != null) { 301 ids[i++] = tab.getId(); 302 String key = Long.toString(tab.getId()); 303 if (outState.containsKey(key)) { 304 // Dump the tab state for debugging purposes 305 for (Tab dt : mTabs) { 306 Log.e(LOGTAG, dt.toString()); 307 } 308 throw new IllegalStateException( 309 "Error saving state, duplicate tab ids!"); 310 } 311 outState.putBundle(key, tabState); 312 } else { 313 ids[i++] = -1; 314 // Since we won't be restoring the thumbnail, delete it 315 tab.deleteThumbnail(); 316 } 317 } 318 if (!outState.isEmpty()) { 319 outState.putLongArray(POSITIONS, ids); 320 Tab current = getCurrentTab(); 321 long cid = -1; 322 if (current != null) { 323 cid = current.getId(); 324 } 325 outState.putLong(CURRENT, cid); 326 } 327 } 328 329 /** 330 * Check if the state can be restored. If the state can be restored, the 331 * current tab id is returned. This can be passed to restoreState below 332 * in order to restore the correct tab. Otherwise, -1 is returned and the 333 * state cannot be restored. 334 */ 335 long canRestoreState(Bundle inState, boolean restoreIncognitoTabs) { 336 final long[] ids = (inState == null) ? null : inState.getLongArray(POSITIONS); 337 if (ids == null) { 338 return -1; 339 } 340 final long oldcurrent = inState.getLong(CURRENT); 341 long current = -1; 342 if (restoreIncognitoTabs || (hasState(oldcurrent, inState) && !isIncognito(oldcurrent, inState))) { 343 current = oldcurrent; 344 } else { 345 // pick first non incognito tab 346 for (long id : ids) { 347 if (hasState(id, inState) && !isIncognito(id, inState)) { 348 current = id; 349 break; 350 } 351 } 352 } 353 return current; 354 } 355 356 private boolean hasState(long id, Bundle state) { 357 if (id == -1) return false; 358 Bundle tab = state.getBundle(Long.toString(id)); 359 return ((tab != null) && !tab.isEmpty()); 360 } 361 362 private boolean isIncognito(long id, Bundle state) { 363 Bundle tabstate = state.getBundle(Long.toString(id)); 364 if ((tabstate != null) && !tabstate.isEmpty()) { 365 return tabstate.getBoolean(Tab.INCOGNITO); 366 } 367 return false; 368 } 369 370 /** 371 * Restore the state of all the tabs. 372 * @param currentId The tab id to restore. 373 * @param inState The saved state of all the tabs. 374 * @param restoreIncognitoTabs Restoring private browsing tabs 375 * @param restoreAll All webviews get restored, not just the current tab 376 * (this does not override handling of incognito tabs) 377 */ 378 void restoreState(Bundle inState, long currentId, 379 boolean restoreIncognitoTabs, boolean restoreAll) { 380 if (currentId == -1) { 381 return; 382 } 383 long[] ids = inState.getLongArray(POSITIONS); 384 long maxId = -Long.MAX_VALUE; 385 HashMap<Long, Tab> tabMap = new HashMap<Long, Tab>(); 386 for (long id : ids) { 387 if (id > maxId) { 388 maxId = id; 389 } 390 final String idkey = Long.toString(id); 391 Bundle state = inState.getBundle(idkey); 392 if (state == null || state.isEmpty()) { 393 // Skip tab 394 continue; 395 } else if (!restoreIncognitoTabs 396 && state.getBoolean(Tab.INCOGNITO)) { 397 // ignore tab 398 } else if (id == currentId || restoreAll) { 399 Tab t = createNewTab(state, false); 400 if (t == null) { 401 // We could "break" at this point, but we want 402 // sNextId to be set correctly. 403 continue; 404 } 405 tabMap.put(id, t); 406 // Me must set the current tab before restoring the state 407 // so that all the client classes are set. 408 if (id == currentId) { 409 setCurrentTab(t); 410 } 411 } else { 412 // Create a new tab and don't restore the state yet, add it 413 // to the tab list 414 Tab t = new Tab(mController, state); 415 tabMap.put(id, t); 416 mTabs.add(t); 417 // added the tab to the front as they are not current 418 mTabQueue.add(0, t); 419 } 420 } 421 422 // make sure that there is no id overlap between the restored 423 // and new tabs 424 sNextId = maxId + 1; 425 426 if (mCurrentTab == -1) { 427 if (getTabCount() > 0) { 428 setCurrentTab(getTab(0)); 429 } 430 } 431 // restore parent/child relationships 432 for (long id : ids) { 433 final Tab tab = tabMap.get(id); 434 final Bundle b = inState.getBundle(Long.toString(id)); 435 if ((b != null) && (tab != null)) { 436 final long parentId = b.getLong(Tab.PARENTTAB, -1); 437 if (parentId != -1) { 438 final Tab parent = tabMap.get(parentId); 439 if (parent != null) { 440 parent.addChildTab(tab); 441 } 442 } 443 } 444 } 445 } 446 447 /** 448 * Free the memory in this order, 1) free the background tabs; 2) free the 449 * WebView cache; 450 */ 451 void freeMemory() { 452 if (getTabCount() == 0) return; 453 454 // free the least frequently used background tabs 455 Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab()); 456 if (tabs.size() > 0) { 457 Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser"); 458 for (Tab t : tabs) { 459 // store the WebView's state. 460 t.saveState(); 461 // destroy the tab 462 t.destroy(); 463 } 464 return; 465 } 466 467 // free the WebView's unused memory (this includes the cache) 468 Log.w(LOGTAG, "Free WebView's unused memory and cache"); 469 WebView view = getCurrentWebView(); 470 if (view != null) { 471 view.freeMemory(); 472 } 473 } 474 475 private Vector<Tab> getHalfLeastUsedTabs(Tab current) { 476 Vector<Tab> tabsToGo = new Vector<Tab>(); 477 478 // Don't do anything if we only have 1 tab or if the current tab is 479 // null. 480 if (getTabCount() == 1 || current == null) { 481 return tabsToGo; 482 } 483 484 if (mTabQueue.size() == 0) { 485 return tabsToGo; 486 } 487 488 // Rip through the queue starting at the beginning and tear down half of 489 // available tabs which are not the current tab or the parent of the 490 // current tab. 491 int openTabCount = 0; 492 for (Tab t : mTabQueue) { 493 if (t != null && t.getWebView() != null) { 494 openTabCount++; 495 if (t != current && t != current.getParent()) { 496 tabsToGo.add(t); 497 } 498 } 499 } 500 501 openTabCount /= 2; 502 if (tabsToGo.size() > openTabCount) { 503 tabsToGo.setSize(openTabCount); 504 } 505 506 return tabsToGo; 507 } 508 509 Tab getLeastUsedTab(Tab current) { 510 if (getTabCount() == 1 || current == null) { 511 return null; 512 } 513 if (mTabQueue.size() == 0) { 514 return null; 515 } 516 // find a tab which is not the current tab or the parent of the 517 // current tab 518 for (Tab t : mTabQueue) { 519 if (t != null && t.getWebView() != null) { 520 if (t != current && t != current.getParent()) { 521 return t; 522 } 523 } 524 } 525 return null; 526 } 527 528 /** 529 * Show the tab that contains the given WebView. 530 * @param view The WebView used to find the tab. 531 */ 532 Tab getTabFromView(WebView view) { 533 for (Tab t : mTabs) { 534 if (t.getSubWebView() == view || t.getWebView() == view) { 535 return t; 536 } 537 } 538 return null; 539 } 540 541 /** 542 * Return the tab with the matching application id. 543 * @param id The application identifier. 544 */ 545 Tab getTabFromAppId(String id) { 546 if (id == null) { 547 return null; 548 } 549 for (Tab t : mTabs) { 550 if (id.equals(t.getAppId())) { 551 return t; 552 } 553 } 554 return null; 555 } 556 557 /** 558 * Stop loading in all opened WebView including subWindows. 559 */ 560 void stopAllLoading() { 561 for (Tab t : mTabs) { 562 final WebView webview = t.getWebView(); 563 if (webview != null) { 564 webview.stopLoading(); 565 } 566 final WebView subview = t.getSubWebView(); 567 if (subview != null) { 568 subview.stopLoading(); 569 } 570 } 571 } 572 573 // This method checks if a tab matches the given url. 574 private boolean tabMatchesUrl(Tab t, String url) { 575 return url.equals(t.getUrl()) || url.equals(t.getOriginalUrl()); 576 } 577 578 /** 579 * Return the tab that matches the given url. 580 * @param url The url to search for. 581 */ 582 Tab findTabWithUrl(String url) { 583 if (url == null) { 584 return null; 585 } 586 // Check the current tab first. 587 Tab currentTab = getCurrentTab(); 588 if (currentTab != null && tabMatchesUrl(currentTab, url)) { 589 return currentTab; 590 } 591 // Now check all the rest. 592 for (Tab tab : mTabs) { 593 if (tabMatchesUrl(tab, url)) { 594 return tab; 595 } 596 } 597 return null; 598 } 599 600 /** 601 * Recreate the main WebView of the given tab. 602 */ 603 void recreateWebView(Tab t) { 604 final WebView w = t.getWebView(); 605 if (w != null) { 606 t.destroy(); 607 } 608 // Create a new WebView. If this tab is the current tab, we need to put 609 // back all the clients so force it to be the current tab. 610 t.setWebView(createNewWebView(), false); 611 if (getCurrentTab() == t) { 612 setCurrentTab(t, true); 613 } 614 } 615 616 /** 617 * Creates a new WebView and registers it with the global settings. 618 */ 619 private WebView createNewWebView() { 620 return createNewWebView(false); 621 } 622 623 /** 624 * Creates a new WebView and registers it with the global settings. 625 * @param privateBrowsing When true, enables private browsing in the new 626 * WebView. 627 */ 628 private WebView createNewWebView(boolean privateBrowsing) { 629 return mController.getWebViewFactory().createWebView(privateBrowsing); 630 } 631 632 /** 633 * Put the current tab in the background and set newTab as the current tab. 634 * @param newTab The new tab. If newTab is null, the current tab is not 635 * set. 636 */ 637 boolean setCurrentTab(Tab newTab) { 638 return setCurrentTab(newTab, false); 639 } 640 641 /** 642 * If force is true, this method skips the check for newTab == current. 643 */ 644 private boolean setCurrentTab(Tab newTab, boolean force) { 645 Tab current = getTab(mCurrentTab); 646 if (current == newTab && !force) { 647 return true; 648 } 649 if (current != null) { 650 current.putInBackground(); 651 mCurrentTab = -1; 652 } 653 if (newTab == null) { 654 return false; 655 } 656 657 // Move the newTab to the end of the queue 658 int index = mTabQueue.indexOf(newTab); 659 if (index != -1) { 660 mTabQueue.remove(index); 661 } 662 mTabQueue.add(newTab); 663 664 // Display the new current tab 665 mCurrentTab = mTabs.indexOf(newTab); 666 WebView mainView = newTab.getWebView(); 667 boolean needRestore = mainView == null; 668 if (needRestore) { 669 // Same work as in createNewTab() except don't do new Tab() 670 mainView = createNewWebView(); 671 newTab.setWebView(mainView); 672 } 673 newTab.putInForeground(); 674 return true; 675 } 676 677 public void setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener) { 678 mOnThumbnailUpdatedListener = listener; 679 for (Tab t : mTabs) { 680 WebView web = t.getWebView(); 681 if (web != null) { 682 web.setPictureListener(listener != null ? t : null); 683 } 684 } 685 } 686 687 public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() { 688 return mOnThumbnailUpdatedListener; 689 } 690 691 } 692