diff -r 000000000000 -r 6474c204b198 mobile/android/base/Tabs.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/Tabs.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,829 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import org.json.JSONObject; +import org.mozilla.gecko.db.BrowserDB; +import org.mozilla.gecko.favicons.Favicons; +import org.mozilla.gecko.fxa.FirefoxAccounts; +import org.mozilla.gecko.mozglue.JNITarget; +import org.mozilla.gecko.mozglue.RobocopTarget; +import org.mozilla.gecko.sync.setup.SyncAccounts; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.ThreadUtils; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.OnAccountsUpdateListener; +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import android.os.Handler; +import android.util.Log; + +public class Tabs implements GeckoEventListener { + private static final String LOGTAG = "GeckoTabs"; + + // mOrder and mTabs are always of the same cardinality, and contain the same values. + private final CopyOnWriteArrayList mOrder = new CopyOnWriteArrayList(); + + // All writes to mSelectedTab must be synchronized on the Tabs instance. + // In general, it's preferred to always use selectTab()). + private volatile Tab mSelectedTab; + + // All accesses to mTabs must be synchronized on the Tabs instance. + private final HashMap mTabs = new HashMap(); + + private AccountManager mAccountManager; + private OnAccountsUpdateListener mAccountListener = null; + + public static final int LOADURL_NONE = 0; + public static final int LOADURL_NEW_TAB = 1 << 0; + public static final int LOADURL_USER_ENTERED = 1 << 1; + public static final int LOADURL_PRIVATE = 1 << 2; + public static final int LOADURL_PINNED = 1 << 3; + public static final int LOADURL_DELAY_LOAD = 1 << 4; + public static final int LOADURL_DESKTOP = 1 << 5; + public static final int LOADURL_BACKGROUND = 1 << 6; + public static final int LOADURL_EXTERNAL = 1 << 7; + + private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 5; + + private static AtomicInteger sTabId = new AtomicInteger(0); + private volatile boolean mInitialTabsAdded; + + private Context mAppContext; + private ContentObserver mContentObserver; + + private final Runnable mPersistTabsRunnable = new Runnable() { + @Override + public void run() { + try { + final Context context = getAppContext(); + boolean syncIsSetup = SyncAccounts.syncAccountsExist(context) || + FirefoxAccounts.firefoxAccountsExist(context); + if (syncIsSetup) { + TabsAccessor.persistLocalTabs(getContentResolver(), getTabsInOrder()); + } + } catch (SecurityException se) {} // will fail without android.permission.GET_ACCOUNTS + } + }; + + private Tabs() { + registerEventListener("Session:RestoreEnd"); + registerEventListener("SessionHistory:New"); + registerEventListener("SessionHistory:Back"); + registerEventListener("SessionHistory:Forward"); + registerEventListener("SessionHistory:Goto"); + registerEventListener("SessionHistory:Purge"); + registerEventListener("Tab:Added"); + registerEventListener("Tab:Close"); + registerEventListener("Tab:Select"); + registerEventListener("Content:LocationChange"); + registerEventListener("Content:SecurityChange"); + registerEventListener("Content:ReaderEnabled"); + registerEventListener("Content:StateChange"); + registerEventListener("Content:LoadError"); + registerEventListener("Content:PageShow"); + registerEventListener("DOMContentLoaded"); + registerEventListener("DOMTitleChanged"); + registerEventListener("Link:Favicon"); + registerEventListener("Link:Feed"); + registerEventListener("Link:OpenSearch"); + registerEventListener("DesktopMode:Changed"); + registerEventListener("Tab:ViewportMetadata"); + registerEventListener("Tab:StreamStart"); + registerEventListener("Tab:StreamStop"); + + } + + public synchronized void attachToContext(Context context) { + final Context appContext = context.getApplicationContext(); + if (mAppContext == appContext) { + return; + } + + if (mAppContext != null) { + // This should never happen. + Log.w(LOGTAG, "The application context has changed!"); + } + + mAppContext = appContext; + mAccountManager = AccountManager.get(appContext); + + mAccountListener = new OnAccountsUpdateListener() { + @Override + public void onAccountsUpdated(Account[] accounts) { + persistAllTabs(); + } + }; + + // The listener will run on the background thread (see 2nd argument). + mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false); + + if (mContentObserver != null) { + BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver); + } + } + + /** + * Gets the tab count corresponding to the private state of the selected + * tab. + * + * If the selected tab is a non-private tab, this will return the number of + * non-private tabs; likewise, if this is a private tab, this will return + * the number of private tabs. + * + * @return the number of tabs in the current private state + */ + public synchronized int getDisplayCount() { + // Once mSelectedTab is non-null, it cannot be null for the remainder + // of the object's lifetime. + boolean getPrivate = mSelectedTab != null && mSelectedTab.isPrivate(); + int count = 0; + for (Tab tab : mOrder) { + if (tab.isPrivate() == getPrivate) { + count++; + } + } + return count; + } + + public int isOpen(String url) { + for (Tab tab : mOrder) { + if (tab.getURL().equals(url)) { + return tab.getId(); + } + } + return -1; + } + + // Must be synchronized to avoid racing on mContentObserver. + private void lazyRegisterBookmarkObserver() { + if (mContentObserver == null) { + mContentObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + for (Tab tab : mOrder) { + tab.updateBookmark(); + } + } + }; + BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver); + } + } + + private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate) { + final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) : + new Tab(mAppContext, id, url, external, parentId, title); + synchronized (this) { + lazyRegisterBookmarkObserver(); + mTabs.put(id, tab); + mOrder.add(tab); + } + + // Suppress the ADDED event to prevent animation of tabs created via session restore. + if (mInitialTabsAdded) { + notifyListeners(tab, TabEvents.ADDED); + } + + return tab; + } + + public synchronized void removeTab(int id) { + if (mTabs.containsKey(id)) { + Tab tab = getTab(id); + mOrder.remove(tab); + mTabs.remove(id); + } + } + + public synchronized Tab selectTab(int id) { + if (!mTabs.containsKey(id)) + return null; + + final Tab oldTab = getSelectedTab(); + final Tab tab = mTabs.get(id); + + // This avoids a NPE below, but callers need to be careful to + // handle this case. + if (tab == null || oldTab == tab) { + return null; + } + + mSelectedTab = tab; + notifyListeners(tab, TabEvents.SELECTED); + + if (oldTab != null) { + notifyListeners(oldTab, TabEvents.UNSELECTED); + } + + // Pass a message to Gecko to update tab state in BrowserApp. + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Selected", String.valueOf(tab.getId()))); + return tab; + } + + private int getIndexOf(Tab tab) { + return mOrder.lastIndexOf(tab); + } + + private Tab getNextTabFrom(Tab tab, boolean getPrivate) { + int numTabs = mOrder.size(); + int index = getIndexOf(tab); + for (int i = index + 1; i < numTabs; i++) { + Tab next = mOrder.get(i); + if (next.isPrivate() == getPrivate) { + return next; + } + } + return null; + } + + private Tab getPreviousTabFrom(Tab tab, boolean getPrivate) { + int index = getIndexOf(tab); + for (int i = index - 1; i >= 0; i--) { + Tab prev = mOrder.get(i); + if (prev.isPrivate() == getPrivate) { + return prev; + } + } + return null; + } + + /** + * Gets the selected tab. + * + * The selected tab can be null if we're doing a session restore after a + * crash and Gecko isn't ready yet. + * + * @return the selected tab, or null if no tabs exist + */ + public Tab getSelectedTab() { + return mSelectedTab; + } + + public boolean isSelectedTab(Tab tab) { + return tab != null && tab == mSelectedTab; + } + + public boolean isSelectedTabId(int tabId) { + final Tab selected = mSelectedTab; + return selected != null && selected.getId() == tabId; + } + + @RobocopTarget + public synchronized Tab getTab(int id) { + if (id == -1) + return null; + + if (mTabs.size() == 0) + return null; + + if (!mTabs.containsKey(id)) + return null; + + return mTabs.get(id); + } + + /** Close tab and then select the default next tab */ + @RobocopTarget + public synchronized void closeTab(Tab tab) { + closeTab(tab, getNextTab(tab)); + } + + /** Close tab and then select nextTab */ + public synchronized void closeTab(final Tab tab, Tab nextTab) { + if (tab == null) + return; + + int tabId = tab.getId(); + removeTab(tabId); + + if (nextTab == null) { + nextTab = loadUrl(AboutPages.HOME, LOADURL_NEW_TAB); + } + + selectTab(nextTab.getId()); + + tab.onDestroy(); + + // Pass a message to Gecko to update tab state in BrowserApp + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Closed", String.valueOf(tabId))); + } + + /** Return the tab that will be selected by default after this one is closed */ + public Tab getNextTab(Tab tab) { + Tab selectedTab = getSelectedTab(); + if (selectedTab != tab) + return selectedTab; + + boolean getPrivate = tab.isPrivate(); + Tab nextTab = getNextTabFrom(tab, getPrivate); + if (nextTab == null) + nextTab = getPreviousTabFrom(tab, getPrivate); + if (nextTab == null && getPrivate) { + // If there are no private tabs remaining, get the last normal tab + Tab lastTab = mOrder.get(mOrder.size() - 1); + if (!lastTab.isPrivate()) { + nextTab = lastTab; + } else { + nextTab = getPreviousTabFrom(lastTab, false); + } + } + + Tab parent = getTab(tab.getParentId()); + if (parent != null) { + // If the next tab is a sibling, switch to it. Otherwise go back to the parent. + if (nextTab != null && nextTab.getParentId() == tab.getParentId()) + return nextTab; + else + return parent; + } + return nextTab; + } + + public Iterable getTabsInOrder() { + return mOrder; + } + + /** + * @return the current GeckoApp instance, or throws if + * we aren't correctly initialized. + */ + private synchronized Context getAppContext() { + if (mAppContext == null) { + throw new IllegalStateException("Tabs not initialized with a GeckoApp instance."); + } + return mAppContext; + } + + public ContentResolver getContentResolver() { + return getAppContext().getContentResolver(); + } + + // Make Tabs a singleton class. + private static class TabsInstanceHolder { + private static final Tabs INSTANCE = new Tabs(); + } + + @RobocopTarget + public static Tabs getInstance() { + return Tabs.TabsInstanceHolder.INSTANCE; + } + + // GeckoEventListener implementation + @Override + public void handleMessage(String event, JSONObject message) { + Log.d(LOGTAG, "handleMessage: " + event); + try { + if (event.equals("Session:RestoreEnd")) { + notifyListeners(null, TabEvents.RESTORED); + return; + } + + // All other events handled below should contain a tabID property + int id = message.getInt("tabID"); + Tab tab = getTab(id); + + // "Tab:Added" is a special case because tab will be null if the tab was just added + if (event.equals("Tab:Added")) { + String url = message.isNull("uri") ? null : message.getString("uri"); + + if (message.getBoolean("stub")) { + if (tab == null) { + // Tab was already closed; abort + return; + } + } else { + tab = addTab(id, url, message.getBoolean("external"), + message.getInt("parentId"), + message.getString("title"), + message.getBoolean("isPrivate")); + + // If we added the tab as a stub, we should have already + // selected it, so ignore this flag for stubbed tabs. + if (message.getBoolean("selected")) + selectTab(id); + } + + if (message.getBoolean("delayLoad")) + tab.setState(Tab.STATE_DELAYED); + if (message.getBoolean("desktopMode")) + tab.setDesktopMode(true); + return; + } + + // Tab was already closed; abort + if (tab == null) + return; + + if (event.startsWith("SessionHistory:")) { + event = event.substring("SessionHistory:".length()); + tab.handleSessionHistoryMessage(event, message); + } else if (event.equals("Tab:Close")) { + closeTab(tab); + } else if (event.equals("Tab:Select")) { + selectTab(tab.getId()); + } else if (event.equals("Content:LocationChange")) { + tab.handleLocationChange(message); + } else if (event.equals("Content:SecurityChange")) { + tab.updateIdentityData(message.getJSONObject("identity")); + notifyListeners(tab, TabEvents.SECURITY_CHANGE); + } else if (event.equals("Content:ReaderEnabled")) { + tab.setReaderEnabled(true); + notifyListeners(tab, TabEvents.READER_ENABLED); + } else if (event.equals("Content:StateChange")) { + int state = message.getInt("state"); + if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) { + if ((state & GeckoAppShell.WPL_STATE_START) != 0) { + boolean restoring = message.getBoolean("restoring"); + tab.handleDocumentStart(restoring, message.getString("uri")); + notifyListeners(tab, Tabs.TabEvents.START); + } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) { + tab.handleDocumentStop(message.getBoolean("success")); + notifyListeners(tab, Tabs.TabEvents.STOP); + } + } + } else if (event.equals("Content:LoadError")) { + tab.handleContentLoaded(); + notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR); + } else if (event.equals("Content:PageShow")) { + notifyListeners(tab, TabEvents.PAGE_SHOW); + } else if (event.equals("DOMContentLoaded")) { + tab.handleContentLoaded(); + String backgroundColor = message.getString("bgColor"); + if (backgroundColor != null) { + tab.setBackgroundColor(backgroundColor); + } else { + // Default to white if no color is given + tab.setBackgroundColor(Color.WHITE); + } + tab.setErrorType(message.optString("errorType")); + notifyListeners(tab, Tabs.TabEvents.LOADED); + } else if (event.equals("DOMTitleChanged")) { + tab.updateTitle(message.getString("title")); + } else if (event.equals("Link:Favicon")) { + tab.updateFaviconURL(message.getString("href"), message.getInt("size")); + notifyListeners(tab, TabEvents.LINK_FAVICON); + } else if (event.equals("Link:Feed")) { + tab.setHasFeeds(true); + notifyListeners(tab, TabEvents.LINK_FEED); + } else if (event.equals("Link:OpenSearch")) { + boolean visible = message.getBoolean("visible"); + tab.setHasOpenSearch(visible); + } else if (event.equals("DesktopMode:Changed")) { + tab.setDesktopMode(message.getBoolean("desktopMode")); + notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE); + } else if (event.equals("Tab:ViewportMetadata")) { + tab.setZoomConstraints(new ZoomConstraints(message)); + tab.setIsRTL(message.getBoolean("isRTL")); + notifyListeners(tab, TabEvents.VIEWPORT_CHANGE); + } else if (event.equals("Tab:StreamStart")) { + tab.setRecording(true); + notifyListeners(tab, TabEvents.RECORDING_CHANGE); + } else if (event.equals("Tab:StreamStop")) { + tab.setRecording(false); + notifyListeners(tab, TabEvents.RECORDING_CHANGE); + } + + } catch (Exception e) { + Log.w(LOGTAG, "handleMessage threw for " + event, e); + } + } + + /** + * Set the favicon for any tabs loaded with this page URL. + */ + public void updateFaviconForURL(String pageURL, Bitmap favicon) { + // The tab might be pointing to another URL by the time the + // favicon is finally loaded, in which case we won't find the tab. + // See also: Bug 920331. + for (Tab tab : mOrder) { + String tabURL = tab.getURL(); + if (pageURL.equals(tabURL)) { + tab.setFaviconLoadId(Favicons.NOT_LOADING); + if (tab.updateFavicon(favicon)) { + notifyListeners(tab, TabEvents.FAVICON); + } + } + } + } + + public void refreshThumbnails() { + final ThumbnailHelper helper = ThumbnailHelper.getInstance(); + ThreadUtils.postToBackgroundThread(new Runnable() { + @Override + public void run() { + for (final Tab tab : mOrder) { + helper.getAndProcessThumbnailFor(tab); + } + } + }); + } + + public interface OnTabsChangedListener { + public void onTabChanged(Tab tab, TabEvents msg, Object data); + } + + private static final List TABS_CHANGED_LISTENERS = new CopyOnWriteArrayList(); + + public static void registerOnTabsChangedListener(OnTabsChangedListener listener) { + TABS_CHANGED_LISTENERS.add(listener); + } + + public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) { + TABS_CHANGED_LISTENERS.remove(listener); + } + + public enum TabEvents { + CLOSED, + START, + LOADED, + LOAD_ERROR, + STOP, + FAVICON, + THUMBNAIL, + TITLE, + SELECTED, + UNSELECTED, + ADDED, + RESTORED, + LOCATION_CHANGE, + MENU_UPDATED, + PAGE_SHOW, + LINK_FAVICON, + LINK_FEED, + SECURITY_CHANGE, + READER_ENABLED, + DESKTOP_MODE_CHANGE, + VIEWPORT_CHANGE, + RECORDING_CHANGE + } + + public void notifyListeners(Tab tab, TabEvents msg) { + notifyListeners(tab, msg, ""); + } + + public void notifyListeners(final Tab tab, final TabEvents msg, final Object data) { + if (tab == null && + msg != TabEvents.RESTORED) { + throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab."); + } + + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + onTabChanged(tab, msg, data); + + if (TABS_CHANGED_LISTENERS.isEmpty()) { + return; + } + + Iterator items = TABS_CHANGED_LISTENERS.iterator(); + while (items.hasNext()) { + items.next().onTabChanged(tab, msg, data); + } + } + }); + } + + private void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { + switch (msg) { + case LOCATION_CHANGE: + queuePersistAllTabs(); + break; + case RESTORED: + mInitialTabsAdded = true; + break; + + // When one tab is deselected, another one is always selected, so only + // queue a single persist operation. When tabs are added/closed, they + // are also selected/unselected, so it would be redundant to also listen + // for ADDED/CLOSED events. + case SELECTED: + queuePersistAllTabs(); + case UNSELECTED: + tab.onChange(); + break; + default: + break; + } + } + + // This method persists the current ordered list of tabs in our tabs content provider. + public void persistAllTabs() { + ThreadUtils.postToBackgroundThread(mPersistTabsRunnable); + } + + /** + * Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS + * milliseconds have elapsed. If any existing requests are already queued then + * those requests are removed. + */ + private void queuePersistAllTabs() { + Handler backgroundHandler = ThreadUtils.getBackgroundHandler(); + backgroundHandler.removeCallbacks(mPersistTabsRunnable); + backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS); + } + + private void registerEventListener(String event) { + GeckoAppShell.getEventDispatcher().registerEventListener(event, this); + } + + /** + * Looks for an open tab with the given URL. + * @param url the URL of the tab we're looking for + * + * @return first Tab with the given URL, or null if there is no such tab. + */ + public Tab getFirstTabForUrl(String url) { + return getFirstTabForUrlHelper(url, null); + } + + /** + * Looks for an open tab with the given URL and private state. + * @param url the URL of the tab we're looking for + * @param isPrivate if true, only look for tabs that are private. if false, + * only look for tabs that are non-private. + * + * @return first Tab with the given URL, or null if there is no such tab. + */ + public Tab getFirstTabForUrl(String url, boolean isPrivate) { + return getFirstTabForUrlHelper(url, isPrivate); + } + + private Tab getFirstTabForUrlHelper(String url, Boolean isPrivate) { + if (url == null) { + return null; + } + + for (Tab tab : mOrder) { + if (isPrivate != null && isPrivate != tab.isPrivate()) { + continue; + } + String tabUrl = tab.getURL(); + if (AboutPages.isAboutReader(tabUrl)) { + tabUrl = ReaderModeUtils.getUrlFromAboutReader(tabUrl); + } + if (url.equals(tabUrl)) { + return tab; + } + } + + return null; + } + + /** + * Loads a tab with the given URL in the currently selected tab. + * + * @param url URL of page to load, or search term used if searchEngine is given + */ + @RobocopTarget + public Tab loadUrl(String url) { + return loadUrl(url, LOADURL_NONE); + } + + /** + * Loads a tab with the given URL. + * + * @param url URL of page to load, or search term used if searchEngine is given + * @param flags flags used to load tab + * + * @return the Tab if a new one was created; null otherwise + */ + public Tab loadUrl(String url, int flags) { + return loadUrl(url, null, -1, flags); + } + + /** + * Loads a tab with the given URL. + * + * @param url URL of page to load, or search term used if searchEngine is given + * @param searchEngine if given, the search engine with this name is used + * to search for the url string; if null, the URL is loaded directly + * @param parentId ID of this tab's parent, or -1 if it has no parent + * @param flags flags used to load tab + * + * @return the Tab if a new one was created; null otherwise + */ + public Tab loadUrl(String url, String searchEngine, int parentId, int flags) { + JSONObject args = new JSONObject(); + Tab added = null; + boolean delayLoad = (flags & LOADURL_DELAY_LOAD) != 0; + + // delayLoad implies background tab + boolean background = delayLoad || (flags & LOADURL_BACKGROUND) != 0; + + try { + boolean isPrivate = (flags & LOADURL_PRIVATE) != 0; + boolean userEntered = (flags & LOADURL_USER_ENTERED) != 0; + boolean desktopMode = (flags & LOADURL_DESKTOP) != 0; + boolean external = (flags & LOADURL_EXTERNAL) != 0; + + args.put("url", url); + args.put("engine", searchEngine); + args.put("parentId", parentId); + args.put("userEntered", userEntered); + args.put("newTab", (flags & LOADURL_NEW_TAB) != 0); + args.put("isPrivate", isPrivate); + args.put("pinned", (flags & LOADURL_PINNED) != 0); + args.put("delayLoad", delayLoad); + args.put("desktopMode", desktopMode); + args.put("selected", !background); + + if ((flags & LOADURL_NEW_TAB) != 0) { + int tabId = getNextTabId(); + args.put("tabID", tabId); + + // The URL is updated for the tab once Gecko responds with the + // Tab:Added message. We can preliminarily set the tab's URL as + // long as it's a valid URI. + String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null; + + added = addTab(tabId, tabUrl, external, parentId, url, isPrivate); + added.setDesktopMode(desktopMode); + } + } catch (Exception e) { + Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e); + } + + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString())); + + if (added == null) { + return null; + } + + if (!delayLoad && !background) { + selectTab(added.getId()); + } + + // TODO: surely we could just fetch *any* cached icon? + if (AboutPages.isDefaultIconPage(url)) { + Log.d(LOGTAG, "Setting about: tab favicon inline."); + added.updateFavicon(getAboutPageFavicon(url)); + } + + return added; + } + + /** + * These favicons are only used for the URL bar, so + * we fetch with that size. + * + * This method completes on the calling thread. + */ + private Bitmap getAboutPageFavicon(final String url) { + int faviconSize = Math.round(mAppContext.getResources().getDimension(R.dimen.browser_toolbar_favicon_size)); + return Favicons.getSizedFaviconForPageFromCache(url, faviconSize); + } + + /** + * Open the url as a new tab, and mark the selected tab as its "parent". + * + * If the url is already open in a tab, the existing tab is selected. + * Use this for tabs opened by the browser chrome, so users can press the + * "Back" button to return to the previous tab. + * + * @param url URL of page to load + */ + public void loadUrlInTab(String url) { + Iterable tabs = getTabsInOrder(); + for (Tab tab : tabs) { + if (url.equals(tab.getURL())) { + selectTab(tab.getId()); + return; + } + } + + // getSelectedTab() can return null if no tab has been created yet + // (i.e., we're restoring a session after a crash). In these cases, + // don't mark any tabs as a parent. + int parentId = -1; + Tab selectedTab = getSelectedTab(); + if (selectedTab != null) { + parentId = selectedTab.getId(); + } + + loadUrl(url, null, parentId, LOADURL_NEW_TAB); + } + + /** + * Gets the next tab ID. + */ + @JNITarget + public static int getNextTabId() { + return sTabId.getAndIncrement(); + } +}