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 */ TabControl(BrowserActivity activity)57 TabControl(BrowserActivity activity) { 58 mActivity = activity; 59 mThumbnailDir = activity.getDir("thumbnails", 0); 60 } 61 getThumbnailDir()62 File getThumbnailDir() { 63 return mThumbnailDir; 64 } 65 getBrowserActivity()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 */ getCurrentWebView()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 */ getCurrentTopWebView()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 */ getCurrentSubWindow()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 */ getTab(int index)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 */ getCurrentTab()124 Tab getCurrentTab() { 125 return getTab(mCurrentTab); 126 } 127 128 /** 129 * Return the current tab index. 130 * @return The current tab index 131 */ getCurrentIndex()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 */ getTabIndex(Tab tab)141 int getTabIndex(Tab tab) { 142 if (tab == null) { 143 return -1; 144 } 145 return mTabs.indexOf(tab); 146 } 147 canCreateNewTab()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 */ createNewTab(boolean closeOnExit, String appId, String url)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 */ createNewTab()177 Tab createNewTab() { 178 return createNewTab(false, null, null); 179 } 180 181 /** 182 * Remove the parent child relationships from all tabs. 183 */ removeParentChildRelationShips()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 */ removeTab(Tab t)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 // Remove it from the queue of viewed tabs. 233 mTabQueue.remove(t); 234 return true; 235 } 236 237 /** 238 * Destroy all the tabs and subwindows 239 */ destroy()240 void destroy() { 241 for (Tab t : mTabs) { 242 t.destroy(); 243 } 244 mTabs.clear(); 245 mTabQueue.clear(); 246 } 247 248 /** 249 * Returns the number of tabs created. 250 * @return The number of tabs created. 251 */ getTabCount()252 int getTabCount() { 253 return mTabs.size(); 254 } 255 256 257 /** 258 * Save the state of all the Tabs. 259 * @param outState The Bundle to save the state to. 260 */ saveState(Bundle outState)261 void saveState(Bundle outState) { 262 final int numTabs = getTabCount(); 263 outState.putInt(Tab.NUMTABS, numTabs); 264 final int index = getCurrentIndex(); 265 outState.putInt(Tab.CURRTAB, (index >= 0 && index < numTabs) ? index : 0); 266 for (int i = 0; i < numTabs; i++) { 267 final Tab t = getTab(i); 268 if (t.saveState()) { 269 outState.putBundle(Tab.WEBVIEW + i, t.getSavedState()); 270 } 271 } 272 } 273 274 /** 275 * Restore the state of all the tabs. 276 * @param inState The saved state of all the tabs. 277 * @return True if there were previous tabs that were restored. False if 278 * there was no saved state or restoring the state failed. 279 */ restoreState(Bundle inState)280 boolean restoreState(Bundle inState) { 281 final int numTabs = (inState == null) 282 ? -1 : inState.getInt(Tab.NUMTABS, -1); 283 if (numTabs == -1) { 284 return false; 285 } else { 286 final int currentTab = inState.getInt(Tab.CURRTAB, -1); 287 for (int i = 0; i < numTabs; i++) { 288 if (i == currentTab) { 289 Tab t = createNewTab(); 290 // Me must set the current tab before restoring the state 291 // so that all the client classes are set. 292 setCurrentTab(t); 293 if (!t.restoreState(inState.getBundle(Tab.WEBVIEW + i))) { 294 Log.w(LOGTAG, "Fail in restoreState, load home page."); 295 t.getWebView().loadUrl(BrowserSettings.getInstance() 296 .getHomePage()); 297 } 298 } else { 299 // Create a new tab and don't restore the state yet, add it 300 // to the tab list 301 Tab t = new Tab(mActivity, null, false, null, null); 302 Bundle state = inState.getBundle(Tab.WEBVIEW + i); 303 if (state != null) { 304 t.setSavedState(state); 305 t.populatePickerDataFromSavedState(); 306 // Need to maintain the app id and original url so we 307 // can possibly reuse this tab. 308 t.setAppId(state.getString(Tab.APPID)); 309 t.setOriginalUrl(state.getString(Tab.ORIGINALURL)); 310 } 311 mTabs.add(t); 312 // added the tab to the front as they are not current 313 mTabQueue.add(0, t); 314 } 315 } 316 // Rebuild the tree of tabs. Do this after all tabs have been 317 // created/restored so that the parent tab exists. 318 for (int i = 0; i < numTabs; i++) { 319 final Bundle b = inState.getBundle(Tab.WEBVIEW + i); 320 final Tab t = getTab(i); 321 if (b != null && t != null) { 322 final int parentIndex = b.getInt(Tab.PARENTTAB, -1); 323 if (parentIndex != -1) { 324 final Tab parent = getTab(parentIndex); 325 if (parent != null) { 326 parent.addChildTab(t); 327 } 328 } 329 } 330 } 331 } 332 return true; 333 } 334 335 /** 336 * Free the memory in this order, 1) free the background tabs; 2) free the 337 * WebView cache; 338 */ freeMemory()339 void freeMemory() { 340 if (getTabCount() == 0) return; 341 342 // free the least frequently used background tabs 343 Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab()); 344 if (tabs.size() > 0) { 345 Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser"); 346 for (Tab t : tabs) { 347 // store the WebView's state. 348 t.saveState(); 349 // destroy the tab 350 t.destroy(); 351 } 352 return; 353 } 354 355 // free the WebView's unused memory (this includes the cache) 356 Log.w(LOGTAG, "Free WebView's unused memory and cache"); 357 WebView view = getCurrentWebView(); 358 if (view != null) { 359 view.freeMemory(); 360 } 361 } 362 getHalfLeastUsedTabs(Tab current)363 private Vector<Tab> getHalfLeastUsedTabs(Tab current) { 364 Vector<Tab> tabsToGo = new Vector<Tab>(); 365 366 // Don't do anything if we only have 1 tab or if the current tab is 367 // null. 368 if (getTabCount() == 1 || current == null) { 369 return tabsToGo; 370 } 371 372 if (mTabQueue.size() == 0) { 373 return tabsToGo; 374 } 375 376 // Rip through the queue starting at the beginning and tear down half of 377 // available tabs which are not the current tab or the parent of the 378 // current tab. 379 int openTabCount = 0; 380 for (Tab t : mTabQueue) { 381 if (t != null && t.getWebView() != null) { 382 openTabCount++; 383 if (t != current && t != current.getParentTab()) { 384 tabsToGo.add(t); 385 } 386 } 387 } 388 389 openTabCount /= 2; 390 if (tabsToGo.size() > openTabCount) { 391 tabsToGo.setSize(openTabCount); 392 } 393 394 return tabsToGo; 395 } 396 397 /** 398 * Show the tab that contains the given WebView. 399 * @param view The WebView used to find the tab. 400 */ getTabFromView(WebView view)401 Tab getTabFromView(WebView view) { 402 final int size = getTabCount(); 403 for (int i = 0; i < size; i++) { 404 final Tab t = getTab(i); 405 if (t.getSubWebView() == view || t.getWebView() == view) { 406 return t; 407 } 408 } 409 return null; 410 } 411 412 /** 413 * Return the tab with the matching application id. 414 * @param id The application identifier. 415 */ getTabFromId(String id)416 Tab getTabFromId(String id) { 417 if (id == null) { 418 return null; 419 } 420 final int size = getTabCount(); 421 for (int i = 0; i < size; i++) { 422 final Tab t = getTab(i); 423 if (id.equals(t.getAppId())) { 424 return t; 425 } 426 } 427 return null; 428 } 429 430 /** 431 * Stop loading in all opened WebView including subWindows. 432 */ stopAllLoading()433 void stopAllLoading() { 434 final int size = getTabCount(); 435 for (int i = 0; i < size; i++) { 436 final Tab t = getTab(i); 437 final WebView webview = t.getWebView(); 438 if (webview != null) { 439 webview.stopLoading(); 440 } 441 final WebView subview = t.getSubWebView(); 442 if (subview != null) { 443 webview.stopLoading(); 444 } 445 } 446 } 447 448 // This method checks if a non-app tab (one created within the browser) 449 // matches the given url. tabMatchesUrl(Tab t, String url)450 private boolean tabMatchesUrl(Tab t, String url) { 451 if (t.getAppId() != null) { 452 return false; 453 } 454 WebView webview = t.getWebView(); 455 if (webview == null) { 456 return false; 457 } else if (url.equals(webview.getUrl()) 458 || url.equals(webview.getOriginalUrl())) { 459 return true; 460 } 461 return false; 462 } 463 464 /** 465 * Return the tab that has no app id associated with it and the url of the 466 * tab matches the given url. 467 * @param url The url to search for. 468 */ findUnusedTabWithUrl(String url)469 Tab findUnusedTabWithUrl(String url) { 470 if (url == null) { 471 return null; 472 } 473 // Check the current tab first. 474 Tab t = getCurrentTab(); 475 if (t != null && tabMatchesUrl(t, url)) { 476 return t; 477 } 478 // Now check all the rest. 479 final int size = getTabCount(); 480 for (int i = 0; i < size; i++) { 481 t = getTab(i); 482 if (tabMatchesUrl(t, url)) { 483 return t; 484 } 485 } 486 return null; 487 } 488 489 /** 490 * Recreate the main WebView of the given tab. Returns true if the WebView 491 * requires a load, whether it was due to the fact that it was deleted, or 492 * it is because it was a voice search. 493 */ recreateWebView(Tab t, BrowserActivity.UrlData urlData)494 boolean recreateWebView(Tab t, BrowserActivity.UrlData urlData) { 495 final String url = urlData.mUrl; 496 final WebView w = t.getWebView(); 497 if (w != null) { 498 if (url != null && url.equals(t.getOriginalUrl()) 499 // Treat a voice intent as though it is a different URL, 500 // since it most likely is. 501 && urlData.mVoiceIntent == null) { 502 // The original url matches the current url. Just go back to the 503 // first history item so we can load it faster than if we 504 // rebuilt the WebView. 505 final WebBackForwardList list = w.copyBackForwardList(); 506 if (list != null) { 507 w.goBackOrForward(-list.getCurrentIndex()); 508 w.clearHistory(); // maintains the current page. 509 return false; 510 } 511 } 512 t.destroy(); 513 } 514 // Create a new WebView. If this tab is the current tab, we need to put 515 // back all the clients so force it to be the current tab. 516 t.setWebView(createNewWebView()); 517 if (getCurrentTab() == t) { 518 setCurrentTab(t, true); 519 } 520 // Clear the saved state and picker data 521 t.setSavedState(null); 522 t.clearPickerData(); 523 // Save the new url in order to avoid deleting the WebView. 524 t.setOriginalUrl(url); 525 return true; 526 } 527 528 /** 529 * Creates a new WebView and registers it with the global settings. 530 */ createNewWebView()531 private WebView createNewWebView() { 532 // Create a new WebView 533 WebView w = new WebView(mActivity); 534 w.setScrollbarFadingEnabled(true); 535 w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); 536 w.setMapTrackballToArrowKeys(false); // use trackball directly 537 // Enable the built-in zoom 538 w.getSettings().setBuiltInZoomControls(true); 539 // Add this WebView to the settings observer list and update the 540 // settings 541 final BrowserSettings s = BrowserSettings.getInstance(); 542 s.addObserver(w.getSettings()).update(s, null); 543 544 // pick a default 545 if (false) { 546 MeshTracker mt = new MeshTracker(2); 547 Paint paint = new Paint(); 548 Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(), 549 R.drawable.pattern_carbon_fiber_dark); 550 paint.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT, 551 Shader.TileMode.REPEAT)); 552 mt.setBGPaint(paint); 553 w.setDragTracker(mt); 554 } 555 return w; 556 } 557 558 /** 559 * Put the current tab in the background and set newTab as the current tab. 560 * @param newTab The new tab. If newTab is null, the current tab is not 561 * set. 562 */ setCurrentTab(Tab newTab)563 boolean setCurrentTab(Tab newTab) { 564 return setCurrentTab(newTab, false); 565 } 566 pauseCurrentTab()567 void pauseCurrentTab() { 568 Tab t = getCurrentTab(); 569 if (t != null) { 570 t.pause(); 571 } 572 } 573 resumeCurrentTab()574 void resumeCurrentTab() { 575 Tab t = getCurrentTab(); 576 if (t != null) { 577 t.resume(); 578 } 579 } 580 581 /** 582 * If force is true, this method skips the check for newTab == current. 583 */ setCurrentTab(Tab newTab, boolean force)584 private boolean setCurrentTab(Tab newTab, boolean force) { 585 Tab current = getTab(mCurrentTab); 586 if (current == newTab && !force) { 587 return true; 588 } 589 if (current != null) { 590 current.putInBackground(); 591 mCurrentTab = -1; 592 } 593 if (newTab == null) { 594 return false; 595 } 596 597 // Move the newTab to the end of the queue 598 int index = mTabQueue.indexOf(newTab); 599 if (index != -1) { 600 mTabQueue.remove(index); 601 } 602 mTabQueue.add(newTab); 603 604 // Display the new current tab 605 mCurrentTab = mTabs.indexOf(newTab); 606 WebView mainView = newTab.getWebView(); 607 boolean needRestore = (mainView == null); 608 if (needRestore) { 609 // Same work as in createNewTab() except don't do new Tab() 610 mainView = createNewWebView(); 611 newTab.setWebView(mainView); 612 } 613 newTab.putInForeground(); 614 if (needRestore) { 615 // Have to finish setCurrentTab work before calling restoreState 616 if (!newTab.restoreState(newTab.getSavedState())) { 617 mainView.loadUrl(BrowserSettings.getInstance().getHomePage()); 618 } 619 } 620 return true; 621 } 622 } 623