This commit is contained in:
Ryan VanderMeulen 2014-09-05 11:54:57 -04:00
Родитель c78b7345bf 9b5fa0c78c
Коммит 6ca1d53198
113 изменённых файлов: 4647 добавлений и 393 удалений

Просмотреть файл

@ -20,7 +20,6 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&loadError.label;</title>
<link rel="stylesheet" href="chrome://browser/content/aboutneterror/netError.css" type="text/css" media="all" />
<link rel="stylesheet" href="chrome://browser/skin/aboutNetError.css" type="text/css" media="all" />
<!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
toolkit/components/places/src/nsFaviconService.h should be updated. -->

Просмотреть файл

@ -1,69 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@import url("chrome://global/skin/in-content/common.css");
body {
display: flex;
box-sizing: padding-box;
min-height: 100vh;
padding: 0 48px;
align-items: center;
justify-content: center;
}
ul, ol {
margin: 0;
padding: 0;
-moz-margin-start: 1em;
}
ul > li, ol > li {
margin-bottom: .5em;
}
ul {
list-style: disc;
}
#errorPageContainer {
min-width: 320px;
max-width: 512px;
}
#errorTitleText {
background: url("info.svg") left 0 no-repeat;
background-size: 1.2em;
-moz-margin-start: -2em;
-moz-padding-start: 2em;
}
#errorTitleText:-moz-dir(rtl) {
background-position: right 0;
}
#errorTryAgain {
margin-top: 1.2em;
min-width: 150px
}
#errorContainer {
display: none;
}
@media (max-width: 675px) {
#errorTitleText {
padding-top: 0;
background-image: none;
-moz-padding-start: 0;
-moz-margin-start: 0;
}
}
/* Pressing the retry button will cause the cursor to flicker from a pointer to
* not-allowed. Override the disabled cursor behaviour since we will never show
* the button disabled as the initial state. */
button:disabled {
cursor: pointer;
}

Просмотреть файл

