270 lines, 125 LOC, 51 covered (40%)
2 | 1 | /* ***** BEGIN LICENSE BLOCK ***** |
2 | * Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
|
3 | * |
|
4 | * The contents of this file are subject to the Mozilla Public License Version |
|
5 | * 1.1 (the "License"); you may not use this file except in compliance with |
|
6 | * the License. You may obtain a copy of the License at |
|
7 | * http://www.mozilla.org/MPL/ |
|
8 | * |
|
9 | * Software distributed under the License is distributed on an "AS IS" basis, |
|
10 | * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
|
11 | * for the specific language governing rights and limitations under the |
|
12 | * License. |
|
13 | * |
|
14 | * The Original Code is Bookmarks Sync. |
|
15 | * |
|
16 | * The Initial Developer of the Original Code is Mozilla. |
|
17 | * Portions created by the Initial Developer are Copyright (C) 2008 |
|
18 | * the Initial Developer. All Rights Reserved. |
|
19 | * |
|
20 | * Contributor(s): |
|
21 | * Myk Melez <myk@mozilla.org> |
|
22 | * Jono DiCarlo <jdicarlo@mozilla.com> |
|
23 | * |
|
24 | * Alternatively, the contents of this file may be used under the terms of |
|
25 | * either the GNU General Public License Version 2 or later (the "GPL"), or |
|
26 | * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
|
27 | * in which case the provisions of the GPL or the LGPL are applicable instead |
|
28 | * of those above. If you wish to allow use of your version of this file only |
|
29 | * under the terms of either the GPL or the LGPL, and not to allow others to |
|
30 | * use your version of this file under the terms of the MPL, indicate your |
|
31 | * decision by deleting the provisions above and replace them with the notice |
|
32 | * and other provisions required by the GPL or the LGPL. If you do not delete |
|
33 | * the provisions above, a recipient may use your version of this file under |
|
34 | * the terms of any one of the MPL, the GPL or the LGPL. |
|
35 | * |
|
36 | * ***** END LICENSE BLOCK ***** */ |
|
37 | ||
14 | 38 | const EXPORTED_SYMBOLS = ['TabEngine']; |
39 | ||
8 | 40 | const Cc = Components.classes; |
8 | 41 | const Ci = Components.interfaces; |
8 | 42 | const Cu = Components.utils; |
43 | ||
10 | 44 | Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
10 | 45 | Cu.import("resource://weave/util.js"); |
10 | 46 | Cu.import("resource://weave/engines.js"); |
10 | 47 | Cu.import("resource://weave/stores.js"); |
10 | 48 | Cu.import("resource://weave/trackers.js"); |
10 | 49 | Cu.import("resource://weave/type_records/tabs.js"); |
10 | 50 | Cu.import("resource://weave/engines/clients.js"); |
51 | ||
6 | 52 | function TabEngine() { |
12 | 53 | SyncEngine.call(this, "Tabs"); |
54 | ||
55 | // Reset the client on every startup so that we fetch recent tabs |
|
10 | 56 | this._resetClient(); |
57 | } |
|
4 | 58 | TabEngine.prototype = { |
6 | 59 | __proto__: SyncEngine.prototype, |
4 | 60 | _storeObj: TabStore, |
4 | 61 | _trackerObj: TabTracker, |
4 | 62 | _recordObj: TabSetRecord, |
63 | ||
64 | // API for use by Weave UI code to give user choices of tabs to open: |
|
4 | 65 | getAllClients: function TabEngine_getAllClients() { |
66 | return this._store._remoteClients; |
|
67 | }, |
|
68 | ||
4 | 69 | getClientById: function TabEngine_getClientById(id) { |
70 | return this._store._remoteClients[id]; |
|
71 | }, |
|
72 | ||
6 | 73 | _resetClient: function TabEngine__resetClient() { |
14 | 74 | SyncEngine.prototype._resetClient.call(this); |
12 | 75 | this._store.wipe(); |
76 | }, |
|
77 | ||
78 | /* The intent is not to show tabs in the menu if they're already |
|
79 | * open locally. There are a couple ways to interpret this: for |
|
80 | * instance, we could do it by removing a tab from the list when |
|
81 | * you open it -- but then if you close it, you can't get back to |
|
82 | * it. So the way I'm doing it here is to not show a tab in the menu |
|
83 | * if you have a tab open to the same URL, even though this means |
|
84 | * that as soon as you navigate anywhere, the original tab will |
|
85 | * reappear in the menu. |
|
86 | */ |
|
10 | 87 | locallyOpenTabMatchesURL: function TabEngine_localTabMatches(url) { |
88 | return this._store.getAllTabs().some(function(tab) { |
|
89 | return tab.urlHistory[0] == url; |
|
90 | }); |
|
91 | } |
|
92 | }; |
|
93 | ||
94 | ||
6 | 95 | function TabStore(name) { |
14 | 96 | Store.call(this, name); |
97 | } |
|
4 | 98 | TabStore.prototype = { |
6 | 99 | __proto__: Store.prototype, |
100 | ||
4 | 101 | itemExists: function TabStore_itemExists(id) { |
102 | return id == Clients.localID; |
|
103 | }, |
|
104 | ||
4 | 105 | getAllTabs: function getAllTabs(filter) { |
106 | let filteredUrls = new RegExp(Svc.Prefs.get("engine.tabs.filteredUrls"), "i"); |
|
107 | ||
108 | let allTabs = []; |
|
109 | ||
110 | let currentState = JSON.parse(Svc.Session.getBrowserState()); |
|
111 | currentState.windows.forEach(function(window) { |
|
112 | window.tabs.forEach(function(tab) { |
|
113 | // Make sure there are history entries to look at. |
|
114 | if (!tab.entries.length) |
|
115 | return; |
|
116 | // Until we store full or partial history, just grab the current entry. |
|
117 | // index is 1 based, so make sure we adjust. |
|
118 | let entry = tab.entries[tab.index - 1]; |
|
119 | ||
120 | // Filter out some urls if necessary. SessionStore can return empty |
|
121 | // tabs in some cases - easiest thing is to just ignore them for now. |
|
122 | if (!entry.url || filter && filteredUrls.test(entry.url)) |
|
123 | return; |
|
124 | ||
125 | // weaveLastUsed will only be set if the tab was ever selected (or |
|
126 | // opened after Weave was running). So it might not ever be set. |
|
127 | // I think it's also possible that attributes[.image] might not be set |
|
128 | // so handle that as well. |
|
129 | allTabs.push({ |
|
130 | title: entry.title || "", |
|
131 | urlHistory: [entry.url], |
|
132 | icon: tab.attributes && tab.attributes.image || "", |
|
133 | lastUsed: tab.extData && tab.extData.weaveLastUsed || 0 |
|
134 | }); |
|
135 | }); |
|
136 | }); |
|
137 | ||
138 | return allTabs; |
|
139 | }, |
|
140 | ||
4 | 141 | createRecord: function createRecord(guid) { |
142 | let record = new TabSetRecord(); |
|
143 | record.clientName = Clients.localName; |
|
144 | ||
145 | // Sort tabs in descending-used order to grab the most recently used |
|
146 | let tabs = this.getAllTabs(true).sort(function(a, b) { |
|
147 | return b.lastUsed - a.lastUsed; |
|
148 | }); |
|
149 | ||
150 | // Figure out how many tabs we can pack into a payload. Starting with a 28KB |
|
151 | // payload, we can estimate various overheads from encryption/JSON/WBO. |
|
152 | let size = JSON.stringify(tabs).length; |
|
153 | let origLength = tabs.length; |
|
154 | const MAX_TAB_SIZE = 20000; |
|
155 | if (size > MAX_TAB_SIZE) { |
|
156 | // Estimate a little more than the direct fraction to maximize packing |
|
157 | let cutoff = Math.ceil(tabs.length * MAX_TAB_SIZE / size); |
|
158 | tabs = tabs.slice(0, cutoff + 1); |
|
159 | ||
160 | // Keep dropping off the last entry until the data fits |
|
161 | while (JSON.stringify(tabs).length > MAX_TAB_SIZE) |
|
162 | tabs.pop(); |
|
163 | } |
|
164 | ||
165 | this._log.trace("Created tabs " + tabs.length + " of " + origLength); |
|
166 | tabs.forEach(function(tab) { |
|
167 | this._log.trace("Wrapping tab: " + JSON.stringify(tab)); |
|
168 | }, this); |
|
169 | ||
170 | record.tabs = tabs; |
|
171 | return record; |
|
172 | }, |
|
173 | ||
4 | 174 | getAllIDs: function TabStore_getAllIds() { |
175 | let ids = {}; |
|
176 | ids[Clients.localID] = true; |
|
177 | return ids; |
|
178 | }, |
|
179 | ||
6 | 180 | wipe: function TabStore_wipe() { |
10 | 181 | this._remoteClients = {}; |
182 | }, |
|
183 | ||
10 | 184 | create: function TabStore_create(record) { |
185 | this._log.debug("Adding remote tabs from " + record.clientName); |
|
186 | this._remoteClients[record.id] = record.cleartext; |
|
187 | ||
188 | // Lose some precision, but that's good enough (seconds) |
|
189 | let roundModify = Math.floor(record.modified / 1000); |
|
190 | let notifyState = Svc.Prefs.get("notifyTabState"); |
|
191 | // If there's no existing pref, save this first modified time |
|
192 | if (notifyState == null) |
|
193 | Svc.Prefs.set("notifyTabState", roundModify); |
|
194 | // Don't change notifyState if it's already 0 (don't notify) |
|
195 | else if (notifyState == 0) |
|
196 | return; |
|
197 | // We must have gotten a new tab that isn't the same as last time |
|
198 | else if (notifyState != roundModify) |
|
199 | Svc.Prefs.set("notifyTabState", 0); |
|
200 | } |
|
201 | }; |
|
202 | ||
203 | ||
6 | 204 | function TabTracker(name) { |
12 | 205 | Tracker.call(this, name); |
206 | ||
207 | // Make sure "this" pointer is always set correctly for event listeners |
|
14 | 208 | this.onTab = Utils.bind2(this, this.onTab); |
209 | ||
210 | // Register as an observer so we can catch windows opening and closing: |
|
12 | 211 | Svc.WinWatcher.registerNotification(this); |
212 | ||
213 | // Also register listeners on already open windows |
|
12 | 214 | let wins = Svc.WinMediator.getEnumerator("navigator:browser"); |
10 | 215 | while (wins.hasMoreElements()) |
2 | 216 | this._registerListenersForWindow(wins.getNext()); |
217 | } |
|
4 | 218 | TabTracker.prototype = { |
6 | 219 | __proto__: Tracker.prototype, |
220 | ||
20 | 221 | QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]), |
222 | ||
4 | 223 | _registerListenersForWindow: function TabTracker__registerListen(window) { |
224 | this._log.trace("Registering tab listeners in new window"); |
|
225 | ||
226 | // For each topic, add or remove onTab as the listener |
|
227 | let topics = ["pageshow", "TabOpen", "TabClose", "TabSelect"]; |
|
228 | let onTab = this.onTab; |
|
229 | let addRem = function(add) topics.forEach(function(topic) { |
|
230 | window[(add ? "add" : "remove") + "EventListener"](topic, onTab, false); |
|
231 | }); |
|
232 | ||
233 | // Add the listeners now and remove them on unload |
|
234 | addRem(true); |
|
235 | window.addEventListener("unload", function() addRem(false), false); |
|
236 | }, |
|
237 | ||
4 | 238 | observe: function TabTracker_observe(aSubject, aTopic, aData) { |
239 | // Add tab listeners now that a window has opened |
|
240 | if (aTopic == "domwindowopened") { |
|
241 | let self = this; |
|
242 | aSubject.addEventListener("load", function onLoad(event) { |
|
243 | aSubject.removeEventListener("load", onLoad, false); |
|
244 | // Only register after the window is done loading to avoid unloads |
|
245 | self._registerListenersForWindow(aSubject); |
|
246 | }, false); |
|
247 | } |
|
248 | }, |
|
249 | ||
10 | 250 | onTab: function onTab(event) { |
251 | this._log.trace("onTab event: " + event.type); |
|
252 | this.addChangedID(Clients.localID); |
|
253 | ||
254 | // For pageshow events, only give a partial score bump (~.1) |
|
255 | let chance = .1; |
|
256 | ||
257 | // For regular Tab events, do a full score bump and remember when it changed |
|
258 | if (event.type != "pageshow") { |
|
259 | chance = 1; |
|
260 | ||
261 | // Store a timestamp in the tab to track when it was last used |
|
262 | Svc.Session.setTabValue(event.originalTarget, "weaveLastUsed", |
|
263 | Math.floor(Date.now() / 1000)); |
|
264 | } |
|
265 | ||
266 | // Only increase the score by whole numbers, so use random for partial score |
|
267 | if (Math.random() < chance) |
|
268 | this.score++; |
|
269 | }, |
|
2 | 270 | } |