mobile/android/base/Tabs.java

changeset 0
6474c204b198
     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 +}

mercurial