@ -125,6 +125,10 @@ let gBrowserThumbnails = {
// FIXME: This should be part of the PageThumbs API. (bug 1062414)
_shouldCapture: function Thumbnails_shouldCapture(aBrowser) {
// Don't try to capture in e10s yet (because of bug 698371)
if (gMultiProcessBrowser)
return false;
// Capture only if it's the currently selected tab.
if (aBrowser != gBrowser.selectedBrowser)
return false;

Просмотреть файл

@ -4,10 +4,22 @@
const Cu = Components.utils;
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-sync/main.js");
Cu.import("resource:///modules/PlacesUIUtils.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
#ifdef MOZ_SERVICES_CLOUDSYNC
XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
"resource://gre/modules/CloudSync.jsm");
#else
let CloudSync = null;
#endif
let RemoteTabViewer = {
_tabsList: null,
@ -16,6 +28,8 @@ let RemoteTabViewer = {
Services.obs.addObserver(this, "weave:service:login:finish", false);
Services.obs.addObserver(this, "weave:engine:sync:finish", false);
Services.obs.addObserver(this, "cloudsync:tabs:update", false);
this._tabsList = document.getElementById("tabsList");
this.buildList(true);
@ -24,63 +38,62 @@ let RemoteTabViewer = {
uninit: function () {
Services.obs.removeObserver(this, "weave:service:login:finish");
Services.obs.removeObserver(this, "weave:engine:sync:finish");
Services.obs.removeObserver(this, "cloudsync:tabs:update");
},
buildList: function(force) {
if (!Weave.Service.isLoggedIn || !this._refetchTabs(force))
return;
//XXXzpao We should say something about not being logged in & not having data
// or tell the appropriate condition. (bug 583344)
this._generateTabList();
},
createItem: function(attrs) {
createItem: function (attrs) {
let item = document.createElement("richlistitem");
// Copy the attributes from the argument into the item
for (let attr in attrs)
// Copy the attributes from the argument into the item.
for (let attr in attrs) {
item.setAttribute(attr, attrs[attr]);
}
if (attrs["type"] == "tab")
if (attrs["type"] == "tab") {
item.label = attrs.title != "" ? attrs.title : attrs.url;
}
return item;
},
filterTabs: function(event) {
filterTabs: function (event) {
let val = event.target.value.toLowerCase();
let numTabs = this._tabsList.getRowCount();
let clientTabs = 0;
let currentClient = null;
for (let i = 0;i < numTabs;i++) {
for (let i = 0; i < numTabs; i++) {
let item = this._tabsList.getItemAtIndex(i);
let hide = false;
if (item.getAttribute("type") == "tab") {
if (!item.getAttribute("url").toLowerCase().contains(val) &&
!item.getAttribute("title").toLowerCase().contains(val))
if (!item.getAttribute("url").toLowerCase().contains(val) &&
!item.getAttribute("title").toLowerCase().contains(val)) {
hide = true;
else
} else {
clientTabs++;
}
}
else if (item.getAttribute("type") == "client") {
if (currentClient) {
if (clientTabs == 0)
if (clientTabs == 0) {
currentClient.hidden = true;
}
}
currentClient = item;
clientTabs = 0;
}
item.hidden = hide;
}
if (clientTabs == 0)
if (clientTabs == 0) {
currentClient.hidden = true;
}
},
openSelected: function() {
openSelected: function () {
let items = this._tabsList.selectedItems;
let urls = [];
for (let i = 0;i < items.length;i++) {
for (let i = 0; i < items.length; i++) {
if (items[i].getAttribute("type") == "tab") {
urls.push(items[i].getAttribute("url"));
let index = this._tabsList.getIndexOfItem(items[i]);
@ -93,7 +106,7 @@ let RemoteTabViewer = {
}
},
bookmarkSingleTab: function() {
bookmarkSingleTab: function () {
let item = this._tabsList.selectedItems[0];
let uri = Weave.Utils.makeURI(item.getAttribute("url"));
let title = item.getAttribute("title");
@ -108,14 +121,15 @@ let RemoteTabViewer = {
}, window.top);
},
bookmarkSelectedTabs: function() {
bookmarkSelectedTabs: function () {
let items = this._tabsList.selectedItems;
let URIs = [];
for (let i = 0;i < items.length;i++) {
for (let i = 0; i < items.length; i++) {
if (items[i].getAttribute("type") == "tab") {
let uri = Weave.Utils.makeURI(items[i].getAttribute("url"));
if (!uri)
if (!uri) {
continue;
}
URIs.push(uri);
}
@ -133,7 +147,7 @@ let RemoteTabViewer = {
try {
let iconURI = Weave.Utils.makeURI(iconUri);
return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
} catch(ex) {
} catch (ex) {
// Do nothing.
}
@ -141,16 +155,58 @@ let RemoteTabViewer = {
return defaultIcon || PlacesUtils.favicons.defaultFavicon.spec;
},
_generateTabList: function() {
let engine = Weave.Service.engineManager.get("tabs");
_waitingForBuildList: false,
_buildListRequested: false,
buildList: function (force) {
if (this._waitingForBuildList) {
this._buildListRequested = true;
return;
}
this._waitingForBuildList = true;
this._buildListRequested = false;
this._clearTabList();
if (Weave.Service.isLoggedIn && this._refetchTabs(force)) {
this._generateWeaveTabList();
} else {
//XXXzpao We should say something about not being logged in & not having data
// or tell the appropriate condition. (bug 583344)
}
function complete() {
this._waitingForBuildList = false;
if (this._buildListRequested) {
CommonUtils.nextTick(this.buildList, this);
}
}
if (CloudSync && CloudSync.ready && CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs()) {
this._generateCloudSyncTabList()
.then(complete, complete);
} else {
complete();
}
},
_clearTabList: function () {
let list = this._tabsList;
// clear out existing richlistitems
// Clear out existing richlistitems.
let count = list.getRowCount();
if (count > 0) {
for (let i = count - 1; i >= 0; i--)
for (let i = count - 1; i >= 0; i--) {
list.removeItemAt(i);
}
}
},
_generateWeaveTabList: function () {
let engine = Weave.Service.engineManager.get("tabs");
let list = this._tabsList;
let seenURLs = new Set();
let localURLs = engine.getOpenURLs();
@ -189,7 +245,37 @@ let RemoteTabViewer = {
}
},
adjustContextMenu: function(event) {
_generateCloudSyncTabList: function () {
let updateTabList = function (remoteTabs) {
let list = this._tabsList;
for each (let client in remoteTabs) {
let clientAttrs = {
type: "client",
clientName: client.name,
};
let clientEnt = this.createItem(clientAttrs);
list.appendChild(clientEnt);
for (let tab of client.tabs) {
let tabAttrs = {
type: "tab",
title: tab.title,
url: tab.url,
icon: this.getIcon(tab.icon),
};
let tabEnt = this.createItem(tabAttrs);
list.appendChild(tabEnt);
}
}
}.bind(this);
return CloudSync().tabs.getRemoteTabs()
.then(updateTabList, Promise.reject);
},
adjustContextMenu: function (event) {
let mode = "all";
switch (this._tabsList.selectedItems.length) {
case 0:
@ -201,33 +287,40 @@ let RemoteTabViewer = {
mode = "multiple";
break;
}
let menu = document.getElementById("tabListContext");
let el = menu.firstChild;
while (el) {
let showFor = el.getAttribute("showFor");
if (showFor)
if (showFor) {
el.hidden = showFor != mode && showFor != "all";
}
el = el.nextSibling;
}
},
_refetchTabs: function(force) {
_refetchTabs: function (force) {
if (!force) {
// Don't bother refetching tabs if we already did so recently
let lastFetch = 0;
try {
lastFetch = Services.prefs.getIntPref("services.sync.lastTabFetch");
}
catch (e) { /* Just use the default value of 0 */ }
catch (e) {
/* Just use the default value of 0 */
}
let now = Math.floor(Date.now() / 1000);
if (now - lastFetch < 30)
if (now - lastFetch < 30) {
return false;
}
}
// if Clients hasn't synced yet this session, need to sync it as well
if (Weave.Service.clientsEngine.lastSync == 0)
// if Clients hasn't synced yet this session, we need to sync it as well.
if (Weave.Service.clientsEngine.lastSync == 0) {
Weave.Service.clientsEngine.sync();
}
// Force a sync only for the tabs engine
let engine = Weave.Service.engineManager.get("tabs");
@ -239,21 +332,26 @@ let RemoteTabViewer = {
return true;
},
observe: function(subject, topic, data) {
observe: function (subject, topic, data) {
switch (topic) {
case "weave:service:login:finish":
this.buildList(true);
break;
case "weave:engine:sync:finish":
if (subject == "tabs")
this._generateTabList();
if (subject == "tabs") {
this.buildList(false);
}
break;
case "cloudsync:tabs:update":
this.buildList(false);
break;
}
},
handleClick: function(event) {
if (event.target.getAttribute("type") != "tab")
handleClick: function (event) {
if (event.target.getAttribute("type") != "tab") {
return;
}
if (event.button == 1) {
let url = event.target.getAttribute("url");

Просмотреть файл

@ -46,9 +46,7 @@ browser.jar:
content/browser/abouthome/restore-large@2x.png (content/abouthome/restore-large@2x.png)
content/browser/abouthome/mozilla@2x.png (content/abouthome/mozilla@2x.png)
content/browser/aboutneterror/netError.xhtml (content/aboutneterror/netError.xhtml)
content/browser/aboutneterror/netError.css (content/aboutneterror/netError.css)
content/browser/aboutneterror/info.svg (content/aboutneterror/info.svg)
content/browser/aboutNetError.xhtml (content/aboutNetError.xhtml)
#ifdef MOZ_SERVICES_HEALTHREPORT
content/browser/abouthealthreport/abouthealth.xhtml (content/abouthealthreport/abouthealth.xhtml)
@ -111,7 +109,7 @@ browser.jar:
content/browser/pageinfo/security.js (content/pageinfo/security.js)
#ifdef MOZ_SERVICES_SYNC
content/browser/sync/aboutSyncTabs.xul (content/sync/aboutSyncTabs.xul)
content/browser/sync/aboutSyncTabs.js (content/sync/aboutSyncTabs.js)
* content/browser/sync/aboutSyncTabs.js (content/sync/aboutSyncTabs.js)
content/browser/sync/aboutSyncTabs.css (content/sync/aboutSyncTabs.css)
content/browser/sync/aboutSyncTabs-bindings.xml (content/sync/aboutSyncTabs-bindings.xml)
* content/browser/sync/setup.xul (content/sync/setup.xul)
@ -175,4 +173,4 @@ browser.jar:
% overlay chrome://browser/content/browser.xul chrome://browser/content/report-phishing-overlay.xul
#endif
% override chrome://global/content/netError.xhtml chrome://browser/content/aboutneterror/netError.xhtml
% override chrome://global/content/netError.xhtml chrome://browser/content/aboutNetError.xhtml

Просмотреть файл

@ -1845,12 +1845,25 @@ let CustomizableUIInternal = {
if (gInBatchStack || !gDirty) {
return;
}
let state = { placements: gPlacements,
// Clone because we want to modify this map:
let state = { placements: new Map(gPlacements),
seen: gSeenWidgets,
dirtyAreaCache: gDirtyAreaCache,
currentVersion: kVersion,
newElementCount: gNewElementCount };
// Merge in previously saved areas if not present in gPlacements.
// This way, state is still persisted for e.g. temporarily disabled
// add-ons - see bug 989338.
if (gSavedState && gSavedState.placements) {
for (let area of Object.keys(gSavedState.placements)) {
if (!state.placements.has(area)) {
let placements = gSavedState.placements[area];
state.placements.set(area, placements);
}
}
}
LOG("Saving state.");
let serialized = JSON.stringify(state, this.serializerHelper);
LOG("State saved as: " + serialized);

Просмотреть файл

@ -104,6 +104,7 @@ skip-if = os == "linux"
[browser_985815_propagate_setToolbarVisibility.js]
[browser_981305_separator_insertion.js]
[browser_988072_sidebar_events.js]
[browser_989338_saved_placements_not_resaved.js]
[browser_989751_subviewbutton_class.js]
[browser_987177_destroyWidget_xul.js]
[browser_987177_xul_wrapper_updating.js]

Просмотреть файл

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const BUTTONID = "test-widget-saved-earlier";
const AREAID = "test-area-saved-earlier";
let hadSavedState;
function test() {
// Hack our way into the module to fake a saved state that isn't there...
let backstagePass = Cu.import("resource:///modules/CustomizableUI.jsm", {});
hadSavedState = backstagePass.gSavedState != null;
if (!hadSavedState) {
backstagePass.gSavedState = {placements: {}};
}
backstagePass.gSavedState.placements[AREAID] = [BUTTONID];
// Put bogus stuff in the saved state for the nav-bar, so as to check the current placements
// override this one...
backstagePass.gSavedState.placements[CustomizableUI.AREA_NAVBAR] = ["bogus-navbar-item"];
backstagePass.gDirty = true;
backstagePass.CustomizableUIInternal.saveState();
let newSavedState = JSON.parse(Services.prefs.getCharPref("browser.uiCustomization.state"));
let savedArea = Array.isArray(newSavedState.placements[AREAID]);
ok(savedArea, "Should have re-saved the state, even though the area isn't registered");
if (savedArea) {
placementArraysEqual(AREAID, newSavedState.placements[AREAID], [BUTTONID]);
}
ok(!backstagePass.gPlacements.has(AREAID), "Placements map shouldn't have been affected");
let savedNavbar = Array.isArray(newSavedState.placements[CustomizableUI.AREA_NAVBAR]);
ok(savedNavbar, "Should have saved nav-bar contents");
if (savedNavbar) {
placementArraysEqual(CustomizableUI.AREA_NAVBAR, newSavedState.placements[CustomizableUI.AREA_NAVBAR],
CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR));
}
};
registerCleanupFunction(function() {
let backstagePass = Cu.import("resource:///modules/CustomizableUI.jsm", {});
if (!hadSavedState) {
backstagePass.gSavedState = null;
} else {
let savedPlacements = backstagePass.gSavedState.placements;
delete savedPlacements[AREAID];
let realNavBarPlacements = CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR);
savedPlacements[CustomizableUI.AREA_NAVBAR] = realNavBarPlacements;
}
backstagePass.gDirty = true;
backstagePass.CustomizableUIInternal.saveState();
});

Просмотреть файл

@ -19,6 +19,13 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
#ifdef MOZ_SERVICES_CLOUDSYNC
XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
"resource://gre/modules/CloudSync.jsm");
#else
let CloudSync = null;
#endif
#ifdef MOZ_SERVICES_SYNC
XPCOMUtils.defineLazyModuleGetter(this, "Weave",
"resource://services-sync/main.js");
@ -569,7 +576,7 @@ this.PlacesUIUtils = {
var uriList = PlacesUtils.toISupportsString(urls.join("|"));
var args = Cc["@mozilla.org/supports-array;1"].
createInstance(Ci.nsISupportsArray);
args.AppendElement(uriList);
args.AppendElement(uriList);
browserWindow = Services.ww.openWindow(aWindow,
"chrome://browser/content/browser.xul",
null, "chrome,dialog=no,all", args);
@ -1002,17 +1009,18 @@ this.PlacesUIUtils = {
},
shouldShowTabsFromOtherComputersMenuitem: function() {
// If Sync isn't configured yet, then don't show the menuitem.
return Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
Weave.Svc.Prefs.get("firstSync", "") != "notReady";
let weaveOK = Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED &&
Weave.Svc.Prefs.get("firstSync", "") != "notReady";
let cloudSyncOK = CloudSync && CloudSync.ready && CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs();
return weaveOK || cloudSyncOK;
},
shouldEnableTabsFromOtherComputersMenuitem: function() {
// The tabs engine might never be inited (if services.sync.registerEngines
// is modified), so make sure we avoid undefined errors.
return Weave.Service.isLoggedIn &&
Weave.Service.engineManager.get("tabs") &&
Weave.Service.engineManager.get("tabs").enabled;
let weaveEnabled = Weave.Service.isLoggedIn &&
Weave.Service.engineManager.get("tabs") &&
Weave.Service.engineManager.get("tabs").enabled;
let cloudSyncEnabled = CloudSync && CloudSync.ready && CloudSync().tabsReady && CloudSync().tabs.hasRemoteTabs();
return weaveEnabled || cloudSyncEnabled;
},
};

Просмотреть файл

@ -3,6 +3,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/TelemetryStopwatch.jsm");
var gHistoryTree;
var gSearchBox;
var gHistoryGrouping = "";
@ -79,10 +81,16 @@ function searchHistory(aInput)
options.resultType = resultType;
options.includeHidden = !!aInput;
if (gHistoryGrouping == "lastvisited")
this.TelemetryStopwatch.start("HISTORY_LASTVISITED_TREE_QUERY_TIME_MS");
// call load() on the tree manually
// instead of setting the place attribute in history-panel.xul
// otherwise, we will end up calling load() twice
gHistoryTree.load([query], options);
if (gHistoryGrouping == "lastvisited")
this.TelemetryStopwatch.finish("HISTORY_LASTVISITED_TREE_QUERY_TIME_MS");
}
window.addEventListener("SidebarFocused",

Просмотреть файл

@ -4,6 +4,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MigrationUtils",
"resource:///modules/MigrationUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
@ -16,6 +17,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
"resource://gre/modules/DownloadUtils.jsm");
const RESTORE_FILEPICKER_FILTER_EXT = "*.json;*.jsonlz4";
const HISTORY_LIBRARY_SEARCH_TELEMETRY = "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS";
var PlacesOrganizer = {
_places: null,
@ -855,7 +857,9 @@ var PlacesSearchBox = {
currentView.load([query], options);
}
else {
TelemetryStopwatch.start(HISTORY_LIBRARY_SEARCH_TELEMETRY);
currentView.applyFilter(filterString, null, true);
TelemetryStopwatch.finish(HISTORY_LIBRARY_SEARCH_TELEMETRY);
}
break;
case "downloads":

Просмотреть файл

@ -5,7 +5,7 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
const DBG_XUL = "chrome://browser/content/devtools/framework/toolbox-process-window.xul";
const CHROME_DEBUGGER_PROFILE_NAME = "chrome_debugger_profile";

Просмотреть файл

@ -1520,7 +1520,7 @@ Toolbox.prototype = {
this._telemetry.toolClosed("toolbox");
this._telemetry.destroy();
return this._destroyer = promise.all(outstanding).then(() => {
this._destroyer = promise.all(outstanding).then(() => {
// Targets need to be notified that the toolbox is being torn down.
// This is done after other destruction tasks since it may tear down
// fronts and the debugger transport which earlier destroy methods may
@ -1552,6 +1552,20 @@ Toolbox.prototype = {
.garbageCollect();
}
}).then(null, console.error);
let leakCheckObserver = ({wrappedJSObject: barrier}) => {
// Make the leak detector wait until this toolbox is properly destroyed.
barrier.client.addBlocker("DevTools: Wait until toolbox is destroyed",
this._destroyer);
};
let topic = "shutdown-leaks-before-check";
Services.obs.addObserver(leakCheckObserver, topic, false);
this._destroyer.then(() => {
Services.obs.removeObserver(leakCheckObserver, topic);
});
return this._destroyer;
},
_highlighterReady: function() {

Просмотреть файл

@ -593,7 +593,7 @@ Column.prototype = {
* Selects the row at the `index` index
*/
selectRowAt: function(index) {
if (this.selectedRow) {
if (this.selectedRow != null) {
this.cells[this.items[this.selectedRow]].toggleClass("theme-selected");
}
if (index < 0) {
@ -930,7 +930,7 @@ Cell.prototype = {
set value(value) {
this._value = value;
if (!value) {
if (value == null) {
this.label.setAttribute("value", "");
return;
}

Просмотреть файл

@ -1785,6 +1785,12 @@ Messages.ConsoleTable.prototype = Heritage.extend(Messages.Extended.prototype,
this._columns["_index"] = l10n.getStr("table.index");
}
if (data.class == "Array") {
if (index == parseInt(index)) {
index = parseInt(index);
}
}
let property = ownProperties[index].value;
let item = { _index: index };
@ -1832,10 +1838,9 @@ Messages.ConsoleTable.prototype = Heritage.extend(Messages.Extended.prototype,
}
let rowCount = 0;
for (let index of Object.keys(entries || {})) {
let [key, value] = entries[index];
for (let [key, value] of entries) {
let item = {
_index: index,
_index: rowCount,
_key: this._renderValueGrip(key, { concise: true }),
_value: this._renderValueGrip(value, { concise: true })
};
@ -1857,11 +1862,10 @@ Messages.ConsoleTable.prototype = Heritage.extend(Messages.Extended.prototype,
}
let rowCount = 0;
for (let index of Object.keys(entries || {})) {
let value = entries[index];
for (let entry of entries) {
let item = {
_index : index,
_value: this._renderValueGrip(value, { concise: true })
_index : rowCount,
_value: this._renderValueGrip(entry, { concise: true })
};
this._data.push(item);

Просмотреть файл

@ -12,27 +12,27 @@ const TEST_DATA = [
{
command: "console.table(languages1)",
data: [
{ _index: "0", name: "\"JavaScript\"", fileExtension: "Array[1]" },
{ _index: "1", name: "Object", fileExtension: "\".ts\"" },
{ _index: "2", name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
{ _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
{ _index: 1, name: "Object", fileExtension: "\".ts\"" },
{ _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
],
columns: { _index: "(index)", name: "name", fileExtension: "fileExtension" }
},
{
command: "console.table(languages1, 'name')",
data: [
{ _index: "0", name: "\"JavaScript\"", fileExtension: "Array[1]" },
{ _index: "1", name: "Object", fileExtension: "\".ts\"" },
{ _index: "2", name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
{ _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
{ _index: 1, name: "Object", fileExtension: "\".ts\"" },
{ _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
],
columns: { _index: "(index)", name: "name" }
},
{
command: "console.table(languages1, ['name'])",
data: [
{ _index: "0", name: "\"JavaScript\"", fileExtension: "Array[1]" },
{ _index: "1", name: "Object", fileExtension: "\".ts\"" },
{ _index: "2", name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
{ _index: 0, name: "\"JavaScript\"", fileExtension: "Array[1]" },
{ _index: 1, name: "Object", fileExtension: "\".ts\"" },
{ _index: 2, name: "\"CoffeeScript\"", fileExtension: "\".coffee\"" }
],
columns: { _index: "(index)", name: "name" }
},
@ -47,8 +47,8 @@ const TEST_DATA = [
{
command: "console.table([[1, 2], [3, 4]])",
data: [
{ _index: "0", 0: "1", 1: "2" },
{ _index: "1", 0: "3", 1: "4" }
{ _index: 0, 0: "1", 1: "2" },
{ _index: 1, 0: "3", 1: "4" }
],
columns: { _index: "(index)", 0: "0", 1: "1" }
},
@ -93,19 +93,19 @@ const TEST_DATA = [
{
command: "console.table(mySet)",
data: [
{ _index: "0", _value: "1" },
{ _index: "1", _value: "5" },
{ _index: "2", _value: "\"some text\"" },
{ _index: "3", _value: "null" },
{ _index: "4", _value: "undefined" }
{ _index: 0, _value: "1" },
{ _index: 1, _value: "5" },
{ _index: 2, _value: "\"some text\"" },
{ _index: 3, _value: "null" },
{ _index: 4, _value: "undefined" }
],
columns: { _index: "(iteration index)", _value: "Values" }
},
{
command: "console.table(myMap)",
data: [
{ _index: "0", _key: "\"a string\"", _value: "\"value associated with 'a string'\"" },
{ _index: "1", _key: "5", _value: "\"value associated with 5\"" },
{ _index: 0, _key: "\"a string\"", _value: "\"value associated with 'a string'\"" },
{ _index: 1, _key: "5", _value: "\"value associated with 5\"" },
],
columns: { _index: "(iteration index)", _key: "Key", _value: "Values" }
}

Просмотреть файл

@ -87,21 +87,30 @@ this.ContentSearch = {
// { controller, previousFormHistoryResult }. See _onMessageGetSuggestions.
_suggestionMap: new WeakMap(),
// Resolved when we finish shutting down.
_destroyedPromise: null,
init: function () {
Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager).
addMessageListener(INBOUND_MESSAGE, this);
Services.obs.addObserver(this, "browser-search-engine-modified", false);
Services.obs.addObserver(this, "shutdown-leaks-before-check", false);
},
destroy: function () {
if (this._destroyedPromise) {
return this._destroyedPromise;
}
Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager).
removeMessageListener(INBOUND_MESSAGE, this);
Services.obs.removeObserver(this, "browser-search-engine-modified");
Services.obs.removeObserver(this, "shutdown-leaks-before-check");
this._eventQueue.length = 0;
return Promise.resolve(this._currentEventPromise);
return this._destroyedPromise = Promise.resolve(this._currentEventPromise);
},
/**
@ -148,6 +157,10 @@ this.ContentSearch = {
});
this._processEventQueue();
break;
case "shutdown-leaks-before-check":
subj.wrappedJSObject.client.addBlocker(
"ContentSearch: Wait until the service is destroyed", () => this.destroy());
break;
}
},

Просмотреть файл

@ -14,6 +14,7 @@ browser.jar:
skin/classic/browser/aboutCertError_sectionCollapsed-rtl.png
skin/classic/browser/aboutCertError_sectionExpanded.png
skin/classic/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
skin/classic/browser/aboutSocialError.css
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/aboutSyncTabs.css

Просмотреть файл

@ -6,6 +6,7 @@ browser.jar:
% skin browser classic/1.0 %skin/classic/browser/
skin/classic/browser/sanitizeDialog.css (sanitizeDialog.css)
skin/classic/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
* skin/classic/browser/aboutSessionRestore.css (aboutSessionRestore.css)
skin/classic/browser/aboutSessionRestore-window-icon.png
skin/classic/browser/aboutWelcomeBack.css (../shared/aboutWelcomeBack.css)

Просмотреть файл

@ -1 +1,69 @@
/* This deliberately left empty for themes to use/override. */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@import url("chrome://global/skin/in-content/common.css");
body {
display: flex;
box-sizing: padding-box;
min-height: 100vh;
padding: 0 48px;
align-items: center;
justify-content: center;
}
ul, ol {
margin: 0;
padding: 0;
-moz-margin-start: 1em;
}
ul > li, ol > li {
margin-bottom: .5em;
}
ul {
list-style: disc;
}
#errorPageContainer {
min-width: 320px;
max-width: 512px;
}
#errorTitleText {
background: url("aboutNetError_info.svg") left 0 no-repeat;
background-size: 1.2em;
-moz-margin-start: -2em;
-moz-padding-start: 2em;
}
#errorTitleText:-moz-dir(rtl) {
background-position: right 0;
}
#errorTryAgain {
margin-top: 1.2em;
min-width: 150px
}
#errorContainer {
display: none;
}
@media (max-width: 675px) {
#errorTitleText {
padding-top: 0;
background-image: none;
-moz-padding-start: 0;
-moz-margin-start: 0;
}
}
/* Pressing the retry button will cause the cursor to flicker from a pointer to
* not-allowed. Override the disabled cursor behaviour since we will never show
* the button disabled as the initial state. */
button:disabled {
cursor: pointer;
}

Просмотреть файл

До

Ширина:  |  Высота:  |  Размер: 348 B

После

Ширина:  |  Высота:  |  Размер: 348 B

Просмотреть файл

@ -16,6 +16,7 @@ browser.jar:
skin/classic/browser/aboutCertError_sectionCollapsed-rtl.png
skin/classic/browser/aboutCertError_sectionExpanded.png
skin/classic/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
skin/classic/browser/aboutSocialError.css
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/aboutSyncTabs.css
@ -430,6 +431,7 @@ browser.jar:
skin/classic/aero/browser/aboutCertError_sectionCollapsed-rtl.png
skin/classic/aero/browser/aboutCertError_sectionExpanded.png
skin/classic/aero/browser/aboutNetError.css (../shared/aboutNetError.css)
skin/classic/aero/browser/aboutNetError_info.svg (../shared/aboutNetError_info.svg)
skin/classic/aero/browser/aboutSocialError.css
#ifdef MOZ_SERVICES_SYNC
skin/classic/aero/browser/aboutSyncTabs.css

Просмотреть файл

@ -250,6 +250,45 @@ AC_SUBST([STLPORT_LIBS])
])
AC_DEFUN([MOZ_ANDROID_GOOGLE_PLAY_SERVICES],
[
if test -n "$MOZ_NATIVE_DEVICES" ; then
AC_SUBST(MOZ_NATIVE_DEVICES)
AC_MSG_CHECKING([for google play services])
GOOGLE_PLAY_SERVICES_LIB="${ANDROID_SDK_ROOT}/extras/google/google_play_services/libproject/google-play-services_lib/libs/google-play-services.jar"
GOOGLE_PLAY_SERVICES_RES="${ANDROID_SDK_ROOT}/extras/google/google_play_services/libproject/google-play-services_lib/res"
AC_SUBST(GOOGLE_PLAY_SERVICES_LIB)
AC_SUBST(GOOGLE_PLAY_SERVICES_RES)
if ! test -e $GOOGLE_PLAY_SERVICES_LIB ; then
AC_MSG_ERROR([You must download Google Play Services to build with native video casting support enabled. Run the Android SDK tool and install Google Play Services under Extras. See http://developer.android.com/google/play-services/setup.html for more info. (looked for $GOOGLE_PLAY_SERVICES_LIB) ])
fi
AC_MSG_RESULT([$GOOGLE_PLAY_SERVICES_LIB])
ANDROID_APPCOMPAT_LIB="$ANDROID_COMPAT_DIR_BASE/v7/appcompat/libs/android-support-v7-appcompat.jar"
ANDROID_APPCOMPAT_RES="$ANDROID_COMPAT_DIR_BASE/v7/appcompat/res"
AC_MSG_CHECKING([for v7 appcompat library])
if ! test -e $ANDROID_APPCOMPAT_LIB ; then
AC_MSG_ERROR([You must download the v7 app compat Android support library when targeting Android with native video casting support enabled. Run the Android SDK tool and install Android Support Library under Extras. See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_APPCOMPAT_LIB)])
fi
AC_MSG_RESULT([$ANDROID_APPCOMPAT_LIB])
AC_SUBST(ANDROID_APPCOMPAT_LIB)
AC_SUBST(ANDROID_APPCOMPAT_RES)
ANDROID_MEDIAROUTER_LIB="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/libs/android-support-v7-mediarouter.jar"
ANDROID_MEDIAROUTER_RES="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/res"
AC_MSG_CHECKING([for v7 mediarouter library])
if ! test -e $ANDROID_MEDIAROUTER_LIB ; then
AC_MSG_ERROR([You must download the v7 media router Android support library when targeting Android with native video casting support enabled. Run the Android SDK tool and install Android Support Library under Extras. See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_MEDIAROUTER_LIB)])
fi
AC_MSG_RESULT([$ANDROID_MEDIAROUTER_LIB])
AC_SUBST(ANDROID_MEDIAROUTER_LIB)
AC_SUBST(ANDROID_MEDIAROUTER_RES)
fi
])
AC_DEFUN([MOZ_ANDROID_SDK],
[
@ -347,40 +386,6 @@ case "$target" in
fi
AC_MSG_RESULT([$ANDROID_COMPAT_LIB])
if test -n "$MOZ_NATIVE_DEVICES" ; then
AC_SUBST(MOZ_NATIVE_DEVICES)
AC_MSG_CHECKING([for google play services])
GOOGLE_PLAY_SERVICES_LIB="${ANDROID_SDK_ROOT}/extras/google/google_play_services/libproject/google-play-services_lib/libs/google-play-services.jar"
GOOGLE_PLAY_SERVICES_RES="${ANDROID_SDK_ROOT}/extras/google/google_play_services/libproject/google-play-services_lib/res"
AC_SUBST(GOOGLE_PLAY_SERVICES_LIB)
AC_SUBST(GOOGLE_PLAY_SERVICES_RES)
if ! test -e $GOOGLE_PLAY_SERVICES_LIB ; then
AC_MSG_ERROR([You must download Google Play Services to build with native video casting support enabled. Run the Android SDK tool and install Google Play Services under Extras. See http://developer.android.com/google/play-services/setup.html for more info. (looked for $GOOGLE_PLAY_SERVICES_LIB) ])
fi
AC_MSG_RESULT([$GOOGLE_PLAY_SERVICES_LIB])
ANDROID_APPCOMPAT_LIB="$ANDROID_COMPAT_DIR_BASE/v7/appcompat/libs/android-support-v7-appcompat.jar"
ANDROID_APPCOMPAT_RES="$ANDROID_COMPAT_DIR_BASE/v7/appcompat/res"
AC_MSG_CHECKING([for v7 appcompat library])
if ! test -e $ANDROID_APPCOMPAT_LIB ; then
AC_MSG_ERROR([You must download the v7 app compat Android support library when targeting Android with native video casting support enabled. Run the Android SDK tool and install Android Support Library under Extras. See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_APPCOMPAT_LIB)])
fi
AC_MSG_RESULT([$ANDROID_APPCOMPAT_LIB])
AC_SUBST(ANDROID_APPCOMPAT_LIB)
AC_SUBST(ANDROID_APPCOMPAT_RES)
ANDROID_MEDIAROUTER_LIB="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/libs/android-support-v7-mediarouter.jar"
ANDROID_MEDIAROUTER_RES="$ANDROID_COMPAT_DIR_BASE/v7/mediarouter/res"
AC_MSG_CHECKING([for v7 mediarouter library])
if ! test -e $ANDROID_MEDIAROUTER_LIB ; then
AC_MSG_ERROR([You must download the v7 media router Android support library when targeting Android with native video casting support enabled. Run the Android SDK tool and install Android Support Library under Extras. See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_MEDIAROUTER_LIB)])
fi
AC_MSG_RESULT([$ANDROID_MEDIAROUTER_LIB])
AC_SUBST(ANDROID_MEDIAROUTER_LIB)
AC_SUBST(ANDROID_MEDIAROUTER_RES)
fi
dnl Google has a history of moving the Android tools around. We don't
dnl care where they are, so let's try to find them anywhere we can.
ALL_ANDROID_TOOLS_PATHS="$ANDROID_TOOLS:$ANDROID_BUILD_TOOLS:$ANDROID_PLATFORM_TOOLS"

Просмотреть файл

@ -4599,6 +4599,14 @@ AC_DEFINE_UNQUOTED(MOZ_DISTRIBUTION_ID,"$MOZ_DISTRIBUTION_ID")
AC_SUBST(MOZ_DISTRIBUTION_ID)
dnl ========================================================
dnl Google Play Services, placed here so it can depend on
dnl values set by configure.sh above.
dnl ========================================================
MOZ_ANDROID_GOOGLE_PLAY_SERVICES
dnl ========================================================
dnl = Pango
dnl ========================================================
@ -8343,6 +8351,12 @@ if test -n "$MOZ_SERVICES_SYNC"; then
AC_DEFINE(MOZ_SERVICES_SYNC)
fi
dnl Build Services/CloudSync if required
AC_SUBST(MOZ_SERVICES_CLOUDSYNC)
if test -n "$MOZ_SERVICES_CLOUDSYNC"; then
AC_DEFINE(MOZ_SERVICES_CLOUDSYNC)
fi
dnl Build Captive Portal Detector if required
AC_SUBST(MOZ_CAPTIVEDETECT)
if test -n "$MOZ_CAPTIVEDETECT"; then

Просмотреть файл

@ -82,12 +82,11 @@ public:
*/
static TemporaryRef<CompositingRenderTargetOGL>
RenderTargetForWindow(CompositorOGL* aCompositor,
const gfx::IntPoint& aOrigin,
const gfx::IntSize& aSize,
const gfx::Matrix& aTransform)
{
RefPtr<CompositingRenderTargetOGL> result
= new CompositingRenderTargetOGL(aCompositor, aOrigin, 0, 0);
= new CompositingRenderTargetOGL(aCompositor, gfx::IntPoint(0, 0), 0, 0);
result->mTransform = aTransform;
result->mInitParams = InitParams(aSize, 0, INIT_MODE_NONE);
result->mInitParams.mStatus = InitParams::INITIALIZED;

Просмотреть файл

@ -596,6 +596,10 @@ CompositorOGL::PrepareViewport(const gfx::IntSize& aSize,
viewMatrix.Scale(1.0f, -1.0f);
}
if (!mTarget) {
viewMatrix.Translate(mRenderOffset.x, mRenderOffset.y);
}
viewMatrix = aWorldTransform * viewMatrix;
Matrix4x4 matrix3d = Matrix4x4::From2D(viewMatrix);
@ -761,17 +765,8 @@ CompositorOGL::BeginFrame(const nsIntRegion& aInvalidRegion,
TexturePoolOGL::Fill(gl());
#endif
// Make sure the render offset is respected. We ignore this when we have a
// target to stop tests failing - this is only used by the Android browser
// UI for its dynamic toolbar.
IntPoint origin;
if (!mTarget) {
origin = -TruncatedToInt(mRenderOffset.ToUnknownPoint());
}
mCurrentRenderTarget =
CompositingRenderTargetOGL::RenderTargetForWindow(this,
origin,
IntSize(width, height),
aTransform);
mCurrentRenderTarget->BindRenderTarget();
@ -1030,11 +1025,12 @@ CompositorOGL::DrawQuad(const Rect& aRect,
MOZ_ASSERT(mFrameInProgress, "frame not started");
IntRect intClipRect;
aClipRect.ToIntRect(&intClipRect);
Rect clipRect = aClipRect;
if (!mTarget) {
intClipRect.MoveBy(mRenderOffset.x, mRenderOffset.y);
clipRect.MoveBy(mRenderOffset.x, mRenderOffset.y);
}
IntRect intClipRect;
clipRect.ToIntRect(&intClipRect);
gl()->fScissor(intClipRect.x, FlipY(intClipRect.y + intClipRect.height),
intClipRect.width, intClipRect.height);

Просмотреть файл

@ -10,7 +10,6 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
SOURCES += [
'MozillaRuntimeMainAndroid.cpp',
]
FINAL_TARGET = 'dist/bin/lib'
else:
SOURCES += [
'MozillaRuntimeMain.cpp',

Просмотреть файл

@ -483,7 +483,7 @@ pref("plugin.default.state", 1);
pref("breakpad.reportURL", "https://crash-stats.mozilla.com/report/index/");
pref("app.support.baseURL", "http://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/");
// Used to submit data to input from about:feedback
pref("app.feedback.postURL", "https://input.mozilla.org/%LOCALE%/feedback");
pref("app.feedback.postURL", "https://input.mozilla.org/api/v1/feedback/");
pref("app.privacyURL", "https://www.mozilla.org/privacy/firefox/");
pref("app.creditsURL", "http://www.mozilla.org/credits/");
pref("app.channelURL", "http://www.mozilla.org/%LOCALE%/firefox/channel/");

Просмотреть файл

@ -1,41 +1,84 @@
package org.mozilla.gecko;
public class Assert
{
/**
* Static helper class to provide debug assertions for Java.
*
* Used in preference to JSR 41 assertions due to their difficulty of use on Android and their
* poor behaviour w.r.t bytecode bloat (and to a lesser extent, runtime performance when disabled)
*
* Calls to methods in this class will be stripped by Proguard for release builds, so may be used
* arbitrarily liberally at zero cost.
* Under no circumstances should the argument expressions to methods in this class have side effects
* relevant to the correctness of execution of the program. Such side effects shall not be checked
* for when stripping assertions.
*/
public class Assert {
// Static helper class.
private Assert() {}
public static void equals(Object a, Object b)
{
equals(a, b, null);
/**
* Verify that two objects are equal according to their equals method.
*/
public static void equal(Object a, Object b) {
equal(a, b, "Assertion failure: !" + a + ".equals(" + b + ')');
}
public static void equal(Object a, Object b, String message) {
isTrue(a.equals(b), message);
}
public static void equals(Object a, Object b, String message)
{
Assert.isTrue(a.equals(b), message);
}
public static void isTrue(boolean a)
{
/**
* Verify that an arbitrary boolean expression is true.
*/
public static void isTrue(boolean a) {
isTrue(a, null);
}
public static void isTrue(boolean a, String message)
{
if (!AppConstants.DEBUG_BUILD) {
return;
}
public static void isTrue(boolean a, String message) {
if (!a) {
throw new AssertException(message);
throw new AssertionError(message);
}
}
public static class AssertException extends RuntimeException
{
private static final long serialVersionUID = 0L;
public AssertException(String message) {
super(message);
/**
* Verify that an arbitrary boolean expression is false.
*/
public static void isFalse(boolean a) {
isTrue(a, null);
}
public static void isFalse(boolean a, String message) {
if (a) {
throw new AssertionError(message);
}
}
}
/**
* Verify that a given object is null.
*/
public static void isNull(Object o) {
isNull(o, "Assertion failure: " + o + " must be null!");
}
public static void isNull(Object o, String message) {
isTrue(o == null, message);
}
/**
* Verify that a given object is non-null.
*/
public static void isNotNull(Object o) {
isNotNull(o, "Assertion failure: " + o + " cannot be null!");
}
public static void isNotNull(Object o, String message) {
isTrue(o != null, message);
}
/**
* Fail. Should be used whenever an impossible state is encountered (such as the default branch
* of a switch over all possible values of an enum: such an assertion may save future developers
* time when they try to add new states)
*/
public static void fail() {
isTrue(false);
}
public static void fail(String message) {
isTrue(false, message);
}
}

Просмотреть файл

@ -16,6 +16,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
/**
@ -106,6 +107,18 @@ public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter {
final long now = System.currentTimeMillis();
lastModifiedView.setText(TabsAccessor.getLastSyncedString(context, now, client.lastModified));
// This view exists only in some of our group views: it's present
// for the home panel groups and not for the tabs tray groups.
// Therefore, we must handle null.
final ImageView deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
if (deviceTypeView != null) {
if ("desktop".equals(client.deviceType)) {
deviceTypeView.setBackgroundResource(R.drawable.sync_desktop);
} else {
deviceTypeView.setBackgroundResource(R.drawable.sync_mobile);
}
}
return view;
}

Просмотреть файл

@ -8,8 +8,8 @@ package org.mozilla.gecko.home;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.R;
import org.mozilla.gecko.home.RemoteTabsPanel;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -39,6 +39,7 @@ public final class HomeConfig {
TOP_SITES("top_sites", TopSitesPanel.class),
BOOKMARKS("bookmarks", BookmarksPanel.class),
HISTORY("history", HistoryPanel.class),
REMOTE_TABS("remote_tabs", RemoteTabsPanel.class),
READING_LIST("reading_list", ReadingListPanel.class),
RECENT_TABS("recent_tabs", RecentTabsPanel.class),
DYNAMIC("dynamic", DynamicPanel.class);
@ -1495,6 +1496,7 @@ public final class HomeConfig {
private static final String READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";
private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8";
private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0";
private final HomeConfigBackend mBackend;
@ -1545,6 +1547,11 @@ public final class HomeConfig {
id = HISTORY_PANEL_ID;
break;
case REMOTE_TABS:
titleId = R.string.home_remote_tabs_title;
id = REMOTE_TABS_PANEL_ID;
break;
case READING_LIST:
titleId = R.string.reading_list_title;
id = READING_LIST_PANEL_ID;

Просмотреть файл

@ -9,7 +9,6 @@ import static org.mozilla.gecko.home.HomeConfig.createBuiltinPanelConfig;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import org.json.JSONArray;
@ -28,7 +27,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
@ -37,7 +35,7 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
private static final String LOGTAG = "GeckoHomeConfigBackend";
// Increment this to trigger a migration.
private static final int VERSION = 1;
private static final int VERSION = 2;
// This key was originally used to store only an array of panel configs.
private static final String PREFS_CONFIG_KEY_OLD = "home_panels";
@ -83,15 +81,18 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
final PanelConfig historyEntry = createBuiltinPanelConfig(mContext, PanelType.HISTORY);
final PanelConfig recentTabsEntry = createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS);
final PanelConfig remoteTabsEntry = createBuiltinPanelConfig(mContext, PanelType.REMOTE_TABS);
// On tablets, the history panel is the last.
// On phones, the history panel is the first one.
// On tablets, we go [...|History|Recent Tabs|Synced Tabs].
// On phones, we go [Synced Tabs|Recent Tabs|History|...].
if (HardwareUtils.isTablet()) {
panelConfigs.add(historyEntry);
panelConfigs.add(recentTabsEntry);
panelConfigs.add(remoteTabsEntry);
} else {
panelConfigs.add(0, historyEntry);
panelConfigs.add(0, recentTabsEntry);
panelConfigs.add(0, remoteTabsEntry);
}
return new State(panelConfigs, true);
@ -113,6 +114,53 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
return true;
}
protected enum Position {
NONE, // Not present.
FRONT, // At the front of the list of panels.
BACK, // At the back of the list of panels.
}
/**
* Create and insert a built-in panel configuration.
*
* @param context Android context.
* @param jsonPanels array of JSON panels to update in place.
* @param panelType to add.
* @param positionOnPhones where to place the new panel on phones.
* @param positionOnTablets where to place the new panel on tablets.
* @throws JSONException
*/
protected static void addBuiltinPanelConfig(Context context, JSONArray jsonPanels,
PanelType panelType, Position positionOnPhones, Position positionOnTablets) throws JSONException {
// Add the new panel.
final JSONObject jsonPanelConfig =
createBuiltinPanelConfig(context, panelType).toJSON();
// If any panel is enabled, then we should make the new panel enabled.
jsonPanelConfig.put(PanelConfig.JSON_KEY_DISABLED,
allPanelsAreDisabled(jsonPanels));
final boolean isTablet = HardwareUtils.isTablet();
final boolean isPhone = !isTablet;
// Maybe add the new panel to the front of the array.
if ((isPhone && positionOnPhones == Position.FRONT) ||
(isTablet && positionOnTablets == Position.FRONT)) {
// This is an inefficient way to stretch [a, b, c] to [a, a, b, c].
for (int i = jsonPanels.length(); i >= 1; i--) {
jsonPanels.put(i, jsonPanels.get(i - 1));
}
// And this inserts [d, a, b, c].
jsonPanels.put(0, jsonPanelConfig);
}
// Maybe add the new panel to the back of the array.
if ((isPhone && positionOnPhones == Position.BACK) ||
(isTablet && positionOnTablets == Position.BACK)) {
jsonPanels.put(jsonPanelConfig);
}
}
/**
* Migrates JSON config data storage.
*
@ -131,27 +179,26 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
// Make sure we only do this version check once.
sMigrationDone = true;
final JSONArray originalJsonPanels;
final JSONArray jsonPanels;
final int version;
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(context);
if (prefs.contains(PREFS_CONFIG_KEY_OLD)) {
// Our original implementation did not contain versioning, so this is implicitly version 0.
originalJsonPanels = new JSONArray(jsonString);
jsonPanels = new JSONArray(jsonString);
version = 0;
} else {
final JSONObject json = new JSONObject(jsonString);
originalJsonPanels = json.getJSONArray(JSON_KEY_PANELS);
jsonPanels = json.getJSONArray(JSON_KEY_PANELS);
version = json.getInt(JSON_KEY_VERSION);
}
if (version == VERSION) {
return originalJsonPanels;
return jsonPanels;
}
Log.d(LOGTAG, "Performing migration");
final JSONArray newJsonPanels = new JSONArray();
final SharedPreferences.Editor prefsEditor = prefs.edit();
for (int v = version + 1; v <= VERSION; v++) {
@ -159,47 +206,31 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
switch (v) {
case 1:
// Add "Recent Tabs" panel
final JSONObject jsonRecentTabsConfig =
createBuiltinPanelConfig(context, PanelType.RECENT_TABS).toJSON();
// If any panel is enabled, then we should make the recent tabs
// panel enabled.
jsonRecentTabsConfig.put(PanelConfig.JSON_KEY_DISABLED,
allPanelsAreDisabled(originalJsonPanels));
// Add the new panel to the front of the array on phones.
if (!HardwareUtils.isTablet()) {
newJsonPanels.put(jsonRecentTabsConfig);
}
// Copy the original panel configs.
final int count = originalJsonPanels.length();
for (int i = 0; i < count; i++) {
final JSONObject jsonPanelConfig = originalJsonPanels.getJSONObject(i);
newJsonPanels.put(jsonPanelConfig);
}
// Add the new panel to the end of the array on tablets.
if (HardwareUtils.isTablet()) {
newJsonPanels.put(jsonRecentTabsConfig);
}
// Add "Recent Tabs" panel.
addBuiltinPanelConfig(context, jsonPanels,
PanelType.RECENT_TABS, Position.FRONT, Position.BACK);
// Remove the old pref key.
prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
break;
case 2:
// Add "Remote Tabs"/"Synced Tabs" panel.
addBuiltinPanelConfig(context, jsonPanels,
PanelType.REMOTE_TABS, Position.FRONT, Position.BACK);
break;
}
}
// Save the new panel config and the new version number.
final JSONObject newJson = new JSONObject();
newJson.put(JSON_KEY_PANELS, newJsonPanels);
newJson.put(JSON_KEY_PANELS, jsonPanels);
newJson.put(JSON_KEY_VERSION, VERSION);
prefsEditor.putString(PREFS_CONFIG_KEY, newJson.toString());
prefsEditor.apply();
return newJsonPanels;
return jsonPanels;
}
private State loadConfigFromString(String jsonString) {

Просмотреть файл

@ -5,13 +5,13 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.util.StringUtils;
import android.database.Cursor;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ExpandableListAdapter;
/**
* A ContextMenuInfo for HomeListView
@ -52,10 +52,17 @@ public class HomeContextMenuInfo extends AdapterContextMenuInfo {
return StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS));
}
/*
* Interface for creating ContextMenuInfo from cursors
/**
* Interface for creating ContextMenuInfo instances from cursors.
*/
public interface Factory {
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor);
}
/**
* Interface for creating ContextMenuInfo instances from ExpandableListAdapters.
*/
public interface ExpandableFactory {
public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ExpandableListAdapter adapter);
}
}

Просмотреть файл

@ -0,0 +1,68 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import android.content.Context;
import android.util.AttributeSet;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.ExpandableListView;
/**
* <code>HomeExpandableListView</code> is a custom extension of
* <code>ExpandableListView<code>, that packs a <code>HomeContextMenuInfo</code>
* when any of its rows is long pressed.
* <p>
* This is the <code>ExpandableListView</code> equivalent of
* <code>HomeListView</code>.
*/
public class HomeExpandableListView extends ExpandableListView
implements OnItemLongClickListener {
// ContextMenuInfo associated with the currently long pressed list item.
private HomeContextMenuInfo mContextMenuInfo;
// ContextMenuInfo factory.
private HomeContextMenuInfo.ExpandableFactory mContextMenuInfoFactory;
public HomeExpandableListView(Context context) {
this(context, null);
}
public HomeExpandableListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public HomeExpandableListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setOnItemLongClickListener(this);
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
if (mContextMenuInfoFactory == null) {
return false;
}
// HomeExpandableListView items can correspond to groups and children.
// The factory can determine whether to add context menu for either,
// both, or none by unpacking the given position.
mContextMenuInfo = mContextMenuInfoFactory.makeInfoForAdapter(view, position, id, getExpandableListAdapter());
return showContextMenuForChild(HomeExpandableListView.this);
}
@Override
public ContextMenuInfo getContextMenuInfo() {
return mContextMenuInfo;
}
public void setContextMenuInfoFactory(final HomeContextMenuInfo.ExpandableFactory factory) {
mContextMenuInfoFactory = factory;
}
}

Просмотреть файл

@ -50,7 +50,7 @@ import android.widget.Toast;
* <p>
* The containing activity <b>must</b> implement {@link OnUrlOpenListener}.
*/
abstract class HomeFragment extends Fragment {
public abstract class HomeFragment extends Fragment {
// Log Tag.
private static final String LOGTAG="GeckoHomeFragment";

Просмотреть файл

@ -70,6 +70,7 @@ public class HomePager extends ViewPager {
public static final String LIST_TAG_TOP_SITES = "top_sites";
public static final String LIST_TAG_RECENT_TABS = "recent_tabs";
public static final String LIST_TAG_BROWSER_SEARCH = "browser_search";
public static final String LIST_TAG_REMOTE_TABS = "remote_tabs";
public interface OnUrlOpenListener {
public enum Flags {
@ -120,8 +121,8 @@ public class HomePager extends ViewPager {
LOADED
}
static final String CAN_LOAD_ARG = "canLoad";
static final String PANEL_CONFIG_ARG = "panelConfig";
public static final String CAN_LOAD_ARG = "canLoad";
public static final String PANEL_CONFIG_ARG = "panelConfig";
public HomePager(Context context) {
this(context, null);

Просмотреть файл

@ -0,0 +1,267 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import java.util.EnumSet;
import java.util.List;
import org.mozilla.gecko.R;
import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
import org.mozilla.gecko.TabsAccessor;
import org.mozilla.gecko.TabsAccessor.RemoteClient;
import org.mozilla.gecko.TabsAccessor.RemoteTab;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout;
import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout.OnRefreshListener;
import android.accounts.Account;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ExpandableListView.OnGroupClickListener;
import android.widget.ImageView;
import android.widget.TextView;
/**
* Fragment that displays tabs from other devices in an <code>ExpandableListView<code>.
* <p>
* This is intended to be used on phones, and possibly in portrait mode on tablets.
*/
public class RemoteTabsExpandableListFragment extends HomeFragment {
// Logging tag name.
private static final String LOGTAG = "GeckoRemoteTabsExpList";
// Cursor loader ID.
private static final int LOADER_ID_REMOTE_TABS = 0;
private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" };
// Adapter for the list of remote tabs.
private RemoteTabsExpandableListAdapter mAdapter;
// The view shown by the fragment.
private HomeExpandableListView mList;
// Reference to the View to display when there are no results.
private View mEmptyView;
// Callbacks used for the loader.
private CursorLoaderCallbacks mCursorLoaderCallbacks;
// Child refresh layout view.
private GeckoSwipeRefreshLayout mRefreshLayout;
// Sync listener that stops refreshing when a sync is completed.
private RemoteTabsSyncListener mSyncStatusListener;
public static RemoteTabsExpandableListFragment newInstance() {
return new RemoteTabsExpandableListFragment();
}
public RemoteTabsExpandableListFragment() {
super();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.home_remote_tabs_list_panel, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mRefreshLayout = (GeckoSwipeRefreshLayout) view.findViewById(R.id.remote_tabs_refresh_layout);
mRefreshLayout.setColorScheme(
R.color.swipe_refresh_orange, R.color.swipe_refresh_white,
R.color.swipe_refresh_orange, R.color.swipe_refresh_white);
mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener());
mSyncStatusListener = new RemoteTabsSyncListener();
FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
mList = (HomeExpandableListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_REMOTE_TABS);
mList.setOnChildClickListener(new OnChildClickListener() {
@Override
public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
final ExpandableListAdapter adapter = parent.getExpandableListAdapter();
final RemoteTab tab = (RemoteTab) adapter.getChild(groupPosition, childPosition);
if (tab == null) {
return false;
}
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM);
// This item is a TwoLinePageRow, so we allow switch-to-tab.
mUrlOpenListener.onUrlOpen(tab.url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
return true;
}
});
mList.setOnGroupClickListener(new OnGroupClickListener() {
@Override
public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
// Since we don't indicate the expansion state yet, don't allow
// collapsing groups at all.
return true;
}
});
// Show a context menu only for tabs (not for clients).
mList.setContextMenuInfoFactory(new HomeContextMenuInfo.ExpandableFactory() {
@Override
public HomeContextMenuInfo makeInfoForAdapter(View view, int position, long id, ExpandableListAdapter adapter) {
long packedPosition = mList.getExpandableListPosition(position);
if (ExpandableListView.getPackedPositionType(packedPosition) != ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
return null;
}
final int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition);
final int childPosition = ExpandableListView.getPackedPositionChild(packedPosition);
final Object child = adapter.getChild(groupPosition, childPosition);
if (child instanceof RemoteTab) {
final RemoteTab tab = (RemoteTab) child;
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = tab.url;
info.title = tab.title;
return info;
} else {
return null;
}
}
});
registerForContextMenu(mList);
}
@Override
public void onDestroyView() {
super.onDestroyView();
mList = null;
mEmptyView = null;
if (mSyncStatusListener != null) {
FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
mSyncStatusListener = null;
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Intialize adapter
mAdapter = new RemoteTabsExpandableListAdapter(R.layout.home_remote_tabs_group, R.layout.home_remote_tabs_child, null);
mList.setAdapter(mAdapter);
// Create callbacks before the initial loader is started
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
loadIfVisible();
}
private void updateUiFromClients(List<RemoteClient> clients) {
if (clients != null && !clients.isEmpty()) {
for (int i = 0; i < mList.getExpandableListAdapter().getGroupCount(); i++) {
mList.expandGroup(i);
}
return;
}
// Cursor is empty, so set the empty view if it hasn't been set already.
if (mEmptyView == null) {
// Set empty panel view. We delay this so that the empty view won't flash.
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
mEmptyView = emptyViewStub.inflate();
final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
emptyIcon.setImageResource(R.drawable.icon_remote_tabs_empty);
final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
emptyText.setText(R.string.home_remote_tabs_empty);
mList.setEmptyView(mEmptyView);
}
}
@Override
protected void load() {
getLoaderManager().initLoader(LOADER_ID_REMOTE_TABS, null, mCursorLoaderCallbacks);
}
private static class RemoteTabsCursorLoader extends SimpleCursorLoader {
public RemoteTabsCursorLoader(Context context) {
super(context);
}
@Override
public Cursor loadCursor() {
return TabsAccessor.getRemoteTabsCursor(getContext());
}
}
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new RemoteTabsCursorLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
final List<RemoteClient> clients = TabsAccessor.getClientsFromCursor(c);
mAdapter.replaceClients(clients);
updateUiFromClients(clients);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.replaceClients(null);
}
}
private class RemoteTabsRefreshListener implements OnRefreshListener {
@Override
public void onRefresh() {
if (FirefoxAccounts.firefoxAccountsExist(getActivity())) {
final Account account = FirefoxAccounts.getFirefoxAccount(getActivity());
FirefoxAccounts.requestSync(account, FirefoxAccounts.FORCE, STAGES_TO_SYNC_ON_REFRESH, null);
} else {
Log.wtf(LOGTAG, "No Firefox Account found; this should never happen. Ignoring.");
mRefreshLayout.setRefreshing(false);
}
}
}
private class RemoteTabsSyncListener implements FirefoxAccounts.SyncStatusListener {
@Override
public Context getContext() {
return getActivity();
}
@Override
public Account getAccount() {
return FirefoxAccounts.getFirefoxAccount(getContext());
}
public void onSyncStarted() {
}
public void onSyncFinished() {
mRefreshLayout.setRefreshing(false);
}
}
}

Просмотреть файл

@ -0,0 +1,221 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import java.util.HashMap;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.AccountLoader;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.sync.SyncConstants;
import android.accounts.Account;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
/**
* A <code>HomeFragment</code> that, depending on the state of accounts on the
* device:
* <ul>
* <li>displays remote tabs from other devices;</li>
* <li>offers to re-connect a Firefox Account;</li>
* <li>offers to create a new Firefox Account.</li>
* </ul>
*/
public class RemoteTabsPanel extends HomeFragment {
private static final String LOGTAG = "GeckoRemoteTabsPanel";
// Loader ID for Android Account loader.
private static final int LOADER_ID_ACCOUNT = 0;
// Callback for loaders.
private AccountLoaderCallbacks mAccountLoaderCallbacks;
// The current fragment being shown to reflect the system account state. We
// don't want to detach and re-attach panels unnecessarily, because that
// causes flickering.
private Fragment mCurrentFragment;
// A lazily-populated cache of fragments corresponding to the possible
// system account states. We don't want to re-create panels unnecessarily,
// because that can cause flickering. Be aware that null is a valid key; it
// corresponds to "no Account, neither Firefox nor Legacy Sync."
private final HashMap<Action, Fragment> mFragmentCache = new HashMap<Action, Fragment>();
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.home_remote_tabs_panel, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Create callbacks before the initial loader is started.
mAccountLoaderCallbacks = new AccountLoaderCallbacks();
loadIfVisible();
}
@Override
public void load() {
getLoaderManager().initLoader(LOADER_ID_ACCOUNT, null, mAccountLoaderCallbacks);
}
private void showSubPanel(Fragment subPanel) {
if (mCurrentFragment == subPanel) {
return;
}
mCurrentFragment = subPanel;
Bundle args = subPanel.getArguments();
if (args == null) {
args = new Bundle();
}
args.putBoolean(HomePager.CAN_LOAD_ARG, getCanLoadHint());
subPanel.setArguments(args);
getChildFragmentManager()
.beginTransaction()
.addToBackStack(null)
.replace(R.id.remote_tabs_container, subPanel)
.commitAllowingStateLoss();
}
/**
* Get whatever <code>Action</code> is required to continue healthy syncing
* of Remote Tabs.
* <p>
* A Firefox Account can be in many states, from healthy to requiring a
* Fennec upgrade to continue use. If we have a Firefox Account, but the
* state seems corrupt, the best we can do is ask for a password, which
* resets most of the Account state. The health of a Sync account is
* essentially opaque in this respect.
* <p>
* A null Account means there is no Account (Sync or Firefox) on the device.
*
* @param account
* Android Account (Sync or Firefox); may be null.
*/
private Action getActionNeeded(Account account) {
if (account == null) {
return null;
}
if (SyncConstants.ACCOUNTTYPE_SYNC.equals(account.type)) {
return Action.None;
}
if (!FxAccountConstants.ACCOUNT_TYPE.equals(account.type)) {
Log.wtf(LOGTAG, "Non Sync, non Firefox Android Account returned by AccountLoader; returning null.");
return null;
}
final State state = FirefoxAccounts.getFirefoxAccountState(getActivity());
if (state == null) {
Log.wtf(LOGTAG, "Firefox Account with null state found; offering needs password.");
return Action.NeedsPassword;
}
final Action actionNeeded = state.getNeededAction();
if (actionNeeded == null) {
Log.wtf(LOGTAG, "Firefox Account with non-null state but null action needed; offering needs password.");
return Action.NeedsPassword;
}
return actionNeeded;
}
private Fragment makeFragmentForAction(Action action) {
if (action == null) {
// This corresponds to no Account: neither Sync nor Firefox.
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_setup);
}
switch (action) {
case None:
return new RemoteTabsExpandableListFragment();
case NeedsVerification:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_verification);
case NeedsPassword:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_password);
case NeedsUpgrade:
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_upgrade);
default:
// This should never happen, but we're confident we have a Firefox
// Account at this point, so let's show the needs password screen.
// That's our best hope of righting the ship.
Log.wtf(LOGTAG, "Got unexpected action needed; offering needs password.");
return RemoteTabsStaticFragment.newInstance(R.layout.remote_tabs_needs_password);
}
}
/**
* Get the <code>Fragment</code> that reflects the given
* <code>Account</code> and its state.
* <p>
* A null Account means there is no Account (Sync or Firefox) on the device.
*
* @param account
* Android Account (Sync or Firefox); may be null.
*/
private Fragment getFragmentNeeded(Account account) {
final Action actionNeeded = getActionNeeded(account);
// We use containsKey rather than get because null is a valid key.
if (!mFragmentCache.containsKey(actionNeeded)) {
final Fragment fragment = makeFragmentForAction(actionNeeded);
mFragmentCache.put(actionNeeded, fragment);
}
return mFragmentCache.get(actionNeeded);
}
/**
* Update the UI to reflect the given <code>Account</code> and its state.
* <p>
* A null Account means there is no Account (Sync or Firefox) on the device.
*
* @param account
* Android Account (Sync or Firefox); may be null.
*/
protected void updateUiFromAccount(Account account) {
if (getView() == null) {
// Early abort. When the fragment is detached, we get a loader
// reset, which calls this with a null account parameter. A null
// account is valid (it means there is no account, either Sync or
// Firefox), and so we start to offer the setup flow. But this all
// happens after the view has been destroyed, which means inserting
// the setup flow fails. In this case, just abort.
return;
}
showSubPanel(getFragmentNeeded(account));
}
private class AccountLoaderCallbacks implements LoaderCallbacks<Account> {
@Override
public Loader<Account> onCreateLoader(int id, Bundle args) {
return new AccountLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<Account> loader, Account account) {
updateUiFromAccount(account);
}
@Override
public void onLoaderReset(Loader<Account> loader) {
updateUiFromAccount(null);
}
}
}

Просмотреть файл

@ -0,0 +1,130 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import java.util.EnumSet;
import java.util.Locale;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity;
import org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
/**
* A <code>HomeFragment</code> which displays one of a small set of static views
* in response to different Firefox Account states. When the Firefox Account is
* healthy and syncing normally, these views should not be shown.
* <p>
* This class exists to handle view-specific actions when buttons and links
* shown by the different static views are clicked. For example, a static view
* offers to set up a Firefox Account to a user who has no account (Firefox or
* Sync) on their device.
* <p>
* This could be a vanilla <code>Fragment</code>, except it needs to open URLs.
* To do so, it expects its containing <code>Activity</code> to implement
* <code>OnUrlOpenListener<code>; to suggest this invariant at compile time, we
* inherit from <code>HomeFragment</code>.
*/
public class RemoteTabsStaticFragment extends HomeFragment implements OnClickListener {
@SuppressWarnings("unused")
private static final String LOGTAG = "GeckoRemoteTabsStatic";
protected static final String RESOURCE_ID = "resource_id";
protected static final int DEFAULT_RESOURCE_ID = R.layout.remote_tabs_setup;
private static final String CONFIRM_ACCOUNT_SUPPORT_URL =
"https://support.mozilla.org/kb/im-having-problems-confirming-my-firefox-account";
protected int mLayoutId;
public static RemoteTabsStaticFragment newInstance(int resourceId) {
final RemoteTabsStaticFragment fragment = new RemoteTabsStaticFragment();
final Bundle args = new Bundle();
args.putInt(RESOURCE_ID, resourceId);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Bundle args = getArguments();
if (args != null) {
mLayoutId = args.getInt(RESOURCE_ID, DEFAULT_RESOURCE_ID);
} else {
mLayoutId = DEFAULT_RESOURCE_ID;
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(mLayoutId, container, false);
}
protected boolean maybeSetOnClickListener(View view, int resourceId) {
final View button = view.findViewById(resourceId);
if (button != null) {
button.setOnClickListener(this);
return true;
}
return false;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
for (int resourceId : new int[] {
R.id.remote_tabs_setup_get_started,
R.id.remote_tabs_setup_old_sync_link,
R.id.remote_tabs_needs_verification_resend_email,
R.id.remote_tabs_needs_verification_help,
R.id.remote_tabs_needs_password_sign_in, }) {
maybeSetOnClickListener(view, resourceId);
}
}
@Override
public void onClick(final View v) {
final int id = v.getId();
if (id == R.id.remote_tabs_setup_get_started) {
// This Activity will redirect to the correct Activity as needed.
final Intent intent = new Intent(getActivity(), FxAccountCreateAccountActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
} else if (id == R.id.remote_tabs_setup_old_sync_link) {
final String url = FirefoxAccounts.getOldSyncUpgradeURL(getResources(), Locale.getDefault());
// Don't allow switch-to-tab.
final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
mUrlOpenListener.onUrlOpen(url, flags);
} else if (id == R.id.remote_tabs_needs_verification_resend_email) {
// Send a fresh email; this displays a toast, so the user gets feedback.
FirefoxAccounts.resendVerificationEmail(getActivity());
} else if (id == R.id.remote_tabs_needs_verification_help) {
// Don't allow switch-to-tab.
final EnumSet<OnUrlOpenListener.Flags> flags = EnumSet.noneOf(OnUrlOpenListener.Flags.class);
mUrlOpenListener.onUrlOpen(CONFIRM_ACCOUNT_SUPPORT_URL, flags);
} else if (id == R.id.remote_tabs_needs_password_sign_in) {
// This Activity will redirect to the correct Activity as needed.
final Intent intent = new Intent(getActivity(), FxAccountUpdateCredentialsActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
}
@Override
protected void load() {
// We're static, so nothing to do here!
}
}

Просмотреть файл

@ -403,6 +403,13 @@ size. -->
previous location in the navigation, such as the previous folder -->
<!ENTITY home_move_up_to_filter "Up to &formatS;">
<!ENTITY home_remote_tabs_title "Synced Tabs">
<!ENTITY home_remote_tabs_empty "Your tabs from other devices show up here.">
<!ENTITY home_remote_tabs_unable_to_connect "Unable to connect">
<!ENTITY home_remote_tabs_need_to_sign_in "Please sign in to reconnect your Firefox Account and continue syncing.">
<!ENTITY home_remote_tabs_trouble_verifying "Trouble verifying your account?">
<!ENTITY home_remote_tabs_need_to_verify "Please verify your Firefox Account to start syncing.">
<!ENTITY private_browsing_title "Private Browsing">
<!ENTITY private_tabs_panel_empty_desc "Your private tabs will show up here. While we don\'t keep any of your browsing history or cookies, bookmarks and files that you download will still be saved on your device.">
<!ENTITY private_tabs_panel_learn_more "Want to learn more?">

Просмотреть файл

@ -275,6 +275,7 @@ gbjar.sources += [
'home/HomeConfigLoader.java',
'home/HomeConfigPrefsBackend.java',
'home/HomeContextMenuInfo.java',
'home/HomeExpandableListView.java',
'home/HomeFragment.java',
'home/HomeListView.java',
'home/HomePager.java',
@ -297,6 +298,9 @@ gbjar.sources += [
'home/ReadingListPanel.java',
'home/ReadingListRow.java',
'home/RecentTabsPanel.java',
'home/RemoteTabsExpandableListFragment.java',
'home/RemoteTabsPanel.java',
'home/RemoteTabsStaticFragment.java',
'home/SearchEngine.java',
'home/SearchEngineRow.java',
'home/SearchLoader.java',
@ -687,7 +691,12 @@ main.recursive_make_targets += ['generated/' + f for f in gbjar.generated_source
main.extra_jars += [CONFIG['ANDROID_COMPAT_LIB']]
main.assets = TOPOBJDIR + '/dist/' + CONFIG['MOZ_APP_NAME'] + '/assets'
main.libs = TOPOBJDIR + '/dist/' + CONFIG['MOZ_APP_NAME'] + '/lib'
main.libs = [
(TOPOBJDIR + '/dist/' + CONFIG['MOZ_APP_NAME'] + '/lib/' + CONFIG['ANDROID_CPU_ARCH'] + '/libmozglue.so',
'libs/' + CONFIG['ANDROID_CPU_ARCH'] + '/libmozglue.so'),
(TOPOBJDIR + '/dist/' + CONFIG['MOZ_APP_NAME'] + '/lib/' + CONFIG['ANDROID_CPU_ARCH'] + '/libplugin-container.so',
'libs/' + CONFIG['ANDROID_CPU_ARCH'] + '/libplugin-container.so'),
]
main.res = None
cpe = main.add_classpathentry('src', SRCDIR,

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 744 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 645 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 947 B

Просмотреть файл

@ -12,7 +12,7 @@
android:paddingLeft="2dp"
android:paddingRight="2dp">
<TextView android:id="@+id/tab"
<TextView android:id="@+id/title"
style="@style/TabRowTextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"

Просмотреть файл

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<org.mozilla.gecko.home.TwoLinePageRow xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.RemoteTabsItemView"
android:layout_width="match_parent"
android:layout_height="@dimen/page_row_height"
android:minHeight="@dimen/page_row_height"/>

Просмотреть файл

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
style="@style/Widget.RemoteTabsClientView"
android:layout_width="match_parent"
android:layout_height="@dimen/page_row_height"
android:gravity="center_vertical"
android:minHeight="@dimen/page_row_height" >
<ImageView
android:id="@+id/device_type"
android:layout_width="@dimen/favicon_bg"
android:layout_height="@dimen/favicon_bg"
android:layout_marginLeft="10dip"
android:layout_marginRight="10dip" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginRight="10dip"
android:orientation="vertical" >
<org.mozilla.gecko.widget.FadedTextView
android:id="@+id/client"
style="@style/Widget.TwoLinePageRow.Title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
gecko:fadeWidth="30dp" />
<TextView
android:id="@+id/last_synced"
style="@style/Widget.TwoLinePageRow.Url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="5dp"
android:maxLength="1024" />
</LinearLayout>
</LinearLayout>

Просмотреть файл

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ViewStub android:id="@+id/home_empty_view_stub"
android:layout="@layout/home_empty_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.mozilla.gecko.widget.GeckoSwipeRefreshLayout
android:id="@+id/remote_tabs_refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.mozilla.gecko.home.HomeExpandableListView
android:id="@+id/list"
style="@style/Widget.RemoteTabsListView"
android:groupIndicator="@android:color/transparent"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</org.mozilla.gecko.widget.GeckoSwipeRefreshLayout>
</LinearLayout>

Просмотреть файл

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout android:id="@+id/remote_tabs_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

Просмотреть файл

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RemoteTabsPanelFrame"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_remote_tabs_unable_to_connect" />
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_remote_tabs_need_to_sign_in" />
<Button
android:id="@+id/remote_tabs_needs_password_sign_in"
style="@style/RemoteTabsPanelItem.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_sign_in_button_label" />
</LinearLayout>

Просмотреть файл

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RemoteTabsPanelFrame"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_remote_tabs_unable_to_connect" />
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_status_needs_upgrade" />
</LinearLayout>

Просмотреть файл

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RemoteTabsPanelFrame"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_confirm_account_header" />
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_remote_tabs_need_to_verify" />
<Button
android:id="@+id/remote_tabs_needs_verification_resend_email"
style="@style/RemoteTabsPanelItem.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_confirm_account_resend_email" />
<TextView
android:id="@+id/remote_tabs_needs_verification_help"
style="@style/RemoteTabsPanelItem.TextAppearance.Linkified"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/home_remote_tabs_trouble_verifying" />
</LinearLayout>

Просмотреть файл

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RemoteTabsPanelFrame"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_getting_started_welcome_to_sync" />
<TextView
style="@style/RemoteTabsPanelItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_getting_started_description" />
<Button
android:id="@+id/remote_tabs_setup_get_started"
style="@style/RemoteTabsPanelItem.Button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_getting_started_get_started" />
<TextView
android:id="@+id/remote_tabs_setup_old_sync_link"
style="@style/RemoteTabsPanelItem.TextAppearance.Linkified"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_getting_started_old_firefox" />
</LinearLayout>

Просмотреть файл

@ -576,6 +576,16 @@
<item name="android:layout_height">wrap_content</item>
</style>
<style name="Widget.RemoteTabsItemView" parent="Widget.TwoLinePageRow"/>
<style name="Widget.RemoteTabsClientView" parent="Widget.TwoLinePageRow">
<item name="android:background">#fff5f7f9</item>
</style>
<style name="Widget.RemoteTabsListView" parent="Widget.HomeListView">
<item name="android:childDivider">#E7ECF0</item>
</style>
<!-- TabsTray Row -->
<style name="TabRowTextAppearance">
<item name="android:textColor">#FFFFFFFF</item>
@ -808,4 +818,46 @@
<item name="android:textSize">18sp</item>
</style>
<!-- Remote Tabs home panel -->
<style name="RemoteTabsPanelFrame">
<item name="android:paddingLeft">32dp</item>
<item name="android:paddingRight">32dp</item>
<item name="android:paddingTop">48dp</item>
<item name="android:orientation">vertical</item>
</style>
<style name="RemoteTabsPanelItem">
<item name="android:layout_gravity">center</item>
<item name="android:gravity">center</item>
<item name="android:layout_marginBottom">16dp</item>
</style>
<style name="RemoteTabsPanelItem.TextAppearance">
<item name="android:textColor">#777777</item>
<item name="android:textSize">16sp</item>
<item name="android:lineSpacingMultiplier">1.35</item>
</style>
<style name="RemoteTabsPanelItem.TextAppearance.Header">
<item name="android:textColor">#222222</item>
<item name="android:textSize">20sp</item>
</style>
<style name="RemoteTabsPanelItem.TextAppearance.Linkified">
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
<item name="android:textColor">#0092DB</item>
</style>
<style name="RemoteTabsPanelItem.Button">
<item name="android:background">@drawable/remote_tabs_setup_button_background</item>
<item name="android:textColor">#FFFFFF</item>
<item name="android:textSize">20sp</item>
<item name="android:gravity">center</item>
<item name="android:paddingTop">16dp</item>
<item name="android:paddingBottom">16dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:paddingRight">8dp</item>
</style>
</resources>

Просмотреть файл

@ -351,6 +351,12 @@
<string name="home_reading_list_hint_accessible">&home_reading_list_hint_accessible;</string>
<string name="home_default_empty">&home_default_empty;</string>
<string name="home_move_up_to_filter">&home_move_up_to_filter;</string>
<string name="home_remote_tabs_title">&home_remote_tabs_title;</string>
<string name="home_remote_tabs_empty">&home_remote_tabs_empty;</string>
<string name="home_remote_tabs_unable_to_connect">&home_remote_tabs_unable_to_connect;</string>
<string name="home_remote_tabs_need_to_sign_in">&home_remote_tabs_need_to_sign_in;</string>
<string name="home_remote_tabs_trouble_verifying">&home_remote_tabs_trouble_verifying;</string>
<string name="home_remote_tabs_need_to_verify">&home_remote_tabs_need_to_verify;</string>
<string name="private_browsing_title">&private_browsing_title;</string>
<string name="private_tabs_panel_empty_desc">&private_tabs_panel_empty_desc;</string>
<string name="private_tabs_panel_learn_more">&private_tabs_panel_learn_more;</string>

Просмотреть файл

@ -4,5 +4,4 @@
<!ENTITY brandShortName "Aurora">
<!ENTITY brandFullName "Mozilla Aurora">
<!ENTITY vendorShortName "Mozilla">
<!ENTITY logoTrademark "">
<!ENTITY vendorShortName "Mozilla">

Просмотреть файл

@ -4,5 +4,4 @@
<!ENTITY brandShortName "Firefox Beta">
<!ENTITY brandFullName "Mozilla Firefox Beta">
<!ENTITY vendorShortName "Mozilla">
<!ENTITY logoTrademark "Firefox and the Firefox logos are trademarks of the Mozilla Foundation.">
<!ENTITY vendorShortName "Mozilla">

Просмотреть файл

@ -4,5 +4,4 @@
<!ENTITY brandShortName "Nightly">
<!ENTITY brandFullName "Mozilla Nightly">
<!ENTITY vendorShortName "Mozilla">
<!ENTITY logoTrademark "">
<!ENTITY vendorShortName "Mozilla">

Просмотреть файл

@ -4,5 +4,4 @@
<!ENTITY brandShortName "Firefox">
<!ENTITY brandFullName "Mozilla Firefox">
<!ENTITY vendorShortName "Mozilla">
<!ENTITY logoTrademark "Firefox and the Firefox logos are trademarks of the Mozilla Foundation.">
<!ENTITY vendorShortName "Mozilla">

Просмотреть файл

@ -4,5 +4,4 @@
<!ENTITY brandShortName "Fennec">
<!ENTITY brandFullName "Mozilla Fennec">
<!ENTITY vendorShortName "Mozilla">
<!ENTITY logoTrademark "">
<!ENTITY vendorShortName "Mozilla">

Просмотреть файл

@ -65,9 +65,11 @@
<div class="bottom-border"></div>
</ul>
#ifdef RELEASE_BUILD
<div id="aboutDetails">
<p>&logoTrademark;</p>
<p>&aboutPage.logoTrademark;</p>
</div>
#endif
<script type="application/javascript;version=1.8"><![CDATA[
let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils, Cr = Components.results;

Просмотреть файл

@ -4,6 +4,9 @@
"use strict";
// input.mozilla.org expects "Firefox for Android" as the product.
const FEEDBACK_PRODUCT_STRING = "Firefox for Android";
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
@ -97,9 +100,10 @@ function sendFeedback(aEvent) {
if (!descriptionElement.validity.valid)
return;
let data = new FormData();
data.append("description", descriptionElement.value);
data.append("_type", 2);
let data = {};
data["happy"] = false;
data["description"] = descriptionElement.value;
data["product"] = FEEDBACK_PRODUCT_STRING;
let urlElement = document.getElementById("last-url");
// Bail if the URL value isn't valid. HTML5 form validation will take care
@ -109,13 +113,13 @@ function sendFeedback(aEvent) {
// Only send a URL string if the user provided one.
if (urlElement.value) {
data.append("add_url", true);
data.append("url", urlElement.value);
data["url"] = urlElement.value;
}
let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
data.append("device", sysInfo.get("device"));
data.append("manufacturer", sysInfo.get("manufacturer"));
data["device"] = sysInfo.get("device");
data["manufacturer"] = sysInfo.get("manufacturer");
data["source"] = "about:feedback";
let req = new XMLHttpRequest();
req.addEventListener("error", function() {
@ -127,7 +131,8 @@ function sendFeedback(aEvent) {
let postURL = Services.urlFormatter.formatURLPref("app.feedback.postURL");
req.open("POST", postURL, true);
req.send(data);
req.setRequestHeader("Content-type", "application/json");
req.send(JSON.stringify(data));
switchSection("thanks-" + section);
}

Просмотреть файл

@ -86,5 +86,9 @@ if test ! "$RELEASE_BUILD"; then
MOZ_ANDROID_SHARE_OVERLAY=1
fi
# Don't enable the Mozilla Location Service stumbler.
# MOZ_ANDROID_MLS_STUMBLER=1
# Enable the Mozilla Location Service stumbler in Nightly.
if test "$NIGHTLY_BUILD"; then
MOZ_ANDROID_MLS_STUMBLER=1
else
MOZ_ANDROID_MLS_STUMBLER=
fi

Просмотреть файл

@ -47,8 +47,9 @@ package: local.properties project.properties AndroidManifest.xml FORCE
cp $(DEPTH)/mobile/android/base/*.jar libs/
$(RM) libs/gecko-R.jar
# Copy the SOs
cp $(_ABS_DIST)/bin/libmozglue.so $(_ABS_DIST)/bin/lib/libplugin-container.so libs/$(ANDROID_CPU_ARCH)/
# Copy the SOs. The latter should be $(MOZ_CHILD_PROCESS_NAME), but
# it includes a "lib/" prefix.
cp $(_ABS_DIST)/bin/libmozglue.so $(_ABS_DIST)/bin/libplugin-container.so libs/$(ANDROID_CPU_ARCH)/
# Copy the resources
$(RM) -rf res

Просмотреть файл

@ -16,12 +16,12 @@ MOZ_PKG_REMOVALS = $(srcdir)/removed-files.in
MOZ_PKG_MANIFEST_P = $(srcdir)/package-manifest.in
DEFINES += \
-DMOZ_APP_NAME=$(MOZ_APP_NAME) \
-DPREF_DIR=$(PREF_DIR) \
$(NULL)
DEFINES += -DJAREXT=
DEFINES += -DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME)
-DMOZ_APP_NAME=$(MOZ_APP_NAME) \
-DPREF_DIR=$(PREF_DIR) \
-DJAREXT= \
-DMOZ_CHILD_PROCESS_NAME=$(MOZ_CHILD_PROCESS_NAME) \
-DANDROID_CPU_ARCH=$(ANDROID_CPU_ARCH) \
$(NULL)
ifdef MOZ_DEBUG
DEFINES += -DMOZ_DEBUG=1

Просмотреть файл

@ -75,10 +75,13 @@
@BINPATH@/@DLL_PREFIX@mozsqlite3@DLL_SUFFIX@
#endif
[lib destdir="lib/@ANDROID_CPU_ARCH@"]
@BINPATH@/@DLL_PREFIX@mozglue@DLL_SUFFIX@
# This should be MOZ_CHILD_PROCESS_NAME, but that has a "lib/" prefix.
@BINPATH@/@DLL_PREFIX@plugin-container@DLL_SUFFIX@
[xpcom]
@BINPATH@/dependentlibs.list
@BINPATH@/@DLL_PREFIX@mozglue@DLL_SUFFIX@
@BINPATH@/@MOZ_CHILD_PROCESS_NAME@
@BINPATH@/AndroidManifest.xml
@BINPATH@/resources.arsc

Просмотреть файл

@ -19,3 +19,6 @@
<!ENTITY aboutPage.relNotes.label "Release Notes">
<!ENTITY aboutPage.credits.label "Credits">
<!ENTITY aboutPage.license.label "Licensing Information">
<!-- LOCALIZATION NOTE (aboutPage.logoTrademark): The message is explicitly about the word "Firefox" being trademarked, that's why we use it, instead of brandShortName. -->
<!ENTITY aboutPage.logoTrademark "Firefox and the Firefox logos are trademarks of the Mozilla Foundation.">

Просмотреть файл

@ -0,0 +1,124 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.search.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import org.mozilla.search.R;
public class FacetBar extends RadioGroup {
// Ensure facets have equal width and match the bar's height. Supplying these
// in styles.xml/FacetButtonStyle does not work. See:
// http://stackoverflow.com/questions/24213193/android-ignores-layout-weight-parameter-from-styles-xml
private static final RadioGroup.LayoutParams FACET_LAYOUT_PARAMS =
new RadioGroup.LayoutParams(0, LayoutParams.MATCH_PARENT, 1.0f);
// A loud default color to make it obvious that setUnderlineColor should be called.
private int underlineColor = Color.RED;
// Used for assigning unique view ids when facet buttons are being created.
private int nextButtonId = 0;
public FacetBar(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Add a new button to the facet bar.
*
* @param facetName The text to be used in the button.
*/
public void addFacet(String facetName) {
addFacet(facetName, false);
}
/**
* Add a new button to the facet bar.
*
* @param facetName The text to be used in the button.
* @param checked Whether the button should be checked. If true, the
* onCheckChange listener *will* be fired.
*/
public void addFacet(String facetName, boolean checked) {
final FacetButton button = new FacetButton(getContext(), facetName, underlineColor);
// The ids are used internally by RadioGroup to manage which button is
// currently checked. Since we are programmatically creating the buttons,
// we need to manually assign an id.
button.setId(nextButtonId++);
// Ensure the buttons are equally spaced.
button.setLayoutParams(FACET_LAYOUT_PARAMS);
// If true, this will fire the onCheckChange listener.
button.setChecked(checked);
addView(button);
}
/**
* Update the brand color for all of the buttons.
*/
public void setUnderlineColor(int underlineColor) {
this.underlineColor = underlineColor;
if (getChildCount() > 0) {
for (int i = 0; i < getChildCount(); i++) {
((FacetButton) getChildAt(i)).setUnderlineColor(underlineColor);
}
}
}
/**
* A custom TextView that includes a bottom border. The bottom border
* can have a custom color and thickness.
*/
private static class FacetButton extends RadioButton {
private final Paint underlinePaint = new Paint();
public FacetButton(Context context, String text, int color) {
super(context, null, R.attr.facetButtonStyle);
setText(text);
underlinePaint.setStyle(Paint.Style.STROKE);
underlinePaint.setStrokeWidth(getResources().getDimension(R.dimen.facet_button_underline_thickness));
underlinePaint.setColor(color);
}
@Override
public void setChecked(boolean checked) {
super.setChecked(checked);
// Force the button to redraw to update the underline state.
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isChecked()) {
// Translate the line upward so that it isn't clipped by the button's boundary.
// We divide by 2 since, without offset, the line would be drawn with its
// midpoint at the bottom of the button -- half of the stroke going up,
// and half of the stroke getting clipped.
final float yPos = getHeight() - underlinePaint.getStrokeWidth() / 2;
canvas.drawLine(0, yPos, getWidth(), yPos, underlinePaint);
}
}
public void setUnderlineColor(int color) {
underlinePaint.setColor(color);
}
}
}

Просмотреть файл

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- facet is selected -->
<item android:state_checked="true" android:color="@color/facet_button_text_color_selected" />
<!-- default -->
<item android:color="@color/facet_button_text_color_default" />
</selector>

Просмотреть файл

@ -0,0 +1,15 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!--facet button is pressed (omitting currently-selected facet)-->
<item
android:state_pressed="true"
android:state_checked="false"
android:drawable="@drawable/facet_button_background_pressed"/>
<!--default-->
<item
android:drawable="@drawable/facet_button_background_default"/>
</selector>

Просмотреть файл

@ -0,0 +1,8 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/facet_button_background_color_default" />
</shape>

Просмотреть файл

@ -0,0 +1,8 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/facet_button_background_color_pressed" />
</shape>

Просмотреть файл

@ -8,6 +8,11 @@
<style name="AppTheme" parent="@android:style/Theme.Holo.Light.NoActionBar">
<item name="android:windowBackground">@color/global_background_color</item>
<item name="android:colorBackground">@color/global_background_color</item>
<!--This attribute is required so that we can create a facet button-->
<!--pragmatically. The defStyle param used in the View constructor-->
<!--must be an attr, see: https://code.google.com/p/android/issues/detail?id=12683-->
<item name="facetButtonStyle">@style/FacetButtonStyle</item>
</style>
<style name="SettingsTheme" parent="@android:style/Theme.Holo.Light"/>

Просмотреть файл

@ -0,0 +1,12 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<declare-styleable name="FacetButton"/>
<!--This attribute is required so that we can create a facet button-->
<!--pragmatically. The defStyle param used in the View constructor-->
<!--must be an attr, see: https://code.google.com/p/android/issues/detail?id=12683-->
<attr name="facetButtonStyle" />
</resources>

Просмотреть файл

@ -19,5 +19,10 @@
<color name="widget_button_pressed">#33000000</color>
<color name="widget_text_color">#5F6368</color>
<!-- Search suggestion highlight color is defined in SearchFragment.java -->
<!--Facet button colors-->
<color name="facet_button_background_color_default">@android:color/white</color>
<color name="facet_button_background_color_pressed">#FAFAFA</color>
<color name="facet_button_text_color_default">#ADB0B1</color>
<color name="facet_button_text_color_selected">#383E42</color>
</resources>

Просмотреть файл

@ -39,4 +39,6 @@
<dimen name="widget_drawable_corner_radius">4dp</dimen>
<dimen name="widget_bg_border_offset">3dp</dimen>
<dimen name="facet_bar_height">50dp</dimen>
<dimen name="facet_button_underline_thickness">5dp</dimen>
</resources>

Просмотреть файл

@ -8,8 +8,23 @@
<style name="AppTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">@color/global_background_color</item>
<item name="android:colorBackground">@color/global_background_color</item>
<!--This attribute is required so that we can create a facet button-->
<!--pragmatically. The defStyle param used in the View constructor-->
<!--must be an attr, see: https://code.google.com/p/android/issues/detail?id=12683-->
<item name="facetButtonStyle">@style/FacetButtonStyle</item>
</style>
<style name="SettingsTheme" parent="@android:style/Theme.Light"/>
<style name="SettingsTheme" parent="@android:style/Theme.Light" />
<style name="FacetButtonStyle">
<!--Since we're not inflating xml, we have to apply the layout params -->
<!--after instantiation. See FacetBar.addFacet.-->
<item name="android:textSize">15sp</item>
<item name="android:textColor">@color/facet_button_text_color</item>
<item name="android:background">@drawable/facet_button_background</item>
<item name="android:gravity">center</item>
<item name="android:clickable">true</item>
</style>
</resources>

Просмотреть файл

@ -18,4 +18,5 @@ search_activity_sources = [
'java/org/mozilla/search/SearchPreferenceActivity.java',
'java/org/mozilla/search/SearchWidget.java',
'java/org/mozilla/search/ui/BackCaptureEditText.java',
'java/org/mozilla/search/ui/FacetBar.java',
]

Просмотреть файл

@ -176,17 +176,18 @@ class AndroidEclipseBackend(CommonBackend):
for cpe in project._classpathentries:
manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir)
# JARs and native libraries go in the same place. For now,
# we're adding class path entries with the full path to
# required JAR files (which makes sense for JARs in the source
# directory, but probably doesn't for JARs in the object
# directory). This could be a problem because we only know
# the contents of (a subdirectory of) libs/ after a successful
# build and package, which is after build-backend time. So we
# use a pattern symlink that is resolved at manifest install
# time.
if project.libs:
manifest.add_pattern_copy(mozpath.join(srcdir, project.libs), '**', 'libs')
# JARs and native libraries go in the same place. For now, we're adding
# class path entries with the full path to required JAR files (which
# makes sense for JARs in the source directory, but probably doesn't for
# JARs in the object directory). This could be a problem because we only
# know the contents of (a subdirectory of) libs/ after a successful
# build and package, which is after build-backend time. At the cost of
# some flexibility, we explicitly copy certain libraries here; if the
# libraries aren't present -- namely, when the tree hasn't been packaged
# -- this fails. That's by design, to avoid crashes on device caused by
# missing native libraries.
for src, dst in project.libs:
manifest.add_copy(mozpath.join(srcdir, src), dst)
return manifest
@ -239,6 +240,7 @@ class AndroidEclipseBackend(CommonBackend):
else:
defines['IDE_PROJECT_FILTERED_RESOURCES'] = ''
defines['ANDROID_TARGET_SDK'] = self.environment.substs['ANDROID_TARGET_SDK']
defines['MOZ_ANDROID_MIN_SDK_VERSION'] = self.environment.defines['MOZ_ANDROID_MIN_SDK_VERSION']
copier = FileCopier()
finder = FileFinder(template_directory)

Просмотреть файл

@ -1,15 +1,15 @@
#filter substitution
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
org.eclipse.jdt.core.compiler.compliance=1.6
org.eclipse.jdt.core.compiler.compliance=1.7
org.eclipse.jdt.core.compiler.debug.lineNumber=generate
org.eclipse.jdt.core.compiler.debug.localVariable=generate
org.eclipse.jdt.core.compiler.debug.sourceFile=generate
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
org.eclipse.jdt.core.compiler.source=1.6
org.eclipse.jdt.core.compiler.source=1.7
org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0

Просмотреть файл

@ -5,7 +5,7 @@
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="16" />
android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
android:targetSdkVersion="@ANDROID_TARGET_SDK@" />
</manifest>

Просмотреть файл

@ -34,7 +34,9 @@ CONFIGS = defaultdict(lambda: {
'substs': [],
}, {
'android_eclipse': {
'defines': [],
'defines': [
('MOZ_ANDROID_MIN_SDK_VERSION', '9'),
],
'non_global_defines': [],
'substs': [
('ANDROID_TARGET_SDK', '16'),

Просмотреть файл

@ -0,0 +1,89 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["CloudSync"];
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Adapters",
"resource://gre/modules/CloudSyncAdapters.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Local",
"resource://gre/modules/CloudSyncLocal.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
"resource://gre/modules/CloudSyncBookmarks.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Tabs",
"resource://gre/modules/CloudSyncTabs.jsm");
let API_VERSION = 1;
let _CloudSync = function () {
};
_CloudSync.prototype = {
_adapters: null,
get adapters () {
if (!this._adapters) {
this._adapters = new Adapters();
}
return this._adapters;
},
_bookmarks: null,
get bookmarks () {
if (!this._bookmarks) {
this._bookmarks = new Bookmarks();
}
return this._bookmarks;
},
_local: null,
get local () {
if (!this._local) {
this._local = new Local();
}
return this._local;
},
_tabs: null,
get tabs () {
if (!this._tabs) {
this._tabs = new Tabs();
}
return this._tabs;
},
get tabsReady () {
return this._tabs ? true: false;
},
get version () {
return API_VERSION;
},
};
this.CloudSync = function CloudSync () {
return _cloudSyncInternal.instance;
};
Object.defineProperty(CloudSync, "ready", {
get: function () {
return _cloudSyncInternal.ready;
}
});
let _cloudSyncInternal = {
instance: null,
ready: false,
};
XPCOMUtils.defineLazyGetter(_cloudSyncInternal, "instance", function () {
_cloudSyncInternal.ready = true;
return new _CloudSync();
}.bind(this));

Просмотреть файл

@ -0,0 +1,88 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["Adapters"];
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/CloudSyncEventSource.jsm");
this.Adapters = function () {
let eventTypes = [
"sync",
];
let suspended = true;
let suspend = function () {
if (!suspended) {
Services.obs.removeObserver(observer, "cloudsync:user-sync", false);
suspended = true;
}
}.bind(this);
let resume = function () {
if (suspended) {
Services.obs.addObserver(observer, "cloudsync:user-sync", false);
suspended = false;
}
}.bind(this);
let eventSource = new EventSource(eventTypes, suspend, resume);
let registeredAdapters = new Map();
function register (name, opts) {
opts = opts || {};
registeredAdapters.set(name, opts);
}
function unregister (name) {
if (!registeredAdapters.has(name)) {
throw new Error("adapter is not registered: " + name)
}
registeredAdapters.delete(name);
}
function getAdapterNames () {
let result = [];
for (let name of registeredAdapters.keys()) {
result.push(name);
}
return result;
}
function getAdapter (name) {
if (!registeredAdapters.has(name)) {
throw new Error("adapter is not registered: " + name)
}
return registeredAdapters.get(name);
}
function countAdapters () {
return registeredAdapters.size;
}
let observer = {
observe: function (subject, topic, data) {
switch (topic) {
case "cloudsync:user-sync":
eventSource.emit("sync");
break;
}
}
};
this.addEventListener = eventSource.addEventListener;
this.removeEventListener = eventSource.removeEventListener;
this.register = register.bind(this);
this.get = getAdapter.bind(this);
this.unregister = unregister.bind(this);
this.__defineGetter__("names", getAdapterNames);
this.__defineGetter__("count", countAdapters);
};
Adapters.prototype = {
};

Просмотреть файл

@ -0,0 +1,787 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["Bookmarks"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource:///modules/PlacesUIUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/CloudSyncPlacesWrapper.jsm");
Cu.import("resource://gre/modules/CloudSyncEventSource.jsm");
Cu.import("resource://gre/modules/CloudSyncBookmarksFolderCache.jsm");
const ITEM_TYPES = [
"NULL",
"BOOKMARK",
"FOLDER",
"SEPARATOR",
"DYNAMIC_CONTAINER", // no longer used by Places, but this ID should not be used for future item types
];
const CS_UNKNOWN = 0x1;
const CS_FOLDER = 0x1 << 1;
const CS_SEPARATOR = 0x1 << 2;
const CS_QUERY = 0x1 << 3;
const CS_LIVEMARK = 0x1 << 4;
const CS_BOOKMARK = 0x1 << 5;
const EXCLUDE_BACKUP_ANNO = "places/excludeFromBackup";
const DATA_VERSION = 1;
function asyncCallback(ctx, func, args) {
function invoke() {
func.apply(ctx, args);
}
CommonUtils.nextTick(invoke);
}
let Record = function (params) {
this.id = params.guid;
this.parent = params.parent || null;
this.index = params.position;
this.title = params.title;
this.dateAdded = Math.floor(params.dateAdded/1000);
this.lastModified = Math.floor(params.lastModified/1000);
this.uri = params.url;
let annos = params.annos || {};
Object.defineProperty(this, "annos", {
get: function () {
return annos;
},
enumerable: false
});
switch (params.type) {
case PlacesUtils.bookmarks.TYPE_FOLDER:
if (PlacesUtils.LMANNO_FEEDURI in annos) {
this.type = CS_LIVEMARK;
this.feed = annos[PlacesUtils.LMANNO_FEEDURI];
this.site = annos[PlacesUtils.LMANNO_SITEURI];
} else {
this.type = CS_FOLDER;
}
break;
case PlacesUtils.bookmarks.TYPE_BOOKMARK:
if (this.uri.startsWith("place:")) {
this.type = CS_QUERY;
} else {
this.type = CS_BOOKMARK;
}
break;
case PlacesUtils.bookmarks.TYPE_SEPARATOR:
this.type = CS_SEPARATOR;
break;
default:
this.type = CS_UNKNOWN;
}
};
Record.prototype = {
version: DATA_VERSION,
};
let Bookmarks = function () {
let createRootFolder = function (name) {
let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name;
let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name;
let deferred = Promise.defer();
let placesRootId = PlacesUtils.placesRootId;
let rootFolderId;
let rootShortcutId;
function createAdapterShortcut(result) {
rootFolderId = result;
let uri = "place:folder=" + rootFolderId;
return PlacesWrapper.insertBookmark(PlacesUIUtils.allBookmarksFolderId, uri,
PlacesUtils.bookmarks.DEFAULT_INDEX, name);
}
function setRootFolderCloudSyncAnnotation(result) {
rootShortcutId = result;
return PlacesWrapper.setItemAnnotation(rootFolderId, ROOT_FOLDER_ANNO,
1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
}
function setRootShortcutCloudSyncAnnotation() {
return PlacesWrapper.setItemAnnotation(rootShortcutId, ROOT_SHORTCUT_ANNO,
1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
}
function setRootFolderExcludeFromBackupAnnotation() {
return PlacesWrapper.setItemAnnotation(rootFolderId, EXCLUDE_BACKUP_ANNO,
1, 0, PlacesUtils.annotations.EXPIRE_NEVER);
}
function finish() {
deferred.resolve(rootFolderId);
}
Promise.resolve(PlacesUtils.bookmarks.createFolder(placesRootId, name, PlacesUtils.bookmarks.DEFAULT_INDEX))
.then(createAdapterShortcut)
.then(setRootFolderCloudSyncAnnotation)
.then(setRootShortcutCloudSyncAnnotation)
.then(setRootFolderExcludeFromBackupAnnotation)
.then(finish, deferred.reject);
return deferred.promise;
};
let getRootFolder = function (name) {
let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name;
let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name;
let deferred = Promise.defer();
function checkRootFolder(folderIds) {
if (!folderIds.length) {
return createRootFolder(name);
}
return Promise.resolve(folderIds[0]);
}
function createFolderObject(folderId) {
return new RootFolder(folderId, name);
}
PlacesWrapper.getLocalIdsWithAnnotation(ROOT_FOLDER_ANNO)
.then(checkRootFolder, deferred.reject)
.then(createFolderObject)
.then(deferred.resolve, deferred.reject);
return deferred.promise;
};
let deleteRootFolder = function (name) {
let ROOT_FOLDER_ANNO = "cloudsync/rootFolder/" + name;
let ROOT_SHORTCUT_ANNO = "cloudsync/rootShortcut/" + name;
let deferred = Promise.defer();
let placesRootId = PlacesUtils.placesRootId;
function getRootShortcutId() {
return PlacesWrapper.getLocalIdsWithAnnotation(ROOT_SHORTCUT_ANNO);
}
function deleteShortcut(shortcutIds) {
if (!shortcutIds.length) {
return Promise.resolve();
}
return PlacesWrapper.removeItem(shortcutIds[0]);
}
function getRootFolderId() {
return PlacesWrapper.getLocalIdsWithAnnotation(ROOT_FOLDER_ANNO);
}
function deleteFolder(folderIds) {
let deleteFolderDeferred = Promise.defer();
if (!folderIds.length) {
return Promise.resolve();
}
let rootFolderId = folderIds[0];
PlacesWrapper.removeFolderChildren(rootFolderId).then(
function () {
return PlacesWrapper.removeItem(rootFolderId);
}
).then(deleteFolderDeferred.resolve, deleteFolderDeferred.reject);
return deleteFolderDeferred.promise;
}
getRootShortcutId().then(deleteShortcut)
.then(getRootFolderId)
.then(deleteFolder)
.then(deferred.resolve, deferred.reject);
return deferred.promise;
};
/* PUBLIC API */
this.getRootFolder = getRootFolder.bind(this);
this.deleteRootFolder = deleteRootFolder.bind(this);
};
this.Bookmarks = Bookmarks;
let RootFolder = function (rootId, rootName) {
let suspended = true;
let ignoreAll = false;
let suspend = function () {
if (!suspended) {
PlacesUtils.bookmarks.removeObserver(observer);
suspended = true;
}
}.bind(this);
let resume = function () {
if (suspended) {
PlacesUtils.bookmarks.addObserver(observer, false);
suspended = false;
}
}.bind(this);
let eventTypes = [
"add",
"remove",
"change",
"move",
];
let eventSource = new EventSource(eventTypes, suspend, resume);
let folderCache = new FolderCache;
folderCache.insert(rootId, null);
let getCachedFolderIds = function (cache, roots) {
let nodes = [...roots];
let results = [];
while (nodes.length) {
let node = nodes.shift();
results.push(node);
let children = cache.getChildren(node);
nodes = nodes.concat([...children]);
}
return results;
};
let getLocalItems = function () {
let deferred = Promise.defer();
let folders = getCachedFolderIds(folderCache, folderCache.getChildren(rootId));
function getFolders(ids) {
let types = [
PlacesUtils.bookmarks.TYPE_FOLDER,
];
return PlacesWrapper.getItemsById(ids, types);
}
function getContents(parents) {
parents.push(rootId);
let types = [
PlacesUtils.bookmarks.TYPE_BOOKMARK,
PlacesUtils.bookmarks.TYPE_SEPARATOR,
];
return PlacesWrapper.getItemsByParentId(parents, types)
}
function getParentGuids(results) {
results = Array.prototype.concat.apply([], results);
let promises = [];
results.map(function (result) {
let promise = PlacesWrapper.localIdToGuid(result.parent).then(
function (guidResult) {
result.parent = guidResult;
return Promise.resolve(result);
},
Promise.reject
);
promises.push(promise);
});
return Promise.all(promises);
}
function getAnnos(results) {
results = Array.prototype.concat.apply([], results);
let promises = [];
results.map(function (result) {
let promise = PlacesWrapper.getItemAnnotationsForLocalId(result.id).then(
function (annos) {
result.annos = annos;
return Promise.resolve(result);
},
Promise.reject
);
promises.push(promise);
});
return Promise.all(promises);
}
let promises = [
getFolders(folders),
getContents(folders),
];
Promise.all(promises)
.then(getParentGuids)
.then(getAnnos)
.then(function (results) {
results = results.map((result) => new Record(result));
deferred.resolve(results);
},
deferred.reject);
return deferred.promise;
};
let getLocalItemsById = function (guids) {
let deferred = Promise.defer();
let types = [
PlacesUtils.bookmarks.TYPE_BOOKMARK,
PlacesUtils.bookmarks.TYPE_FOLDER,
PlacesUtils.bookmarks.TYPE_SEPARATOR,
PlacesUtils.bookmarks.TYPE_DYNAMIC_CONTAINER,
];
function getParentGuids(results) {
let promises = [];
results.map(function (result) {
let promise = PlacesWrapper.localIdToGuid(result.parent).then(
function (guidResult) {
result.parent = guidResult;
return Promise.resolve(result);
},
Promise.reject
);
promises.push(promise);
});
return Promise.all(promises);
}
PlacesWrapper.getItemsByGuid(guids, types)
.then(getParentGuids)
.then(function (results) {
results = results.map((result) => new Record(result));
deferred.resolve(results);
},
deferred.reject);
return deferred.promise;
};
let _createItem = function (item) {
let deferred = Promise.defer();
function getFolderId() {
if (item.parent) {
return PlacesWrapper.guidToLocalId(item.parent);
}
return Promise.resolve(rootId);
}
function create(folderId) {
let deferred = Promise.defer();
if (!folderId) {
folderId = rootId;
}
let index = item.hasOwnProperty("index") ? item.index : PlacesUtils.bookmarks.DEFAULT_INDEX;
function complete(localId) {
folderCache.insert(localId, folderId);
deferred.resolve(localId);
}
switch (item.type) {
case CS_BOOKMARK:
case CS_QUERY:
PlacesWrapper.insertBookmark(folderId, item.uri, index, item.title, item.id)
.then(complete, deferred.reject);
break;
case CS_FOLDER:
PlacesWrapper.createFolder(folderId, item.title, index, item.id)
.then(complete, deferred.reject);
break;
case CS_SEPARATOR:
PlacesWrapper.insertSeparator(folderId, index, item.id)
.then(complete, deferred.reject);
break;
case CS_LIVEMARK:
let livemark = {
title: item.title,
parentId: folderId,
index: item.index,
feedURI: item.feed,
siteURI: item.site,
guid: item.id,
};
PlacesUtils.livemarks.addLivemark(livemark)
.then(complete, deferred.reject);
break;
default:
deferred.reject("invalid item type: " + item.type);
}
return deferred.promise;
}
getFolderId().then(create)
.then(deferred.resolve, deferred.reject);
return deferred.promise;
};
let _deleteItem = function (item) {
let deferred = Promise.defer();
PlacesWrapper.guidToLocalId(item.id).then(
function (localId) {
folderCache.remove(localId);
return PlacesWrapper.removeItem(localId);
}
).then(deferred.resolve, deferred.reject);
return deferred.promise;
};
let _updateItem = function (item) {
let deferred = Promise.defer();
PlacesWrapper.guidToLocalId(item.id).then(
function (localId) {
let promises = [];
if (item.hasOwnProperty("dateAdded")) {
promises.push(PlacesWrapper.setItemDateAdded(localId, item.dateAdded));
}
if (item.hasOwnProperty("lastModified")) {
promises.push(PlacesWrapper.setItemLastModified(localId, item.lastModified));
}
if ((CS_BOOKMARK | CS_FOLDER) & item.type && item.hasOwnProperty("title")) {
promises.push(PlacesWrapper.setItemTitle(localId, item.title));
}
if (CS_BOOKMARK & item.type && item.hasOwnProperty("uri")) {
promises.push(PlacesWrapper.changeBookmarkURI(localId, item.uri));
}
if (item.hasOwnProperty("parent")) {
let deferred = Promise.defer();
PlacesWrapper.guidToLocalId(item.parent)
.then(
function (parent) {
let index = item.hasOwnProperty("index") ? item.index : PlacesUtils.bookmarks.DEFAULT_INDEX;
if (CS_FOLDER & item.type) {
folderCache.setParent(localId, parent);
}
return PlacesWrapper.moveItem(localId, parent, index);
}
)
.then(deferred.resolve, deferred.reject);
promises.push(deferred.promise);
}
if (item.hasOwnProperty("index") && !item.hasOwnProperty("parent")) {
promises.push(PlacesWrapper.bookmarks.setItemIndex(localId, item.index));
}
Promise.all(promises)
.then(deferred.resolve, deferred.reject);
}
);
return deferred.promise;
};
let mergeRemoteItems = function (items) {
ignoreAll = true;
let deferred = Promise.defer();
let newFolders = {};
let newItems = [];
let updatedItems = [];
let deletedItems = [];
let sortItems = function () {
let promises = [];
let exists = function (item) {
let existsDeferred = Promise.defer();
if (!item.id) {
Object.defineProperty(item, "__exists__", {
value: false,
enumerable: false
});
existsDeferred.resolve(item);
} else {
PlacesWrapper.guidToLocalId(item.id).then(
function (localId) {
Object.defineProperty(item, "__exists__", {
value: localId ? true : false,
enumerable: false
});
existsDeferred.resolve(item);
},
existsDeferred.reject
);
}
return existsDeferred.promise;
}
let handleSortedItem = function (item) {
if (!item.__exists__ && !item.deleted) {
if (CS_FOLDER == item.type) {
newFolders[item.id] = item;
item._children = [];
} else {
newItems.push(item);
}
} else if (item.__exists__ && item.deleted) {
deletedItems.push(item);
} else if (item.__exists__) {
updatedItems.push(item);
}
}
for each (let item in items) {
if (!item || 'object' !== typeof(item)) {
continue;
}
let promise = exists(item).then(handleSortedItem, Promise.reject);
promises.push(promise);
}
return Promise.all(promises);
}
let processNewFolders = function () {
let newFolderGuids = Object.keys(newFolders);
let newFolderRoots = [];
let promises = [];
for each (let guid in newFolderGuids) {
let item = newFolders[guid];
if (item.parent && newFolderGuids.indexOf(item.parent) >= 0) {
let parent = newFolders[item.parent];
parent._children.push(item.id);
} else {
newFolderRoots.push(guid);
}
};
let promises = [];
for each (let guid in newFolderRoots) {
let root = newFolders[guid];
let promise = Promise.resolve();
promise = promise.then(
function () {
return _createItem(root);
},
Promise.reject
);
let items = [].concat(root._children);
while (items.length) {
let item = newFolders[items.shift()];
items = items.concat(item._children);
promise = promise.then(
function () {
return _createItem(item);
},
Promise.reject
);
}
promises.push(promise);
}
return Promise.all(promises);
}
let processItems = function () {
let promises = [];
for each (let item in newItems) {
promises.push(_createItem(item));
}
for each (let item in updatedItems) {
promises.push(_updateItem(item));
}
for each (let item in deletedItems) {
_deleteItem(item);
}
return Promise.all(promises);
}
sortItems().then(processNewFolders)
.then(processItems)
.then(function () {
ignoreAll = false;
deferred.resolve(items);
},
function (err) {
ignoreAll = false;
deferred.reject(err);
});
return deferred.promise;
};
let ignore = function (id, parent) {
if (ignoreAll) {
return true;
}
if (rootId == parent || folderCache.has(parent)) {
return false;
}
return true;
};
let handleItemAdded = function (id, parent, index, type, uri, title, dateAdded, guid, parentGuid) {
let deferred = Promise.defer();
if (PlacesUtils.bookmarks.TYPE_FOLDER == type) {
folderCache.insert(id, parent);
}
eventSource.emit("add", guid);
deferred.resolve();
return deferred.promise;
};
let handleItemRemoved = function (id, parent, index, type, uri, guid, parentGuid) {
let deferred = Promise.defer();
if (PlacesUtils.bookmarks.TYPE_FOLDER == type) {
folderCache.remove(id);
}
eventSource.emit("remove", guid);
deferred.resolve();
return deferred.promise;
};
let handleItemChanged = function (id, property, isAnnotation, newValue, lastModified, type, parent, guid, parentGuid) {
let deferred = Promise.defer();
eventSource.emit('change', guid);
deferred.resolve();
return deferred.promise;
};
let handleItemMoved = function (id, oldParent, oldIndex, newParent, newIndex, type, guid, oldParentGuid, newParentGuid) {
let deferred = Promise.defer();
function complete() {
eventSource.emit('move', guid);
deferred.resolve();
}
if (PlacesUtils.bookmarks.TYPE_FOLDER != type) {
complete();
return deferred.promise;
}
if (folderCache.has(oldParent) && folderCache.has(newParent)) {
// Folder move inside cloudSync root, so just update parents/children.
folderCache.setParent(id, newParent);
complete();
} else if (!folderCache.has(oldParent)) {
// Folder moved in from ouside cloudSync root.
PlacesWrapper.updateCachedFolderIds(folderCache, newParent)
.then(complete, complete);
} else if (!folderCache.has(newParent)) {
// Folder moved out from inside cloudSync root.
PlacesWrapper.updateCachedFolderIds(folderCache, oldParent)
.then(complete, complete);
}
return deferred.promise;
};
let observer = {
onBeginBatchUpdate: function () {
},
onEndBatchUpdate: function () {
},
onItemAdded: function (id, parent, index, type, uri, title, dateAdded, guid, parentGuid) {
if (ignore(id, parent)) {
return;
}
asyncCallback(this, handleItemAdded, Array.prototype.slice.call(arguments));
},
onItemRemoved: function (id, parent, index, type, uri, guid, parentGuid) {
if (ignore(id, parent)) {
return;
}
asyncCallback(this, handleItemRemoved, Array.prototype.slice.call(arguments));
},
onItemChanged: function (id, property, isAnnotation, newValue, lastModified, type, parent, guid, parentGuid) {
if (ignore(id, parent)) {
return;
}
asyncCallback(this, handleItemChanged, Array.prototype.slice.call(arguments));
},
onItemMoved: function (id, oldParent, oldIndex, newParent, newIndex, type, guid, oldParentGuid, newParentGuid) {
if (ignore(id, oldParent) && ignore(id, newParent)) {
return;
}
asyncCallback(this, handleItemMoved, Array.prototype.slice.call(arguments));
}
};
/* PUBLIC API */
this.addEventListener = eventSource.addEventListener;
this.removeEventListener = eventSource.removeEventListener;
this.getLocalItems = getLocalItems.bind(this);
this.getLocalItemsById = getLocalItemsById.bind(this);
this.mergeRemoteItems = mergeRemoteItems.bind(this);
let rootGuid = null; // resolved before becoming ready (below)
this.__defineGetter__("id", function () {
return rootGuid;
});
this.__defineGetter__("name", function () {
return rootName;
});
let deferred = Promise.defer();
let getGuidForRootFolder = function () {
return PlacesWrapper.localIdToGuid(rootId);
}
PlacesWrapper.updateCachedFolderIds(folderCache, rootId)
.then(getGuidForRootFolder, getGuidForRootFolder)
.then(function (guid) {
rootGuid = guid;
deferred.resolve(this);
}.bind(this),
deferred.reject);
return deferred.promise;
};
RootFolder.prototype = {
BOOKMARK: CS_BOOKMARK,
FOLDER: CS_FOLDER,
SEPARATOR: CS_SEPARATOR,
QUERY: CS_QUERY,
LIVEMARK: CS_LIVEMARK,
};

Просмотреть файл

@ -0,0 +1,105 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FolderCache"];
// Cache for bookmarks folder heirarchy.
let FolderCache = function () {
this.cache = new Map();
}
FolderCache.prototype = {
has: function (id) {
return this.cache.has(id);
},
insert: function (id, parentId) {
if (this.cache.has(id)) {
return;
}
if (parentId && !(this.cache.has(parentId))) {
throw new Error("insert :: parentId not found in cache: " + parentId);
}
this.cache.set(id, {
parent: parentId || null,
children: new Set(),
});
if (parentId) {
this.cache.get(parentId).children.add(id);
}
},
remove: function (id) {
if (!(this.cache.has(id))) {
throw new Error("remote :: id not found in cache: " + id);
}
let parentId = this.cache.get(id).parent;
if (parentId) {
this.cache.get(parentId).children.delete(id);
}
for (let child of this.cache.get(id).children) {
this.cache.get(child).parent = null;
}
this.cache.delete(id);
},
setParent: function (id, parentId) {
if (!(this.cache.has(id))) {
throw new Error("setParent :: id not found in cache: " + id);
}
if (parentId && !(this.cache.has(parentId))) {
throw new Error("setParent :: parentId not found in cache: " + parentId);
}
let oldParent = this.cache.get(id).parent;
if (oldParent) {
this.cache.get(oldParent).children.delete(id);
}
this.cache.get(id).parent = parentId;
this.cache.get(parentId).children.add(id);
return true;
},
getParent: function (id) {
if (this.cache.has(id)) {
return this.cache.get(id).parent;
}
throw new Error("getParent :: id not found in cache: " + id);
},
getChildren: function (id) {
if (this.cache.has(id)) {
return this.cache.get(id).children;
}
throw new Error("getChildren :: id not found in cache: " + id);
},
setChildren: function (id, children) {
for (let child of children) {
if (!this.cache.has(child)) {
this.insert(child, id);
} else {
this.setParent(child, id);
}
}
},
dump: function () {
dump("FolderCache: " + JSON.stringify(this.cache) + "\n");
},
};
this.FolderCache = FolderCache;

Просмотреть файл

@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
this.EXPORTED_SYMBOLS = ["EventSource"];
Components.utils.import("resource://services-common/utils.js");
let EventSource = function (types, suspendFunc, resumeFunc) {
this.listeners = new Map();
for each (let type in types) {
this.listeners.set(type, new Set());
}
this.suspend = suspendFunc || function () {};
this.resume = resumeFunc || function () {};
this.addEventListener = this.addEventListener.bind(this);
this.removeEventListener = this.removeEventListener.bind(this);
};
EventSource.prototype = {
addEventListener: function (type, listener) {
if (!this.listeners.has(type)) {
return;
}
this.listeners.get(type).add(listener);
this.resume();
},
removeEventListener: function (type, listener) {
if (!this.listeners.has(type)) {
return;
}
this.listeners.get(type).delete(listener);
if (!this.hasListeners()) {
this.suspend();
}
},
hasListeners: function () {
for (let l of this.listeners.values()) {
if (l.size > 0) {
return true;
}
}
return false;
},
emit: function (type, arg) {
if (!this.listeners.has(type)) {
return;
}
CommonUtils.nextTick(
function () {
for (let listener of this.listeners.get(type)) {
listener.call(undefined, arg);
}
},
this
);
},
};
this.EventSource = EventSource;

Просмотреть файл

@ -0,0 +1,87 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["Local"];
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://services-common/stringbundle.js");
Cu.import("resource://services-common/utils.js");
Cu.import("resource://services-crypto/utils.js");
Cu.import("resource://gre/modules/Preferences.jsm");
function lazyStrings(name) {
let bundle = "chrome://weave/locale/services/" + name + ".properties";
return () => new StringBundle(bundle);
}
this.Str = {};
XPCOMUtils.defineLazyGetter(Str, "errors", lazyStrings("errors"));
XPCOMUtils.defineLazyGetter(Str, "sync", lazyStrings("sync"));
function makeGUID() {
return CommonUtils.encodeBase64URL(CryptoUtils.generateRandomBytes(9));
}
this.Local = function () {
let prefs = new Preferences("services.cloudsync.");
this.__defineGetter__("prefs", function () {
return prefs;
});
};
Local.prototype = {
get id() {
let clientId = this.prefs.get("client.GUID", "");
return clientId == "" ? this.id = makeGUID(): clientId;
},
set id(value) {
this.prefs.set("client.GUID", value);
},
get name() {
let clientName = this.prefs.get("client.name", "");
if (clientName != "") {
return clientName;
}
// Generate a client name if we don't have a useful one yet
let env = Cc["@mozilla.org/process/environment;1"]
.getService(Ci.nsIEnvironment);
let user = env.get("USER") || env.get("USERNAME");
let appName;
let brand = new StringBundle("chrome://branding/locale/brand.properties");
let brandName = brand.get("brandShortName");
try {
let syncStrings = new StringBundle("chrome://browser/locale/sync.properties");
appName = syncStrings.getFormattedString("sync.defaultAccountApplication", [brandName]);
} catch (ex) {
}
appName = appName || brandName;
let system =
// 'device' is defined on unix systems
Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("device") ||
// hostname of the system, usually assigned by the user or admin
Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2).get("host") ||
// fall back on ua info string
Cc["@mozilla.org/network/protocol;1?name=http"].getService(Ci.nsIHttpProtocolHandler).oscpu;
return this.name = Str.sync.get("client.name2", [user, appName, system]);
},
set name(value) {
this.prefs.set("client.name", value);
},
};

Просмотреть файл

@ -0,0 +1,392 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["PlacesWrapper"];
const {interfaces: Ci, utils: Cu} = Components;
const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR;
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource:///modules/PlacesUIUtils.jsm");
Cu.import("resource://services-common/utils.js");
let PlacesQueries = function () {
}
PlacesQueries.prototype = {
cachedStmts: {},
getQuery: function (queryString) {
if (queryString in this.cachedStmts) {
return this.cachedStmts[queryString];
}
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
return this.cachedStmts[queryString] = db.createAsyncStatement(queryString);
}
};
let PlacesWrapper = function () {
}
PlacesWrapper.prototype = {
placesQueries: new PlacesQueries(),
guidToLocalId: function (guid) {
let deferred = Promise.defer();
let stmt = "SELECT id AS item_id " +
"FROM moz_bookmarks " +
"WHERE guid = :guid";
let query = this.placesQueries.getQuery(stmt);
function getLocalId(results) {
let result = results[0] && results[0]["item_id"];
return Promise.resolve(result);
}
query.params.guid = guid.toString();
this.asyncQuery(query, ["item_id"])
.then(getLocalId, deferred.reject)
.then(deferred.resolve, deferred.reject);
return deferred.promise;
},
localIdToGuid: function (id) {
let deferred = Promise.defer();
let stmt = "SELECT guid " +
"FROM moz_bookmarks " +
"WHERE id = :item_id";
let query = this.placesQueries.getQuery(stmt);
function getGuid(results) {
let result = results[0] && results[0]["guid"];
return Promise.resolve(result);
}
query.params.item_id = id;
this.asyncQuery(query, ["guid"])
.then(getGuid, deferred.reject)
.then(deferred.resolve, deferred.reject);
return deferred.promise;
},
setGuidForLocalId: function (localId, guid) {
let deferred = Promise.defer();
let stmt = "UPDATE moz_bookmarks " +
"SET guid = :guid " +
"WHERE id = :item_id";
let query = this.placesQueries.getQuery(stmt);
query.params.guid = guid;
query.params.item_id = localId;
this.asyncQuery(query)
.then(deferred.resolve, deferred.reject);
return deferred.promise;
},
getItemsById: function (ids, types) {
let deferred = Promise.defer();
let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " +
"FROM moz_bookmarks b " +
"LEFT JOIN moz_places p ON b.fk = p.id " +
"WHERE b.id in (" + ids.join(",") + ") AND b.type in (" + types.join(",") + ")";
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
let query = db.createAsyncStatement(stmt);
this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"])
.then(deferred.resolve, deferred.reject);
return deferred.promise;
},
getItemsByParentId: function (parents, types) {
let deferred = Promise.defer();
let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " +
"FROM moz_bookmarks b " +
"LEFT JOIN moz_places p ON b.fk = p.id " +
"WHERE b.parent in (" + parents.join(",") + ") AND b.type in (" + types.join(",") + ")";
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
let query = db.createAsyncStatement(stmt);
this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"])
.then(deferred.resolve, deferred.reject);
return deferred.promise;
},
getItemsByGuid: function (guids, types) {
let deferred = Promise.defer();
guids = guids.map(JSON.stringify);
let stmt = "SELECT b.id, b.type, b.parent, b.position, b.title, b.guid, b.dateAdded, b.lastModified, p.url " +
"FROM moz_bookmarks b " +
"LEFT JOIN moz_places p ON b.fk = p.id " +
"WHERE b.guid in (" + guids.join(",") + ") AND b.type in (" + types.join(",") + ")";
let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
let query = db.createAsyncStatement(stmt);
this.asyncQuery(query, ["id", "type", "parent", "position", "title", "guid", "dateAdded", "lastModified", "url"])
.then(deferred.resolve, deferred.reject);
return deferred.promise;
},
updateCachedFolderIds: function (folderCache, folder) {
let deferred = Promise.defer();
let stmt = "SELECT id, guid " +
"FROM moz_bookmarks " +
"WHERE parent = :parent_id AND type = :item_type";
let query = this.placesQueries.getQuery(stmt);
query.params.parent_id = folder;
query.params.item_type = PlacesUtils.bookmarks.TYPE_FOLDER;
this.asyncQuery(query, ["id", "guid"]).then(
function (items) {
let previousIds = folderCache.getChildren(folder);
let currentIds = new Set();
for each (let item in items) {
currentIds.add(item.id);
}
let newIds = new Set();
let missingIds = new Set();
for (let currentId of currentIds) {
if (!previousIds.has(currentId)) {
newIds.add(currentId);
}
}
for (let previousId of previousIds) {
if (!currentIds.has(previousId)) {
missingIds.add(previousId);
}
}
folderCache.setChildren(folder, currentIds);
let promises = [];
for (let newId of newIds) {
promises.push(this.updateCachedFolderIds(folderCache, newId));
}
Promise.all(promises)
.then(deferred.resolve, deferred.reject);
for (let missingId of missingIds) {
folderCache.remove(missingId);
}
}.bind(this)
);
return deferred.promise;
},
getLocalIdsWithAnnotation: function (anno) {
let deferred = Promise.defer();
let stmt = "SELECT a.item_id " +
"FROM moz_anno_attributes n " +
"JOIN moz_items_annos a ON n.id = a.anno_attribute_id " +
"WHERE n.name = :anno_name";
let query = this.placesQueries.getQuery(stmt);
query.params.anno_name = anno.toString();
this.asyncQuery(query, ["item_id"])
.then(function (items) {
let results = [];
for each(let item in items) {
results.push(item.item_id);
}
deferred.resolve(results);
},
deferred.reject);
return deferred.promise;
},
getItemAnnotationsForLocalId: function (id) {
let deferred = Promise.defer();
let stmt = "SELECT a.name, b.content " +
"FROM moz_anno_attributes a " +
"JOIN moz_items_annos b ON a.id = b.anno_attribute_id " +
"WHERE b.item_id = :item_id";
let query = this.placesQueries.getQuery(stmt);
query.params.item_id = id;
this.asyncQuery(query, ["name", "content"])
.then(function (results) {
let annos = {};
for each(let result in results) {
annos[result.name] = result.content;
}
deferred.resolve(annos);
},
deferred.reject);
return deferred.promise;
},
insertBookmark: function (parent, uri, index, title, guid) {
let parsedURI;
try {
parsedURI = CommonUtils.makeURI(uri)
} catch (e) {
return Promise.reject("unable to parse URI '" + uri + "': " + e);
}
try {
let id = PlacesUtils.bookmarks.insertBookmark(parent, parsedURI, index, title, guid);
return Promise.resolve(id);
} catch (e) {
return Promise.reject("unable to insert bookmark " + JSON.stringify(arguments) + ": " + e);
}
},
setItemAnnotation: function (item, anno, value, flags, exp) {
try {
return Promise.resolve(PlacesUtils.annotations.setItemAnnotation(item, anno, value, flags, exp));
} catch (e) {
return Promise.reject(e);
}
},
itemHasAnnotation: function (item, anno) {
try {
return Promise.resolve(PlacesUtils.annotations.itemHasAnnotation(item, anno));
} catch (e) {
return Promise.reject(e);
}
},
createFolder: function (parent, name, index, guid) {
try {
return Promise.resolve(PlacesUtils.bookmarks.createFolder(parent, name, index, guid));
} catch (e) {
return Promise.reject("unable to create folder ['" + name + "']: " + e);
}
},
removeFolderChildren: function (folder) {
try {
PlacesUtils.bookmarks.removeFolderChildren(folder);
return Promise.resolve();
} catch (e) {
return Promise.reject(e);
}
},
insertSeparator: function (parent, index, guid) {
try {
return Promise.resolve(PlacesUtils.bookmarks.insertSeparator(parent, index, guid));
} catch (e) {
return Promise.reject(e);
}
},
removeItem: function (item) {
try {
return Promise.resolve(PlacesUtils.bookmarks.removeItem(item));
} catch (e) {
return Promise.reject(e);
}
},
setItemDateAdded: function (item, dateAdded) {
try {
return Promise.resolve(PlacesUtils.bookmarks.setItemDateAdded(item, dateAdded));
} catch (e) {
return Promise.reject(e);
}
},
setItemLastModified: function (item, lastModified) {
try {
return Promise.resolve(PlacesUtils.bookmarks.setItemLastModified(item, lastModified));
} catch (e) {
return Promise.reject(e);
}
},
setItemTitle: function (item, title) {
try {
return Promise.resolve(PlacesUtils.bookmarks.setItemTitle(item, title));
} catch (e) {
return Promise.reject(e);
}
},
changeBookmarkURI: function (item, uri) {
try {
uri = CommonUtils.makeURI(uri);
return Promise.resolve(PlacesUtils.bookmarks.changeBookmarkURI(item, uri));
} catch (e) {
return Promise.reject(e);
}
},
moveItem: function (item, parent, index) {
try {
return Promise.resolve(PlacesUtils.bookmarks.moveItem(item, parent, index));
} catch (e) {
return Promise.reject(e);
}
},
setItemIndex: function (item, index) {
try {
return Promise.resolve(PlacesUtils.bookmarks.setItemIndex(item, index));
} catch (e) {
return Promise.reject(e);
}
},
asyncQuery: function (query, names) {
let deferred = Promise.defer();
let storageCallback = {
results: [],
handleResult: function (results) {
if (!names) {
return;
}
let row;
while ((row = results.getNextRow()) != null) {
let item = {};
for each (let name in names) {
item[name] = row.getResultByName(name);
}
this.results.push(item);
}
},
handleError: function (error) {
deferred.reject(error);
},
handleCompletion: function (reason) {
if (REASON_ERROR == reason) {
return;
}
deferred.resolve(this.results);
}
};
query.executeAsync(storageCallback);
return deferred.promise;
},
};
this.PlacesWrapper = new PlacesWrapper();

Просмотреть файл

@ -0,0 +1,319 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["Tabs"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/CloudSyncEventSource.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://services-common/observers.js");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Session", "@mozilla.org/browser/sessionstore;1", "nsISessionStore");
const DATA_VERSION = 1;
let ClientRecord = function (params) {
this.id = params.id;
this.name = params.name || "?";
this.tabs = new Set();
}
ClientRecord.prototype = {
version: DATA_VERSION,
update: function (params) {
if (this.id !== params.id) {
throw new Error("expected " + this.id + " to equal " + params.id);
}
this.name = params.name;
}
};
let TabRecord = function (params) {
this.url = params.url || "";
this.update(params);
};
TabRecord.prototype = {
version: DATA_VERSION,
update: function (params) {
if (this.url && this.url !== params.url) {
throw new Error("expected " + this.url + " to equal " + params.url);
}
if (params.lastUsed && params.lastUsed < this.lastUsed) {
return;
}
this.title = params.title || "";
this.icon = params.icon || "";
this.lastUsed = params.lastUsed || 0;
},
};
let TabCache = function () {
this.tabs = new Map();
this.clients = new Map();
};
TabCache.prototype = {
merge: function (client, tabs) {
if (!client || !client.id) {
return;
}
if (!tabs) {
return;
}
let cRecord;
if (this.clients.has(client.id)) {
try {
cRecord = this.clients.get(client.id);
} catch (e) {
throw new Error("unable to update client: " + e);
}
} else {
cRecord = new ClientRecord(client);
this.clients.set(cRecord.id, cRecord);
}
for each (let tab in tabs) {
if (!tab || 'object' !== typeof(tab)) {
continue;
}
let tRecord;
if (this.tabs.has(tab.url)) {
tRecord = this.tabs.get(tab.url);
try {
tRecord.update(tab);
} catch (e) {
throw new Error("unable to update tab: " + e);
}
} else {
tRecord = new TabRecord(tab);
this.tabs.set(tRecord.url, tRecord);
}
if (tab.deleted) {
cRecord.tabs.delete(tRecord);
} else {
cRecord.tabs.add(tRecord);
}
}
},
clear: function (client) {
if (client) {
this.clients.delete(client.id);
} else {
this.clients = new Map();
this.tabs = new Map();
}
},
get: function () {
let results = [];
for (let client of this.clients.values()) {
results.push(client);
}
return results;
},
isEmpty: function () {
return 0 == this.clients.size;
},
};
this.Tabs = function () {
let suspended = true;
let topics = [
"pageshow",
"TabOpen",
"TabClose",
"TabSelect",
];
let update = function (event) {
if (event.originalTarget.linkedBrowser) {
let win = event.originalTarget.linkedBrowser.contentWindow;
if (PrivateBrowsingUtils.isWindowPrivate(win) &&
!PrivateBrowsingUtils.permanentPrivateBrowsing) {
return;
}
}
eventSource.emit("change");
};
let registerListenersForWindow = function (window) {
for each (let topic in topics) {
window.addEventListener(topic, update, false);
}
window.addEventListener("unload", unregisterListeners, false);
};
let unregisterListenersForWindow = function (window) {
window.removeEventListener("unload", unregisterListeners, false);
for each (let topic in topics) {
window.removeEventListener(topic, update, false);
}
};
let unregisterListeners = function (event) {
unregisterListenersForWindow(event.target);
};
let observer = {
observe: function (subject, topic, data) {
switch (topic) {
case "domwindowopened":
let onLoad = () => {
subject.removeEventListener("load", onLoad, false);
// Only register after the window is done loading to avoid unloads.
registerListenersForWindow(subject);
};
// Add tab listeners now that a window has opened.
subject.addEventListener("load", onLoad, false);
break;
}
}
};
let resume = function () {
if (suspended) {
Observers.add("domwindowopened", observer);
let wins = Services.wm.getEnumerator("navigator:browser");
while (wins.hasMoreElements()) {
registerListenersForWindow(wins.getNext());
}
}
}.bind(this);
let suspend = function () {
if (!suspended) {
Observers.remove("domwindowopened", observer);
let wins = Services.wm.getEnumerator("navigator:browser");
while (wins.hasMoreElements()) {
unregisterListenersForWindow(wins.getNext());
}
}
}.bind(this);
let eventTypes = [
"change",
];
let eventSource = new EventSource(eventTypes, suspend, resume);
let tabCache = new TabCache();
let getWindowEnumerator = function () {
return Services.wm.getEnumerator("navigator:browser");
};
let shouldSkipWindow = function (win) {
return win.closed ||
PrivateBrowsingUtils.isWindowPrivate(win);
};
let getTabState = function (tab) {
return JSON.parse(Session.getTabState(tab));
};
let getLocalTabs = function (filter) {
let deferred = Promise.defer();
filter = (undefined === filter) ? true : filter;
let filteredUrls = new RegExp("^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*)$"); // FIXME: should be a pref (B#1044304)
let allTabs = [];
let currentState = JSON.parse(Session.getBrowserState());
currentState.windows.forEach(function (window) {
if (window.isPrivate) {
return;
}
window.tabs.forEach(function (tab) {
if (!tab.entries.length) {
return;
}
// Get only the latest entry
// FIXME: support full history (B#1044306)
let entry = tab.entries[tab.index - 1];
if (!entry.url || filter && filteredUrls.test(entry.url)) {
return;
}
allTabs.push(new TabRecord({
title: entry.title,
url: entry.url,
icon: tab.attributes && tab.attributes.image || "",
lastUsed: tab.lastAccessed,
}));
});
});
deferred.resolve(allTabs);
return deferred.promise;
};
let mergeRemoteTabs = function (client, tabs) {
let deferred = Promise.defer();
deferred.resolve(tabCache.merge(client, tabs));
Observers.notify("cloudsync:tabs:update");
return deferred.promise;
};
let clearRemoteTabs = function (client) {
let deferred = Promise.defer();
deferred.resolve(tabCache.clear(client));
Observers.notify("cloudsync:tabs:update");
return deferred.promise;
};
let getRemoteTabs = function () {
let deferred = Promise.defer();
deferred.resolve(tabCache.get());
return deferred.promise;
};
let hasRemoteTabs = function () {
return !tabCache.isEmpty();
};
/* PUBLIC API */
this.addEventListener = eventSource.addEventListener;
this.removeEventListener = eventSource.removeEventListener;
this.getLocalTabs = getLocalTabs.bind(this);
this.mergeRemoteTabs = mergeRemoteTabs.bind(this);
this.clearRemoteTabs = clearRemoteTabs.bind(this);
this.getRemoteTabs = getRemoteTabs.bind(this);
this.hasRemoteTabs = hasRemoteTabs.bind(this);
};
Tabs.prototype = {
};
this.Tabs = Tabs;

Просмотреть файл

@ -0,0 +1,234 @@
### Importing the JS module
````
Cu.import("resource://gre/modules/CloudSync.jsm");
let cloudSync = CloudSync();
console.log(cloudSync); // Module is imported
````
### cloudSync.local
#### id
Local device ID. Is unique.
````
let localId = cloudSync.local.id;
````
#### name
Local device name.
````
let localName = cloudSync.local.name;
````
### CloudSync.tabs
#### addEventListener(type, callback)
Add an event handler for Tabs events. Valid type is `change`. The callback receives no arguments.
````
function handleTabChange() {
// Tabs have changed.
}
cloudSync.tabs.addEventListener("change", handleTabChange);
````
Change events are emitted when a tab is opened or closed, when a tab is selected, or when the page changes for an open tab.
#### removeEventListener(type, callback)
Remove an event handler. Pass the type and function that were passed to addEventListener.
````
cloudSync.tabs.removeEventListener("change", handleTabChange);
````
#### mergeRemoteTabs(client, tabs)
Merge remote tabs from upstream by updating existing items, adding new tabs, and deleting existing tabs. Accepts a client and a list of tabs. Returns a promise.
````
let remoteClient = {
id: "fawe78",
name: "My Firefox client",
};
let remoteTabs = [
{title: "Google",
url: "https://www.google.com",
icon: "https://www.google.com/favicon.ico",
lastUsed: 1400799296192},
{title: "Reddit",
url: "http://www.reddit.com",
icon: "http://www.reddit.com/favicon.ico",
lastUsed: 1400799296192
deleted: true},
];
cloudSync.tabs.mergeRemoteTabs(client, tabs).then(
function() {
console.log("merge complete");
}
);
````
#### getLocalTabs()
Returns a promise. Passes a list of local tabs when complete.
````
cloudSync.tabs.getLocalTabs().then(
function(tabs) {
console.log(JSON.stringify(tabs));
}
);
````
#### clearRemoteTabs(client)
Clears all tabs for a remote client.
````
let remoteClient = {
id: "fawe78",
name: "My Firefox client",
};
cloudSync.tabs.clearRemoteTabs(client);
````
### cloudSync.bookmarks
#### getRootFolder(name)
Gets the named root folder, creating it if it doesn't exist. The root folder object has a number of methods (see the next section for details).
````
cloudSync.bookmarks.getRootFolder("My Bookmarks").then(
function(rootFolder) {
console.log(rootFolder);
}
);
````
### cloudSync.bookmarks.RootFolder
This is a root folder object for bookmarks, created by `cloudSync.bookmarks.getRootFolder`.
#### BOOKMARK
Bookmark type. Used in results objects.
````
let bookmarkType = rootFolder.BOOKMARK;
````
#### FOLDER
Folder type. Used in results objects.
````
let folderType = rootFolder.FOLDER;
````
#### SEPARATOR
Separator type. Used in results objects.
````
let separatorType = rootFolder.SEPARATOR;
````
#### addEventListener(type, callback)
Add an event handler for Tabs events. Valid types are `add, remove, change, move`. The callback receives an ID corresponding to the target item.
````
function handleBoookmarkEvent(id) {
console.log("event for id:", id);
}
rootFolder.addEventListener("add", handleBookmarkEvent);
rootFolder.addEventListener("remove", handleBookmarkEvent);
rootFolder.addEventListener("change", handleBookmarkEvent);
rootFolder.addEventListener("move", handleBookmarkEvent);
````
#### removeEventListener(type, callback)
Remove an event handler. Pass the type and function that were passed to addEventListener.
````
rootFolder.removeEventListener("add", handleBookmarkEvent);
rootFolder.removeEventListener("remove", handleBookmarkEvent);
rootFolder.removeEventListener("change", handleBookmarkEvent);
rootFolder.removeEventListener("move", handleBookmarkEvent);
````
#### getLocalItems()
Callback receives a list of items on the local client. Results have the following form:
````
{
id: "faw8e7f", // item guid
parent: "f7sydf87y", // parent folder guid
dateAdded: 1400799296192, // timestamp
lastModified: 1400799296192, // timestamp
uri: "https://www.google.ca", // null for FOLDER and SEPARATOR
title: "Google"
type: rootFolder.BOOKMARK, // should be one of rootFolder.{BOOKMARK, FOLDER, SEPARATOR},
index: 0 // must be unique among folder items
}
````
````
rootFolder.getLocalItems().then(
function(items) {
console.log(JSON.stringify(items));
}
);
````
#### getLocalItemsById([...])
Callback receives a list of items, specified by ID, on the local client. Results have the same form as `getLocalItems()` above.
````
rootFolder.getLocalItemsById(["213r23f", "f22fy3f3"]).then(
function(items) {
console.log(JSON.stringify(items));
}
);
````
#### mergeRemoteItems([...])
Merge remote items from upstream by updating existing items, adding new items, and deleting existing items. Folders are created first so that subsequent operations will succeed. Items have the same form as `getLocalItems()` above. Items that do not have an ID will have an ID generated for them. The results structure will contain this generated ID.
````
rootFolder.mergeRemoteItems([
{
id: 'f2398f23',
type: rootFolder.FOLDER,
title: 'Folder 1',
parent: '9f8237f928'
},
{
id: '9f8237f928',
type: rootFolder.FOLDER,
title: 'Folder 0',
}
]).then(
function(items) {
console.log(items); // any generated IDs are filled in now
console.log("merge completed");
}
);
````

Просмотреть файл

@ -0,0 +1,54 @@
.. _cloudsync_architecture:
============
Architecture
============
CloudSync offers functionality similar to Firefox Sync for data sources. Third-party addons
(sync adapters) consume local data, send and receive updates from the cloud, and merge remote data.
Files
=====
CloudSync.jsm
Main module; Includes other modules and exposes them.
CloudSyncAdapters.jsm
Provides an API for addons to register themselves. Will be used to
list available adapters and to notify adapters when sync operations
are requested manually by the user.
CloudSyncBookmarks.jsm
Provides operations for interacting with bookmarks.
CloudSyncBookmarksFolderCache.jsm
Implements a cache used to store folder hierarchy for filtering bookmark events.
CloudSyncEventSource.jsm
Implements an event emitter. Used to provide addEventListener and removeEventListener
for tabs and bookmarks.
CloudSyncLocal.jsm
Provides information about the local device, such as name and a unique id.
CloudSyncPlacesWrapper.jsm
Wraps parts of the Places API in promises. Some methods are implemented to be asynchronous
where they are not in the places API.
CloudSyncTabs.jsm
Provides operations for fetching local tabs and for populating the about:sync-tabs page.
Data Sources
============
CloudSync provides data for tabs and bookmarks. For tabs, local open pages can be enumerated and
remote tabs can be merged for displaying in about:sync-tabs. For bookmarks, updates are tracked
for a named folder (given by each adapter) and handled by callbacks registered using addEventListener,
and remote changes can be merged into the local database.
Versioning
==========
The API carries an integer version number (clouySync.version). Data records are versioned separately and individually.

Просмотреть файл

@ -0,0 +1,77 @@
.. _cloudsync_dataformat:
=========
Data Format
=========
All fields are required unless noted otherwise.
Bookmarks
=========
Record
------
type:
record type; one of CloudSync.bookmarks.{BOOKMARK, FOLDER, SEPARATOR, QUERY, LIVEMARK}
id:
GUID for this bookmark item
parent:
id of parent folder
index:
item index in parent folder; should be unique and contiguous, or they will be adjusted internally
title:
bookmark or folder title; not meaningful for separators
dateAdded:
timestamp (in milliseconds) for item added
lastModified:
timestamp (in milliseconds) for last modification
uri:
bookmark URI; not meaningful for folders or separators
version:
data layout version
Tabs
====
ClientRecord
------------
id:
GUID for this client
name:
name for this client; not guaranteed to be unique
tabs:
list of tabs open on this client; see TabRecord
version:
data layout version
TabRecord
---------
title:
name for this tab
url:
URL for this tab; only one tab for each URL is stored
icon:
favicon URL for this tab; optional
lastUsed:
timetamp (in milliseconds) for last use
version:
data layout version

Просмотреть файл

@ -0,0 +1,132 @@
.. _cloudsync_example:
=======
Example
=======
.. code-block:: javascript
Cu.import("resource://gre/modules/CloudSync.jsm");
let HelloWorld = {
onLoad: function() {
let cloudSync = CloudSync();
console.log("CLOUDSYNC -- hello world", cloudSync.local.id, cloudSync.local.name, cloudSync.adapters);
cloudSync.adapters.register('helloworld', {});
console.log("CLOUDSYNC -- " + JSON.stringify(cloudSync.adapters.getAdapterNames()));
cloudSync.tabs.addEventListener("change", function() {
console.log("tab change");
cloudSync.tabs.getLocalTabs().then(
function(records) {
console.log(JSON.stringify(records));
}
);
});
cloudSync.tabs.getLocalTabs().then(
function(records) {
console.log(JSON.stringify(records));
}
);
let remoteClient = {
id: "001",
name: "FakeClient",
};
let remoteTabs1 = [
{url:"https://www.google.ca",title:"Google",icon:"https://www.google.ca/favicon.ico",lastUsed:Date.now()},
];
let remoteTabs2 = [
{url:"https://www.google.ca",title:"Google Canada",icon:"https://www.google.ca/favicon.ico",lastUsed:Date.now()},
{url:"http://www.reddit.com",title:"Reddit",icon:"http://www.reddit.com/favicon.ico",lastUsed:Date.now()},
];
cloudSync.tabs.mergeRemoteTabs(remoteClient, remoteTabs1).then(
function() {
return cloudSync.tabs.mergeRemoteTabs(remoteClient, remoteTabs2);
}
).then(
function() {
return cloudSync.tabs.getRemoteTabs();
}
).then(
function(tabs) {
console.log("remote tabs:", tabs);
}
);
cloudSync.bookmarks.getRootFolder("Hello World").then(
function(rootFolder) {
console.log(rootFolder.name, rootFolder.id);
rootFolder.addEventListener("add", function(guid) {
console.log("CLOUDSYNC -- bookmark item added: " + guid);
rootFolder.getLocalItemsById([guid]).then(
function(items) {
console.log("CLOUDSYNC -- items: " + JSON.stringify(items));
}
);
});
rootFolder.addEventListener("remove", function(guid) {
console.log("CLOUDSYNC -- bookmark item removed: " + guid);
rootFolder.getLocalItemsById([guid]).then(
function(items) {
console.log("CLOUDSYNC -- items: " + JSON.stringify(items));
}
);
});
rootFolder.addEventListener("change", function(guid) {
console.log("CLOUDSYNC -- bookmark item changed: " + guid);
rootFolder.getLocalItemsById([guid]).then(
function(items) {
console.log("CLOUDSYNC -- items: " + JSON.stringify(items));
}
);
});
rootFolder.addEventListener("move", function(guid) {
console.log("CLOUDSYNC -- bookmark item moved: " + guid);
rootFolder.getLocalItemsById([guid]).then(
function(items) {
console.log("CLOUDSYNC -- items: " + JSON.stringify(items));
}
);
});
function logLocalItems() {
return rootFolder.getLocalItems().then(
function(items) {
console.log("CLOUDSYNC -- local items: " + JSON.stringify(items));
}
);
}
let items = [
{"id":"9fdoci2KOME6","type":rootFolder.FOLDER,"parent":rootFolder.id,"title":"My Bookmarks 1"},
{"id":"1fdoci2KOME5","type":rootFolder.FOLDER,"parent":rootFolder.id,"title":"My Bookmarks 2"},
{"id":"G_UL4ZhOyX8m","type":rootFolder.BOOKMARK,"parent":"1fdoci2KOME5","title":"reddit: the front page of the internet","uri":"http://www.reddit.com/"},
];
function mergeSomeItems() {
return rootFolder.mergeRemoteItems(items);
}
logLocalItems().then(
mergeSomeItems
).then(
function(processedItems) {
console.log("!!!", processedItems);
console.log("merge complete");
},
function(error) {
console.log("merge failed:", error);
}
).then(
logLocalItems
);
}
);
},
};
window.addEventListener("load", function(e) { HelloWorld.onLoad(e); }, false);

Просмотреть файл

@ -0,0 +1,19 @@
.. _cloudsync:
=====================
CloudSync
=====================
CloudSync is a service that provides access to tabs and bookmarks data
for third-party sync addons. Addons can read local bookmarks and tabs.
Bookmarks and tab data can be merged from remote devices.
Addons are responsible for maintaining an upstream representation, as
well as sending and receiving data over the network.
.. toctree::
:maxdepth: 1
architecture
dataformat
example

Просмотреть файл

@ -0,0 +1,21 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
SPHINX_TREES['cloudsync'] = 'docs'
EXTRA_JS_MODULES += [
'CloudSync.jsm',
'CloudSyncAdapters.jsm',
'CloudSyncBookmarks.jsm',
'CloudSyncBookmarksFolderCache.jsm',
'CloudSyncEventSource.jsm',
'CloudSyncLocal.jsm',
'CloudSyncPlacesWrapper.jsm',
'CloudSyncTabs.jsm',
]
XPCSHELL_TESTS_MANIFESTS += ['tests/xpcshell/xpcshell.ini']
BROWSER_CHROME_MANIFESTS += ['tests/mochitest/browser.ini']

Просмотреть файл

@ -0,0 +1,5 @@
[DEFAULT]
support-files=
other_window.html
[browser_tabEvents.js]

Просмотреть файл

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
function test() {
let local = {};
Components.utils.import("resource://gre/modules/CloudSync.jsm", local);
Components.utils.import("resource:///modules/sessionstore/TabState.jsm", local);
let cloudSync = local.CloudSync();
let opentabs = [];
waitForExplicitFinish();
let testURL = "chrome://mochitests/content/browser/services/cloudsync/tests/mochitest/other_window.html";
let expected = [
testURL,
testURL+"?x=1",
testURL+"?x=%20a",
// testURL+"?x=å",
];
let nevents = 0;
function handleTabChangeEvent () {
cloudSync.tabs.removeEventListener("change", handleTabChangeEvent);
++ nevents;
}
function getLocalTabs() {
cloudSync.tabs.getLocalTabs().then(
function (tabs) {
for (let tab of tabs) {
ok(expected.indexOf(tab.url) >= 0, "found an expected tab");
}
is(tabs.length, expected.length, "found the right number of tabs");
opentabs.forEach(function (tab) {
gBrowser.removeTab(tab);
});
is(nevents, 1, "expected number of change events");
finish();
}
)
}
cloudSync.tabs.addEventListener("change", handleTabChangeEvent);
let nflushed = 0;
expected.forEach(function(url) {
let tab = gBrowser.addTab(url);
function flush() {
tab.linkedBrowser.removeEventListener("load", flush);
local.TabState.flush(tab.linkedBrowser);
++ nflushed;
if (nflushed == expected.length) {
getLocalTabs();
}
}
tab.linkedBrowser.addEventListener("load", flush, true);
opentabs.push(tab);
});
}

Просмотреть файл

@ -0,0 +1,7 @@
<!--
Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/
-->
<!DOCTYPE HTML>
<html>
</html>

Просмотреть файл

@ -0,0 +1,10 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
"use strict";
(function initCloudSyncTestingInfrastructure () {
do_get_profile();
}).call(this);

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше