/* -*- 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 org.mozilla.gecko.db.BrowserDB; 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.content.Intent; import android.os.SystemClock; import android.util.Log; import android.widget.Toast; 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); GeckoAppShell.registerGeckoEventListener("Reader:Added", this); GeckoAppShell.registerGeckoEventListener("Reader:Share", 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"); final 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() { notifyListeners(tab, TabEvents.ADDED); } }); } Log.i(LOGTAG, "Added a tab with id: " + id); return tab; } public void removeTab(int id) { if (tabs.containsKey(id)) { Tab tab = getTab(id); order.remove(tab); tabs.remove(id); tab.freeBuffer(); 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; selectedTab = tab; GeckoApp.mAppContext.mMainHandler.post(new Runnable() { public void run() { GeckoApp.mFormAssistPopup.hide(); if (isSelectedTab(tab)) { String url = tab.getURL(); 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; } 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); 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() { notifyListeners(null, TabEvents.RESTORED); } }); } else if (event.equals("Reader:Added")) { final boolean success = message.getBoolean("success"); final String title = message.getString("title"); final String url = message.getString("url"); handleReaderAdded(success, title, url); } else if (event.equals("Reader:Share")) { final String title = message.getString("title"); final String url = message.getString("url"); GeckoAppShell.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title); } } catch (Exception e) { Log.i(LOGTAG, "handleMessage throws " + e + " for message: " + event); } } void handleReaderAdded(boolean success, final String title, final String url) { if (!success) { GeckoApp.mAppContext.mMainHandler.post(new Runnable() { public void run() { Toast.makeText(GeckoApp.mAppContext, R.string.reading_list_failed, Toast.LENGTH_SHORT).show(); } }); return; } GeckoAppShell.getHandler().post(new Runnable() { public void run() { BrowserDB.addReadingListItem(GeckoApp.mAppContext.getContentResolver(), title, url); GeckoApp.mAppContext.mMainHandler.post(new Runnable() { public void run() { Toast.makeText(GeckoApp.mAppContext, R.string.reading_list_added, Toast.LENGTH_SHORT).show(); } }); } }); } 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); } }); } } public interface OnTabsChangedListener { public void onTabChanged(Tab tab, TabEvents msg, Object data); } 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, LOAD_ERROR, STOP, FAVICON, THUMBNAIL, TITLE, SELECTED, UNSELECTED, ADDED, RESTORED, LOCATION_CHANGE } public void notifyListeners(Tab tab, TabEvents msg) { notifyListeners(tab, msg, ""); } public void notifyListeners(Tab tab, TabEvents msg, Object data) { if (mTabsChangedListeners == null) return; Iterator items = mTabsChangedListeners.iterator(); while (items.hasNext()) { items.next().onTabChanged(tab, msg, data); } } }