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