/* -*- 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 android.support.annotation.Nullable; import org.json.JSONException; import org.json.JSONObject; import org.mozilla.gecko.annotation.JNITarget; import org.mozilla.gecko.annotation.RobocopTarget; import org.mozilla.gecko.AppConstants.Versions; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.gfx.LayerView; import org.mozilla.gecko.mozglue.SafeIntent; import org.mozilla.gecko.notifications.WhatsNewReceiver; import org.mozilla.gecko.reader.ReaderModeUtils; 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.database.sqlite.SQLiteException; import android.graphics.Color; import android.net.Uri; import android.os.Handler; import android.provider.Browser; import android.support.v4.content.ContextCompat; 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; 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; /** Indicates the url has been specified by a source external to the app. */ public static final int LOADURL_EXTERNAL = 1 << 7; /** Indicates the tab is the first shown after Firefox is hidden and restored. */ public static final int LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN = 1 << 8; private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 2; public static final int INVALID_TAB_ID = -1; private static final AtomicInteger sTabId = new AtomicInteger(0); private volatile boolean mInitialTabsAdded; private Context mAppContext; private LayerView mLayerView; private ContentObserver mBookmarksContentObserver; private PersistTabsRunnable mPersistTabsRunnable; private int mPrivateClearColor; private static class PersistTabsRunnable implements Runnable { private final BrowserDB db; private final Context context; private final Iterable tabs; public PersistTabsRunnable(final Context context, Iterable tabsInOrder) { this.context = context; this.db = BrowserDB.from(context); this.tabs = tabsInOrder; } @Override public void run() { try { db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs); } catch (SQLiteException e) { Log.w(LOGTAG, "Error persisting local tabs", e); } } }; private Tabs() { EventDispatcher.getInstance().registerGeckoThreadListener(this, "Tab:Added", "Tab:Close", "Tab:Select", "Content:LocationChange", "Content:SecurityChange", "Content:StateChange", "Content:LoadError", "Content:PageShow", "DOMTitleChanged", "Link:Favicon", "Link:Touchicon", "Link:Feed", "Link:OpenSearch", "DesktopMode:Changed", "Tab:StreamStart", "Tab:StreamStop", "Tab:AudioPlayingChange", "Tab:MediaPlaybackChange"); mPrivateClearColor = Color.RED; } public synchronized void attachToContext(Context context, LayerView layerView) { 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; mLayerView = layerView; mPrivateClearColor = ContextCompat.getColor(context, R.color.tabs_tray_grey_pressed); mAccountManager = AccountManager.get(appContext); mAccountListener = new OnAccountsUpdateListener() { @Override public void onAccountsUpdated(Account[] accounts) { queuePersistAllTabs(); } }; // The listener will run on the background thread (see 2nd argument). mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false); if (mBookmarksContentObserver != null) { // It's safe to use the db here since we aren't doing any I/O. final GeckoProfile profile = GeckoProfile.get(context); BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver); } } /** * 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 mBookmarksContentObserver. private void lazyRegisterBookmarkObserver() { if (mBookmarksContentObserver == null) { mBookmarksContentObserver = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { for (Tab tab : mOrder) { tab.updateBookmark(); } } }; // It's safe to use the db here since we aren't doing any I/O. final GeckoProfile profile = GeckoProfile.get(mAppContext); BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver); } } private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate, int tabIndex) { 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); if (tabIndex > -1) { mOrder.add(tabIndex, tab); } else { mOrder.add(tab); } } // Suppress the ADDED event to prevent animation of tabs created via session restore. if (mInitialTabsAdded) { notifyListeners(tab, TabEvents.ADDED, Integer.toString(getPrivacySpecificTabIndex(tabIndex, isPrivate))); } return tab; } // Return the index, among those tabs whose privacy setting matches isPrivate, of the tab at // position index in mOrder. Returns -1, for "new last tab", when index is -1. private int getPrivacySpecificTabIndex(int index, boolean isPrivate) { int privacySpecificIndex = -1; for (int i = 0; i <= index; i++) { final Tab tab = mOrder.get(i); if (tab.isPrivate() == isPrivate) { privacySpecificIndex++; } } return privacySpecificIndex; } 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 tab; } mSelectedTab = tab; notifyListeners(tab, TabEvents.SELECTED); if (mLayerView != null) { mLayerView.setClearColor(getTabColor(tab)); } if (oldTab != null) { notifyListeners(oldTab, TabEvents.UNSELECTED); } // Pass a message to Gecko to update tab state in BrowserApp. GeckoAppShell.notifyObservers("Tab:Selected", String.valueOf(tab.getId())); return tab; } public synchronized boolean selectLastTab() { if (mOrder.isEmpty()) { return false; } selectTab(mOrder.get(mOrder.size() - 1).getId()); return true; } 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 */ @Nullable 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); } public synchronized Tab getTabForApplicationId(final String applicationId) { if (applicationId == null) { return null; } for (final Tab tab : mOrder) { if (applicationId.equals(tab.getApplicationId())) { return tab; } } return null; } /** Close tab and then select the default next tab */ @RobocopTarget public synchronized void closeTab(Tab tab) { closeTab(tab, getNextTab(tab)); } public synchronized void closeTab(Tab tab, Tab nextTab) { closeTab(tab, nextTab, false); } public synchronized void closeTab(Tab tab, boolean showUndoToast) { closeTab(tab, getNextTab(tab), showUndoToast); } /** Close tab and then select nextTab */ public synchronized void closeTab(final Tab tab, Tab nextTab, boolean showUndoToast) { 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(); final JSONObject args = new JSONObject(); try { args.put("tabId", String.valueOf(tabId)); args.put("showUndoToast", showUndoToast); } catch (JSONException e) { Log.e(LOGTAG, "Error building Tab:Closed arguments: " + e); } // Pass a message to Gecko to update tab state in BrowserApp GeckoAppShell.notifyObservers("Tab:Closed", args.toString()); } /** 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 { // 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("cancelEditMode")) { final Tab oldTab = getSelectedTab(); if (oldTab != null) { oldTab.setIsEditing(false); } } 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"), message.getInt("tabIndex")); // 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.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: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")) { tab.setLoadedFromCache(message.getBoolean("fromCache")); tab.updateUserRequested(message.getString("userRequested")); notifyListeners(tab, TabEvents.PAGE_SHOW); } else if (event.equals("DOMTitleChanged")) { tab.updateTitle(message.getString("title")); } else if (event.equals("Link:Favicon")) { // Add the favicon to the set of available icons for this tab. tab.addFavicon(message.getString("href"), message.getInt("size"), message.getString("mime")); // Load the favicon. If the tab is still loading, we actually do the load once the // page has loaded, in an attempt to prevent the favicon load from having a // detrimental effect on page load time. if (tab.getState() != Tab.STATE_LOADING) { tab.loadFavicon(); } } else if (event.equals("Link:Touchicon")) { tab.addTouchicon(message.getString("href"), message.getInt("size"), message.getString("mime")); } 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:StreamStart")) { tab.setRecording(true); notifyListeners(tab, TabEvents.RECORDING_CHANGE); } else if (event.equals("Tab:StreamStop")) { tab.setRecording(false); notifyListeners(tab, TabEvents.RECORDING_CHANGE); } else if (event.equals("Tab:AudioPlayingChange")) { tab.setIsAudioPlaying(message.getBoolean("isAudioPlaying")); notifyListeners(tab, TabEvents.AUDIO_PLAYING_CHANGE); } else if (event.equals("Tab:MediaPlaybackChange")) { final String status = message.getString("status"); if (status.equals("resume")) { notifyListeners(tab, TabEvents.MEDIA_PLAYING_RESUME); } else { tab.setIsMediaPlaying(status.equals("start")); notifyListeners(tab, TabEvents.MEDIA_PLAYING_CHANGE); } } } catch (Exception e) { Log.w(LOGTAG, "handleMessage threw for " + event, e); } } public void refreshThumbnails() { final BrowserDB db = BrowserDB.from(mAppContext); ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { for (final Tab tab : mOrder) { if (tab.getThumbnail() == null) { tab.loadThumbnailFromDB(db); } } } }); } public interface OnTabsChangedListener { void onTabChanged(Tab tab, TabEvents msg, String 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_FEED, SECURITY_CHANGE, DESKTOP_MODE_CHANGE, RECORDING_CHANGE, BOOKMARK_ADDED, BOOKMARK_REMOVED, AUDIO_PLAYING_CHANGE, OPENED_FROM_TABS_TRAY, MEDIA_PLAYING_CHANGE, MEDIA_PLAYING_RESUME } public void notifyListeners(Tab tab, TabEvents msg) { notifyListeners(tab, msg, ""); } public void notifyListeners(final Tab tab, final TabEvents msg, final String 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) { // We want the tab record to have an accurate favicon, so queue // the persisting of tabs when it changes. case FAVICON: 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: if (mLayerView != null) { mLayerView.setSurfaceBackgroundColor(getTabColor(tab)); mLayerView.setPaintState(LayerView.PAINT_START); } queuePersistAllTabs(); case UNSELECTED: tab.onChange(); break; default: break; } } /** * 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() { final Handler backgroundHandler = ThreadUtils.getBackgroundHandler(); // Note: Its safe to modify the runnable here because all of the callers are on the same thread. if (mPersistTabsRunnable != null) { backgroundHandler.removeCallbacks(mPersistTabsRunnable); mPersistTabsRunnable = null; } mPersistTabsRunnable = new PersistTabsRunnable(mAppContext, getTabsInOrder()); backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS); } /** * 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; } if (url.equals(tab.getURL())) { return tab; } } return null; } /** * Looks for a reader mode enabled open tab with the given URL and private * state. * * @param url * The URL of the tab we're looking for. The url parameter can be * the actual article URL or the reader mode article URL. * @param isPrivate * If true, only look for tabs that are private. If false, only * look for tabs that are not private. * * @return The first Tab with the given URL, or null if there is no such * tab. */ public Tab getFirstReaderTabForUrl(String url, boolean isPrivate) { if (url == null) { return null; } url = ReaderModeUtils.stripAboutReaderUrl(url); for (Tab tab : mOrder) { if (isPrivate != tab.isPrivate()) { continue; } String tabUrl = tab.getURL(); if (AboutPages.isAboutReader(tabUrl)) { tabUrl = ReaderModeUtils.stripAboutReaderUrl(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 */ @RobocopTarget public Tab loadUrl(String url, int flags) { return loadUrl(url, null, -1, null, flags); } public Tab loadUrlWithIntentExtras(final String url, final SafeIntent intent, final int flags) { // We can't directly create a listener to tell when the user taps on the "What's new" // notification, so we use this intent handling as a signal that they tapped the notification. if (intent.getBooleanExtra(WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION, false)) { Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION); } // Note: we don't get the URL from the intent so the calling // method has the opportunity to change the URL if applicable. return loadUrl(url, null, -1, intent, flags); } public Tab loadUrl(final String url, final String searchEngine, final int parentId, final int flags) { return loadUrl(url, searchEngine, parentId, null, 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 intent an intent whose extras are used to modify the request * @param flags flags used to load tab * * @return the Tab if a new one was created; null otherwise */ public Tab loadUrl(final String url, final String searchEngine, final int parentId, final SafeIntent intent, final int flags) { JSONObject args = new JSONObject(); Tab tabToSelect = 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; final boolean isFirstShownAfterActivityUnhidden = (flags & LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN) != 0; args.put("url", url); args.put("engine", searchEngine); args.put("parentId", parentId); args.put("userEntered", userEntered); args.put("isPrivate", isPrivate); args.put("pinned", (flags & LOADURL_PINNED) != 0); args.put("desktopMode", desktopMode); final boolean needsNewTab; final String applicationId = (intent == null) ? null : intent.getStringExtra(Browser.EXTRA_APPLICATION_ID); if (applicationId == null) { needsNewTab = (flags & LOADURL_NEW_TAB) != 0; } else { // If you modify this code, be careful that intent != null. final boolean extraCreateNewTab = intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false); final Tab applicationTab = getTabForApplicationId(applicationId); if (applicationTab == null || extraCreateNewTab) { needsNewTab = true; } else { needsNewTab = false; delayLoad = false; background = false; tabToSelect = applicationTab; final int tabToSelectId = tabToSelect.getId(); args.put("tabID", tabToSelectId); // This must be called before the "Tab:Load" event is sent. I think addTab gets // away with it because having "newTab" == true causes the selected tab to be // updated in JS for the "Tab:Load" event but "newTab" is false in our case. // This makes me think the other selectTab is not necessary (bug 1160673). // // Note: that makes the later call redundant but selectTab exits early so I'm // fine not adding the complex logic to avoid calling it again. selectTab(tabToSelect.getId()); } } args.put("newTab", needsNewTab); args.put("delayLoad", delayLoad); args.put("selected", !background); if (needsNewTab) { 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; // Add the new tab to the end of the tab order. final int tabIndex = -1; tabToSelect = addTab(tabId, tabUrl, external, parentId, url, isPrivate, tabIndex); tabToSelect.setDesktopMode(desktopMode); tabToSelect.setApplicationId(applicationId); if (isFirstShownAfterActivityUnhidden) { // We just opened Firefox so we want to show // the toolbar but not animate it to avoid jank. tabToSelect.setShouldShowToolbarWithoutAnimationOnFirstSelection(true); } } } catch (Exception e) { Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e); } GeckoAppShell.notifyObservers("Tab:Load", args.toString()); if (tabToSelect == null) { return null; } if (!delayLoad && !background) { selectTab(tabToSelect.getId()); } // Load favicon instantly for about:home page because it's already cached if (AboutPages.isBuiltinIconPage(url)) { tabToSelect.loadFavicon(); } return tabToSelect; } public Tab addTab() { return loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB); } public Tab addPrivateTab() { return loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE); } /** * 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. * * This method will open a new private tab if the currently selected tab * is also private. * * @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; int flags = LOADURL_NEW_TAB; final Tab selectedTab = getSelectedTab(); if (selectedTab != null) { parentId = selectedTab.getId(); if (selectedTab.isPrivate()) { flags = flags | LOADURL_PRIVATE; } } loadUrl(url, null, parentId, flags); } /** * Gets the next tab ID. */ @JNITarget public static int getNextTabId() { return sTabId.getAndIncrement(); } private int getTabColor(Tab tab) { if (tab != null) { return tab.isPrivate() ? mPrivateClearColor : Color.WHITE; } return Color.WHITE; } }