1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/Tabs.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,829 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import java.util.HashMap; 1.12 +import java.util.Iterator; 1.13 +import java.util.List; 1.14 +import java.util.concurrent.CopyOnWriteArrayList; 1.15 +import java.util.concurrent.atomic.AtomicInteger; 1.16 + 1.17 +import org.json.JSONObject; 1.18 +import org.mozilla.gecko.db.BrowserDB; 1.19 +import org.mozilla.gecko.favicons.Favicons; 1.20 +import org.mozilla.gecko.fxa.FirefoxAccounts; 1.21 +import org.mozilla.gecko.mozglue.JNITarget; 1.22 +import org.mozilla.gecko.mozglue.RobocopTarget; 1.23 +import org.mozilla.gecko.sync.setup.SyncAccounts; 1.24 +import org.mozilla.gecko.util.GeckoEventListener; 1.25 +import org.mozilla.gecko.util.ThreadUtils; 1.26 + 1.27 +import android.accounts.Account; 1.28 +import android.accounts.AccountManager; 1.29 +import android.accounts.OnAccountsUpdateListener; 1.30 +import android.content.ContentResolver; 1.31 +import android.content.Context; 1.32 +import android.database.ContentObserver; 1.33 +import android.graphics.Bitmap; 1.34 +import android.graphics.Color; 1.35 +import android.net.Uri; 1.36 +import android.os.Handler; 1.37 +import android.util.Log; 1.38 + 1.39 +public class Tabs implements GeckoEventListener { 1.40 + private static final String LOGTAG = "GeckoTabs"; 1.41 + 1.42 + // mOrder and mTabs are always of the same cardinality, and contain the same values. 1.43 + private final CopyOnWriteArrayList<Tab> mOrder = new CopyOnWriteArrayList<Tab>(); 1.44 + 1.45 + // All writes to mSelectedTab must be synchronized on the Tabs instance. 1.46 + // In general, it's preferred to always use selectTab()). 1.47 + private volatile Tab mSelectedTab; 1.48 + 1.49 + // All accesses to mTabs must be synchronized on the Tabs instance. 1.50 + private final HashMap<Integer, Tab> mTabs = new HashMap<Integer, Tab>(); 1.51 + 1.52 + private AccountManager mAccountManager; 1.53 + private OnAccountsUpdateListener mAccountListener = null; 1.54 + 1.55 + public static final int LOADURL_NONE = 0; 1.56 + public static final int LOADURL_NEW_TAB = 1 << 0; 1.57 + public static final int LOADURL_USER_ENTERED = 1 << 1; 1.58 + public static final int LOADURL_PRIVATE = 1 << 2; 1.59 + public static final int LOADURL_PINNED = 1 << 3; 1.60 + public static final int LOADURL_DELAY_LOAD = 1 << 4; 1.61 + public static final int LOADURL_DESKTOP = 1 << 5; 1.62 + public static final int LOADURL_BACKGROUND = 1 << 6; 1.63 + public static final int LOADURL_EXTERNAL = 1 << 7; 1.64 + 1.65 + private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 5; 1.66 + 1.67 + private static AtomicInteger sTabId = new AtomicInteger(0); 1.68 + private volatile boolean mInitialTabsAdded; 1.69 + 1.70 + private Context mAppContext; 1.71 + private ContentObserver mContentObserver; 1.72 + 1.73 + private final Runnable mPersistTabsRunnable = new Runnable() { 1.74 + @Override 1.75 + public void run() { 1.76 + try { 1.77 + final Context context = getAppContext(); 1.78 + boolean syncIsSetup = SyncAccounts.syncAccountsExist(context) || 1.79 + FirefoxAccounts.firefoxAccountsExist(context); 1.80 + if (syncIsSetup) { 1.81 + TabsAccessor.persistLocalTabs(getContentResolver(), getTabsInOrder()); 1.82 + } 1.83 + } catch (SecurityException se) {} // will fail without android.permission.GET_ACCOUNTS 1.84 + } 1.85 + }; 1.86 + 1.87 + private Tabs() { 1.88 + registerEventListener("Session:RestoreEnd"); 1.89 + registerEventListener("SessionHistory:New"); 1.90 + registerEventListener("SessionHistory:Back"); 1.91 + registerEventListener("SessionHistory:Forward"); 1.92 + registerEventListener("SessionHistory:Goto"); 1.93 + registerEventListener("SessionHistory:Purge"); 1.94 + registerEventListener("Tab:Added"); 1.95 + registerEventListener("Tab:Close"); 1.96 + registerEventListener("Tab:Select"); 1.97 + registerEventListener("Content:LocationChange"); 1.98 + registerEventListener("Content:SecurityChange"); 1.99 + registerEventListener("Content:ReaderEnabled"); 1.100 + registerEventListener("Content:StateChange"); 1.101 + registerEventListener("Content:LoadError"); 1.102 + registerEventListener("Content:PageShow"); 1.103 + registerEventListener("DOMContentLoaded"); 1.104 + registerEventListener("DOMTitleChanged"); 1.105 + registerEventListener("Link:Favicon"); 1.106 + registerEventListener("Link:Feed"); 1.107 + registerEventListener("Link:OpenSearch"); 1.108 + registerEventListener("DesktopMode:Changed"); 1.109 + registerEventListener("Tab:ViewportMetadata"); 1.110 + registerEventListener("Tab:StreamStart"); 1.111 + registerEventListener("Tab:StreamStop"); 1.112 + 1.113 + } 1.114 + 1.115 + public synchronized void attachToContext(Context context) { 1.116 + final Context appContext = context.getApplicationContext(); 1.117 + if (mAppContext == appContext) { 1.118 + return; 1.119 + } 1.120 + 1.121 + if (mAppContext != null) { 1.122 + // This should never happen. 1.123 + Log.w(LOGTAG, "The application context has changed!"); 1.124 + } 1.125 + 1.126 + mAppContext = appContext; 1.127 + mAccountManager = AccountManager.get(appContext); 1.128 + 1.129 + mAccountListener = new OnAccountsUpdateListener() { 1.130 + @Override 1.131 + public void onAccountsUpdated(Account[] accounts) { 1.132 + persistAllTabs(); 1.133 + } 1.134 + }; 1.135 + 1.136 + // The listener will run on the background thread (see 2nd argument). 1.137 + mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false); 1.138 + 1.139 + if (mContentObserver != null) { 1.140 + BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver); 1.141 + } 1.142 + } 1.143 + 1.144 + /** 1.145 + * Gets the tab count corresponding to the private state of the selected 1.146 + * tab. 1.147 + * 1.148 + * If the selected tab is a non-private tab, this will return the number of 1.149 + * non-private tabs; likewise, if this is a private tab, this will return 1.150 + * the number of private tabs. 1.151 + * 1.152 + * @return the number of tabs in the current private state 1.153 + */ 1.154 + public synchronized int getDisplayCount() { 1.155 + // Once mSelectedTab is non-null, it cannot be null for the remainder 1.156 + // of the object's lifetime. 1.157 + boolean getPrivate = mSelectedTab != null && mSelectedTab.isPrivate(); 1.158 + int count = 0; 1.159 + for (Tab tab : mOrder) { 1.160 + if (tab.isPrivate() == getPrivate) { 1.161 + count++; 1.162 + } 1.163 + } 1.164 + return count; 1.165 + } 1.166 + 1.167 + public int isOpen(String url) { 1.168 + for (Tab tab : mOrder) { 1.169 + if (tab.getURL().equals(url)) { 1.170 + return tab.getId(); 1.171 + } 1.172 + } 1.173 + return -1; 1.174 + } 1.175 + 1.176 + // Must be synchronized to avoid racing on mContentObserver. 1.177 + private void lazyRegisterBookmarkObserver() { 1.178 + if (mContentObserver == null) { 1.179 + mContentObserver = new ContentObserver(null) { 1.180 + @Override 1.181 + public void onChange(boolean selfChange) { 1.182 + for (Tab tab : mOrder) { 1.183 + tab.updateBookmark(); 1.184 + } 1.185 + } 1.186 + }; 1.187 + BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver); 1.188 + } 1.189 + } 1.190 + 1.191 + private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate) { 1.192 + final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) : 1.193 + new Tab(mAppContext, id, url, external, parentId, title); 1.194 + synchronized (this) { 1.195 + lazyRegisterBookmarkObserver(); 1.196 + mTabs.put(id, tab); 1.197 + mOrder.add(tab); 1.198 + } 1.199 + 1.200 + // Suppress the ADDED event to prevent animation of tabs created via session restore. 1.201 + if (mInitialTabsAdded) { 1.202 + notifyListeners(tab, TabEvents.ADDED); 1.203 + } 1.204 + 1.205 + return tab; 1.206 + } 1.207 + 1.208 + public synchronized void removeTab(int id) { 1.209 + if (mTabs.containsKey(id)) { 1.210 + Tab tab = getTab(id); 1.211 + mOrder.remove(tab); 1.212 + mTabs.remove(id); 1.213 + } 1.214 + } 1.215 + 1.216 + public synchronized Tab selectTab(int id) { 1.217 + if (!mTabs.containsKey(id)) 1.218 + return null; 1.219 + 1.220 + final Tab oldTab = getSelectedTab(); 1.221 + final Tab tab = mTabs.get(id); 1.222 + 1.223 + // This avoids a NPE below, but callers need to be careful to 1.224 + // handle this case. 1.225 + if (tab == null || oldTab == tab) { 1.226 + return null; 1.227 + } 1.228 + 1.229 + mSelectedTab = tab; 1.230 + notifyListeners(tab, TabEvents.SELECTED); 1.231 + 1.232 + if (oldTab != null) { 1.233 + notifyListeners(oldTab, TabEvents.UNSELECTED); 1.234 + } 1.235 + 1.236 + // Pass a message to Gecko to update tab state in BrowserApp. 1.237 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Selected", String.valueOf(tab.getId()))); 1.238 + return tab; 1.239 + } 1.240 + 1.241 + private int getIndexOf(Tab tab) { 1.242 + return mOrder.lastIndexOf(tab); 1.243 + } 1.244 + 1.245 + private Tab getNextTabFrom(Tab tab, boolean getPrivate) { 1.246 + int numTabs = mOrder.size(); 1.247 + int index = getIndexOf(tab); 1.248 + for (int i = index + 1; i < numTabs; i++) { 1.249 + Tab next = mOrder.get(i); 1.250 + if (next.isPrivate() == getPrivate) { 1.251 + return next; 1.252 + } 1.253 + } 1.254 + return null; 1.255 + } 1.256 + 1.257 + private Tab getPreviousTabFrom(Tab tab, boolean getPrivate) { 1.258 + int index = getIndexOf(tab); 1.259 + for (int i = index - 1; i >= 0; i--) { 1.260 + Tab prev = mOrder.get(i); 1.261 + if (prev.isPrivate() == getPrivate) { 1.262 + return prev; 1.263 + } 1.264 + } 1.265 + return null; 1.266 + } 1.267 + 1.268 + /** 1.269 + * Gets the selected tab. 1.270 + * 1.271 + * The selected tab can be null if we're doing a session restore after a 1.272 + * crash and Gecko isn't ready yet. 1.273 + * 1.274 + * @return the selected tab, or null if no tabs exist 1.275 + */ 1.276 + public Tab getSelectedTab() { 1.277 + return mSelectedTab; 1.278 + } 1.279 + 1.280 + public boolean isSelectedTab(Tab tab) { 1.281 + return tab != null && tab == mSelectedTab; 1.282 + } 1.283 + 1.284 + public boolean isSelectedTabId(int tabId) { 1.285 + final Tab selected = mSelectedTab; 1.286 + return selected != null && selected.getId() == tabId; 1.287 + } 1.288 + 1.289 + @RobocopTarget 1.290 + public synchronized Tab getTab(int id) { 1.291 + if (id == -1) 1.292 + return null; 1.293 + 1.294 + if (mTabs.size() == 0) 1.295 + return null; 1.296 + 1.297 + if (!mTabs.containsKey(id)) 1.298 + return null; 1.299 + 1.300 + return mTabs.get(id); 1.301 + } 1.302 + 1.303 + /** Close tab and then select the default next tab */ 1.304 + @RobocopTarget 1.305 + public synchronized void closeTab(Tab tab) { 1.306 + closeTab(tab, getNextTab(tab)); 1.307 + } 1.308 + 1.309 + /** Close tab and then select nextTab */ 1.310 + public synchronized void closeTab(final Tab tab, Tab nextTab) { 1.311 + if (tab == null) 1.312 + return; 1.313 + 1.314 + int tabId = tab.getId(); 1.315 + removeTab(tabId); 1.316 + 1.317 + if (nextTab == null) { 1.318 + nextTab = loadUrl(AboutPages.HOME, LOADURL_NEW_TAB); 1.319 + } 1.320 + 1.321 + selectTab(nextTab.getId()); 1.322 + 1.323 + tab.onDestroy(); 1.324 + 1.325 + // Pass a message to Gecko to update tab state in BrowserApp 1.326 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Closed", String.valueOf(tabId))); 1.327 + } 1.328 + 1.329 + /** Return the tab that will be selected by default after this one is closed */ 1.330 + public Tab getNextTab(Tab tab) { 1.331 + Tab selectedTab = getSelectedTab(); 1.332 + if (selectedTab != tab) 1.333 + return selectedTab; 1.334 + 1.335 + boolean getPrivate = tab.isPrivate(); 1.336 + Tab nextTab = getNextTabFrom(tab, getPrivate); 1.337 + if (nextTab == null) 1.338 + nextTab = getPreviousTabFrom(tab, getPrivate); 1.339 + if (nextTab == null && getPrivate) { 1.340 + // If there are no private tabs remaining, get the last normal tab 1.341 + Tab lastTab = mOrder.get(mOrder.size() - 1); 1.342 + if (!lastTab.isPrivate()) { 1.343 + nextTab = lastTab; 1.344 + } else { 1.345 + nextTab = getPreviousTabFrom(lastTab, false); 1.346 + } 1.347 + } 1.348 + 1.349 + Tab parent = getTab(tab.getParentId()); 1.350 + if (parent != null) { 1.351 + // If the next tab is a sibling, switch to it. Otherwise go back to the parent. 1.352 + if (nextTab != null && nextTab.getParentId() == tab.getParentId()) 1.353 + return nextTab; 1.354 + else 1.355 + return parent; 1.356 + } 1.357 + return nextTab; 1.358 + } 1.359 + 1.360 + public Iterable<Tab> getTabsInOrder() { 1.361 + return mOrder; 1.362 + } 1.363 + 1.364 + /** 1.365 + * @return the current GeckoApp instance, or throws if 1.366 + * we aren't correctly initialized. 1.367 + */ 1.368 + private synchronized Context getAppContext() { 1.369 + if (mAppContext == null) { 1.370 + throw new IllegalStateException("Tabs not initialized with a GeckoApp instance."); 1.371 + } 1.372 + return mAppContext; 1.373 + } 1.374 + 1.375 + public ContentResolver getContentResolver() { 1.376 + return getAppContext().getContentResolver(); 1.377 + } 1.378 + 1.379 + // Make Tabs a singleton class. 1.380 + private static class TabsInstanceHolder { 1.381 + private static final Tabs INSTANCE = new Tabs(); 1.382 + } 1.383 + 1.384 + @RobocopTarget 1.385 + public static Tabs getInstance() { 1.386 + return Tabs.TabsInstanceHolder.INSTANCE; 1.387 + } 1.388 + 1.389 + // GeckoEventListener implementation 1.390 + @Override 1.391 + public void handleMessage(String event, JSONObject message) { 1.392 + Log.d(LOGTAG, "handleMessage: " + event); 1.393 + try { 1.394 + if (event.equals("Session:RestoreEnd")) { 1.395 + notifyListeners(null, TabEvents.RESTORED); 1.396 + return; 1.397 + } 1.398 + 1.399 + // All other events handled below should contain a tabID property 1.400 + int id = message.getInt("tabID"); 1.401 + Tab tab = getTab(id); 1.402 + 1.403 + // "Tab:Added" is a special case because tab will be null if the tab was just added 1.404 + if (event.equals("Tab:Added")) { 1.405 + String url = message.isNull("uri") ? null : message.getString("uri"); 1.406 + 1.407 + if (message.getBoolean("stub")) { 1.408 + if (tab == null) { 1.409 + // Tab was already closed; abort 1.410 + return; 1.411 + } 1.412 + } else { 1.413 + tab = addTab(id, url, message.getBoolean("external"), 1.414 + message.getInt("parentId"), 1.415 + message.getString("title"), 1.416 + message.getBoolean("isPrivate")); 1.417 + 1.418 + // If we added the tab as a stub, we should have already 1.419 + // selected it, so ignore this flag for stubbed tabs. 1.420 + if (message.getBoolean("selected")) 1.421 + selectTab(id); 1.422 + } 1.423 + 1.424 + if (message.getBoolean("delayLoad")) 1.425 + tab.setState(Tab.STATE_DELAYED); 1.426 + if (message.getBoolean("desktopMode")) 1.427 + tab.setDesktopMode(true); 1.428 + return; 1.429 + } 1.430 + 1.431 + // Tab was already closed; abort 1.432 + if (tab == null) 1.433 + return; 1.434 + 1.435 + if (event.startsWith("SessionHistory:")) { 1.436 + event = event.substring("SessionHistory:".length()); 1.437 + tab.handleSessionHistoryMessage(event, message); 1.438 + } else if (event.equals("Tab:Close")) { 1.439 + closeTab(tab); 1.440 + } else if (event.equals("Tab:Select")) { 1.441 + selectTab(tab.getId()); 1.442 + } else if (event.equals("Content:LocationChange")) { 1.443 + tab.handleLocationChange(message); 1.444 + } else if (event.equals("Content:SecurityChange")) { 1.445 + tab.updateIdentityData(message.getJSONObject("identity")); 1.446 + notifyListeners(tab, TabEvents.SECURITY_CHANGE); 1.447 + } else if (event.equals("Content:ReaderEnabled")) { 1.448 + tab.setReaderEnabled(true); 1.449 + notifyListeners(tab, TabEvents.READER_ENABLED); 1.450 + } else if (event.equals("Content:StateChange")) { 1.451 + int state = message.getInt("state"); 1.452 + if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) { 1.453 + if ((state & GeckoAppShell.WPL_STATE_START) != 0) { 1.454 + boolean restoring = message.getBoolean("restoring"); 1.455 + tab.handleDocumentStart(restoring, message.getString("uri")); 1.456 + notifyListeners(tab, Tabs.TabEvents.START); 1.457 + } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) { 1.458 + tab.handleDocumentStop(message.getBoolean("success")); 1.459 + notifyListeners(tab, Tabs.TabEvents.STOP); 1.460 + } 1.461 + } 1.462 + } else if (event.equals("Content:LoadError")) { 1.463 + tab.handleContentLoaded(); 1.464 + notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR); 1.465 + } else if (event.equals("Content:PageShow")) { 1.466 + notifyListeners(tab, TabEvents.PAGE_SHOW); 1.467 + } else if (event.equals("DOMContentLoaded")) { 1.468 + tab.handleContentLoaded(); 1.469 + String backgroundColor = message.getString("bgColor"); 1.470 + if (backgroundColor != null) { 1.471 + tab.setBackgroundColor(backgroundColor); 1.472 + } else { 1.473 + // Default to white if no color is given 1.474 + tab.setBackgroundColor(Color.WHITE); 1.475 + } 1.476 + tab.setErrorType(message.optString("errorType")); 1.477 + notifyListeners(tab, Tabs.TabEvents.LOADED); 1.478 + } else if (event.equals("DOMTitleChanged")) { 1.479 + tab.updateTitle(message.getString("title")); 1.480 + } else if (event.equals("Link:Favicon")) { 1.481 + tab.updateFaviconURL(message.getString("href"), message.getInt("size")); 1.482 + notifyListeners(tab, TabEvents.LINK_FAVICON); 1.483 + } else if (event.equals("Link:Feed")) { 1.484 + tab.setHasFeeds(true); 1.485 + notifyListeners(tab, TabEvents.LINK_FEED); 1.486 + } else if (event.equals("Link:OpenSearch")) { 1.487 + boolean visible = message.getBoolean("visible"); 1.488 + tab.setHasOpenSearch(visible); 1.489 + } else if (event.equals("DesktopMode:Changed")) { 1.490 + tab.setDesktopMode(message.getBoolean("desktopMode")); 1.491 + notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE); 1.492 + } else if (event.equals("Tab:ViewportMetadata")) { 1.493 + tab.setZoomConstraints(new ZoomConstraints(message)); 1.494 + tab.setIsRTL(message.getBoolean("isRTL")); 1.495 + notifyListeners(tab, TabEvents.VIEWPORT_CHANGE); 1.496 + } else if (event.equals("Tab:StreamStart")) { 1.497 + tab.setRecording(true); 1.498 + notifyListeners(tab, TabEvents.RECORDING_CHANGE); 1.499 + } else if (event.equals("Tab:StreamStop")) { 1.500 + tab.setRecording(false); 1.501 + notifyListeners(tab, TabEvents.RECORDING_CHANGE); 1.502 + } 1.503 + 1.504 + } catch (Exception e) { 1.505 + Log.w(LOGTAG, "handleMessage threw for " + event, e); 1.506 + } 1.507 + } 1.508 + 1.509 + /** 1.510 + * Set the favicon for any tabs loaded with this page URL. 1.511 + */ 1.512 + public void updateFaviconForURL(String pageURL, Bitmap favicon) { 1.513 + // The tab might be pointing to another URL by the time the 1.514 + // favicon is finally loaded, in which case we won't find the tab. 1.515 + // See also: Bug 920331. 1.516 + for (Tab tab : mOrder) { 1.517 + String tabURL = tab.getURL(); 1.518 + if (pageURL.equals(tabURL)) { 1.519 + tab.setFaviconLoadId(Favicons.NOT_LOADING); 1.520 + if (tab.updateFavicon(favicon)) { 1.521 + notifyListeners(tab, TabEvents.FAVICON); 1.522 + } 1.523 + } 1.524 + } 1.525 + } 1.526 + 1.527 + public void refreshThumbnails() { 1.528 + final ThumbnailHelper helper = ThumbnailHelper.getInstance(); 1.529 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.530 + @Override 1.531 + public void run() { 1.532 + for (final Tab tab : mOrder) { 1.533 + helper.getAndProcessThumbnailFor(tab); 1.534 + } 1.535 + } 1.536 + }); 1.537 + } 1.538 + 1.539 + public interface OnTabsChangedListener { 1.540 + public void onTabChanged(Tab tab, TabEvents msg, Object data); 1.541 + } 1.542 + 1.543 + private static final List<OnTabsChangedListener> TABS_CHANGED_LISTENERS = new CopyOnWriteArrayList<OnTabsChangedListener>(); 1.544 + 1.545 + public static void registerOnTabsChangedListener(OnTabsChangedListener listener) { 1.546 + TABS_CHANGED_LISTENERS.add(listener); 1.547 + } 1.548 + 1.549 + public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) { 1.550 + TABS_CHANGED_LISTENERS.remove(listener); 1.551 + } 1.552 + 1.553 + public enum TabEvents { 1.554 + CLOSED, 1.555 + START, 1.556 + LOADED, 1.557 + LOAD_ERROR, 1.558 + STOP, 1.559 + FAVICON, 1.560 + THUMBNAIL, 1.561 + TITLE, 1.562 + SELECTED, 1.563 + UNSELECTED, 1.564 + ADDED, 1.565 + RESTORED, 1.566 + LOCATION_CHANGE, 1.567 + MENU_UPDATED, 1.568 + PAGE_SHOW, 1.569 + LINK_FAVICON, 1.570 + LINK_FEED, 1.571 + SECURITY_CHANGE, 1.572 + READER_ENABLED, 1.573 + DESKTOP_MODE_CHANGE, 1.574 + VIEWPORT_CHANGE, 1.575 + RECORDING_CHANGE 1.576 + } 1.577 + 1.578 + public void notifyListeners(Tab tab, TabEvents msg) { 1.579 + notifyListeners(tab, msg, ""); 1.580 + } 1.581 + 1.582 + public void notifyListeners(final Tab tab, final TabEvents msg, final Object data) { 1.583 + if (tab == null && 1.584 + msg != TabEvents.RESTORED) { 1.585 + throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab."); 1.586 + } 1.587 + 1.588 + ThreadUtils.postToUiThread(new Runnable() { 1.589 + @Override 1.590 + public void run() { 1.591 + onTabChanged(tab, msg, data); 1.592 + 1.593 + if (TABS_CHANGED_LISTENERS.isEmpty()) { 1.594 + return; 1.595 + } 1.596 + 1.597 + Iterator<OnTabsChangedListener> items = TABS_CHANGED_LISTENERS.iterator(); 1.598 + while (items.hasNext()) { 1.599 + items.next().onTabChanged(tab, msg, data); 1.600 + } 1.601 + } 1.602 + }); 1.603 + } 1.604 + 1.605 + private void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { 1.606 + switch (msg) { 1.607 + case LOCATION_CHANGE: 1.608 + queuePersistAllTabs(); 1.609 + break; 1.610 + case RESTORED: 1.611 + mInitialTabsAdded = true; 1.612 + break; 1.613 + 1.614 + // When one tab is deselected, another one is always selected, so only 1.615 + // queue a single persist operation. When tabs are added/closed, they 1.616 + // are also selected/unselected, so it would be redundant to also listen 1.617 + // for ADDED/CLOSED events. 1.618 + case SELECTED: 1.619 + queuePersistAllTabs(); 1.620 + case UNSELECTED: 1.621 + tab.onChange(); 1.622 + break; 1.623 + default: 1.624 + break; 1.625 + } 1.626 + } 1.627 + 1.628 + // This method persists the current ordered list of tabs in our tabs content provider. 1.629 + public void persistAllTabs() { 1.630 + ThreadUtils.postToBackgroundThread(mPersistTabsRunnable); 1.631 + } 1.632 + 1.633 + /** 1.634 + * Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS 1.635 + * milliseconds have elapsed. If any existing requests are already queued then 1.636 + * those requests are removed. 1.637 + */ 1.638 + private void queuePersistAllTabs() { 1.639 + Handler backgroundHandler = ThreadUtils.getBackgroundHandler(); 1.640 + backgroundHandler.removeCallbacks(mPersistTabsRunnable); 1.641 + backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS); 1.642 + } 1.643 + 1.644 + private void registerEventListener(String event) { 1.645 + GeckoAppShell.getEventDispatcher().registerEventListener(event, this); 1.646 + } 1.647 + 1.648 + /** 1.649 + * Looks for an open tab with the given URL. 1.650 + * @param url the URL of the tab we're looking for 1.651 + * 1.652 + * @return first Tab with the given URL, or null if there is no such tab. 1.653 + */ 1.654 + public Tab getFirstTabForUrl(String url) { 1.655 + return getFirstTabForUrlHelper(url, null); 1.656 + } 1.657 + 1.658 + /** 1.659 + * Looks for an open tab with the given URL and private state. 1.660 + * @param url the URL of the tab we're looking for 1.661 + * @param isPrivate if true, only look for tabs that are private. if false, 1.662 + * only look for tabs that are non-private. 1.663 + * 1.664 + * @return first Tab with the given URL, or null if there is no such tab. 1.665 + */ 1.666 + public Tab getFirstTabForUrl(String url, boolean isPrivate) { 1.667 + return getFirstTabForUrlHelper(url, isPrivate); 1.668 + } 1.669 + 1.670 + private Tab getFirstTabForUrlHelper(String url, Boolean isPrivate) { 1.671 + if (url == null) { 1.672 + return null; 1.673 + } 1.674 + 1.675 + for (Tab tab : mOrder) { 1.676 + if (isPrivate != null && isPrivate != tab.isPrivate()) { 1.677 + continue; 1.678 + } 1.679 + String tabUrl = tab.getURL(); 1.680 + if (AboutPages.isAboutReader(tabUrl)) { 1.681 + tabUrl = ReaderModeUtils.getUrlFromAboutReader(tabUrl); 1.682 + } 1.683 + if (url.equals(tabUrl)) { 1.684 + return tab; 1.685 + } 1.686 + } 1.687 + 1.688 + return null; 1.689 + } 1.690 + 1.691 + /** 1.692 + * Loads a tab with the given URL in the currently selected tab. 1.693 + * 1.694 + * @param url URL of page to load, or search term used if searchEngine is given 1.695 + */ 1.696 + @RobocopTarget 1.697 + public Tab loadUrl(String url) { 1.698 + return loadUrl(url, LOADURL_NONE); 1.699 + } 1.700 + 1.701 + /** 1.702 + * Loads a tab with the given URL. 1.703 + * 1.704 + * @param url URL of page to load, or search term used if searchEngine is given 1.705 + * @param flags flags used to load tab 1.706 + * 1.707 + * @return the Tab if a new one was created; null otherwise 1.708 + */ 1.709 + public Tab loadUrl(String url, int flags) { 1.710 + return loadUrl(url, null, -1, flags); 1.711 + } 1.712 + 1.713 + /** 1.714 + * Loads a tab with the given URL. 1.715 + * 1.716 + * @param url URL of page to load, or search term used if searchEngine is given 1.717 + * @param searchEngine if given, the search engine with this name is used 1.718 + * to search for the url string; if null, the URL is loaded directly 1.719 + * @param parentId ID of this tab's parent, or -1 if it has no parent 1.720 + * @param flags flags used to load tab 1.721 + * 1.722 + * @return the Tab if a new one was created; null otherwise 1.723 + */ 1.724 + public Tab loadUrl(String url, String searchEngine, int parentId, int flags) { 1.725 + JSONObject args = new JSONObject(); 1.726 + Tab added = null; 1.727 + boolean delayLoad = (flags & LOADURL_DELAY_LOAD) != 0; 1.728 + 1.729 + // delayLoad implies background tab 1.730 + boolean background = delayLoad || (flags & LOADURL_BACKGROUND) != 0; 1.731 + 1.732 + try { 1.733 + boolean isPrivate = (flags & LOADURL_PRIVATE) != 0; 1.734 + boolean userEntered = (flags & LOADURL_USER_ENTERED) != 0; 1.735 + boolean desktopMode = (flags & LOADURL_DESKTOP) != 0; 1.736 + boolean external = (flags & LOADURL_EXTERNAL) != 0; 1.737 + 1.738 + args.put("url", url); 1.739 + args.put("engine", searchEngine); 1.740 + args.put("parentId", parentId); 1.741 + args.put("userEntered", userEntered); 1.742 + args.put("newTab", (flags & LOADURL_NEW_TAB) != 0); 1.743 + args.put("isPrivate", isPrivate); 1.744 + args.put("pinned", (flags & LOADURL_PINNED) != 0); 1.745 + args.put("delayLoad", delayLoad); 1.746 + args.put("desktopMode", desktopMode); 1.747 + args.put("selected", !background); 1.748 + 1.749 + if ((flags & LOADURL_NEW_TAB) != 0) { 1.750 + int tabId = getNextTabId(); 1.751 + args.put("tabID", tabId); 1.752 + 1.753 + // The URL is updated for the tab once Gecko responds with the 1.754 + // Tab:Added message. We can preliminarily set the tab's URL as 1.755 + // long as it's a valid URI. 1.756 + String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null; 1.757 + 1.758 + added = addTab(tabId, tabUrl, external, parentId, url, isPrivate); 1.759 + added.setDesktopMode(desktopMode); 1.760 + } 1.761 + } catch (Exception e) { 1.762 + Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e); 1.763 + } 1.764 + 1.765 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString())); 1.766 + 1.767 + if (added == null) { 1.768 + return null; 1.769 + } 1.770 + 1.771 + if (!delayLoad && !background) { 1.772 + selectTab(added.getId()); 1.773 + } 1.774 + 1.775 + // TODO: surely we could just fetch *any* cached icon? 1.776 + if (AboutPages.isDefaultIconPage(url)) { 1.777 + Log.d(LOGTAG, "Setting about: tab favicon inline."); 1.778 + added.updateFavicon(getAboutPageFavicon(url)); 1.779 + } 1.780 + 1.781 + return added; 1.782 + } 1.783 + 1.784 + /** 1.785 + * These favicons are only used for the URL bar, so 1.786 + * we fetch with that size. 1.787 + * 1.788 + * This method completes on the calling thread. 1.789 + */ 1.790 + private Bitmap getAboutPageFavicon(final String url) { 1.791 + int faviconSize = Math.round(mAppContext.getResources().getDimension(R.dimen.browser_toolbar_favicon_size)); 1.792 + return Favicons.getSizedFaviconForPageFromCache(url, faviconSize); 1.793 + } 1.794 + 1.795 + /** 1.796 + * Open the url as a new tab, and mark the selected tab as its "parent". 1.797 + * 1.798 + * If the url is already open in a tab, the existing tab is selected. 1.799 + * Use this for tabs opened by the browser chrome, so users can press the 1.800 + * "Back" button to return to the previous tab. 1.801 + * 1.802 + * @param url URL of page to load 1.803 + */ 1.804 + public void loadUrlInTab(String url) { 1.805 + Iterable<Tab> tabs = getTabsInOrder(); 1.806 + for (Tab tab : tabs) { 1.807 + if (url.equals(tab.getURL())) { 1.808 + selectTab(tab.getId()); 1.809 + return; 1.810 + } 1.811 + } 1.812 + 1.813 + // getSelectedTab() can return null if no tab has been created yet 1.814 + // (i.e., we're restoring a session after a crash). In these cases, 1.815 + // don't mark any tabs as a parent. 1.816 + int parentId = -1; 1.817 + Tab selectedTab = getSelectedTab(); 1.818 + if (selectedTab != null) { 1.819 + parentId = selectedTab.getId(); 1.820 + } 1.821 + 1.822 + loadUrl(url, null, parentId, LOADURL_NEW_TAB); 1.823 + } 1.824 + 1.825 + /** 1.826 + * Gets the next tab ID. 1.827 + */ 1.828 + @JNITarget 1.829 + public static int getNextTabId() { 1.830 + return sTabId.getAndIncrement(); 1.831 + } 1.832 +}