/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Android code. * * The Initial Developer of the Original Code is Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2009-2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Sriram Ramasubramanian * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.mozilla.gecko; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import org.json.JSONException; import org.json.JSONObject; import android.content.ContentResolver; import android.os.SystemClock; import android.util.Log; public class Tabs implements GeckoEventListener { private static final String LOGTAG = "GeckoTabs"; private Tab selectedTab; private HashMap tabs; private ArrayList order; private ContentResolver resolver; private boolean mRestoringSession = false; private Tabs() { tabs = new HashMap(); order = new ArrayList(); GeckoAppShell.registerGeckoEventListener("SessionHistory:New", this); GeckoAppShell.registerGeckoEventListener("SessionHistory:Back", this); GeckoAppShell.registerGeckoEventListener("SessionHistory:Forward", this); GeckoAppShell.registerGeckoEventListener("SessionHistory:Goto", this); GeckoAppShell.registerGeckoEventListener("SessionHistory:Purge", this); GeckoAppShell.registerGeckoEventListener("Tab:Added", this); GeckoAppShell.registerGeckoEventListener("Tab:Close", this); GeckoAppShell.registerGeckoEventListener("Tab:Select", this); GeckoAppShell.registerGeckoEventListener("Session:RestoreBegin", this); GeckoAppShell.registerGeckoEventListener("Session:RestoreEnd", this); } public int getCount() { return tabs.size(); } public Tab addTab(JSONObject params) throws JSONException { int id = params.getInt("tabID"); if (tabs.containsKey(id)) return tabs.get(id); // null strings return "null" (http://code.google.com/p/android/issues/detail?id=13830) String url = params.isNull("uri") ? null : params.getString("uri"); Boolean external = params.getBoolean("external"); int parentId = params.getInt("parentId"); String title = params.getString("title"); Tab tab = new Tab(id, url, external, parentId, title); tabs.put(id, tab); order.add(tab); if (!mRestoringSession) { GeckoApp.mAppContext.mMainHandler.post(new Runnable() { public void run() { GeckoApp.mBrowserToolbar.updateTabCountAndAnimate(getCount()); } }); } Log.i(LOGTAG, "Added a tab with id: " + id); return tab; } public void removeTab(int id) { if (tabs.containsKey(id)) { order.remove(getTab(id)); tabs.remove(id); Log.i(LOGTAG, "Removed a tab with id: " + id); } } public Tab selectTab(int id) { if (!tabs.containsKey(id)) return null; final Tab oldTab = getSelectedTab(); final Tab tab = tabs.get(id); // This avoids a NPE below, but callers need to be careful to // handle this case if (tab == null) return null; if ("about:home".equals(tab.getURL())) GeckoApp.mAppContext.showAboutHome(); else GeckoApp.mAppContext.hideAboutHome(); GeckoApp.mAppContext.mMainHandler.post(new Runnable() { public void run() { GeckoApp.mFormAssistPopup.hide(); // Do we need to do this check? if (isSelectedTab(tab)) { String url = tab.getURL(); GeckoApp.mBrowserToolbar.setTitle(tab.getDisplayTitle()); GeckoApp.mBrowserToolbar.setFavicon(tab.getFavicon()); GeckoApp.mBrowserToolbar.setSecurityMode(tab.getSecurityMode()); GeckoApp.mBrowserToolbar.setProgressVisibility(tab.getState() == Tab.STATE_LOADING); GeckoApp.mDoorHangerPopup.updatePopup(); GeckoApp.mBrowserToolbar.setShadowVisibility((url == null) || !url.startsWith("about:")); notifyListeners(tab, TabEvents.SELECTED); if (oldTab != null) GeckoApp.mAppContext.hidePlugins(oldTab, true); } } }); // Pass a message to Gecko to update tab state in BrowserApp GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Selected", String.valueOf(tab.getId()))); return selectedTab = tab; } public int getIndexOf(Tab tab) { return order.lastIndexOf(tab); } public Tab getTabAt(int index) { if (index >= 0 && index < order.size()) return order.get(index); else return null; } public Tab getSelectedTab() { return selectedTab; } public boolean isSelectedTab(Tab tab) { if (selectedTab == null) return false; return tab == selectedTab; } public Tab getTab(int id) { if (getCount() == 0) return null; if (!tabs.containsKey(id)) return null; return tabs.get(id); } /** Close tab and then select the default next tab */ public void closeTab(Tab tab) { closeTab(tab, getNextTab(tab)); } /** Close tab and then select nextTab */ public void closeTab(final Tab tab, Tab nextTab) { if (tab == null || nextTab == null) return; selectTab(nextTab.getId()); int tabId = tab.getId(); removeTab(tabId); GeckoApp.mAppContext.mMainHandler.post(new Runnable() { public void run() { notifyListeners(tab, TabEvents.CLOSED); GeckoApp.mBrowserToolbar.updateTabCountAndAnimate(Tabs.getInstance().getCount()); GeckoApp.mDoorHangerPopup.updatePopup(); GeckoApp.mAppContext.hidePlugins(tab, true); 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; int index = getIndexOf(tab); Tab nextTab = getTabAt(index + 1); if (nextTab == null) nextTab = getTabAt(index - 1); 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 HashMap getTabs() { if (getCount() == 0) return null; return tabs; } public ArrayList getTabsInOrder() { if (getCount() == 0) return null; return order; } public void setContentResolver(ContentResolver resolver) { this.resolver = resolver; } public ContentResolver getContentResolver() { return resolver; } //Making Tabs a singleton class private static class TabsInstanceHolder { private static final Tabs INSTANCE = new Tabs(); } public static Tabs getInstance() { return Tabs.TabsInstanceHolder.INSTANCE; } // GeckoEventListener implementation public void handleMessage(String event, JSONObject message) { Log.i(LOGTAG, "Got message: " + event); try { if (event.startsWith("SessionHistory:")) { Tab tab = getTab(message.getInt("tabID")); if (tab != null) { event = event.substring("SessionHistory:".length()); tab.handleSessionHistoryMessage(event, message); } } else if (event.equals("Tab:Added")) { Log.i(LOGTAG, "Received message from Gecko: " + SystemClock.uptimeMillis() + " - Tab:Added"); Tab tab = addTab(message); if (message.getBoolean("selected")) selectTab(tab.getId()); if (message.getBoolean("delayLoad")) tab.setState(Tab.STATE_DELAYED); } else if (event.equals("Tab:Close")) { Tab tab = getTab(message.getInt("tabID")); closeTab(tab); } else if (event.equals("Tab:Select")) { selectTab(message.getInt("tabID")); } else if (event.equals("Session:RestoreBegin")) { mRestoringSession = true; } else if (event.equals("Session:RestoreEnd")) { mRestoringSession = false; GeckoApp.mAppContext.mMainHandler.post(new Runnable() { public void run() { GeckoApp.mBrowserToolbar.updateTabCount(getCount()); } }); } } catch (Exception e) { Log.i(LOGTAG, "handleMessage throws " + e + " for message: " + event); } } public void refreshThumbnails() { Iterator iterator = tabs.values().iterator(); while (iterator.hasNext()) { final Tab tab = iterator.next(); GeckoAppShell.getHandler().post(new Runnable() { public void run() { GeckoApp.mAppContext.getAndProcessThumbnailForTab(tab, false); } }); } } public interface OnTabsChangedListener { public void onTabChanged(Tab tab, TabEvents msg); } private static ArrayList mTabsChangedListeners; public static void registerOnTabsChangedListener(OnTabsChangedListener listener) { if (mTabsChangedListeners == null) mTabsChangedListeners = new ArrayList(); mTabsChangedListeners.add(listener); } public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) { if (mTabsChangedListeners == null) return; mTabsChangedListeners.remove(listener); } public enum TabEvents { CLOSED, START, LOADED, STOP, FAVICON, THUMBNAIL, TITLE, SELECTED } public void notifyListeners(Tab tab, TabEvents msg) { if (mTabsChangedListeners == null) return; Iterator items = mTabsChangedListeners.iterator(); while (items.hasNext()) { items.next().onTabChanged(tab, msg); } } }