|
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 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; |
|
23 |
|
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; |
|
35 |
|
36 public class Tabs implements GeckoEventListener { |
|
37 private static final String LOGTAG = "GeckoTabs"; |
|
38 |
|
39 // mOrder and mTabs are always of the same cardinality, and contain the same values. |
|
40 private final CopyOnWriteArrayList<Tab> mOrder = new CopyOnWriteArrayList<Tab>(); |
|
41 |
|
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; |
|
45 |
|
46 // All accesses to mTabs must be synchronized on the Tabs instance. |
|
47 private final HashMap<Integer, Tab> mTabs = new HashMap<Integer, Tab>(); |
|
48 |
|
49 private AccountManager mAccountManager; |
|
50 private OnAccountsUpdateListener mAccountListener = null; |
|
51 |
|
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; |
|
61 |
|
62 private static final long PERSIST_TABS_AFTER_MILLISECONDS = 1000 * 5; |
|
63 |
|
64 private static AtomicInteger sTabId = new AtomicInteger(0); |
|
65 private volatile boolean mInitialTabsAdded; |
|
66 |
|
67 private Context mAppContext; |
|
68 private ContentObserver mContentObserver; |
|
69 |
|
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 }; |
|
83 |
|
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"); |
|
109 |
|
110 } |
|
111 |
|
112 public synchronized void attachToContext(Context context) { |
|
113 final Context appContext = context.getApplicationContext(); |
|
114 if (mAppContext == appContext) { |
|
115 return; |
|
116 } |
|
117 |
|
118 if (mAppContext != null) { |
|
119 // This should never happen. |
|
120 Log.w(LOGTAG, "The application context has changed!"); |
|
121 } |
|
122 |
|
123 mAppContext = appContext; |
|
124 mAccountManager = AccountManager.get(appContext); |
|
125 |
|
126 mAccountListener = new OnAccountsUpdateListener() { |
|
127 @Override |
|
128 public void onAccountsUpdated(Account[] accounts) { |
|
129 persistAllTabs(); |
|
130 } |
|
131 }; |
|
132 |
|
133 // The listener will run on the background thread (see 2nd argument). |
|
134 mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false); |
|
135 |
|
136 if (mContentObserver != null) { |
|
137 BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver); |
|
138 } |
|
139 } |
|
140 |
|
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 } |
|
163 |
|
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 } |
|
172 |
|
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 } |
|
187 |
|
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 } |
|
196 |
|
197 // Suppress the ADDED event to prevent animation of tabs created via session restore. |
|
198 if (mInitialTabsAdded) { |
|
199 notifyListeners(tab, TabEvents.ADDED); |
|
200 } |
|
201 |
|
202 return tab; |
|
203 } |
|
204 |
|
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 } |
|
212 |
|
213 public synchronized Tab selectTab(int id) { |
|
214 if (!mTabs.containsKey(id)) |
|
215 return null; |
|
216 |
|
217 final Tab oldTab = getSelectedTab(); |
|
218 final Tab tab = mTabs.get(id); |
|
219 |
|
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 } |
|
225 |
|
226 mSelectedTab = tab; |
|
227 notifyListeners(tab, TabEvents.SELECTED); |
|
228 |
|
229 if (oldTab != null) { |
|
230 notifyListeners(oldTab, TabEvents.UNSELECTED); |
|
231 } |
|
232 |
|
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 } |
|
237 |
|
238 private int getIndexOf(Tab tab) { |
|
239 return mOrder.lastIndexOf(tab); |
|
240 } |
|
241 |
|
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 } |
|
253 |
|
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 } |
|
264 |
|
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 } |
|
276 |
|
277 public boolean isSelectedTab(Tab tab) { |
|
278 return tab != null && tab == mSelectedTab; |
|
279 } |
|
280 |
|
281 public boolean isSelectedTabId(int tabId) { |
|
282 final Tab selected = mSelectedTab; |
|
283 return selected != null && selected.getId() == tabId; |
|
284 } |
|
285 |
|
286 @RobocopTarget |
|
287 public synchronized Tab getTab(int id) { |
|
288 if (id == -1) |
|
289 return null; |
|
290 |
|
291 if (mTabs.size() == 0) |
|
292 return null; |
|
293 |
|
294 if (!mTabs.containsKey(id)) |
|
295 return null; |
|
296 |
|
297 return mTabs.get(id); |
|
298 } |
|
299 |
|
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 } |
|
305 |
|
306 /** Close tab and then select nextTab */ |
|
307 public synchronized void closeTab(final Tab tab, Tab nextTab) { |
|
308 if (tab == null) |
|
309 return; |
|
310 |
|
311 int tabId = tab.getId(); |
|
312 removeTab(tabId); |
|
313 |
|
314 if (nextTab == null) { |
|
315 nextTab = loadUrl(AboutPages.HOME, LOADURL_NEW_TAB); |
|
316 } |
|
317 |
|
318 selectTab(nextTab.getId()); |
|
319 |
|
320 tab.onDestroy(); |
|
321 |
|
322 // Pass a message to Gecko to update tab state in BrowserApp |
|
323 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Closed", String.valueOf(tabId))); |
|
324 } |
|
325 |
|
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; |
|
331 |
|
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 } |
|
345 |
|
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 } |
|
356 |
|
357 public Iterable<Tab> getTabsInOrder() { |
|
358 return mOrder; |
|
359 } |
|
360 |
|
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 } |
|
371 |
|
372 public ContentResolver getContentResolver() { |
|
373 return getAppContext().getContentResolver(); |
|
374 } |
|
375 |
|
376 // Make Tabs a singleton class. |
|
377 private static class TabsInstanceHolder { |
|
378 private static final Tabs INSTANCE = new Tabs(); |
|
379 } |
|
380 |
|
381 @RobocopTarget |
|
382 public static Tabs getInstance() { |
|
383 return Tabs.TabsInstanceHolder.INSTANCE; |
|
384 } |
|
385 |
|
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 } |
|
395 |
|
396 // All other events handled below should contain a tabID property |
|
397 int id = message.getInt("tabID"); |
|
398 Tab tab = getTab(id); |
|
399 |
|
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"); |
|
403 |
|
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")); |
|
414 |
|
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 } |
|
420 |
|
421 if (message.getBoolean("delayLoad")) |
|
422 tab.setState(Tab.STATE_DELAYED); |
|
423 if (message.getBoolean("desktopMode")) |
|
424 tab.setDesktopMode(true); |
|
425 return; |
|
426 } |
|
427 |
|
428 // Tab was already closed; abort |
|
429 if (tab == null) |
|
430 return; |
|
431 |
|
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 } |
|
500 |
|
501 } catch (Exception e) { |
|
502 Log.w(LOGTAG, "handleMessage threw for " + event, e); |
|
503 } |
|
504 } |
|
505 |
|
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 } |
|
523 |
|
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 } |
|
535 |
|
536 public interface OnTabsChangedListener { |
|
537 public void onTabChanged(Tab tab, TabEvents msg, Object data); |
|
538 } |
|
539 |
|
540 private static final List<OnTabsChangedListener> TABS_CHANGED_LISTENERS = new CopyOnWriteArrayList<OnTabsChangedListener>(); |
|
541 |
|
542 public static void registerOnTabsChangedListener(OnTabsChangedListener listener) { |
|
543 TABS_CHANGED_LISTENERS.add(listener); |
|
544 } |
|
545 |
|
546 public static void unregisterOnTabsChangedListener(OnTabsChangedListener listener) { |
|
547 TABS_CHANGED_LISTENERS.remove(listener); |
|
548 } |
|
549 |
|
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 } |
|
574 |
|
575 public void notifyListeners(Tab tab, TabEvents msg) { |
|
576 notifyListeners(tab, msg, ""); |
|
577 } |
|
578 |
|
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 } |
|
584 |
|
585 ThreadUtils.postToUiThread(new Runnable() { |
|
586 @Override |
|
587 public void run() { |
|
588 onTabChanged(tab, msg, data); |
|
589 |
|
590 if (TABS_CHANGED_LISTENERS.isEmpty()) { |
|
591 return; |
|
592 } |
|
593 |
|
594 Iterator<OnTabsChangedListener> items = TABS_CHANGED_LISTENERS.iterator(); |
|
595 while (items.hasNext()) { |
|
596 items.next().onTabChanged(tab, msg, data); |
|
597 } |
|
598 } |
|
599 }); |
|
600 } |
|
601 |
|
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; |
|
610 |
|
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 } |
|
624 |
|
625 // This method persists the current ordered list of tabs in our tabs content provider. |
|
626 public void persistAllTabs() { |
|
627 ThreadUtils.postToBackgroundThread(mPersistTabsRunnable); |
|
628 } |
|
629 |
|
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 } |
|
640 |
|
641 private void registerEventListener(String event) { |
|
642 GeckoAppShell.getEventDispatcher().registerEventListener(event, this); |
|
643 } |
|
644 |
|
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 } |
|
654 |
|
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 } |
|
666 |
|
667 private Tab getFirstTabForUrlHelper(String url, Boolean isPrivate) { |
|
668 if (url == null) { |
|
669 return null; |
|
670 } |
|
671 |
|
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 } |
|
684 |
|
685 return null; |
|
686 } |
|
687 |
|
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 } |
|
697 |
|
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 } |
|
709 |
|
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; |
|
725 |
|
726 // delayLoad implies background tab |
|
727 boolean background = delayLoad || (flags & LOADURL_BACKGROUND) != 0; |
|
728 |
|
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; |
|
734 |
|
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); |
|
745 |
|
746 if ((flags & LOADURL_NEW_TAB) != 0) { |
|
747 int tabId = getNextTabId(); |
|
748 args.put("tabID", tabId); |
|
749 |
|
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; |
|
754 |
|
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 } |
|
761 |
|
762 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tab:Load", args.toString())); |
|
763 |
|
764 if (added == null) { |
|
765 return null; |
|
766 } |
|
767 |
|
768 if (!delayLoad && !background) { |
|
769 selectTab(added.getId()); |
|
770 } |
|
771 |
|
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 } |
|
777 |
|
778 return added; |
|
779 } |
|
780 |
|
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 } |
|
791 |
|
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 } |
|
809 |
|
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 } |
|
818 |
|
819 loadUrl(url, null, parentId, LOADURL_NEW_TAB); |
|
820 } |
|
821 |
|
822 /** |
|
823 * Gets the next tab ID. |
|
824 */ |
|
825 @JNITarget |
|
826 public static int getNextTabId() { |
|
827 return sTabId.getAndIncrement(); |
|
828 } |
|
829 } |