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/. */
5
6 package org.mozilla.gecko;
7
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;
13
14 import android.support.annotation.Nullable;
15 import org.json.JSONException;
16 import org.json.JSONObject;
17
18 import org.mozilla.gecko.annotation.JNITarget;
19 import org.mozilla.gecko.annotation.RobocopTarget;
20 import org.mozilla.gecko.AppConstants.Versions;
21 import org.mozilla.gecko.db.BrowserDB;
22 import org.mozilla.gecko.gfx.LayerView;
23 import org.mozilla.gecko.mozglue.SafeIntent;
24 import org.mozilla.gecko.notifications.WhatsNewReceiver;
25 import org.mozilla.gecko.reader.ReaderModeUtils;
26 import org.mozilla.gecko.util.GeckoEventListener;
27 import org.mozilla.gecko.util.ThreadUtils;
28
SessionTab(String title, String url, boolean isSelected, JSONObject tabObject)29 import android.accounts.Account;
30 import android.accounts.AccountManager;
31 import android.accounts.OnAccountsUpdateListener;
32 import android.content.ContentResolver;
33 import android.content.Context;
34 import android.database.ContentObserver;
35 import android.database.sqlite.SQLiteException;
getTitle()36 import android.graphics.Color;
37 import android.net.Uri;
38 import android.os.Handler;
39 import android.provider.Browser;
getUrl()40 import android.support.v4.content.ContextCompat;
41 import android.util.Log;
42
43 public class Tabs implements GeckoEventListener {
isSelected()44 private static final String LOGTAG = "GeckoTabs";
45
46 // mOrder and mTabs are always of the same cardinality, and contain the same values.
47 private final CopyOnWriteArrayList<Tab> mOrder = new CopyOnWriteArrayList<Tab>();
getTabObject()48
49 // All writes to mSelectedTab must be synchronized on the Tabs instance.
50 // In general, it's preferred to always use selectTab()).
51 private volatile Tab mSelectedTab;
52
53 // All accesses to mTabs must be synchronized on the Tabs instance.
54 private final HashMap<Integer, Tab> mTabs = new HashMap<Integer, Tab>();
isAboutHomeWithoutHistory()55
56 private AccountManager mAccountManager;
57 private OnAccountsUpdateListener mAccountListener;
58
59 public static final int LOADURL_NONE = 0;
60 public static final int LOADURL_NEW_TAB = 1 << 0;
onTabRead(SessionTab tab)61 public static final int LOADURL_USER_ENTERED = 1 << 1;
62 public static final int LOADURL_PRIVATE = 1 << 2;
63 public static final int LOADURL_PINNED = 1 << 3;
64 public static final int LOADURL_DELAY_LOAD = 1 << 4;
65 public static final int LOADURL_DESKTOP = 1 << 5;
66 public static final int LOADURL_BACKGROUND = 1 << 6;
67 /** Indicates the url has been specified by a source external to the app. */
68 public static final int LOADURL_EXTERNAL = 1 << 7;
onClosedTabsRead(final JSONArray closedTabs)69 /** Indicates the tab is the first shown after Firefox is hidden and restored. */
70 public static final int LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN = 1 << 8;
71
72 private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 2;
73
74 public static final int INVALID_TAB_ID = -1;
75
76 private static final AtomicInteger sTabId = new AtomicInteger(0);
77 private volatile boolean mInitialTabsAdded;
parse(String... sessionStrings)78
79 private Context mAppContext;
80 private LayerView mLayerView;
81 private ContentObserver mBookmarksContentObserver;
82 private PersistTabsRunnable mPersistTabsRunnable;
83 private int mPrivateClearColor;
84
85 private static class PersistTabsRunnable implements Runnable {
86 private final BrowserDB db;
87 private final Context context;
88 private final Iterable<Tab> tabs;
89
90 public PersistTabsRunnable(final Context context, Iterable<Tab> tabsInOrder) {
91 this.context = context;
92 this.db = BrowserDB.from(context);
93 this.tabs = tabsInOrder;
94 }
95
96 @Override
97 public void run() {
98 try {
99 db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs);
100 } catch (SQLiteException e) {
101 Log.w(LOGTAG, "Error persisting local tabs", e);
102 }
103 }
104 };
105
106 private Tabs() {
107 EventDispatcher.getInstance().registerGeckoThreadListener(this,
108 "Tab:Added",
109 "Tab:Close",
110 "Tab:Select",
111 "Content:LocationChange",
112 "Content:SecurityChange",
113 "Content:StateChange",
114 "Content:LoadError",
115 "Content:PageShow",
116 "DOMTitleChanged",
117 "Link:Favicon",
118 "Link:Touchicon",
119 "Link:Feed",
120 "Link:OpenSearch",
121 "DesktopMode:Changed",
122 "Tab:StreamStart",
123 "Tab:StreamStop",
124 "Tab:AudioPlayingChange",
125 "Tab:MediaPlaybackChange");
126
127 mPrivateClearColor = Color.RED;
128
129 }
130
131 public synchronized void attachToContext(Context context, LayerView layerView) {
132 final Context appContext = context.getApplicationContext();
133 if (mAppContext == appContext) {
134 return;
135 }
136
137 if (mAppContext != null) {
138 // This should never happen.
139 Log.w(LOGTAG, "The application context has changed!");
140 }
141
142 mAppContext = appContext;
143 mLayerView = layerView;
144 mPrivateClearColor = ContextCompat.getColor(context, R.color.tabs_tray_grey_pressed);
145 mAccountManager = AccountManager.get(appContext);
146
147 mAccountListener = new OnAccountsUpdateListener() {
148 @Override
149 public void onAccountsUpdated(Account[] accounts) {
150 queuePersistAllTabs();
151 }
152 };
153
154 // The listener will run on the background thread (see 2nd argument).
155 mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false);
156
157 if (mBookmarksContentObserver != null) {
158 // It's safe to use the db here since we aren't doing any I/O.
159 final GeckoProfile profile = GeckoProfile.get(context);
160 BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver);
161 }
162 }
163
164 /**
165 * Gets the tab count corresponding to the private state of the selected
166 * tab.
167 *
168 * If the selected tab is a non-private tab, this will return the number of
169 * non-private tabs; likewise, if this is a private tab, this will return
170 * the number of private tabs.
171 *
172 * @return the number of tabs in the current private state
173 */
174 public synchronized int getDisplayCount() {
175 // Once mSelectedTab is non-null, it cannot be null for the remainder
176 // of the object's lifetime.
177 boolean getPrivate = mSelectedTab != null && mSelectedTab.isPrivate();
178 int count = 0;
179 for (Tab tab : mOrder) {
180 if (tab.isPrivate() == getPrivate) {
181 count++;
182 }
183 }
184 return count;
185 }
186
187 public int isOpen(String url) {
188 for (Tab tab : mOrder) {
189 if (tab.getURL().equals(url)) {
190 return tab.getId();
191 }
192 }
193 return -1;
194 }
195
196 // Must be synchronized to avoid racing on mBookmarksContentObserver.
197 private void lazyRegisterBookmarkObserver() {
198 if (mBookmarksContentObserver == null) {
199 mBookmarksContentObserver = new ContentObserver(null) {
200 @Override
201 public void onChange(boolean selfChange) {
202 for (Tab tab : mOrder) {
203 tab.updateBookmark();
204 }
205 }
206 };
207
208 // It's safe to use the db here since we aren't doing any I/O.
209 final GeckoProfile profile = GeckoProfile.get(mAppContext);
210 BrowserDB.from(profile).registerBookmarkObserver(getContentResolver(), mBookmarksContentObserver);
211 }
212 }
213
214 private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate, int tabIndex) {
215 final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) :
216 new Tab(mAppContext, id, url, external, parentId, title);
217 synchronized (this) {
218 lazyRegisterBookmarkObserver();
219 mTabs.put(id, tab);
220
221 if (tabIndex > -1) {
222 mOrder.add(tabIndex, tab);
223 } else {
224 mOrder.add(tab);
225 }
226 }
227
228 // Suppress the ADDED event to prevent animation of tabs created via session restore.
229 if (mInitialTabsAdded) {
230 notifyListeners(tab, TabEvents.ADDED,
231 Integer.toString(getPrivacySpecificTabIndex(tabIndex, isPrivate)));
232 }
233
234 return tab;
235 }
236
237 // Return the index, among those tabs whose privacy setting matches isPrivate, of the tab at
238 // position index in mOrder. Returns -1, for "new last tab", when index is -1.
239 private int getPrivacySpecificTabIndex(int index, boolean isPrivate) {
240 int privacySpecificIndex = -1;
241 for (int i = 0; i <= index; i++) {
242 final Tab tab = mOrder.get(i);
243 if (tab.isPrivate() == isPrivate) {
244 privacySpecificIndex++;
245 }
246 }
247 return privacySpecificIndex;
248 }
249
250 public synchronized void removeTab(int id) {
251 if (mTabs.containsKey(id)) {
252 Tab tab = getTab(id);
253 mOrder.remove(tab);
254 mTabs.remove(id);
255 }
256 }
257
258 public synchronized Tab selectTab(int id) {
259 if (!mTabs.containsKey(id))
260 return null;
261
262 final Tab oldTab = getSelectedTab();
263 final Tab tab = mTabs.get(id);
264
265 // This avoids a NPE below, but callers need to be careful to
266 // handle this case.
267 if (tab == null || oldTab == tab) {
268 return tab;
269 }
270
271 mSelectedTab = tab;
272 notifyListeners(tab, TabEvents.SELECTED);
273
274 if (mLayerView != null) {
275 mLayerView.setClearColor(getTabColor(tab));
276 }
277
278 if (oldTab != null) {
279 notifyListeners(oldTab, TabEvents.UNSELECTED);
280 }
281
282 // Pass a message to Gecko to update tab state in BrowserApp.
283 GeckoAppShell.notifyObservers("Tab:Selected", String.valueOf(tab.getId()));
284 return tab;
285 }
286
287 public synchronized boolean selectLastTab() {
288 if (mOrder.isEmpty()) {
289 return false;
290 }
291
292 selectTab(mOrder.get(mOrder.size() - 1).getId());
293 return true;
294 }
295
296 private int getIndexOf(Tab tab) {
297 return mOrder.lastIndexOf(tab);
298 }
299
300 private Tab getNextTabFrom(Tab tab, boolean getPrivate) {
301 int numTabs = mOrder.size();
302 int index = getIndexOf(tab);
303 for (int i = index + 1; i < numTabs; i++) {
304 Tab next = mOrder.get(i);
305 if (next.isPrivate() == getPrivate) {
306 return next;
307 }
308 }
309 return null;
310 }
311
312 private Tab getPreviousTabFrom(Tab tab, boolean getPrivate) {
313 int index = getIndexOf(tab);
314 for (int i = index - 1; i >= 0; i--) {
315 Tab prev = mOrder.get(i);
316 if (prev.isPrivate() == getPrivate) {
317 return prev;
318 }
319 }
320 return null;
321 }
322
323 /**
324 * Gets the selected tab.
325 *
326 * The selected tab can be null if we're doing a session restore after a
327 * crash and Gecko isn't ready yet.
328 *
329 * @return the selected tab, or null if no tabs exist
330 */
331 @Nullable
332 public Tab getSelectedTab() {
333 return mSelectedTab;
334 }
335
336 public boolean isSelectedTab(Tab tab) {
337 return tab != null && tab == mSelectedTab;
338 }
339
340 public boolean isSelectedTabId(int tabId) {
341 final Tab selected = mSelectedTab;
342 return selected != null && selected.getId() == tabId;
343 }
344
345 @RobocopTarget
346 public synchronized Tab getTab(int id) {
347 if (id == -1)
348 return null;
349
350 if (mTabs.size() == 0)
351 return null;
352
353 if (!mTabs.containsKey(id))
354 return null;
355
356 return mTabs.get(id);
357 }
358
359 public synchronized Tab getTabForApplicationId(final String applicationId) {
360 if (applicationId == null) {
361 return null;
362 }
363
364 for (final Tab tab : mOrder) {
365 if (applicationId.equals(tab.getApplicationId())) {
366 return tab;
367 }
368 }
369
370 return null;
371 }
372
373 /** Close tab and then select the default next tab */
374 @RobocopTarget
375 public synchronized void closeTab(Tab tab) {
376 closeTab(tab, getNextTab(tab));
377 }
378
379 public synchronized void closeTab(Tab tab, Tab nextTab) {
380 closeTab(tab, nextTab, false);
381 }
382
383 public synchronized void closeTab(Tab tab, boolean showUndoToast) {
384 closeTab(tab, getNextTab(tab), showUndoToast);
385 }
386
387 /** Close tab and then select nextTab */
388 public synchronized void closeTab(final Tab tab, Tab nextTab, boolean showUndoToast) {
389 if (tab == null)
390 return;
391
392 int tabId = tab.getId();
393 removeTab(tabId);
394
395 if (nextTab == null) {
396 nextTab = loadUrl(AboutPages.HOME, LOADURL_NEW_TAB);
397 }
398
399 selectTab(nextTab.getId());
400
401 tab.onDestroy();
402
403 final JSONObject args = new JSONObject();
404 try {
405 args.put("tabId", String.valueOf(tabId));
406 args.put("showUndoToast", showUndoToast);
407 } catch (JSONException e) {
408 Log.e(LOGTAG, "Error building Tab:Closed arguments: " + e);
409 }
410
411 // Pass a message to Gecko to update tab state in BrowserApp
412 GeckoAppShell.notifyObservers("Tab:Closed", args.toString());
413 }
414
415 /** Return the tab that will be selected by default after this one is closed */
416 public Tab getNextTab(Tab tab) {
417 Tab selectedTab = getSelectedTab();
418 if (selectedTab != tab)
419 return selectedTab;
420
421 boolean getPrivate = tab.isPrivate();
422 Tab nextTab = getNextTabFrom(tab, getPrivate);
423 if (nextTab == null)
424 nextTab = getPreviousTabFrom(tab, getPrivate);
425 if (nextTab == null && getPrivate) {
426 // If there are no private tabs remaining, get the last normal tab
427 Tab lastTab = mOrder.get(mOrder.size() - 1);
428 if (!lastTab.isPrivate()) {
429 nextTab = lastTab;
430 } else {
431 nextTab = getPreviousTabFrom(lastTab, false);
432 }
433 }
434
435 Tab parent = getTab(tab.getParentId());
436 if (parent != null) {
437 // If the next tab is a sibling, switch to it. Otherwise go back to the parent.
438 if (nextTab != null && nextTab.getParentId() == tab.getParentId())
439 return nextTab;
440 else
441 return parent;
442 }
443 return nextTab;
444 }
445
446 public Iterable<Tab> getTabsInOrder() {
447 return mOrder;
448 }
449
450 /**
451 * @return the current GeckoApp instance, or throws if
452 * we aren't correctly initialized.
453 */
454 private synchronized Context getAppContext() {
455 if (mAppContext == null) {
456 throw new IllegalStateException("Tabs not initialized with a GeckoApp instance.");
457 }
458 return mAppContext;
459 }
460
461 public ContentResolver getContentResolver() {
462 return getAppContext().getContentResolver();
463 }
464
465 // Make Tabs a singleton class.
466 private static class TabsInstanceHolder {
467 private static final Tabs INSTANCE = new Tabs();
468 }
469
470 @RobocopTarget
471 public static Tabs getInstance() {
472 return Tabs.TabsInstanceHolder.INSTANCE;
473 }
474
475 // GeckoEventListener implementation
476 @Override
477 public void handleMessage(String event, JSONObject message) {
478 Log.d(LOGTAG, "handleMessage: " + event);
479 try {
480 // All other events handled below should contain a tabID property
481 int id = message.getInt("tabID");
482 Tab tab = getTab(id);
483
484 // "Tab:Added" is a special case because tab will be null if the tab was just added
485 if (event.equals("Tab:Added")) {
486 String url = message.isNull("uri") ? null : message.getString("uri");
487
488 if (message.getBoolean("cancelEditMode")) {
489 final Tab oldTab = getSelectedTab();
490 if (oldTab != null) {
491 oldTab.setIsEditing(false);
492 }
493 }
494
495 if (message.getBoolean("stub")) {
496 if (tab == null) {
497 // Tab was already closed; abort
498 return;
499 }
500 } else {
501 tab = addTab(id, url, message.getBoolean("external"),
502 message.getInt("parentId"),
503 message.getString("title"),
504 message.getBoolean("isPrivate"),
505 message.getInt("tabIndex"));
506 // If we added the tab as a stub, we should have already
507 // selected it, so ignore this flag for stubbed tabs.
508 if (message.getBoolean("selected"))
509 selectTab(id);
510 }
511
512 if (message.getBoolean("delayLoad"))
513 tab.setState(Tab.STATE_DELAYED);
514 if (message.getBoolean("desktopMode"))
515 tab.setDesktopMode(true);
516 return;
517 }
518
519 // Tab was already closed; abort
520 if (tab == null)
521 return;
522
523 if (event.equals("Tab:Close")) {
524 closeTab(tab);
525 } else if (event.equals("Tab:Select")) {
526 selectTab(tab.getId());
527 } else if (event.equals("Content:LocationChange")) {
528 tab.handleLocationChange(message);
529 } else if (event.equals("Content:SecurityChange")) {
530 tab.updateIdentityData(message.getJSONObject("identity"));
531 notifyListeners(tab, TabEvents.SECURITY_CHANGE);
532 } else if (event.equals("Content:StateChange")) {
533 int state = message.getInt("state");
534 if ((state & GeckoAppShell.WPL_STATE_IS_NETWORK) != 0) {
535 if ((state & GeckoAppShell.WPL_STATE_START) != 0) {
536 boolean restoring = message.getBoolean("restoring");
537 tab.handleDocumentStart(restoring, message.getString("uri"));
538 notifyListeners(tab, Tabs.TabEvents.START);
539 } else if ((state & GeckoAppShell.WPL_STATE_STOP) != 0) {
540 tab.handleDocumentStop(message.getBoolean("success"));
541 notifyListeners(tab, Tabs.TabEvents.STOP);
542 }
543 }
544 } else if (event.equals("Content:LoadError")) {
545 tab.handleContentLoaded();
546 notifyListeners(tab, Tabs.TabEvents.LOAD_ERROR);
547 } else if (event.equals("Content:PageShow")) {
548 tab.setLoadedFromCache(message.getBoolean("fromCache"));
549 tab.updateUserRequested(message.getString("userRequested"));
550 notifyListeners(tab, TabEvents.PAGE_SHOW);
551 } else if (event.equals("DOMTitleChanged")) {
552 tab.updateTitle(message.getString("title"));
553 } else if (event.equals("Link:Favicon")) {
554 // Add the favicon to the set of available icons for this tab.
555
556 tab.addFavicon(message.getString("href"), message.getInt("size"), message.getString("mime"));
557
558 // Load the favicon. If the tab is still loading, we actually do the load once the
559 // page has loaded, in an attempt to prevent the favicon load from having a
560 // detrimental effect on page load time.
561 if (tab.getState() != Tab.STATE_LOADING) {
562 tab.loadFavicon();
563 }
564 } else if (event.equals("Link:Touchicon")) {
565 tab.addTouchicon(message.getString("href"), message.getInt("size"), message.getString("mime"));
566 } else if (event.equals("Link:Feed")) {
567 tab.setHasFeeds(true);
568 notifyListeners(tab, TabEvents.LINK_FEED);
569 } else if (event.equals("Link:OpenSearch")) {
570 boolean visible = message.getBoolean("visible");
571 tab.setHasOpenSearch(visible);
572 } else if (event.equals("DesktopMode:Changed")) {
573 tab.setDesktopMode(message.getBoolean("desktopMode"));
574 notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE);
575 } else if (event.equals("Tab:StreamStart")) {
576 tab.setRecording(true);
577 notifyListeners(tab, TabEvents.RECORDING_CHANGE);
578 } else if (event.equals("Tab:StreamStop")) {
579 tab.setRecording(false);
580 notifyListeners(tab, TabEvents.RECORDING_CHANGE);
581 } else if (event.equals("Tab:AudioPlayingChange")) {
582 tab.setIsAudioPlaying(message.getBoolean("isAudioPlaying"));
583 notifyListeners(tab, TabEvents.AUDIO_PLAYING_CHANGE);
584 } else if (event.equals("Tab:MediaPlaybackChange")) {
585 final String status = message.getString("status");
586 if (status.equals("resume")) {
587 notifyListeners(tab, TabEvents.MEDIA_PLAYING_RESUME);
588 } else {
589 tab.setIsMediaPlaying(status.equals("start"));
590 notifyListeners(tab, TabEvents.MEDIA_PLAYING_CHANGE);
591 }
592 }
593
594 } catch (Exception e) {
595 Log.w(LOGTAG, "handleMessage threw for " + event, e);
596 }
597 }
598
599 public void refreshThumbnails() {
600 final BrowserDB db = BrowserDB.from(mAppContext);
601 ThreadUtils.postToBackgroundThread(new Runnable() {
602 @Override
603 public void run() {
604 for (final Tab tab : mOrder) {
605 if (tab.getThumbnail() == null) {
606 tab.loadThumbnailFromDB(db);
607 }
608 }
609 }
610 });
611 }
612
613 public interface OnTabsChangedListener {
614 void onTabChanged(Tab tab, TabEvents msg, String data);
615 }
616
617 private static final List<OnTabsChangedListener> TABS_CHANGED_LISTENERS = new CopyOnWriteArrayList<OnTabsChangedListener>();
618
619 public static void registerOnTabsChangedListener(OnTabsChangedListener listener) {
620 TABS_CHANGED_LISTENERS.add(listener);
621 }
622
623 public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) {
624 TABS_CHANGED_LISTENERS.remove(listener);
625 }
626
627 public enum TabEvents {
628 CLOSED,
629 START,
630 LOADED,
631 LOAD_ERROR,
632 STOP,
633 FAVICON,
634 THUMBNAIL,
635 TITLE,
636 SELECTED,
637 UNSELECTED,
638 ADDED,
639 RESTORED,
640 LOCATION_CHANGE,
641 MENU_UPDATED,
642 PAGE_SHOW,
643 LINK_FEED,
644 SECURITY_CHANGE,
645 DESKTOP_MODE_CHANGE,
646 RECORDING_CHANGE,
647 BOOKMARK_ADDED,
648 BOOKMARK_REMOVED,
649 AUDIO_PLAYING_CHANGE,
650 OPENED_FROM_TABS_TRAY,
651 MEDIA_PLAYING_CHANGE,
652 MEDIA_PLAYING_RESUME
653 }
654
655 public void notifyListeners(Tab tab, TabEvents msg) {
656 notifyListeners(tab, msg, "");
657 }
658
659 public void notifyListeners(final Tab tab, final TabEvents msg, final String data) {
660 if (tab == null &&
661 msg != TabEvents.RESTORED) {
662 throw new IllegalArgumentException("onTabChanged:" + msg + " must specify a tab.");
663 }
664
665 ThreadUtils.postToUiThread(new Runnable() {
666 @Override
667 public void run() {
668 onTabChanged(tab, msg, data);
669
670 if (TABS_CHANGED_LISTENERS.isEmpty()) {
671 return;
672 }
673
674 Iterator<OnTabsChangedListener> items = TABS_CHANGED_LISTENERS.iterator();
675 while (items.hasNext()) {
676 items.next().onTabChanged(tab, msg, data);
677 }
678 }
679 });
680 }
681
682 private void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
683 switch (msg) {
684 // We want the tab record to have an accurate favicon, so queue
685 // the persisting of tabs when it changes.
686 case FAVICON:
687 case LOCATION_CHANGE:
688 queuePersistAllTabs();
689 break;
690 case RESTORED:
691 mInitialTabsAdded = true;
692 break;
693
694 // When one tab is deselected, another one is always selected, so only
695 // queue a single persist operation. When tabs are added/closed, they
696 // are also selected/unselected, so it would be redundant to also listen
697 // for ADDED/CLOSED events.
698 case SELECTED:
699 if (mLayerView != null) {
700 mLayerView.setSurfaceBackgroundColor(getTabColor(tab));
701 mLayerView.setPaintState(LayerView.PAINT_START);
702 }
703 queuePersistAllTabs();
704 case UNSELECTED:
705 tab.onChange();
706 break;
707 default:
708 break;
709 }
710 }
711
712 /**
713 * Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS
714 * milliseconds have elapsed. If any existing requests are already queued then
715 * those requests are removed.
716 */
717 private void queuePersistAllTabs() {
718 final Handler backgroundHandler = ThreadUtils.getBackgroundHandler();
719
720 // Note: Its safe to modify the runnable here because all of the callers are on the same thread.
721 if (mPersistTabsRunnable != null) {
722 backgroundHandler.removeCallbacks(mPersistTabsRunnable);
723 mPersistTabsRunnable = null;
724 }
725
726 mPersistTabsRunnable = new PersistTabsRunnable(mAppContext, getTabsInOrder());
727 backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS);
728 }
729
730 /**
731 * Looks for an open tab with the given URL.
732 * @param url the URL of the tab we're looking for
733 *
734 * @return first Tab with the given URL, or null if there is no such tab.
735 */
736 public Tab getFirstTabForUrl(String url) {
737 return getFirstTabForUrlHelper(url, null);
738 }
739
740 /**
741 * Looks for an open tab with the given URL and private state.
742 * @param url the URL of the tab we're looking for
743 * @param isPrivate if true, only look for tabs that are private. if false,
744 * only look for tabs that are non-private.
745 *
746 * @return first Tab with the given URL, or null if there is no such tab.
747 */
748 public Tab getFirstTabForUrl(String url, boolean isPrivate) {
749 return getFirstTabForUrlHelper(url, isPrivate);
750 }
751
752 private Tab getFirstTabForUrlHelper(String url, Boolean isPrivate) {
753 if (url == null) {
754 return null;
755 }
756
757 for (Tab tab : mOrder) {
758 if (isPrivate != null && isPrivate != tab.isPrivate()) {
759 continue;
760 }
761 if (url.equals(tab.getURL())) {
762 return tab;
763 }
764 }
765
766 return null;
767 }
768
769 /**
770 * Looks for a reader mode enabled open tab with the given URL and private
771 * state.
772 *
773 * @param url
774 * The URL of the tab we're looking for. The url parameter can be
775 * the actual article URL or the reader mode article URL.
776 * @param isPrivate
777 * If true, only look for tabs that are private. If false, only
778 * look for tabs that are not private.
779 *
780 * @return The first Tab with the given URL, or null if there is no such
781 * tab.
782 */
783 public Tab getFirstReaderTabForUrl(String url, boolean isPrivate) {
784 if (url == null) {
785 return null;
786 }
787
788 url = ReaderModeUtils.stripAboutReaderUrl(url);
789
790 for (Tab tab : mOrder) {
791 if (isPrivate != tab.isPrivate()) {
792 continue;
793 }
794 String tabUrl = tab.getURL();
795 if (AboutPages.isAboutReader(tabUrl)) {
796 tabUrl = ReaderModeUtils.stripAboutReaderUrl(tabUrl);
797 if (url.equals(tabUrl)) {
798 return tab;
799 }
800 }
801 }
802
803 return null;
804 }
805
806 /**
807 * Loads a tab with the given URL in the currently selected tab.
808 *
809 * @param url URL of page to load, or search term used if searchEngine is given
810 */
811 @RobocopTarget
812 public Tab loadUrl(String url) {
813 return loadUrl(url, LOADURL_NONE);
814 }
815
816 /**
817 * Loads a tab with the given URL.
818 *
819 * @param url URL of page to load, or search term used if searchEngine is given
820 * @param flags flags used to load tab
821 *
822 * @return the Tab if a new one was created; null otherwise
823 */
824 @RobocopTarget
825 public Tab loadUrl(String url, int flags) {
826 return loadUrl(url, null, -1, null, flags);
827 }
828
829 public Tab loadUrlWithIntentExtras(final String url, final SafeIntent intent, final int flags) {
830 // We can't directly create a listener to tell when the user taps on the "What's new"
831 // notification, so we use this intent handling as a signal that they tapped the notification.
832 if (intent.getBooleanExtra(WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION, false)) {
833 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION,
834 WhatsNewReceiver.EXTRA_WHATSNEW_NOTIFICATION);
835 }
836
837 // Note: we don't get the URL from the intent so the calling
838 // method has the opportunity to change the URL if applicable.
839 return loadUrl(url, null, -1, intent, flags);
840 }
841
842 public Tab loadUrl(final String url, final String searchEngine, final int parentId, final int flags) {
843 return loadUrl(url, searchEngine, parentId, null, flags);
844 }
845
846 /**
847 * Loads a tab with the given URL.
848 *
849 * @param url URL of page to load, or search term used if searchEngine is given
850 * @param searchEngine if given, the search engine with this name is used
851 * to search for the url string; if null, the URL is loaded directly
852 * @param parentId ID of this tab's parent, or -1 if it has no parent
853 * @param intent an intent whose extras are used to modify the request
854 * @param flags flags used to load tab
855 *
856 * @return the Tab if a new one was created; null otherwise
857 */
858 public Tab loadUrl(final String url, final String searchEngine, final int parentId,
859 final SafeIntent intent, final int flags) {
860 JSONObject args = new JSONObject();
861 Tab tabToSelect = null;
862 boolean delayLoad = (flags & LOADURL_DELAY_LOAD) != 0;
863
864 // delayLoad implies background tab
865 boolean background = delayLoad || (flags & LOADURL_BACKGROUND) != 0;
866
867 try {
868 boolean isPrivate = (flags & LOADURL_PRIVATE) != 0;
869 boolean userEntered = (flags & LOADURL_USER_ENTERED) != 0;
870 boolean desktopMode = (flags & LOADURL_DESKTOP) != 0;
871 boolean external = (flags & LOADURL_EXTERNAL) != 0;
872 final boolean isFirstShownAfterActivityUnhidden = (flags & LOADURL_FIRST_AFTER_ACTIVITY_UNHIDDEN) != 0;
873
874 args.put("url", url);
875 args.put("engine", searchEngine);
876 args.put("parentId", parentId);
877 args.put("userEntered", userEntered);
878 args.put("isPrivate", isPrivate);
879 args.put("pinned", (flags & LOADURL_PINNED) != 0);
880 args.put("desktopMode", desktopMode);
881
882 final boolean needsNewTab;
883 final String applicationId = (intent == null) ? null :
884 intent.getStringExtra(Browser.EXTRA_APPLICATION_ID);
885 if (applicationId == null) {
886 needsNewTab = (flags & LOADURL_NEW_TAB) != 0;
887 } else {
888 // If you modify this code, be careful that intent != null.
889 final boolean extraCreateNewTab = intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false);
890 final Tab applicationTab = getTabForApplicationId(applicationId);
891 if (applicationTab == null || extraCreateNewTab) {
892 needsNewTab = true;
893 } else {
894 needsNewTab = false;
895 delayLoad = false;
896 background = false;
897
898 tabToSelect = applicationTab;
899 final int tabToSelectId = tabToSelect.getId();
900 args.put("tabID", tabToSelectId);
901
902 // This must be called before the "Tab:Load" event is sent. I think addTab gets
903 // away with it because having "newTab" == true causes the selected tab to be
904 // updated in JS for the "Tab:Load" event but "newTab" is false in our case.
905 // This makes me think the other selectTab is not necessary (bug 1160673).
906 //
907 // Note: that makes the later call redundant but selectTab exits early so I'm
908 // fine not adding the complex logic to avoid calling it again.
909 selectTab(tabToSelect.getId());
910 }
911 }
912
913 args.put("newTab", needsNewTab);
914 args.put("delayLoad", delayLoad);
915 args.put("selected", !background);
916
917 if (needsNewTab) {
918 int tabId = getNextTabId();
919 args.put("tabID", tabId);
920
921 // The URL is updated for the tab once Gecko responds with the
922 // Tab:Added message. We can preliminarily set the tab's URL as
923 // long as it's a valid URI.
924 String tabUrl = (url != null && Uri.parse(url).getScheme() != null) ? url : null;
925
926 // Add the new tab to the end of the tab order.
927 final int tabIndex = -1;
928
929 tabToSelect = addTab(tabId, tabUrl, external, parentId, url, isPrivate, tabIndex);
930 tabToSelect.setDesktopMode(desktopMode);
931 tabToSelect.setApplicationId(applicationId);
932 if (isFirstShownAfterActivityUnhidden) {
933 // We just opened Firefox so we want to show
934 // the toolbar but not animate it to avoid jank.
935 tabToSelect.setShouldShowToolbarWithoutAnimationOnFirstSelection(true);
936 }
937 }
938 } catch (Exception e) {
939 Log.w(LOGTAG, "Error building JSON arguments for loadUrl.", e);
940 }
941
942 GeckoAppShell.notifyObservers("Tab:Load", args.toString());
943
944 if (tabToSelect == null) {
945 return null;
946 }
947
948 if (!delayLoad && !background) {
949 selectTab(tabToSelect.getId());
950 }
951
952 // Load favicon instantly for about:home page because it's already cached
953 if (AboutPages.isBuiltinIconPage(url)) {
954 tabToSelect.loadFavicon();
955 }
956
957 return tabToSelect;
958 }
959
960 public Tab addTab() {
961 return loadUrl(AboutPages.HOME, Tabs.LOADURL_NEW_TAB);
962 }
963
964 public Tab addPrivateTab() {
965 return loadUrl(AboutPages.PRIVATEBROWSING, Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_PRIVATE);
966 }
967
968 /**
969 * Open the url as a new tab, and mark the selected tab as its "parent".
970 *
971 * If the url is already open in a tab, the existing tab is selected.
972 * Use this for tabs opened by the browser chrome, so users can press the
973 * "Back" button to return to the previous tab.
974 *
975 * This method will open a new private tab if the currently selected tab
976 * is also private.
977 *
978 * @param url URL of page to load
979 */
980 public void loadUrlInTab(String url) {
981 Iterable<Tab> tabs = getTabsInOrder();
982 for (Tab tab : tabs) {
983 if (url.equals(tab.getURL())) {
984 selectTab(tab.getId());
985 return;
986 }
987 }
988
989 // getSelectedTab() can return null if no tab has been created yet
990 // (i.e., we're restoring a session after a crash). In these cases,
991 // don't mark any tabs as a parent.
992 int parentId = -1;
993 int flags = LOADURL_NEW_TAB;
994
995 final Tab selectedTab = getSelectedTab();
996 if (selectedTab != null) {
997 parentId = selectedTab.getId();
998 if (selectedTab.isPrivate()) {
999 flags = flags | LOADURL_PRIVATE;
1000 }
1001 }
1002
1003 loadUrl(url, null, parentId, flags);
1004 }
1005
1006 /**
1007 * Gets the next tab ID.
1008 */
1009 @JNITarget
1010 public static int getNextTabId() {
1011 return sTabId.getAndIncrement();
1012 }
1013
1014 private int getTabColor(Tab tab) {
1015 if (tab != null) {
1016 return tab.isPrivate() ? mPrivateClearColor : Color.WHITE;
1017 }
1018
1019 return Color.WHITE;
1020 }
1021 }
1022