mobile/android/base/Tabs.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko;
michael@0 7
michael@0 8 import java.util.HashMap;
michael@0 9 import java.util.Iterator;
michael@0 10 import java.util.List;
michael@0 11 import java.util.concurrent.CopyOnWriteArrayList;
michael@0 12 import java.util.concurrent.atomic.AtomicInteger;
michael@0 13
michael@0 14 import org.json.JSONObject;
michael@0 15 import org.mozilla.gecko.db.BrowserDB;
michael@0 16 import org.mozilla.gecko.favicons.Favicons;
michael@0 17 import org.mozilla.gecko.fxa.FirefoxAccounts;
michael@0 18 import org.mozilla.gecko.mozglue.JNITarget;
michael@0 19 import org.mozilla.gecko.mozglue.RobocopTarget;
michael@0 20 import org.mozilla.gecko.sync.setup.SyncAccounts;
michael@0 21 import org.mozilla.gecko.util.GeckoEventListener;
michael@0 22 import org.mozilla.gecko.util.ThreadUtils;
michael@0 23
michael@0 24 import android.accounts.Account;
michael@0 25 import android.accounts.AccountManager;
michael@0 26 import android.accounts.OnAccountsUpdateListener;
michael@0 27 import android.content.ContentResolver;
michael@0 28 import android.content.Context;
michael@0 29 import android.database.ContentObserver;
michael@0 30 import android.graphics.Bitmap;
michael@0 31 import android.graphics.Color;
michael@0 32 import android.net.Uri;
michael@0 33 import android.os.Handler;
michael@0 34 import android.util.Log;
michael@0 35
michael@0 36 public class Tabs implements GeckoEventListener {
michael@0 37 private static final String LOGTAG = "GeckoTabs";
michael@0 38
michael@0 39 // mOrder and mTabs are always of the same cardinality, and contain the same values.
michael@0 40 private final CopyOnWriteArrayList<Tab> mOrder = new CopyOnWriteArrayList<Tab>();
michael@0 41
michael@0 42 // All writes to mSelectedTab must be synchronized on the Tabs instance.
michael@0 43 // In general, it's preferred to always use selectTab()).
michael@0 44 private volatile Tab mSelectedTab;
michael@0 45
michael@0 46 // All accesses to mTabs must be synchronized on the Tabs instance.
michael@0 47 private final HashMap<Integer, Tab> mTabs = new HashMap<Integer, Tab>();
michael@0 48
michael@0 49 private AccountManager mAccountManager;
michael@0 50 private OnAccountsUpdateListener mAccountListener = null;
michael@0 51
michael@0 52 public static final int LOADURL_NONE = 0;
michael@0 53 public static final int LOADURL_NEW_TAB = 1 << 0;
michael@0 54 public static final int LOADURL_USER_ENTERED = 1 << 1;
michael@0 55 public static final int LOADURL_PRIVATE = 1 << 2;
michael@0 56 public static final int LOADURL_PINNED = 1 << 3;
michael@0 57 public static final int LOADURL_DELAY_LOAD = 1 << 4;
michael@0 58 public static final int LOADURL_DESKTOP = 1 << 5;
michael@0 59 public static final int LOADURL_BACKGROUND = 1 << 6;
michael@0 60 public static final int LOADURL_EXTERNAL = 1 << 7;
michael@0 61
michael@0 62 private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 5;
michael@0 63
michael@0 64 private static AtomicInteger sTabId = new AtomicInteger(0);
michael@0 65 private volatile boolean mInitialTabsAdded;
michael@0 66
michael@0 67 private Context mAppContext;
michael@0 68 private ContentObserver mContentObserver;
michael@0 69
michael@0 70 private final Runnable mPersistTabsRunnable = new Runnable() {
michael@0 71 @Override
michael@0 72 public void run() {
michael@0 73 try {
michael@0 74 final Context context = getAppContext();
michael@0 75 boolean syncIsSetup = SyncAccounts.syncAccountsExist(context) ||
michael@0 76 FirefoxAccounts.firefoxAccountsExist(context);
michael@0 77 if (syncIsSetup) {
michael@0 78 TabsAccessor.persistLocalTabs(getContentResolver(), getTabsInOrder());
michael@0 79 }
michael@0 80 } catch (SecurityException se) {} // will fail without android.permission.GET_ACCOUNTS
michael@0 81 }
michael@0 82 };
michael@0 83
michael@0 84 private Tabs() {
michael@0 85 registerEventListener("Session:RestoreEnd");
michael@0 86 registerEventListener("SessionHistory:New");
michael@0 87 registerEventListener("SessionHistory:Back");
michael@0 88 registerEventListener("SessionHistory:Forward");
michael@0 89 registerEventListener("SessionHistory:Goto");
michael@0 90 registerEventListener("SessionHistory:Purge");
michael@0 91 registerEventListener("Tab:Added");
michael@0 92 registerEventListener("Tab:Close");
michael@0 93 registerEventListener("Tab:Select");
michael@0 94 registerEventListener("Content:LocationChange");
michael@0 95 registerEventListener("Content:SecurityChange");
michael@0 96 registerEventListener("Content:ReaderEnabled");
michael@0 97 registerEventListener("Content:StateChange");
michael@0 98 registerEventListener("Content:LoadError");
michael@0 99 registerEventListener("Content:PageShow");
michael@0 100 registerEventListener("DOMContentLoaded");
michael@0 101 registerEventListener("DOMTitleChanged");
michael@0 102 registerEventListener("Link:Favicon");
michael@0 103 registerEventListener("Link:Feed");
michael@0 104 registerEventListener("Link:OpenSearch");
michael@0 105 registerEventListener("DesktopMode:Changed");
michael@0 106 registerEventListener("Tab:ViewportMetadata");
michael@0 107 registerEventListener("Tab:StreamStart");
michael@0 108 registerEventListener("Tab:StreamStop");
michael@0 109
michael@0 110 }
michael@0 111
michael@0 112 public synchronized void attachToContext(Context context) {
michael@0 113 final Context appContext = context.getApplicationContext();
michael@0 114 if (mAppContext == appContext) {
michael@0 115 return;
michael@0 116 }
michael@0 117
michael@0 118 if (mAppContext != null) {
michael@0 119 // This should never happen.
michael@0 120 Log.w(LOGTAG, "The application context has changed!");
michael@0 121 }
michael@0 122
michael@0 123 mAppContext = appContext;
michael@0 124 mAccountManager = AccountManager.get(appContext);
michael@0 125
michael@0 126 mAccountListener = new OnAccountsUpdateListener() {
michael@0 127 @Override
michael@0 128 public void onAccountsUpdated(Account[] accounts) {
michael@0 129 persistAllTabs();
michael@0 130 }
michael@0 131 };
michael@0 132
michael@0 133 // The listener will run on the background thread (see 2nd argument).
michael@0 134 mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false);
michael@0 135
michael@0 136 if (mContentObserver != null) {
michael@0 137 BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver);
michael@0 138 }
michael@0 139 }
michael@0 140
michael@0 141 /**
michael@0 142 * Gets the tab count corresponding to the private state of the selected
michael@0 143 * tab.
michael@0 144 *
michael@0 145 * If the selected tab is a non-private tab, this will return the number of
michael@0 146 * non-private tabs; likewise, if this is a private tab, this will return
michael@0 147 * the number of private tabs.
michael@0 148 *
michael@0 149 * @return the number of tabs in the current private state
michael@0 150 */
michael@0 151 public synchronized int getDisplayCount() {
michael@0 152 // Once mSelectedTab is non-null, it cannot be null for the remainder
michael@0 153 // of the object's lifetime.
michael@0 154 boolean getPrivate = mSelectedTab != null && mSelectedTab.isPrivate();
michael@0 155 int count = 0;
michael@0 156 for (Tab tab : mOrder) {
michael@0 157 if (tab.isPrivate() == getPrivate) {
michael@0 158 count++;
michael@0 159 }
michael@0 160 }
michael@0 161 return count;
michael@0 162 }
michael@0 163
michael@0 164 public int isOpen(String url) {
michael@0 165 for (Tab tab : mOrder) {
michael@0 166 if (tab.getURL().equals(url)) {
michael@0 167 return tab.getId();
michael@0 168 }
michael@0 169 }
michael@0 170 return -1;
michael@0 171 }
michael@0 172
michael@0 173 // Must be synchronized to avoid racing on mContentObserver.
michael@0 174 private void lazyRegisterBookmarkObserver() {
michael@0 175 if (mContentObserver == null) {
michael@0 176 mContentObserver = new ContentObserver(null) {
michael@0 177 @Override
michael@0 178 public void onChange(boolean selfChange) {
michael@0 179 for (Tab tab : mOrder) {
michael@0 180 tab.updateBookmark();
michael@0 181 }
michael@0 182 }
michael@0 183 };
michael@0 184 BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver);
michael@0 185 }
michael@0 186 }
michael@0 187
michael@0 188 private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate) {
michael@0 189 final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) :
michael@0 190 new Tab(mAppContext, id, url, external, parentId, title);
michael@0 191 synchronized (this) {
michael@0 192 lazyRegisterBookmarkObserver();
michael@0 193 mTabs.put(id, tab);
michael@0 194 mOrder.add(tab);
michael@0 195 }
michael@0 196
michael@0 197 // Suppress the ADDED event to prevent animation of tabs created via session restore.
michael@0 198 if (mInitialTabsAdded) {
michael@0 199 notifyListeners(tab, TabEvents.ADDED);
michael@0 200 }
michael@0 201
michael@0 202 return tab;
michael@0 203 }
michael@0 204
michael@0 205 public synchronized void removeTab(int id) {
michael@0 206 if (mTabs.containsKey(id)) {
michael@0 207 Tab tab = getTab(id);
michael@0 208 mOrder.remove(tab);
michael@0 209 mTabs.remove(id);
michael@0 210 }
michael@0 211 }
michael@0 212
michael@0 213 public synchronized Tab selectTab(int id) {
michael@0 214 if (!mTabs.containsKey(id))
michael@0 215 return null;
michael@0 216
michael@0 217 final Tab oldTab = getSelectedTab();
michael@0 218 final Tab tab = mTabs.get(id);
michael@0 219
michael@0 220 // This avoids a NPE below, but callers need to be careful to
michael@0 221 // handle this case.
michael@0 222 if (tab == null || oldTab == tab) {
michael@0 223 return null;
michael@0 224 }
michael@0 225
michael@0 226 mSelectedTab = tab;
michael@0 227 notifyListeners(tab, TabEvents.SELECTED);
michael@0 228
michael@0 229 if (oldTab != null) {
michael@0 230 notifyListeners(oldTab, TabEvents.UNSELECTED);
michael@0 231 }
michael@0 232
michael@0 233 // Pass a message to Gecko to update tab state in BrowserApp.
michael@0 234 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Selected", String.valueOf(tab.getId())));
michael@0 235 return tab;
michael@0 236 }
michael@0 237
michael@0 238 private int getIndexOf(Tab tab) {
michael@0 239 return mOrder.lastIndexOf(tab);
michael@0 240 }
michael@0 241
michael@0 242 private Tab getNextTabFrom(Tab tab, boolean getPrivate) {
michael@0 243 int numTabs = mOrder.size();
michael@0 244 int index = getIndexOf(tab);
michael@0 245 for (int i = index + 1; i < numTabs; i++) {
michael@0 246 Tab next = mOrder.get(i);
michael@0 247 if (next.isPrivate() == getPrivate) {
michael@0 248 return next;
michael@0 249 }
michael@0 250 }
michael@0 251 return null;
michael@0 252 }
michael@0 253
michael@0 254 private Tab getPreviousTabFrom(Tab tab, boolean getPrivate) {
michael@0 255 int index = getIndexOf(tab);
michael@0 256 for (int i = index - 1; i >= 0; i--) {
michael@0 257 Tab prev = mOrder.get(i);
michael@0 258 if (prev.isPrivate() == getPrivate) {
michael@0 259 return prev;
michael@0 260 }
michael@0 261 }
michael@0 262 return null;
michael@0 263 }
michael@0 264
michael@0 265 /**
michael@0 266 * Gets the selected tab.
michael@0 267 *
michael@0 268 * The selected tab can be null if we're doing a session restore after a
michael@0 269 * crash and Gecko isn't ready yet.
michael@0 270 *
michael@0 271 * @return the selected tab, or null if no tabs exist
michael@0 272 */
michael@0 273 public Tab getSelectedTab() {
michael@0 274 return mSelectedTab;
michael@0 275 }
michael@0 276
michael@0 277 public boolean isSelectedTab(Tab tab) {
michael@0 278 return tab != null && tab == mSelectedTab;
michael@0 279 }
michael@0 280
michael@0 281 public boolean isSelectedTabId(int tabId) {
michael@0 282 final Tab selected = mSelectedTab;
michael@0 283 return selected != null && selected.getId() == tabId;
michael@0 284 }
michael@0 285
michael@0 286 @RobocopTarget
michael@0 287 public synchronized Tab getTab(int id) {
michael@0 288 if (id == -1)
michael@0 289 return null;
michael@0 290
michael@0 291 if (mTabs.size() == 0)
michael@0 292 return null;
michael@0 293
michael@0 294 if (!mTabs.containsKey(id))
michael@0 295 return null;
michael@0 296
michael@0 297 return mTabs.get(id);
michael@0 298 }
michael@0 299
michael@0 300 /** Close tab and then select the default next tab */
michael@0 301 @RobocopTarget
michael@0 302 public synchronized void closeTab(Tab tab) {
michael@0 303 closeTab(tab, getNextTab(tab));
michael@0 304 }
michael@0 305
michael@0 306 /** Close tab and then select nextTab */
michael@0 307 public synchronized void closeTab(final Tab tab, Tab nextTab) {
michael@0 308 if (tab == null)
michael@0 309 return;
michael@0 310
michael@0 311 int tabId = tab.getId();
michael@0 312 removeTab(tabId);
michael@0 313
michael@0 314 if (nextTab == null) {
michael@0 315 nextTab = loadUrl(AboutPages.HOME, LOADURL_NEW_TAB);
michael@0 316 }
michael@0 317
michael@0 318 selectTab(nextTab.getId());
michael@0 319
michael@0 320 tab.onDestroy();
michael@0 321
michael@0 322 // Pass a message to Gecko to update tab state in BrowserApp
michael@0 323 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Closed", String.valueOf(tabId)));
michael@0 324 }
michael@0 325
michael@0 326 /** Return the tab that will be selected by default after this one is closed */
michael@0 327 public Tab getNextTab(Tab tab) {
michael@0 328 Tab selectedTab = getSelectedTab();
michael@0 329 if (selectedTab != tab)
michael@0 330 return selectedTab;
michael@0 331
michael@0 332 boolean getPrivate = tab.isPrivate();
michael@0 333 Tab nextTab = getNextTabFrom(tab, getPrivate);
michael@0 334 if (nextTab == null)
michael@0 335 nextTab = getPreviousTabFrom(tab, getPrivate);
michael@0 336 if (nextTab == null && getPrivate) {
michael@0 337 // If there are no private tabs remaining, get the last normal tab
michael@0 338 Tab lastTab = mOrder.get(mOrder.size() - 1);
michael@0 339 if (!lastTab.isPrivate()) {
michael@0 340 nextTab = lastTab;
michael@0 341 } else {
michael@0 342 nextTab = getPreviousTabFrom(lastTab, false);
michael@0 343 }
michael@0 344 }
michael@0 345
michael@0 346 Tab parent = getTab(tab.getParentId());
michael@0 347 if (parent != null) {
michael@0 348 // If the next tab is a sibling, switch to it. Otherwise go back to the parent.
michael@0 349 if (nextTab != null && nextTab.getParentId() == tab.getParentId())
michael@0 350 return nextTab;
michael@0 351 else
michael@0 352 return parent;
michael@0 353 }
michael@0 354 return nextTab;
michael@0 355 }
michael@0 356
michael@0 357 public Iterable<Tab> getTabsInOrder() {
michael@0 358 return mOrder;
michael@0 359 }
michael@0 360
michael@0 361 /**
michael@0 362 * @return the current GeckoApp instance, or throws if
michael@0 363 * we aren't correctly initialized.
michael@0 364 */
michael@0 365 private synchronized Context getAppContext() {
michael@0 366 if (mAppContext == null) {
michael@0 367 throw new IllegalStateException("Tabs not initialized with a GeckoApp instance.");
michael@0 368 }
michael@0 369 return mAppContext;
michael@0 370 }
michael@0 371
michael@0 372 public ContentResolver getContentResolver() {
michael@0 373 return getAppContext().getContentResolver();
michael@0 374 }
michael@0 375
michael@0 376 // Make Tabs a singleton class.
michael@0 377 private static class TabsInstanceHolder {
michael@0 378 private static final Tabs INSTANCE = new Tabs();
michael@0 379 }
michael@0 380
michael@0 381 @RobocopTarget
michael@0 382 public static Tabs getInstance() {
michael@0 383 return Tabs.TabsInstanceHolder.INSTANCE;
michael@0 384 }
michael@0 385
michael@0 386 // GeckoEventListener implementation
michael@0 387 @Override
michael@0 388 public void handleMessage(String event, JSONObject message) {
michael@0 389 Log.d(LOGTAG, "handleMessage: " + event);
michael@0 390 try {
michael@0 391 if (event.equals("Session:RestoreEnd")) {
michael@0 392 notifyListeners(null, TabEvents.RESTORED);
michael@0 393 return;
michael@0 394 }
michael@0 395
michael@0 396 // All other events handled below should contain a tabID property
michael@0 397 int id = message.getInt("tabID");
michael@0 398 Tab tab = getTab(id);
michael@0 399
michael@0 400 // "Tab:Added" is a special case because tab will be null if the tab was just added
michael@0 401 if (event.equals("Tab:Added")) {
michael@0 402 String url = message.isNull("uri") ? null : message.getString("uri");
michael@0 403
michael@0 404 if (message.getBoolean("stub")) {
michael@0 405 if (tab == null) {
michael@0 406 // Tab was already closed; abort
michael@0 407 return;
michael@0 408 }
michael@0 409 } else {
michael@0 410 tab = addTab(id, url, message.getBoolean("external"),
michael@0 411 message.getInt("parentId"),
michael@0 412 message.getString("title"),
michael@0 413 message.getBoolean("isPrivate"));
michael@0 414
michael@0 415 // If we added the tab as a stub, we should have already
michael@0 416 // selected it, so ignore this flag for stubbed tabs.
michael@0 417 if (message.getBoolean("selected"))
michael@0 418 selectTab(id);
michael@0 419 }
michael@0 420
michael@0 421 if (message.getBoolean("delayLoad"))
michael@0 422 tab.setState(Tab.STATE_DELAYED);
michael@0 423 if (message.getBoolean("desktopMode"))
michael@0 424 tab.setDesktopMode(true);
michael@0 425 return;
michael@0 426 }
michael@0 427
michael@0 428 // Tab was already closed; abort
michael@0 429 if (tab == null)
michael@0 430 return;
michael@0 431
michael@0 432 if (event.startsWith("SessionHistory:")) {
michael@0 433 event = event.substring("SessionHistory:".length());
michael@0 434 tab.handleSessionHistoryMessage(event, message);
michael@0 435 } else if (event.equals("Tab:Close")) {
michael@0 436 closeTab(tab);
michael@0 437 } else if (event.equals("Tab:Select")) {
michael@0 438 selectTab(tab.getId());
michael@0 439 } else if (event.equals("Content:LocationChange")) {
michael@0 440 tab.handleLocationChange(message);
michael@0 441 } else if (event.equals("Content:SecurityChange")) {
michael@0 442 tab.updateIdentityData(message.getJSONObject("identity"));
michael@0 443 notifyListeners(tab, TabEvents.SECURITY_CHANGE);
michael@0 444 } else if (event.equals("Content:ReaderEnabled")) {
michael@0 445 tab.setReaderEnabled(true);
michael@0 446 notifyListeners(tab, TabEvents.READER_ENABLED);
michael@0 447 } else if (event.equals("Content:StateChange")) {
michael@0 448 int state = message.getInt("state");
michael@0 449 if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
michael@0 450 if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
michael@0 451 boolean restoring = message.getBoolean("restoring");
michael@0 452 tab.handleDocumentStart(restoring, message.getString("uri"));
michael@0 453 notifyListeners(tab, Tabs.TabEvents.START);
michael@0 454 } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
michael@0 455 tab.handleDocumentStop(message.getBoolean("success"));
michael@0 456 notifyListeners(tab, Tabs.TabEvents.STOP);
michael@0 457 }
michael@0 458 }
michael@0 459 } else if (event.equals("Content:LoadError")) {
michael@0 460 tab.handleContentLoaded();
michael@0 461 notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR);
michael@0 462 } else if (event.equals("Content:PageShow")) {
michael@0 463 notifyListeners(tab, TabEvents.PAGE_SHOW);
michael@0 464 } else if (event.equals("DOMContentLoaded")) {
michael@0 465 tab.handleContentLoaded();
michael@0 466 String backgroundColor = message.getString("bgColor");
michael@0 467 if (backgroundColor != null) {
michael@0 468 tab.setBackgroundColor(backgroundColor);
michael@0 469 } else {
michael@0 470 // Default to white if no color is given
michael@0 471 tab.setBackgroundColor(Color.WHITE);
michael@0 472 }
michael@0 473 tab.setErrorType(message.optString("errorType"));
michael@0 474 notifyListeners(tab, Tabs.TabEvents.LOADED);
michael@0 475 } else if (event.equals("DOMTitleChanged")) {
michael@0 476 tab.updateTitle(message.getString("title"));
michael@0 477 } else if (event.equals("Link:Favicon")) {
michael@0 478 tab.updateFaviconURL(message.getString("href"), message.getInt("size"));
michael@0 479 notifyListeners(tab, TabEvents.LINK_FAVICON);
michael@0 480 } else if (event.equals("Link:Feed")) {
michael@0 481 tab.setHasFeeds(true);
michael@0 482 notifyListeners(tab, TabEvents.LINK_FEED);
michael@0 483 } else if (event.equals("Link:OpenSearch")) {
michael@0 484 boolean visible = message.getBoolean("visible");
michael@0 485 tab.setHasOpenSearch(visible);
michael@0 486 } else if (event.equals("DesktopMode:Changed")) {
michael@0 487 tab.setDesktopMode(message.getBoolean("desktopMode"));
michael@0 488 notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE);
michael@0 489 } else if (event.equals("Tab:ViewportMetadata")) {
michael@0 490 tab.setZoomConstraints(new ZoomConstraints(message));
michael@0 491 tab.setIsRTL(message.getBoolean("isRTL"));
michael@0 492 notifyListeners(tab, TabEvents.VIEWPORT_CHANGE);
michael@0 493 } else if (event.equals("Tab:StreamStart")) {
michael@0 494 tab.setRecording(true);
michael@0 495 notifyListeners(tab, TabEvents.RECORDING_CHANGE);
michael@0 496 } else if (event.equals("Tab:StreamStop")) {
michael@0 497 tab.setRecording(false);
michael@0 498 notifyListeners(tab, TabEvents.RECORDING_CHANGE);
michael@0 499 }
michael@0 500
michael@0 501 } catch (Exception e) {
michael@0 502 Log.w(LOGTAG, "handleMessage threw for " + event, e);
michael@0 503 }
michael@0 504 }
michael@0 505
michael@0 506 /**
michael@0 507 * Set the favicon for any tabs loaded with this page URL.
michael@0 508 */
michael@0 509 public void updateFaviconForURL(String pageURL, Bitmap favicon) {
michael@0 510 // The tab might be pointing to another URL by the time the
michael@0 511 // favicon is finally loaded, in which case we won't find the tab.
michael@0 512 // See also: Bug 920331.
michael@0 513 for (Tab tab : mOrder) {
michael@0 514 String tabURL = tab.getURL();
michael@0 515 if (pageURL.equals(tabURL)) {
michael@0 516 tab.setFaviconLoadId(Favicons.NOT_LOADING);
michael@0 517 if (tab.updateFavicon(favicon)) {
michael@0 518 notifyListeners(tab, TabEvents.FAVICON);
michael@0 519 }
michael@0 520 }
michael@0 521 }
michael@0 522 }
michael@0 523
michael@0 524 public void refreshThumbnails() {
michael@0 525 final ThumbnailHelper helper = ThumbnailHelper.getInstance();
michael@0 526 ThreadUtils.postToBackgroundThread(new Runnable() {
michael@0 527 @Override
michael@0 528 public void run() {
michael@0 529 for (final Tab tab : mOrder) {
michael@0 530 helper.getAndProcessThumbnailFor(tab);
michael@0 531 }
michael@0 532 }
michael@0 533 });
michael@0 534 }
michael@0 535
michael@0 536 public interface OnTabsChangedListener {
michael@0 537 public void onTabChanged(Tab tab, TabEvents msg, Object data);
michael@0 538 }
michael@0 539
michael@0 540 private static final List<OnTabsChangedListener> TABS_CHANGED_LISTENERS = new CopyOnWriteArrayList<OnTabsChangedListener>();
michael@0 541
michael@0 542 public static void registerOnTabsChangedListener(OnTabsChangedListener listener) {
michael@0 543 TABS_CHANGED_LISTENERS.add(listener);
michael@0 544 }
michael@0 545
michael@0 546 public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) {
michael@0 547 TABS_CHANGED_LISTENERS.remove(listener);
michael@0 548 }
michael@0 549
michael@0 550 public enum TabEvents {
michael@0 551 CLOSED,
michael@0 552 START,
michael@0 553 LOADED,
michael@0 554 LOAD_ERROR,
michael@0 555 STOP,
michael@0 556 FAVICON,
michael@0 557 THUMBNAIL,
michael@0 558 TITLE,
michael@0 559 SELECTED,
michael@0 560 UNSELECTED,
michael@0 561 ADDED,
michael@0 562 RESTORED,
michael@0 563 LOCATION_CHANGE,
michael@0 564 MENU_UPDATED,
michael@0 565 PAGE_SHOW,
michael@0 566 LINK_FAVICON,
michael@0 567 LINK_FEED,
michael@0 568 SECURITY_CHANGE,
michael@0 569 READER_ENABLED,
michael@0 570 DESKTOP_MODE_CHANGE,
michael@0 571 VIEWPORT_CHANGE,
michael@0 572 RECORDING_CHANGE
michael@0 573 }
michael@0 574
michael@0 575 public void notifyListeners(Tab tab, TabEvents msg) {
michael@0 576 notifyListeners(tab, msg, "");
michael@0 577 }
michael@0 578
michael@0 579 public void notifyListeners(final Tab tab, final TabEvents msg, final Object data) {
michael@0 580 if (tab == null &&
michael@0 581 msg != TabEvents.RESTORED) {
michael@0 582 throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
michael@0 583 }
michael@0 584
michael@0 585 ThreadUtils.postToUiThread(new Runnable() {
michael@0 586 @Override
michael@0 587 public void run() {
michael@0 588 onTabChanged(tab, msg, data);
michael@0 589
michael@0 590 if (TABS_CHANGED_LISTENERS.isEmpty()) {
michael@0 591 return;
michael@0 592 }
michael@0 593
michael@0 594 Iterator<OnTabsChangedListener> items = TABS_CHANGED_LISTENERS.iterator();
michael@0 595 while (items.hasNext()) {
michael@0 596 items.next().onTabChanged(tab, msg, data);
michael@0 597 }
michael@0 598 }
michael@0 599 });
michael@0 600 }
michael@0 601
michael@0 602 private void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
michael@0 603 switch (msg) {
michael@0 604 case LOCATION_CHANGE:
michael@0 605 queuePersistAllTabs();
michael@0 606 break;
michael@0 607 case RESTORED:
michael@0 608 mInitialTabsAdded = true;
michael@0 609 break;
michael@0 610
michael@0 611 // When one tab is deselected, another one is always selected, so only
michael@0 612 // queue a single persist operation. When tabs are added/closed, they
michael@0 613 // are also selected/unselected, so it would be redundant to also listen
michael@0 614 // for ADDED/CLOSED events.
michael@0 615 case SELECTED:
michael@0 616 queuePersistAllTabs();
michael@0 617 case UNSELECTED:
michael@0 618 tab.onChange();
michael@0 619 break;
michael@0 620 default:
michael@0 621 break;
michael@0 622 }
michael@0 623 }
michael@0 624
michael@0 625 // This method persists the current ordered list of tabs in our tabs content provider.
michael@0 626 public void persistAllTabs() {
michael@0 627 ThreadUtils.postToBackgroundThread(mPersistTabsRunnable);
michael@0 628 }
michael@0 629
michael@0 630 /**
michael@0 631 * Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS
michael@0 632 * milliseconds have elapsed. If any existing requests are already queued then
michael@0 633 * those requests are removed.
michael@0 634 */
michael@0 635 private void queuePersistAllTabs() {
michael@0 636 Handler backgroundHandler = ThreadUtils.getBackgroundHandler();
michael@0 637 backgroundHandler.removeCallbacks(mPersistTabsRunnable);
michael@0 638 backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS);
michael@0 639 }
michael@0 640
michael@0 641 private void registerEventListener(String event) {
michael@0 642 GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
michael@0 643 }
michael@0 644
michael@0 645 /**
michael@0 646 * Looks for an open tab with the given URL.
michael@0 647 * @param url the URL of the tab we're looking for
michael@0 648 *
michael@0 649 * @return first Tab with the given URL, or null if there is no such tab.
michael@0 650 */
michael@0 651 public Tab getFirstTabForUrl(String url) {
michael@0 652 return getFirstTabForUrlHelper(url, null);
michael@0 653 }
michael@0 654
michael@0 655 /**
michael@0 656 * Looks for an open tab with the given URL and private state.
michael@0 657 * @param url the URL of the tab we're looking for
michael@0 658 * @param isPrivate if true, only look for tabs that are private. if false,
michael@0 659 * only look for tabs that are non-private.
michael@0 660 *
michael@0 661 * @return first Tab with the given URL, or null if there is no such tab.
michael@0 662 */
michael@0 663 public Tab getFirstTabForUrl(String url, boolean isPrivate) {
michael@0 664 return getFirstTabForUrlHelper(url, isPrivate);
michael@0 665 }
michael@0 666
michael@0 667 private Tab getFirstTabForUrlHelper(String url, Boolean isPrivate) {
michael@0 668 if (url == null) {
michael@0 669 return null;
michael@0 670 }
michael@0 671
michael@0 672 for (Tab tab : mOrder) {
michael@0 673 if (isPrivate != null && isPrivate != tab.isPrivate()) {
michael@0 674 continue;
michael@0 675 }
michael@0 676 String tabUrl = tab.getURL();
michael@0 677 if (AboutPages.isAboutReader(tabUrl)) {
michael@0 678 tabUrl = ReaderModeUtils.getUrlFromAboutReader(tabUrl);
michael@0 679 }
michael@0 680 if (url.equals(tabUrl)) {
michael@0 681 return tab;
michael@0 682 }
michael@0 683 }
michael@0 684
michael@0 685 return null;
michael@0 686 }
michael@0 687
michael@0 688 /**
michael@0 689 * Loads a tab with the given URL in the currently selected tab.
michael@0 690 *
michael@0 691 * @param url URL of page to load, or search term used if searchEngine is given
michael@0 692 */
michael@0 693 @RobocopTarget
michael@0 694 public Tab loadUrl(String url) {
michael@0 695 return loadUrl(url, LOADURL_NONE);
michael@0 696 }
michael@0 697
michael@0 698 /**
michael@0 699 * Loads a tab with the given URL.
michael@0 700 *
michael@0 701 * @param url URL of page to load, or search term used if searchEngine is given
michael@0 702 * @param flags flags used to load tab
michael@0 703 *
michael@0 704 * @return the Tab if a new one was created; null otherwise
michael@0 705 */
michael@0 706 public Tab loadUrl(String url, int flags) {
michael@0 707 return loadUrl(url, null, -1, flags);
michael@0 708 }
michael@0 709
michael@0 710 /**
michael@0 711 * Loads a tab with the given URL.
michael@0 712 *
michael@0 713 * @param url URL of page to load, or search term used if searchEngine is given
michael@0 714 * @param searchEngine if given, the search engine with this name is used
michael@0 715 * to search for the url string; if null, the URL is loaded directly
michael@0 716 * @param parentId ID of this tab's parent, or -1 if it has no parent
michael@0 717 * @param flags flags used to load tab
michael@0 718 *
michael@0 719 * @return the Tab if a new one was created; null otherwise
michael@0 720 */
michael@0 721 public Tab loadUrl(String url, String searchEngine, int parentId, int flags) {
michael@0 722 JSONObject args = new JSONObject();
michael@0 723 Tab added = null;
michael@0 724 boolean delayLoad = (flags & LOADURL_DELAY_LOAD) != 0;
michael@0 725
michael@0 726 // delayLoad implies background tab
michael@0 727 boolean background = delayLoad || (flags & LOADURL_BACKGROUND) != 0;
michael@0 728
michael@0 729 try {
michael@0 730 boolean isPrivate = (flags & LOADURL_PRIVATE) != 0;
michael@0 731 boolean userEntered = (flags & LOADURL_USER_ENTERED) != 0;
michael@0 732 boolean desktopMode = (flags & LOADURL_DESKTOP) != 0;
michael@0 733 boolean external = (flags & LOADURL_EXTERNAL) != 0;
michael@0 734
michael@0 735 args.put("url", url);
michael@0 736 args.put("engine", searchEngine);
michael@0 737 args.put("parentId", parentId);
michael@0 738 args.put("userEntered", userEntered);
michael@0 739 args.put("newTab", (flags & LOADURL_NEW_TAB) != 0);
michael@0 740 args.put("isPrivate", isPrivate);
michael@0 741 args.put("pinned", (flags & LOADURL_PINNED) != 0);
michael@0 742 args.put("delayLoad", delayLoad);
michael@0 743 args.put("desktopMode", desktopMode);
michael@0 744 args.put("selected", !background);
michael@0 745
michael@0 746 if ((flags & LOADURL_NEW_TAB) != 0) {
michael@0 747 int tabId = getNextTabId();
michael@0 748 args.put("tabID", tabId);
michael@0 749
michael@0 750 // The URL is updated for the tab once Gecko responds with the
michael@0 751 // Tab:Added message. We can preliminarily set the tab's URL as
michael@0 752 // long as it's a valid URI.
michael@0 753 String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null;
michael@0 754
michael@0 755 added = addTab(tabId, tabUrl, external, parentId, url, isPrivate);
michael@0 756 added.setDesktopMode(desktopMode);
michael@0 757 }
michael@0 758 } catch (Exception e) {
michael@0 759 Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
michael@0 760 }
michael@0 761
michael@0 762 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString()));
michael@0 763
michael@0 764 if (added == null) {
michael@0 765 return null;
michael@0 766 }
michael@0 767
michael@0 768 if (!delayLoad && !background) {
michael@0 769 selectTab(added.getId());
michael@0 770 }
michael@0 771
michael@0 772 // TODO: surely we could just fetch *any* cached icon?
michael@0 773 if (AboutPages.isDefaultIconPage(url)) {
michael@0 774 Log.d(LOGTAG, "Setting about: tab favicon inline.");
michael@0 775 added.updateFavicon(getAboutPageFavicon(url));
michael@0 776 }
michael@0 777
michael@0 778 return added;
michael@0 779 }
michael@0 780
michael@0 781 /**
michael@0 782 * These favicons are only used for the URL bar, so
michael@0 783 * we fetch with that size.
michael@0 784 *
michael@0 785 * This method completes on the calling thread.
michael@0 786 */
michael@0 787 private Bitmap getAboutPageFavicon(final String url) {
michael@0 788 int faviconSize = Math.round(mAppContext.getResources().getDimension(R.dimen.browser_toolbar_favicon_size));
michael@0 789 return Favicons.getSizedFaviconForPageFromCache(url, faviconSize);
michael@0 790 }
michael@0 791
michael@0 792 /**
michael@0 793 * Open the url as a new tab, and mark the selected tab as its "parent".
michael@0 794 *
michael@0 795 * If the url is already open in a tab, the existing tab is selected.
michael@0 796 * Use this for tabs opened by the browser chrome, so users can press the
michael@0 797 * "Back" button to return to the previous tab.
michael@0 798 *
michael@0 799 * @param url URL of page to load
michael@0 800 */
michael@0 801 public void loadUrlInTab(String url) {
michael@0 802 Iterable<Tab> tabs = getTabsInOrder();
michael@0 803 for (Tab tab : tabs) {
michael@0 804 if (url.equals(tab.getURL())) {
michael@0 805 selectTab(tab.getId());
michael@0 806 return;
michael@0 807 }
michael@0 808 }
michael@0 809
michael@0 810 // getSelectedTab() can return null if no tab has been created yet
michael@0 811 // (i.e., we're restoring a session after a crash). In these cases,
michael@0 812 // don't mark any tabs as a parent.
michael@0 813 int parentId = -1;
michael@0 814 Tab selectedTab = getSelectedTab();
michael@0 815 if (selectedTab != null) {
michael@0 816 parentId = selectedTab.getId();
michael@0 817 }
michael@0 818
michael@0 819 loadUrl(url, null, parentId, LOADURL_NEW_TAB);
michael@0 820 }
michael@0 821
michael@0 822 /**
michael@0 823 * Gets the next tab ID.
michael@0 824 */
michael@0 825 @JNITarget
michael@0 826 public static int getNextTabId() {
michael@0 827 return sTabId.getAndIncrement();
michael@0 828 }
michael@0 829 }

mercurial