TabControl.java revision dcc5eeb63eadd597587a0b2b49998c267b0bcc11
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 17package com.android.browser; 18 19import android.graphics.Bitmap; 20import android.graphics.BitmapFactory; 21import android.graphics.BitmapShader; 22import android.graphics.Paint; 23import android.graphics.Shader; 24import android.os.Bundle; 25import android.util.Log; 26import android.view.View; 27import android.webkit.WebBackForwardList; 28import android.webkit.WebView; 29 30import java.io.File; 31import java.util.ArrayList; 32import java.util.Vector; 33 34class 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 mTabQueue.add(t); 322 } 323 } 324 // Rebuild the tree of tabs. Do this after all tabs have been 325 // created/restored so that the parent tab exists. 326 for (int i = 0; i < numTabs; i++) { 327 final Bundle b = inState.getBundle(Tab.WEBVIEW + i); 328 final Tab t = getTab(i); 329 if (b != null && t != null) { 330 final int parentIndex = b.getInt(Tab.PARENTTAB, -1); 331 if (parentIndex != -1) { 332 final Tab parent = getTab(parentIndex); 333 if (parent != null) { 334 parent.addChildTab(t); 335 } 336 } 337 } 338 } 339 } 340 return true; 341 } 342 343 /** 344 * Free the memory in this order, 1) free the background tab; 2) free the 345 * WebView cache; 346 */ 347 void freeMemory() { 348 if (getTabCount() == 0) return; 349 350 // free the least frequently used background tab 351 Tab t = getLeastUsedTab(getCurrentTab()); 352 if (t != null) { 353 Log.w(LOGTAG, "Free a tab in the browser"); 354 // store the WebView's state. 355 t.saveState(); 356 // destroy the tab 357 t.destroy(); 358 return; 359 } 360 361 // free the WebView's unused memory (this includes the cache) 362 Log.w(LOGTAG, "Free WebView's unused memory and cache"); 363 WebView view = getCurrentWebView(); 364 if (view != null) { 365 view.freeMemory(); 366 } 367 } 368 369 private Tab getLeastUsedTab(Tab current) { 370 // Don't do anything if we only have 1 tab or if the current tab is 371 // null. 372 if (getTabCount() == 1 || current == null) { 373 return null; 374 } 375 376 // Rip through the queue starting at the beginning and tear down the 377 // next available tab. 378 Tab t = null; 379 int i = 0; 380 final int queueSize = mTabQueue.size(); 381 if (queueSize == 0) { 382 return null; 383 } 384 do { 385 t = mTabQueue.get(i++); 386 } while (i < queueSize 387 && ((t != null && t.getWebView() == null) 388 || t == current.getParentTab())); 389 390 // Don't do anything if the last remaining tab is the current one or if 391 // the last tab has been freed already. 392 if (t == current || t.getWebView() == null) { 393 return null; 394 } 395 396 return t; 397 } 398 399 /** 400 * Show the tab that contains the given WebView. 401 * @param view The WebView used to find the tab. 402 */ 403 Tab getTabFromView(WebView view) { 404 final int size = getTabCount(); 405 for (int i = 0; i < size; i++) { 406 final Tab t = getTab(i); 407 if (t.getSubWebView() == view || t.getWebView() == view) { 408 return t; 409 } 410 } 411 return null; 412 } 413 414 /** 415 * Return the tab with the matching application id. 416 * @param id The application identifier. 417 */ 418 Tab getTabFromId(String id) { 419 if (id == null) { 420 return null; 421 } 422 final int size = getTabCount(); 423 for (int i = 0; i < size; i++) { 424 final Tab t = getTab(i); 425 if (id.equals(t.getAppId())) { 426 return t; 427 } 428 } 429 return null; 430 } 431 432 /** 433 * Stop loading in all opened WebView including subWindows. 434 */ 435 void stopAllLoading() { 436 final int size = getTabCount(); 437 for (int i = 0; i < size; i++) { 438 final Tab t = getTab(i); 439 final WebView webview = t.getWebView(); 440 if (webview != null) { 441 webview.stopLoading(); 442 } 443 final WebView subview = t.getSubWebView(); 444 if (subview != null) { 445 webview.stopLoading(); 446 } 447 } 448 } 449 450 // This method checks if a non-app tab (one created within the browser) 451 // matches the given url. 452 private boolean tabMatchesUrl(Tab t, String url) { 453 if (t.getAppId() != null) { 454 return false; 455 } 456 WebView webview = t.getWebView(); 457 if (webview == null) { 458 return false; 459 } else if (url.equals(webview.getUrl()) 460 || url.equals(webview.getOriginalUrl())) { 461 return true; 462 } 463 return false; 464 } 465 466 /** 467 * Return the tab that has no app id associated with it and the url of the 468 * tab matches the given url. 469 * @param url The url to search for. 470 */ 471 Tab findUnusedTabWithUrl(String url) { 472 if (url == null) { 473 return null; 474 } 475 // Check the current tab first. 476 Tab t = getCurrentTab(); 477 if (t != null && tabMatchesUrl(t, url)) { 478 return t; 479 } 480 // Now check all the rest. 481 final int size = getTabCount(); 482 for (int i = 0; i < size; i++) { 483 t = getTab(i); 484 if (tabMatchesUrl(t, url)) { 485 return t; 486 } 487 } 488 return null; 489 } 490 491 /** 492 * Recreate the main WebView of the given tab. Returns true if the WebView 493 * was deleted. 494 */ 495 boolean recreateWebView(Tab t, String url) { 496 final WebView w = t.getWebView(); 497 if (w != null) { 498 if (url != null && url.equals(t.getOriginalUrl())) { 499 // The original url matches the current url. Just go back to the 500 // first history item so we can load it faster than if we 501 // rebuilt the WebView. 502 final WebBackForwardList list = w.copyBackForwardList(); 503 if (list != null) { 504 w.goBackOrForward(-list.getCurrentIndex()); 505 w.clearHistory(); // maintains the current page. 506 return false; 507 } 508 } 509 t.destroy(); 510 } 511 // Create a new WebView. If this tab is the current tab, we need to put 512 // back all the clients so force it to be the current tab. 513 t.setWebView(createNewWebView()); 514 if (getCurrentTab() == t) { 515 setCurrentTab(t, true); 516 } 517 // Clear the saved state and picker data 518 t.setSavedState(null); 519 t.clearPickerData(); 520 // Save the new url in order to avoid deleting the WebView. 521 t.setOriginalUrl(url); 522 return true; 523 } 524 525 /** 526 * Creates a new WebView and registers it with the global settings. 527 */ 528 private WebView createNewWebView() { 529 // Create a new WebView 530 WebView w = new WebView(mActivity); 531 w.setScrollbarFadingEnabled(true); 532 w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); 533 w.setMapTrackballToArrowKeys(false); // use trackball directly 534 // Enable the built-in zoom 535 w.getSettings().setBuiltInZoomControls(true); 536 // Add this WebView to the settings observer list and update the 537 // settings 538 final BrowserSettings s = BrowserSettings.getInstance(); 539 s.addObserver(w.getSettings()).update(s, null); 540 541 // pick a default 542 if (false) { 543 MeshTracker mt = new MeshTracker(2); 544 Paint paint = new Paint(); 545 Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(), 546 R.drawable.pattern_carbon_fiber_dark); 547 paint.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT, 548 Shader.TileMode.REPEAT)); 549 mt.setBGPaint(paint); 550 w.setDragTracker(mt); 551 } 552 return w; 553 } 554 555 /** 556 * Put the current tab in the background and set newTab as the current tab. 557 * @param newTab The new tab. If newTab is null, the current tab is not 558 * set. 559 */ 560 boolean setCurrentTab(Tab newTab) { 561 return setCurrentTab(newTab, false); 562 } 563 564 void pauseCurrentTab() { 565 Tab t = getCurrentTab(); 566 if (t != null) { 567 t.pause(); 568 } 569 } 570 571 void resumeCurrentTab() { 572 Tab t = getCurrentTab(); 573 if (t != null) { 574 t.resume(); 575 } 576 } 577 578 /** 579 * If force is true, this method skips the check for newTab == current. 580 */ 581 private boolean setCurrentTab(Tab newTab, boolean force) { 582 Tab current = getTab(mCurrentTab); 583 if (current == newTab && !force) { 584 return true; 585 } 586 if (current != null) { 587 current.putInBackground(); 588 mCurrentTab = -1; 589 } 590 if (newTab == null) { 591 return false; 592 } 593 594 // Move the newTab to the end of the queue 595 int index = mTabQueue.indexOf(newTab); 596 if (index != -1) { 597 mTabQueue.remove(index); 598 } 599 mTabQueue.add(newTab); 600 601 // Display the new current tab 602 mCurrentTab = mTabs.indexOf(newTab); 603 WebView mainView = newTab.getWebView(); 604 boolean needRestore = (mainView == null); 605 if (needRestore) { 606 // Same work as in createNewTab() except don't do new Tab() 607 mainView = createNewWebView(); 608 newTab.setWebView(mainView); 609 } 610 newTab.putInForeground(); 611 if (needRestore) { 612 // Have to finish setCurrentTab work before calling restoreState 613 if (!newTab.restoreState(newTab.getSavedState())) { 614 mainView.loadUrl(BrowserSettings.getInstance().getHomePage()); 615 } 616 } 617 return true; 618 } 619} 620