Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2013-10-17 13:49:27 +02:00
Родитель f6b36f29fe 7e043134db
Коммит 390a0af4b0
84 изменённых файлов: 3429 добавлений и 2419 удалений

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

@ -1,4 +1,4 @@
{
"revision": "86e06b1db110e34eb66826d3b1bdee3a5d57b3a7",
"revision": "563d1aa93586165246ab2ab9d40566a598f56387",
"repo_path": "/integration/gaia-central"
}

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

@ -482,7 +482,7 @@
}
// null parameter below specifies HTML response for search
var submission = this.currentEngine.getSubmission(aData);
var submission = this.currentEngine.getSubmission(aData, null, "searchbar");
BrowserSearch.recordSearchInHealthReport(this.currentEngine.name, "searchbar");
openUILinkIn(submission.uri.spec, aWhere, null, submission.postData);
]]></body>
@ -531,14 +531,14 @@
var engine = this.currentEngine;
var connector =
Services.io.QueryInterface(Components.interfaces.nsISpeculativeConnect);
var searchURI = engine.getSubmission("dummy").uri;
var searchURI = engine.getSubmission("dummy", null, "searchbar").uri;
let callbacks = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIWebNavigation)
.QueryInterface(Components.interfaces.nsILoadContext);
connector.speculativeConnect(searchURI, callbacks);
if (engine.supportsResponseType(SUGGEST_TYPE)) {
var suggestURI = engine.getSubmission("dummy", SUGGEST_TYPE).uri;
var suggestURI = engine.getSubmission("dummy", SUGGEST_TYPE, "searchbar").uri;
if (suggestURI.prePath != searchURI.prePath)
connector.speculativeConnect(suggestURI, callbacks);
}

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

@ -5,6 +5,7 @@
ifdef ENABLE_TESTS
pp_mochitest_browser_files := \
browser_google.js \
browser_google_behavior.js \
$(NULL)
pp_mochitest_browser_files_PATH := $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
pp_mochitest_browser_files_FLAGS := -DMOZ_DISTRIBUTION_ID=$(MOZ_DISTRIBUTION_ID)

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

@ -80,6 +80,10 @@ function test() {
is(url, base + "&channel=rcs", "Check context menu search URL for 'foo'");
url = engine.getSubmission("foo", null, "keyword").uri.spec;
is(url, base + "&channel=fflb", "Check keyword search URL for 'foo'");
url = engine.getSubmission("foo", null, "searchbar").uri.spec;
is(url, base + "&channel=sb", "Check search bar search URL for 'foo'");
url = engine.getSubmission("foo", null, "homepage").uri.spec;
is(url, base + "&channel=np&source=hp", "Check homepage search URL for 'foo'");
// Check search suggestion URL.
url = engine.getSubmission("foo", "application/x-suggestions+json").uri.spec;
@ -147,6 +151,11 @@ function test() {
"value": "fflb",
"purpose": "keyword",
},
{
"name": "channel",
"value": "sb",
"purpose": "searchbar",
},
{
"name": "channel",
"value": "np",

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

@ -0,0 +1,187 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* Test Google search plugin URLs
*/
"use strict";
const BROWSER_SEARCH_PREF = "browser.search.";
const MOZ_PARAM_LOCALE = /\{moz:locale\}/g;
const MOZ_PARAM_DIST_ID = /\{moz:distributionID\}/g;
const MOZ_PARAM_OFFICIAL = /\{moz:official\}/g;
// Custom search parameters
#ifdef MOZ_OFFICIAL_BRANDING
const MOZ_OFFICIAL = "official";
#else
const MOZ_OFFICIAL = "unofficial";
#endif
#if MOZ_UPDATE_CHANNEL == beta
const GOOGLE_CLIENT = "firefox-beta";
#elif MOZ_UPDATE_CHANNEL == aurora
const GOOGLE_CLIENT = "firefox-aurora";
#elif MOZ_UPDATE_CHANNEL == nightly
const GOOGLE_CLIENT = "firefox-nightly";
#else
const GOOGLE_CLIENT = "firefox-a";
#endif
#expand const MOZ_DISTRIBUTION_ID = __MOZ_DISTRIBUTION_ID__;
function getLocale() {
const localePref = "general.useragent.locale";
return getLocalizedPref(localePref, Services.prefs.getCharPref(localePref));
}
function getLocalizedPref(aPrefName, aDefault) {
try {
return Services.prefs.getComplexValue(aPrefName, Ci.nsIPrefLocalizedString).data;
} catch (ex) {
return aDefault;
}
return aDefault;
}
function test() {
let engine = Services.search.getEngineByName("Google");
ok(engine, "Google is installed");
is(Services.search.defaultEngine, engine, "Check that Google is the default search engine");
let distributionID;
try {
distributionID = Services.prefs.getCharPref(BROWSER_SEARCH_PREF + "distributionID");
} catch (ex) {
distributionID = MOZ_DISTRIBUTION_ID;
}
let base = "https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t&rls={moz:distributionID}:{moz:locale}:{moz:official}&client=" + GOOGLE_CLIENT;
base = base.replace(MOZ_PARAM_LOCALE, getLocale());
base = base.replace(MOZ_PARAM_DIST_ID, distributionID);
base = base.replace(MOZ_PARAM_OFFICIAL, MOZ_OFFICIAL);
let url;
// Test search URLs (including purposes).
url = engine.getSubmission("foo").uri.spec;
is(url, base, "Check search URL for 'foo'");
waitForExplicitFinish();
var gCurrTest;
var gTests = [
{
name: "context menu search",
searchURL: base + "&channel=rcs",
run: function () {
// Simulate a contextmenu search
// FIXME: This is a bit "low-level"...
BrowserSearch.loadSearch("foo", false, "contextmenu");
}
},
{
name: "keyword search",
searchURL: base + "&channel=fflb",
run: function () {
gURLBar.value = "? foo";
gURLBar.focus();
EventUtils.synthesizeKey("VK_RETURN", {});
}
},
{
name: "search bar search",
searchURL: base + "&channel=sb",
run: function () {
let sb = BrowserSearch.searchBar;
sb.focus();
sb.value = "foo";
registerCleanupFunction(function () {
sb.value = "";
});
EventUtils.synthesizeKey("VK_RETURN", {});
}
},
{
name: "home page search",
searchURL: base + "&channel=np&source=hp",
run: function () {
// load about:home, but remove the listener first so it doesn't
// get in the way
gBrowser.removeProgressListener(listener);
gBrowser.loadURI("about:home");
info("Waiting for about:home load");
tab.linkedBrowser.addEventListener("load", function load(event) {
if (event.originalTarget != tab.linkedBrowser.contentDocument ||
event.target.location.href == "about:blank") {
info("skipping spurious load event");
return;
}
tab.linkedBrowser.removeEventListener("load", load, true);
// Observe page setup
let doc = gBrowser.contentDocument;
let mutationObserver = new MutationObserver(function (mutations) {
for (let mutation of mutations) {
if (mutation.attributeName == "searchEngineName") {
// Re-add the listener, and perform a search
gBrowser.addProgressListener(listener);
doc.getElementById("searchText").value = "foo";
doc.getElementById("searchSubmit").click();
}
}
});
mutationObserver.observe(doc.documentElement, { attributes: true });
}, true);
}
}
];
function nextTest() {
if (gTests.length) {
gCurrTest = gTests.shift();
info("Running : " + gCurrTest.name);
executeSoon(gCurrTest.run);
} else {
finish();
}
}
let tab = gBrowser.selectedTab = gBrowser.addTab();
let listener = {
onStateChange: function onStateChange(webProgress, req, flags, status) {
info("onStateChange");
// Only care about top-level document starts
let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
Ci.nsIWebProgressListener.STATE_START;
if (!(flags & docStart) || !webProgress.isTopLevel)
return;
info("received document start");
ok(req instanceof Ci.nsIChannel, "req is a channel");
is(req.originalURI.spec, gCurrTest.searchURL, "search URL was loaded");
info("Actual URI: " + req.URI.spec);
req.cancel(Components.results.NS_ERROR_FAILURE);
executeSoon(nextTest);
}
}
registerCleanupFunction(function () {
gBrowser.removeProgressListener(listener);
gBrowser.removeTab(tab);
});
tab.linkedBrowser.addEventListener("load", function load() {
tab.linkedBrowser.removeEventListener("load", load, true);
gBrowser.addProgressListener(listener);
nextTest();
}, true);
}

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

@ -25,6 +25,7 @@
#endif
<MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
<MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
<MozParam name="channel" condition="purpose" purpose="searchbar" value="sb"/>
<MozParam name="channel" condition="purpose" purpose="homepage" value="np"/>
<MozParam name="source" condition="purpose" purpose="homepage" value="hp"/>
</Url>

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

@ -9,7 +9,7 @@
* link parameter/model object expected to have a .url property, and optionally .title
*/
function Site(aLink) {
if(!aLink.url) {
if (!aLink.url) {
throw Cr.NS_ERROR_INVALID_ARG;
}
this._link = aLink;
@ -64,7 +64,7 @@ Site.prototype = {
}
}
// is binding already applied?
if (aNode.refresh) {
if ('refresh' in aNode) {
// just update it
aNode.refresh();
} else {

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

@ -27,7 +27,7 @@
<field name="controller">null</field>
<!-- collection of child items excluding empty tiles -->
<property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem');"/>
<property name="items" readonly="true" onget="return this.querySelectorAll('richgriditem[value]');"/>
<property name="itemCount" readonly="true" onget="return this.items.length;"/>
<!-- nsIDOMXULMultiSelectControlElement (not fully implemented) -->
@ -96,7 +96,7 @@
<parameter name="aEvent"/>
<body>
<![CDATA[
if(!this.isBound)
if (!this.isBound)
return;
if ("single" == this.getAttribute("seltype")) {
@ -116,7 +116,7 @@
<parameter name="aEvent"/>
<body>
<![CDATA[
if(!this.isBound || this.suppressOnSelect)
if (!this.isBound || this.suppressOnSelect)
return;
// we'll republish this as a selectionchange event on the grid
aEvent.stopPropagation();
@ -175,7 +175,7 @@
<property name="selectedItems">
<getter>
<![CDATA[
return this.querySelectorAll("richgriditem[selected]");
return this.querySelectorAll("richgriditem[value][selected]");
]]>
</getter>
</property>
@ -204,28 +204,105 @@
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
let addition = this._createItemElement(aLabel, aValue);
this.appendChild(addition);
let item = this.nextSlot();
item.setAttribute("value", aValue);
item.setAttribute("label", aLabel);
if (!aSkipArrange)
this.arrangeItems();
return addition;
return item;
]]>
</body>
</method>
<method name="_slotValues">
<body><![CDATA[
return Array.map(this.children, (cnode) => cnode.getAttribute("value"));
]]></body>
</method>
<property name="minSlots" readonly="true"
onget="return this.getAttribute('minSlots') || 3;"/>
<method name="clearAll">
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
while (this.firstChild) {
this.removeChild(this.firstChild);
const ELEMENT_NODE_TYPE = Components.interfaces.nsIDOMNode.ELEMENT_NODE;
let slotCount = this.minSlots;
let childIndex = 0;
let child = this.firstChild;
while (child) {
// remove excess elements and non-element nodes
if (child.nodeType !== ELEMENT_NODE_TYPE || childIndex+1 > slotCount) {
let orphanNode = child;
child = orphanNode.nextSibling;
this.removeChild(orphanNode);
continue;
}
if (child.hasAttribute("value")) {
this._releaseSlot(child);
}
child = child.nextSibling;
childIndex++;
}
// create our quota of item slots
for (let count = this.childElementCount; count < slotCount; count++) {
this.appendChild( this._createItemElement() );
}
if (!aSkipArrange)
this.arrangeItems();
]]>
</body>
</method>
<method name="_slotAt">
<parameter name="anIndex"/>
<body>
<![CDATA[
// backfill with new slots as necessary
let count = Math.max(1+anIndex, this.minSlots) - this.childElementCount;
for (; count > 0; count--) {
this.appendChild( this._createItemElement() );
}
return this.children[anIndex];
]]>
</body>
</method>
<method name="nextSlot">
<body>
<![CDATA[
if (!this.itemCount) {
return this._slotAt(0);
}
let lastItem = this.items[this.itemCount-1];
let nextIndex = 1 + Array.indexOf(this.children, lastItem);
return this._slotAt(nextIndex);
]]>
</body>
</method>
<method name="_releaseSlot">
<parameter name="anItem"/>
<body>
<![CDATA[
// Flush out data and state attributes so we can recycle this slot/element
let exclude = { value: 1, tiletype: 1 };
let attrNames = [attr.name for (attr of anItem.attributes)];
for (let attrName of attrNames) {
if (!(attrName in exclude))
anItem.removeAttribute(attrName);
}
// clear out inline styles
anItem.removeAttribute("style");
// finally clear the value, which should apply the richgrid-empty-item binding
anItem.removeAttribute("value");
]]>
</body>
</method>
<method name="insertItemAt">
<parameter name="anIndex"/>
<parameter name="aLabel"/>
@ -233,19 +310,30 @@
<parameter name="aSkipArrange"/>
<body>
<![CDATA[
anIndex = Math.min(this.itemCount, anIndex);
let insertedItem;
let existing = this.getItemAtIndex(anIndex);
let addition = this._createItemElement(aLabel, aValue);
if (existing) {
this.insertBefore(addition, existing);
} else {
this.appendChild(addition);
// use an empty slot if we have one, otherwise insert it
let childIndex = Array.indexOf(this.children, existing);
if (childIndex > 0 && !this.children[childIndex-1].hasAttribute("value")) {
insertedItem = this.children[childIndex-1];
} else {
insertedItem = this.insertBefore(this._createItemElement(),existing);
}
}
if (!insertedItem) {
insertedItem = this._slotAt(anIndex);
}
insertedItem.setAttribute("value", aValue);
insertedItem.setAttribute("label", aLabel);
if (!aSkipArrange)
this.arrangeItems();
return addition;
return insertedItem;
]]>
</body>
</method>
<method name="removeItemAt">
<parameter name="anIndex"/>
<parameter name="aSkipArrange"/>
@ -266,7 +354,13 @@
<![CDATA[
if (!aItem || Array.indexOf(this.items, aItem) < 0)
return null;
let removal = this.removeChild(aItem);
// replace the slot if necessary
if (this.childElementCount < this.minSlots) {
this.nextSlot();
}
if (removal && !aSkipArrange)
this.arrangeItems();
@ -422,6 +516,7 @@
<field name="_scheduledArrangeItemsTimerId">null</field>
<field name="_scheduledArrangeItemsTries">0</field>
<field name="_maxArrangeItemsRetries">5</field>
<method name="_scheduleArrangeItems">
<parameter name="aTime"/>
<body>
@ -453,6 +548,7 @@
let itemDims = this._itemSize;
let containerDims = this._containerSize;
let slotsCount = this.childElementCount;
// reset the flags
if (this._scheduledArrangeItemsTimerId) {
@ -468,25 +564,25 @@
if (this.hasAttribute("vertical")) {
this._columnCount = Math.floor(containerDims.width / itemDims.width) || 1;
this._rowCount = Math.floor(this.itemCount / this._columnCount);
this._rowCount = Math.floor(slotsCount / this._columnCount);
} else {
// We favor overflowing horizontally, not vertically (rows then colums)
// rows attribute = max rows
let maxRowCount = Math.min(this.getAttribute("rows") || Infinity, Math.floor(containerDims.height / itemDims.height));
this._rowCount = Math.min(this.itemCount, maxRowCount);
// rows attribute is fixed number of rows
let maxRows = Math.floor(containerDims.height / itemDims.height);
this._rowCount = this.getAttribute("rows") ?
// fit indicated rows when possible
Math.min(maxRows, this.getAttribute("rows")) :
// at least 1 row
Math.min(maxRows, slotsCount) || 1;
// columns attribute = min cols
this._columnCount = this.itemCount ?
Math.max(
// at least 1 column when there are items
this.getAttribute("columns") || 1,
Math.ceil(this.itemCount / this._rowCount)
) : this.getAttribute("columns") || 0;
// columns attribute is min number of cols
this._columnCount = Math.ceil(slotsCount / this._rowCount) || 1;
if (this.getAttribute("columns")) {
this._columnCount = Math.max(this._columnCount, this.getAttribute("columns"));
}
}
// width is typically auto, cap max columns by truncating items collection
// or, setting max-width style property with overflow hidden
// '0' is an invalid value, just leave the property unset when 0 columns
if (this._columnCount) {
gridStyle.MozColumnCount = this._columnCount;
}
@ -521,6 +617,12 @@
<field name="_xslideHandler"/>
<constructor>
<![CDATA[
// create our quota of item slots
for (let count = this.childElementCount, slotCount = this.minSlots;
count < slotCount; count++) {
this.appendChild( this._createItemElement() );
}
if (this.controller && this.controller.gridBoundCallback != undefined)
this.controller.gridBoundCallback();
@ -605,6 +707,7 @@
]]>
</body>
</method>
<method name="_isIndexInBounds">
<parameter name="anIndex"/>
<body>
@ -626,7 +729,7 @@
if (aLabel) {
item.setAttribute("label", aLabel);
}
if(this.hasAttribute("tiletype")) {
if (this.hasAttribute("tiletype")) {
item.setAttribute("tiletype", this.getAttribute("tiletype"));
}
return item;
@ -704,6 +807,7 @@
aItem.setAttribute("bending", true);
]]></body>
</method>
<method name="unbendItem">
<parameter name="aItem"/>
<body><![CDATA[
@ -749,7 +853,7 @@
let state = event.crossSlidingState;
let thresholds = this._xslideHandler.thresholds;
let transformValue;
switch(state) {
switch (state) {
case "cancelled":
this.unbendItem(event.target);
event.target.removeAttribute('crosssliding');
@ -844,7 +948,7 @@
<body>
<![CDATA[
// Prevent an exception in case binding is not done yet.
if(!this.isBound)
if (!this.isBound)
return;
// Seed the binding properties from bound-node attribute values
@ -914,7 +1018,7 @@
<method name="refreshBackgroundImage">
<body><![CDATA[
if(!this.isBound)
if (!this.isBound)
return;
if (this.backgroundImage) {
this._top.style.removeProperty("background-image");
@ -927,7 +1031,7 @@
<property name="contextActions">
<getter>
<![CDATA[
if(!this._contextActions) {
if (!this._contextActions) {
this._contextActions = new Set();
let actionSet = this._contextActions;
let actions = this.getAttribute("data-contextactions");
@ -959,7 +1063,7 @@
// fires for right-click, long-click and (keyboard) contextmenu input
// toggle the selected state of tiles in a grid
let gridParent = this.control;
if(!this.isBound || !gridParent)
if (!this.isBound || !gridParent)
return;
gridParent.handleItemContextMenu(this, event);
]]>
@ -967,4 +1071,10 @@
</handlers>
</binding>
<binding id="richgrid-empty-item">
<content>
<html:div anonid="anon-tile" class="tile-content"></html:div>
</content>
</binding>
</bindings>

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

@ -119,6 +119,9 @@ richgrid {
}
richgriditem {
-moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid-empty-item");
}
richgriditem[value] {
-moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid-item");
}

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

@ -73,11 +73,11 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
},
_getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) {
return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']");
return this._set.querySelector("richgriditem[anonid='" + aBookmarkId + "']");
},
_getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) {
return +aItem.getAttribute("bookmarkId");
return +aItem.getAttribute("anonid");
},
_updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
@ -142,6 +142,7 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
this._set.removeItemAt(this._set.itemCount - 1, true);
}
this._set.arrangeItems();
this._set.removeAttribute("fade");
this._inBatch = false;
rootNode.containerOpen = false;
},
@ -154,7 +155,8 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
},
clearBookmarks: function bv_clearBookmarks() {
this._set.clearAll();
if ('clearAll' in this._set)
this._set.clearAll();
},
addBookmark: function bv_addBookmark(aBookmarkId, aPos) {
@ -162,7 +164,7 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
let item = this._set.insertItemAt(aPos || index, title, uri.spec, this._inBatch);
item.setAttribute("bookmarkId", aBookmarkId);
item.setAttribute("anonid", aBookmarkId);
this._setContextActions(item);
this._updateFavicon(item, uri);
},
@ -198,6 +200,7 @@ BookmarksView.prototype = Util.extend(Object.create(View.prototype), {
let uri = this._bookmarkService.getBookmarkURI(aBookmarkId);
let title = this._bookmarkService.getItemTitle(aBookmarkId) || uri.spec;
item.setAttribute("anonid", aBookmarkId);
item.setAttribute("value", uri.spec);
item.setAttribute("label", title);

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

@ -95,6 +95,7 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), {
rootNode.containerOpen = false;
this._set.arrangeItems();
this._set.removeAttribute("fade");
if (this._inBatch > 0)
this._inBatch--;
},
@ -130,6 +131,9 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), {
let tileGroup = this._set;
let selectedTiles = tileGroup.selectedItems;
// just arrange the grid once at the end of any action handling
this._inBatch = true;
switch (aActionName){
case "delete":
Array.forEach(selectedTiles, function(aNode) {
@ -182,9 +186,11 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), {
break;
default:
this._inBatch = false;
return;
}
this._inBatch = false;
// Send refresh event so all view are in sync.
this._sendNeedsRefresh();
},
@ -254,7 +260,8 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), {
},
onClearHistory: function() {
this._set.clearAll();
if ('clearAll' in this._set)
this._set.clearAll();
},
onPageChanged: function(aURI, aWhat, aValue) {
@ -264,7 +271,7 @@ HistoryView.prototype = Util.extend(Object.create(View.prototype), {
let currIcon = item.getAttribute("iconURI");
if (currIcon != aValue) {
item.setAttribute("iconURI", aValue);
if("refresh" in item)
if ("refresh" in item)
item.refresh();
}
}

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

@ -93,6 +93,7 @@ RemoteTabsView.prototype = Util.extend(Object.create(View.prototype), {
}
this.setUIAccessVisible(show);
this._set.arrangeItems();
this._set.removeAttribute("fade");
},
destruct: function destruct() {

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

@ -48,7 +48,17 @@
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-topsites')">
&narrowTopSitesHeader.label;
</html:div>
<richgrid id="start-topsites-grid" observes="bcast_windowState" set-name="topSites" rows="3" columns="3" tiletype="thumbnail" seltype="multiple" flex="1"/>
<richgrid id="start-topsites-grid" observes="bcast_windowState" set-name="topSites" rows="3" columns="3" tiletype="thumbnail" seltype="multiple" minSlots="8" fade="true" flex="1">
<richgriditem/>
<richgriditem/>
<richgriditem/>
<richgriditem/>
<richgriditem/>
<richgriditem/>
<richgriditem/>
<richgriditem/>
<richgriditem/>
</richgrid>
</vbox>
<vbox id="start-bookmarks" class="meta-section">
@ -56,7 +66,10 @@
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-bookmarks')">
&narrowBookmarksHeader.label;
</html:div>
<richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" flex="1"/>
<richgrid id="start-bookmarks-grid" observes="bcast_windowState" set-name="bookmarks" seltype="multiple" fade="true" flex="1" minSlots="2">
<richgriditem/>
<richgriditem/>
</richgrid>
</vbox>
<vbox id="start-history" class="meta-section">
@ -64,7 +77,11 @@
<html:div class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-history')">
&narrowRecentHistoryHeader.label;
</html:div>
<richgrid id="start-history-grid" observes="bcast_windowState" set-name="recentHistory" seltype="multiple" flex="1"/>
<richgrid id="start-history-grid" observes="bcast_windowState" set-name="recentHistory" seltype="multiple" fade="true" flex="1">
<richgriditem/>
<richgriditem/>
<richgriditem/>
</richgrid>
</vbox>
#ifdef MOZ_SERVICES_SYNC
@ -73,7 +90,12 @@
<html:div id="snappedRemoteTabsLabel" class="meta-section-title narrow-title" onclick="StartUI.onNarrowTitleClick('start-remotetabs')">
&narrowRemoteTabsHeader.label;
</html:div>
<richgrid id="start-remotetabs-grid" observes="bcast_windowState" set-name="remoteTabs" seltype="multiple" flex="1"/>
<richgrid id="start-remotetabs-grid" observes="bcast_windowState" set-name="remoteTabs" seltype="multiple" fade="true" flex="1">
<richgriditem/>
<richgriditem/>
<richgriditem/>
</richgrid>
</vbox>
#endif
</hbox>

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

@ -158,6 +158,9 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), {
},
updateTile: function(aTileNode, aSite, aArrangeGrid) {
if (!(aSite && aSite.url)) {
throw new Error("Invalid Site object passed to TopSitesView updateTile");
}
this._updateFavicon(aTileNode, Util.makeURI(aSite.url));
Task.spawn(function() {
@ -192,14 +195,11 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), {
tileset.clearAll(true);
for (let site of sites) {
// call to private _createItemElement is a temp measure
// we'll eventually just request the next slot
let item = tileset._createItemElement(site.title, site.url);
this.updateTile(item, site);
tileset.appendChild(item);
let slot = tileset.nextSlot();
this.updateTile(slot, site);
}
tileset.arrangeItems();
tileset.removeAttribute("fade");
this.isUpdating = false;
},
@ -244,7 +244,7 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), {
// nsIObservers
observe: function (aSubject, aTopic, aState) {
switch(aTopic) {
switch (aTopic) {
case "Metro:RefreshTopsiteThumbnail":
this.forceReloadOfThumbnail(aState);
break;
@ -269,7 +269,8 @@ TopSitesView.prototype = Util.extend(Object.create(View.prototype), {
},
onClearHistory: function() {
this._set.clearAll();
if ('clearAll' in this._set)
this._set.clearAll();
},
onPageChanged: function(aURI, aWhat, aValue) {

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

@ -67,7 +67,7 @@ gTests.push({
ok(!item, "Item not in grid");
ok(!gStartView._pinHelper.isPinned(uriFromIndex(2)), "Item unpinned");
ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
is(gStartView._set.itemCount, gStartView._limit, "Grid repopulated");
// --------- unpin multiple items
@ -124,7 +124,7 @@ gTests.push({
item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
ok(!item, "Item not in grid");
ok(HistoryTestHelper._nodes[uriFromIndex(2)], "Item not deleted yet");
ok(HistoryTestHelper._nodes[uriFromIndex(2)], "Item not actually deleted yet");
ok(!restoreButton.hidden, "Restore button is visible.");
ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
@ -150,9 +150,13 @@ gTests.push({
ok(!deleteButton.hidden, "Delete button is visible.");
let promise = waitForCondition(() => !restoreButton.hidden);
let populateGridSpy = spyOnMethod(gStartView, "populateGrid");
EventUtils.synthesizeMouse(deleteButton, 10, 10, {}, window);
yield promise;
is(populateGridSpy.callCount, 1, "populateGrid was called in response to the deleting a tile");
item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
ok(!item, "Item not in grid");
@ -163,11 +167,14 @@ gTests.push({
Elements.contextappbar.dismiss();
yield promise;
is(populateGridSpy.callCount, 1, "populateGrid not called when a removed item is actually deleted");
populateGridSpy.restore();
item = gStartView._set.getItemsByUrl(uriFromIndex(2))[0];
ok(!item, "Item not in grid");
ok(!HistoryTestHelper._nodes[uriFromIndex(2)], "Item RIP");
ok(gStartView._set.itemCount === gStartView._limit, "Grid repopulated");
is(gStartView._set.itemCount, gStartView._limit, "Grid repopulated");
// --------- delete multiple items and restore

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

@ -17,6 +17,9 @@
<richgrid id="grid_layout" seltype="single" flex="1">
</richgrid>
</vbox>
<vbox>
<richgrid id="slots_grid" seltype="single" minSlots="6" flex="1"/>
</vbox>
<vbox style="height:600px">
<hbox>
<richgrid id="clearGrid" seltype="single" flex="1" rows="2">
@ -26,7 +29,7 @@
</richgrid>
</hbox>
<hbox>
<richgrid id="emptyGrid" seltype="single" flex="1" rows="2">
<richgrid id="emptyGrid" seltype="single" flex="1" rows="2" minSlots="6">
</richgrid>
</hbox>
<hbox>

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

@ -9,6 +9,14 @@ function test() {
}).then(runTests);
}
function _checkIfBoundByRichGrid_Item(expected, node, idx) {
let binding = node.ownerDocument.defaultView.getComputedStyle(node).MozBinding;
let result = ('url("chrome://browser/content/bindings/grid.xml#richgrid-item")' == binding);
return (result == expected);
}
let isBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, true);
let isNotBoundByRichGrid_Item = _checkIfBoundByRichGrid_Item.bind(this, false);
gTests.push({
desc: "richgrid binding is applied",
run: function() {
@ -17,9 +25,9 @@ gTests.push({
let grid = doc.querySelector("#grid1");
ok(grid, "#grid1 is found");
is(typeof grid.clearSelection, "function", "#grid1 has the binding applied");
is(grid.items.length, 2, "#grid1 has a 2 items");
is(grid.items[0].control, grid, "#grid1 item's control points back at #grid1'");
ok(Array.every(grid.items, isBoundByRichGrid_Item), "All items are bound by richgrid-item");
}
});
@ -125,19 +133,28 @@ gTests.push({
gTests.push({
desc: "empty grid",
run: function() {
// XXX grids have minSlots and may not be ever truly empty
let grid = doc.getElementById("emptyGrid");
grid.arrangeItems();
yield waitForCondition(() => !grid.isArranging);
// grid has rows=2 but 0 items
// grid has 2 rows, 6 slots, 0 items
ok(grid.isBound, "binding was applied");
is(grid.itemCount, 0, "empty grid has 0 items");
is(grid.rowCount, 0, "empty grid has 0 rows");
is(grid.columnCount, 0, "empty grid has 0 cols");
// minSlots attr. creates unpopulated slots
is(grid.rowCount, grid.getAttribute("rows"), "empty grid with rows-attribute has that number of rows");
is(grid.columnCount, 3, "empty grid has expected number of columns");
let columnsNode = grid._grid;
let cStyle = doc.defaultView.getComputedStyle(columnsNode);
is(cStyle.getPropertyValue("-moz-column-count"), "auto", "empty grid has -moz-column-count: auto");
// remove rows attribute and allow space for the grid to find its own height
// for its number of slots
grid.removeAttribute("rows");
grid.parentNode.style.height = 20+(grid.tileHeight*grid.minSlots)+"px";
grid.arrangeItems();
yield waitForCondition(() => !grid.isArranging);
is(grid.rowCount, grid.minSlots, "empty grid has this.minSlots rows");
is(grid.columnCount, 1, "empty grid has 1 column");
}
});
@ -211,16 +228,25 @@ gTests.push({
is(typeof grid.insertItemAt, "function", "insertItemAt is a function on the grid");
let arrangeStub = stubMethod(grid, "arrangeItems");
let insertedItem = grid.insertItemAt(1, "inserted item", "http://example.com/inserted");
let insertedAt0 = grid.insertItemAt(0, "inserted item 0", "http://example.com/inserted0");
let insertedAt00 = grid.insertItemAt(0, "inserted item 00", "http://example.com/inserted00");
ok(insertedItem, "insertItemAt gives back an item");
is(grid.items[1], insertedItem, "item is inserted at the correct index");
is(insertedItem.getAttribute("label"), "inserted item", "insertItemAt creates item with the correct label");
is(insertedItem.getAttribute("value"), "http://example.com/inserted", "insertItemAt creates item with the correct url value");
is(grid.items[2].getAttribute("id"), "grid3_item2", "following item ends up at the correct index");
is(grid.itemCount, 3, "itemCount is incremented when we insertItemAt");
ok(insertedAt0 && insertedAt00, "insertItemAt gives back an item");
is(arrangeStub.callCount, 1, "arrangeItems is called when we insertItemAt");
is(insertedAt0.getAttribute("label"), "inserted item 0", "insertItemAt creates item with the correct label");
is(insertedAt0.getAttribute("value"), "http://example.com/inserted0", "insertItemAt creates item with the correct url value");
is(grid.items[0], insertedAt00, "item is inserted at the correct index");
is(grid.children[0], insertedAt00, "first item occupies the first slot");
is(grid.items[1], insertedAt0, "item is inserted at the correct index");
is(grid.children[1], insertedAt0, "next item occupies the next slot");
is(grid.items[2].getAttribute("label"), "First item", "Old first item is now at index 2");
is(grid.items[3].getAttribute("label"), "2nd item", "Old 2nd item is now at index 3");
is(grid.itemCount, 4, "itemCount is incremented when we insertItemAt");
is(arrangeStub.callCount, 2, "arrangeItems is called when we insertItemAt");
arrangeStub.restore();
}
});
@ -417,3 +443,172 @@ gTests.push({
doc.defaultView.removeEventListener("selectionchange", handler, false);
}
});
function gridSlotsSetup() {
let grid = this.grid = doc.createElement("richgrid");
grid.setAttribute("minSlots", 6);
doc.documentElement.appendChild(grid);
is(grid.ownerDocument, doc, "created grid in the expected document");
}
function gridSlotsTearDown() {
this.grid && this.grid.parentNode.removeChild(this.grid);
}
gTests.push({
desc: "richgrid slots init",
setUp: gridSlotsSetup,
run: function() {
let grid = this.grid;
// grid is initially populated with empty slots matching the minSlots attribute
is(grid.children.length, 6, "minSlots slots are created");
is(grid.itemCount, 0, "slots do not count towards itemCount");
ok(Array.every(grid.children, (node) => node.nodeName == 'richgriditem'), "slots have nodeName richgriditem");
ok(Array.every(grid.children, isNotBoundByRichGrid_Item), "slots aren't bound by the richgrid-item binding");
},
tearDown: gridSlotsTearDown
});
gTests.push({
desc: "richgrid using slots for items",
setUp: gridSlotsSetup, // creates grid with minSlots = num. slots = 6
run: function() {
let grid = this.grid;
let numSlots = grid.getAttribute("minSlots");
is(grid.children.length, numSlots);
// adding items occupies those slots
for (let idx of [0,1,2,3,4,5,6]) {
let slot = grid.children[idx];
let item = grid.appendItem("item "+idx, "about:mozilla");
if (idx < numSlots) {
is(grid.children.length, numSlots);
is(slot, item, "The same node is reused when an item is assigned to a slot");
} else {
is(typeof slot, 'undefined');
ok(item);
is(grid.children.length, grid.itemCount);
}
}
},
tearDown: gridSlotsTearDown
});
gTests.push({
desc: "richgrid assign and release slots",
setUp: function(){
info("assign and release slots setUp");
this.grid = doc.getElementById("slots_grid");
this.grid.scrollIntoView();
let rect = this.grid.getBoundingClientRect();
info("slots grid at top: " + rect.top + ", window.pageYOffset: " + doc.defaultView.pageYOffset);
},
run: function() {
let grid = this.grid;
// start with 5 of 6 slots occupied
for (let idx of [0,1,2,3,4]) {
let item = grid.appendItem("item "+idx, "about:mozilla");
item.setAttribute("id", "test_item_"+idx);
}
is(grid.itemCount, 5);
is(grid.children.length, 6); // see setup, where we init with 6 slots
let firstItem = grid.items[0];
ok(firstItem.ownerDocument, "item has ownerDocument");
is(doc, firstItem.ownerDocument, "item's ownerDocument is the document we expect");
is(firstItem, grid.children[0], "Item and assigned slot are one and the same");
is(firstItem.control, grid, "Item is bound and its .control points back at the grid");
// before releasing, the grid should be nofified of clicks on that slot
let testWindow = grid.ownerDocument.defaultView;
let rect = firstItem.getBoundingClientRect();
{
let handleStub = stubMethod(grid, 'handleItemClick');
// send click to item and wait for next tick;
sendElementTap(testWindow, firstItem);
yield waitForMs(0);
is(handleStub.callCount, 1, "handleItemClick was called when we clicked an item");
handleStub.restore();
}
// _releaseSlot is semi-private, we don't expect consumers of the binding to call it
// but want to be sure it does what we expect
grid._releaseSlot(firstItem);
is(grid.itemCount, 4, "Releasing a slot gives us one less item");
is(firstItem, grid.children[0],"Released slot is still the same node we started with");
// after releasing, the grid should NOT be nofified of clicks
{
let handleStub = stubMethod(grid, 'handleItemClick');
// send click to item and wait for next tick;
sendElementTap(testWindow, firstItem);
yield waitForMs(0);
is(handleStub.callCount, 0, "handleItemClick was NOT called when we clicked a released slot");
handleStub.restore();
}
ok(!firstItem.mozMatchesSelector("richgriditem[value]"), "Released slot doesn't match binding selector");
ok(isNotBoundByRichGrid_Item(firstItem), "Released slot is no longer bound");
waitForCondition(() => isNotBoundByRichGrid_Item(firstItem));
ok(true, "Slot eventually gets unbound");
is(firstItem, grid.children[0], "Released slot is still at expected index in children collection");
let firstSlot = grid.children[0];
firstItem = grid.insertItemAt(0, "New item 0", "about:blank");
ok(firstItem == grid.items[0], "insertItemAt 0 creates item at expected index");
ok(firstItem == firstSlot, "insertItemAt occupies the released slot with the new item");
is(grid.itemCount, 5);
is(grid.children.length, 6);
is(firstItem.control, grid,"Item is bound and its .control points back at the grid");
let nextSlotIndex = grid.itemCount;
let lastItem = grid.insertItemAt(9, "New item 9", "about:blank");
// Check we don't create sparse collection of items
is(lastItem, grid.children[nextSlotIndex], "Item is appended at the next index when an out of bounds index is provided");
is(grid.children.length, 6);
is(grid.itemCount, 6);
grid.appendItem("one more", "about:blank");
is(grid.children.length, 7);
is(grid.itemCount, 7);
// clearAll results in slots being emptied
grid.clearAll();
is(grid.children.length, 6, "Extra slots are trimmed when we clearAll");
ok(!Array.some(grid.children, (node) => node.hasAttribute("value")), "All slots have no value attribute after clearAll")
},
tearDown: gridSlotsTearDown
});
gTests.push({
desc: "richgrid slot management",
setUp: gridSlotsSetup,
run: function() {
let grid = this.grid;
// populate grid with some items
let numSlots = grid.getAttribute("minSlots");
for (let idx of [0,1,2,3,4,5]) {
let item = grid.appendItem("item "+idx, "about:mozilla");
}
is(grid.itemCount, 6, "Grid setup with 6 items");
is(grid.children.length, 6, "Full grid has the expected number of slots");
// removing an item creates a replacement slot *on the end of the stack*
let item = grid.removeItemAt(0);
is(item.getAttribute("label"), "item 0", "removeItemAt gives back the populated node");
is(grid.children.length, 6);
is(grid.itemCount, 5);
is(grid.items[0].getAttribute("label"), "item 1", "removeItemAt removes the node so the nextSibling takes its place");
ok(grid.children[5] && !grid.children[5].hasAttribute("value"), "empty slot is added at the end of the existing children");
let item1 = grid.removeItem(grid.items[0]);
is(grid.children.length, 6);
is(grid.itemCount, 4);
is(grid.items[0].getAttribute("label"), "item 2", "removeItem removes the node so the nextSibling takes its place");
},
tearDown: gridSlotsTearDown
});

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

@ -275,6 +275,26 @@ richgriditem[bending] > .tile-content {
transform-origin: center center;
}
/* Empty/unused tiles */
richgriditem:not([value]) {
visibility: hidden;
}
richgriditem[tiletype="thumbnail"]:not([value]) {
visibility: visible;
}
richgriditem:not([value]) > .tile-content {
padding: 10px 14px;
}
richgriditem[tiletype="thumbnail"]:not([value]) > .tile-content {
box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.05);
background-image: url("chrome://browser/skin/images/firefox-watermark.png");
background-origin: content-box;
background-repeat: no-repeat;
background-color: rgba(255,255,255, 0.2);
background-position: center center;
background-size: @grid_row_height@;
}
/* Snapped-view variation
We use the compact, single-column grid treatment for <=320px */

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

@ -169,7 +169,7 @@ let AboutHome = {
window.BrowserSearch.recordSearchInHealthReport(data.engineName, "abouthome");
#endif
// Trigger a search through nsISearchEngine.getSubmission()
let submission = Services.search.currentEngine.getSubmission(data.searchTerms);
let submission = Services.search.currentEngine.getSubmission(data.searchTerms, null, "homepage");
window.loadURI(submission.uri.spec, null, submission.postData);
break;
}

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

@ -9,11 +9,7 @@ dir-tests := $(DEPTH)/$(mobile-tests)
ANDROID_APK_NAME := robocop-debug
ANDROID_EXTRA_JARS += \
$(srcdir)/robotium-solo-4.2.jar \
$(NULL)
ANDROID_RESFILES = \
res/values/strings.xml \
$(srcdir)/robotium-solo-4.3.jar \
$(NULL)
ANDROID_ASSETS_DIR := $(TESTPATH)/assets

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

@ -1,11 +1,11 @@
Robocop is a Mozilla project which uses Robotium to test Firefox on Android devices.
Robotium is an open source tool licensed under the Apache 2.0 license and the original
Robotium is an open source tool licensed under the Apache 2.0 license and the original
source can be found here:
http://code.google.com/p/robotium/
We are including robotium-solo-4.2.jar as a binary and are not modifying it in any way
from the original download found at:
We are including robotium-solo-4.3.jar as a binary and are not modifying it in any way
from the original download found at:
http://code.google.com/p/robotium/
Firefox for Android developers should read the documentation in

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

@ -6,3 +6,6 @@
MODULE = 'robocop'
ANDROID_RESFILES = [
'res/values/strings.xml',
]

Двоичные данные
build/mobile/robocop/robotium-solo-4.2.jar

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

Двоичные данные
build/mobile/robocop/robotium-solo-4.3.jar Normal file

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

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

@ -20,16 +20,6 @@ JAVAFILES = \
WifiConfiguration.java \
$(NULL)
ANDROID_RESFILES = \
res/drawable/icon.png \
res/drawable/ateamlogo.png \
res/drawable/ic_stat_first.png \
res/drawable/ic_stat_neterror.png \
res/drawable/ic_stat_warning.png \
res/layout/main.xml \
res/values/strings.xml \
$(NULL)
ANDROID_EXTRA_JARS = \
$(srcdir)/network-libs/commons-net-2.0.jar \
$(srcdir)/network-libs/jmdns.jar \

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

@ -11,14 +11,6 @@ JAVAFILES = \
FileCursor.java \
$(NULL)
ANDROID_RESFILES = \
res/drawable-hdpi/icon.png \
res/drawable-ldpi/icon.png \
res/drawable-mdpi/icon.png \
res/layout/main.xml \
res/values/strings.xml \
$(NULL)
include $(topsrcdir)/config/rules.mk
tools:: $(ANDROID_APK_NAME).apk

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

@ -6,3 +6,10 @@
MODULE = 'FenCP'
ANDROID_RESFILES = [
'res/drawable-hdpi/icon.png',
'res/drawable-ldpi/icon.png',
'res/drawable-mdpi/icon.png',
'res/layout/main.xml',
'res/values/strings.xml',
]

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

@ -11,14 +11,6 @@ JAVAFILES = \
FileCursor.java \
$(NULL)
ANDROID_RESFILES = \
res/drawable-hdpi/icon.png \
res/drawable-ldpi/icon.png \
res/drawable-mdpi/icon.png \
res/layout/main.xml \
res/values/strings.xml \
$(NULL)
include $(topsrcdir)/config/rules.mk
tools:: $(ANDROID_APK_NAME).apk

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

@ -6,3 +6,10 @@
MODULE = 'FfxCP'
ANDROID_RESFILES = [
'res/drawable-hdpi/icon.png',
'res/drawable-ldpi/icon.png',
'res/drawable-mdpi/icon.png',
'res/layout/main.xml',
'res/values/strings.xml',
]

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

@ -6,3 +6,12 @@
MODULE = 'sutAgentAndroid'
ANDROID_RESFILES = [
'res/drawable/ateamlogo.png',
'res/drawable/ic_stat_first.png',
'res/drawable/ic_stat_neterror.png',
'res/drawable/ic_stat_warning.png',
'res/drawable/icon.png',
'res/layout/main.xml',
'res/values/strings.xml',
]

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

@ -12,17 +12,6 @@ JAVAFILES = \
WatcherService.java \
$(NULL)
ANDROID_RESFILES = \
res/drawable-hdpi/icon.png \
res/drawable-hdpi/ateamlogo.png \
res/drawable-ldpi/icon.png \
res/drawable-ldpi/ateamlogo.png \
res/drawable-mdpi/icon.png \
res/drawable-mdpi/ateamlogo.png \
res/layout/main.xml \
res/values/strings.xml \
$(NULL)
include $(topsrcdir)/config/rules.mk
tools:: $(ANDROID_APK_NAME).apk

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

@ -6,3 +6,13 @@
MODULE = 'Watcher'
ANDROID_RESFILES = [
'res/drawable-hdpi/ateamlogo.png',
'res/drawable-hdpi/icon.png',
'res/drawable-ldpi/ateamlogo.png',
'res/drawable-ldpi/icon.png',
'res/drawable-mdpi/ateamlogo.png',
'res/drawable-mdpi/icon.png',
'res/layout/main.xml',
'res/values/strings.xml',
]

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

@ -8,6 +8,7 @@
ifndef INCLUDED_JAVA_BUILD_MK #{
ifdef ANDROID_RESFILES #{
ifndef IGNORE_ANDROID_RESFILES #{
res-dep := .deps-copy-java-res
GENERATED_DIRS += res
@ -25,6 +26,7 @@ res-dep-preqs := \
$(res-dep): $(res-dep-preqs)
$(call copy_dir,$(srcdir)/res,$(CURDIR)/res)
@$(TOUCH) $@
endif #} IGNORE_ANDROID_RESFILES
endif #} ANDROID_RESFILES

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

@ -21,6 +21,8 @@ INCLUDED_RULES_MK = 1
# present. If they are, this is a violation of the separation of
# responsibility between Makefile.in and mozbuild files.
_MOZBUILD_EXTERNAL_VARIABLES := \
ANDROID_GENERATED_RESFILES \
ANDROID_RESFILES \
CMMSRCS \
CPP_UNIT_TESTS \
DIRS \

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

@ -4,8 +4,6 @@
#include "nsISupports.idl"
interface nsIDOMMozNetworkStatsInterface;
[scriptable, builtinclass, uuid(3b16fe17-5583-483a-b486-b64a3243221c)]
interface nsIDOMMozNetworkStatsData : nsISupports
{
@ -14,7 +12,7 @@ interface nsIDOMMozNetworkStatsData : nsISupports
readonly attribute jsval date; // Date.
};
[scriptable, builtinclass, uuid(b6fc4b14-628d-4c99-bf4e-e4ed56916cbe)]
[scriptable, builtinclass, uuid(6613ea55-b99c-44f9-91bf-d07da10b9b74)]
interface nsIDOMMozNetworkStats : nsISupports
{
/**
@ -24,12 +22,13 @@ interface nsIDOMMozNetworkStats : nsISupports
readonly attribute DOMString manifestURL;
/**
* Network the returned data belongs to.
* Can be 'mobile', 'wifi' or null.
* If null, stats for both mobile and wifi are returned.
*/
readonly attribute nsIDOMMozNetworkStatsInterface network;
readonly attribute DOMString connectionType;
/**
* Stats for a network.
* Stats for connectionType
*/
readonly attribute jsval data; // array of NetworkStatsData.
// one element per day.

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

@ -6,65 +6,57 @@
interface nsIDOMDOMRequest;
/**
* Represents a data interface for which the manager is recording statistics.
*/
[scriptable, uuid(f540615b-d803-43ff-8200-2a9d145a5645)]
interface nsIDOMMozNetworkStatsInterface : nsISupports
dictionary NetworkStatsOptions
{
readonly attribute long type;
/**
* Id value is '0' for wifi or the iccid for mobile (SIM).
* Connection type used to filter which network stats will be returned:
* 'mobile', 'wifi' or null.
* If null, stats for both mobile and wifi are returned.
*
* Manifest URL used to retrieve network stats per app.
* If null, system stats (regardless of the app) are returned.
*/
readonly attribute DOMString id;
DOMString connectionType;
DOMString manifestURL;
jsval start; // date
jsval end; // date
};
[scriptable, uuid(5fbdcae6-a2cd-47b3-929f-83ac75bd4881)]
[scriptable, uuid(87529a6c-aef6-11e1-a595-4f034275cfa6)]
interface nsIDOMMozNetworkStatsManager : nsISupports
{
/**
* Constants for known interface types.
*/
const long WIFI = 0;
const long MOBILE = 1;
/**
* Find samples between two dates start and end, both included.
* Query network statistics.
*
* If manifestURL is provided, per-app usage is retrieved,
* otherwise the target will be system usage.
* If options.connectionType is not provided, return statistics for all known
* network interfaces.
*
* If success, the request result will be an nsIDOMMozNetworkStats object.
* If options.manifestURL is not provided, return statistics regardless of the app.
*
* If successful, the request result will be an nsIDOMMozNetworkStats object.
*
* If network stats are not available for some dates, then rxBytes &
* txBytes are undefined for those dates.
*/
nsIDOMDOMRequest getSamples(in nsIDOMMozNetworkStatsInterface network,
in jsval start,
in jsval end,
[optional] in DOMString manifestURL);
nsIDOMDOMRequest getNetworkStats(in jsval options);
/**
* Remove all stats related with the provided network from DB.
* Return available connection types.
*/
nsIDOMDOMRequest clearStats(in nsIDOMMozNetworkStatsInterface network);
readonly attribute jsval connectionTypes; // array of DOMStrings.
/**
* Remove all stats in the database.
* Clear all stats from DB.
*/
nsIDOMDOMRequest clearAllStats();
nsIDOMDOMRequest clearAllData();
/**
* Return currently available networks.
* Time in seconds between samples stored in database.
*/
readonly attribute jsval availableNetworks; // array of nsIDOMMozNetworkStatsInterface.
readonly attribute long sampleRate;
/**
* Minimum time in milliseconds between samples stored in the database.
* Maximum number of samples stored in the database per connection type.
*/
readonly attribute long sampleRate;
/**
* Time in milliseconds recorded by the API until present time. All samples
* older than maxStorageAge from now are deleted.
*/
readonly attribute long long maxStorageAge;
readonly attribute long maxStorageSamples;
};

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

@ -4,8 +4,6 @@
#include "nsISupports.idl"
interface nsINetworkInterface;
[scriptable, function, uuid(5f821529-1d80-4ab5-a933-4e1b3585b6bc)]
interface nsINetworkStatsServiceProxyCallback : nsISupports
{
@ -16,20 +14,20 @@ interface nsINetworkStatsServiceProxyCallback : nsISupports
void notify(in boolean aResult, in jsval aMessage);
};
[scriptable, uuid(facef032-3fd9-4509-a396-83d94c1a11ae)]
[scriptable, uuid(8fbd115d-f590-474c-96dc-e2b6803ca975)]
interface nsINetworkStatsServiceProxy : nsISupports
{
/*
* An interface used to record per-app traffic data.
* @param aAppId app id
* @param aNetworkInterface network
* @param aConnectionType network connection type (0 for wifi, 1 for mobile)
* @param aTimeStamp time stamp
* @param aRxBytes received data amount
* @param aTxBytes transmitted data amount
* @param aCallback an optional callback
*/
void saveAppStats(in unsigned long aAppId,
in nsINetworkInterface aNetwork,
in long aConnectionType,
in unsigned long long aTimeStamp,
in unsigned long long aRxBytes,
in unsigned long long aTxBytes,

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

@ -16,7 +16,8 @@ Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
const DB_NAME = "net_stats";
const DB_VERSION = 2;
const STORE_NAME = "net_stats";
const STORE_NAME = "net_stats"; // Deprecated. Use "net_stats_v2" instead.
const STORE_NAME_V2 = "net_stats_v2";
// Constant defining the maximum values allowed per interface. If more, older
// will be erased.
@ -25,11 +26,12 @@ const VALUES_MAX_LENGTH = 6 * 30;
// Constant defining the rate of the samples. Daily.
const SAMPLE_RATE = 1000 * 60 * 60 * 24;
this.NetworkStatsDB = function NetworkStatsDB() {
this.NetworkStatsDB = function NetworkStatsDB(aConnectionTypes) {
if (DEBUG) {
debug("Constructor");
}
this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME]);
this._connectionTypes = aConnectionTypes;
this.initDBHelper(DB_NAME, DB_VERSION, [STORE_NAME_V2]);
}
NetworkStatsDB.prototype = {
@ -42,7 +44,7 @@ NetworkStatsDB.prototype = {
function errorCb(error) {
txnCb(error, null);
}
return this.newTxn(txn_type, STORE_NAME, callback, successCb, errorCb);
return this.newTxn(txn_type, STORE_NAME_V2, callback, successCb, errorCb);
},
upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
@ -67,57 +69,69 @@ NetworkStatsDB.prototype = {
if (DEBUG) {
debug("Created object stores and indexes");
}
// There could be a time delay between the point when the network
// interface comes up and the point when the database is initialized.
// In this short interval some traffic data are generated but are not
// registered by the first sample. The initialization of the database
// should make up the missing sample.
let stats = [];
for (let connection in this._connectionTypes) {
let connectionType = this._connectionTypes[connection].name;
let timestamp = this.normalizeDate(new Date());
stats.push({ connectionType: connectionType,
timestamp: timestamp,
rxBytes: 0,
txBytes: 0,
rxTotalBytes: 0,
txTotalBytes: 0 });
}
this._saveStats(aTransaction, objectStore, stats);
if (DEBUG) {
debug("Database initialized");
}
} else if (currVersion == 1) {
// In order to support per-app traffic data storage, the original
// objectStore needs to be replaced by a new objectStore with new
// key path ("appId") and new index ("appId").
// Also, since now networks are identified by their
// [networkId, networkType] not just by their connectionType,
// to modify the keyPath is mandatory to delete the object store
// and create it again. Old data is going to be deleted because the
// networkId for each sample can not be set.
db.deleteObjectStore(STORE_NAME);
let newObjectStore;
newObjectStore = db.createObjectStore(STORE_NAME_V2, { keyPath: ["appId", "connectionType", "timestamp"] });
newObjectStore.createIndex("appId", "appId", { unique: false });
newObjectStore.createIndex("connectionType", "connectionType", { unique: false });
newObjectStore.createIndex("timestamp", "timestamp", { unique: false });
newObjectStore.createIndex("rxBytes", "rxBytes", { unique: false });
newObjectStore.createIndex("txBytes", "txBytes", { unique: false });
newObjectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
newObjectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
if (DEBUG) {
debug("Created new object stores and indexes");
}
objectStore = db.createObjectStore(STORE_NAME, { keyPath: ["appId", "network", "timestamp"] });
objectStore.createIndex("appId", "appId", { unique: false });
objectStore.createIndex("network", "network", { unique: false });
objectStore.createIndex("networkType", "networkType", { unique: false });
objectStore.createIndex("timestamp", "timestamp", { unique: false });
objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
objectStore.createIndex("txBytes", "txBytes", { unique: false });
objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
// Copy the data from the original objectStore to the new objectStore.
objectStore = aTransaction.objectStore(STORE_NAME);
objectStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
// Delete the original object store.
db.deleteObjectStore(STORE_NAME);
return;
}
debug("Created object stores and indexes for version 2");
let oldStats = cursor.value;
let newStats = { appId: 0,
connectionType: oldStats.connectionType,
timestamp: oldStats.timestamp,
rxBytes: oldStats.rxBytes,
txBytes: oldStats.txBytes,
rxTotalBytes: oldStats.rxTotalBytes,
txTotalBytes: oldStats.txTotalBytes };
this._saveStats(aTransaction, newObjectStore, newStats);
cursor.continue();
}.bind(this);
}
}
},
importData: function importData(aStats) {
let stats = { appId: aStats.appId,
network: [aStats.networkId, aStats.networkType],
timestamp: aStats.timestamp,
rxBytes: aStats.rxBytes,
txBytes: aStats.txBytes,
rxTotalBytes: aStats.rxTotalBytes,
txTotalBytes: aStats.txTotalBytes };
return stats;
},
exportData: function exportData(aStats) {
let stats = { appId: aStats.appId,
networkId: aStats.network[0],
networkType: aStats.network[1],
timestamp: aStats.timestamp,
rxBytes: aStats.rxBytes,
txBytes: aStats.txBytes,
rxTotalBytes: aStats.rxTotalBytes,
txTotalBytes: aStats.txTotalBytes };
return stats;
},
normalizeDate: function normalizeDate(aDate) {
// Convert to UTC according to timezone and
// filter timestamp to get SAMPLE_RATE precission
@ -126,42 +140,29 @@ NetworkStatsDB.prototype = {
return timestamp;
},
saveStats: function saveStats(aStats, aResultCb) {
let timestamp = this.normalizeDate(aStats.date);
saveStats: function saveStats(stats, aResultCb) {
let timestamp = this.normalizeDate(stats.date);
let stats = { appId: aStats.appId,
networkId: aStats.networkId,
networkType: aStats.networkType,
timestamp: timestamp,
rxBytes: (aStats.appId == 0) ? 0 : aStats.rxBytes,
txBytes: (aStats.appId == 0) ? 0 : aStats.txBytes,
rxTotalBytes: (aStats.appId == 0) ? aStats.rxBytes : 0,
txTotalBytes: (aStats.appId == 0) ? aStats.txBytes : 0 };
stats = { appId: stats.appId,
connectionType: stats.connectionType,
timestamp: timestamp,
rxBytes: (stats.appId == 0) ? 0 : stats.rxBytes,
txBytes: (stats.appId == 0) ? 0 : stats.txBytes,
rxTotalBytes: (stats.appId == 0) ? stats.rxBytes : 0,
txTotalBytes: (stats.appId == 0) ? stats.txBytes : 0 };
stats = this.importData(stats);
this.dbNewTxn("readwrite", function(aTxn, aStore) {
this.dbNewTxn("readwrite", function(txn, store) {
if (DEBUG) {
debug("Filtered time: " + new Date(timestamp));
debug("New stats: " + JSON.stringify(stats));
}
let request = aStore.index("network").openCursor(stats.network, "prev");
let request = store.index("connectionType").openCursor(stats.connectionType, "prev");
request.onsuccess = function onsuccess(event) {
let cursor = event.target.result;
if (!cursor) {
// Empty, so save first element.
// There could be a time delay between the point when the network
// interface comes up and the point when the database is initialized.
// In this short interval some traffic data are generated but are not
// registered by the first sample.
if (stats.appId == 0) {
stats.rxBytes = stats.rxTotalBytes;
stats.txBytes = stats.txTotalBytes;
}
this._saveStats(aTxn, aStore, stats);
this._saveStats(txn, store, stats);
return;
}
@ -176,10 +177,10 @@ NetworkStatsDB.prototype = {
}
// Remove stats previous to now - VALUE_MAX_LENGTH
this._removeOldStats(aTxn, aStore, stats.appId, stats.network, stats.timestamp);
this._removeOldStats(txn, store, stats.appId, stats.connectionType, stats.timestamp);
// Process stats before save
this._processSamplesDiff(aTxn, aStore, cursor, stats);
this._processSamplesDiff(txn, store, cursor, stats);
}.bind(this);
}.bind(this), aResultCb);
},
@ -188,21 +189,20 @@ NetworkStatsDB.prototype = {
* This function check that stats are saved in the database following the sample rate.
* In this way is easier to find elements when stats are requested.
*/
_processSamplesDiff: function _processSamplesDiff(aTxn, aStore, aLastSampleCursor, aNewSample) {
let lastSample = aLastSampleCursor.value;
_processSamplesDiff: function _processSamplesDiff(txn, store, lastSampleCursor, newSample) {
let lastSample = lastSampleCursor.value;
// Get difference between last and new sample.
let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE;
let diff = (newSample.timestamp - lastSample.timestamp) / SAMPLE_RATE;
if (diff % 1) {
// diff is decimal, so some error happened because samples are stored as a multiple
// of SAMPLE_RATE
aTxn.abort();
txn.abort();
throw new Error("Error processing samples");
}
if (DEBUG) {
debug("New: " + aNewSample.timestamp + " - Last: " +
lastSample.timestamp + " - diff: " + diff);
debug("New: " + newSample.timestamp + " - Last: " + lastSample.timestamp + " - diff: " + diff);
}
// If the incoming data is obtained from netd (|newSample.appId| is 0),
@ -210,15 +210,15 @@ NetworkStatsDB.prototype = {
// |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|.
// Else, the incoming data is per-app data (|newSample.appId| is not 0),
// the |txBytes|/|rxBytes| is directly the new |txBytes|/|rxBytes|.
if (aNewSample.appId == 0) {
let rxDiff = aNewSample.rxTotalBytes - lastSample.rxTotalBytes;
let txDiff = aNewSample.txTotalBytes - lastSample.txTotalBytes;
if (newSample.appId == 0) {
let rxDiff = newSample.rxTotalBytes - lastSample.rxTotalBytes;
let txDiff = newSample.txTotalBytes - lastSample.txTotalBytes;
if (rxDiff < 0 || txDiff < 0) {
rxDiff = aNewSample.rxTotalBytes;
txDiff = aNewSample.txTotalBytes;
rxDiff = newSample.rxTotalBytes;
txDiff = newSample.txTotalBytes;
}
aNewSample.rxBytes = rxDiff;
aNewSample.txBytes = txDiff;
newSample.rxBytes = rxDiff;
newSample.txBytes = txDiff;
}
if (diff == 1) {
@ -227,12 +227,11 @@ NetworkStatsDB.prototype = {
// If the incoming data is per-data data, new |rxTotalBytes|/|txTotalBytes|
// needs to be obtained by adding new |rxBytes|/|txBytes| to last
// |rxTotalBytes|/|txTotalBytes|.
if (aNewSample.appId != 0) {
aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes;
aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes;
if (newSample.appId != 0) {
newSample.rxTotalBytes = newSample.rxBytes + lastSample.rxTotalBytes;
newSample.txTotalBytes = newSample.txBytes + lastSample.txTotalBytes;
}
this._saveStats(aTxn, aStore, aNewSample);
this._saveStats(txn, store, newSample);
return;
}
if (diff > 1) {
@ -245,20 +244,19 @@ NetworkStatsDB.prototype = {
let data = [];
for (let i = diff - 2; i >= 0; i--) {
let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1);
let sample = { appId: aNewSample.appId,
network: aNewSample.network,
timestamp: time,
rxBytes: 0,
txBytes: 0,
rxTotalBytes: lastSample.rxTotalBytes,
txTotalBytes: lastSample.txTotalBytes };
let time = newSample.timestamp - SAMPLE_RATE * (i + 1);
let sample = {appId: newSample.appId,
connectionType: newSample.connectionType,
timestamp: time,
rxBytes: 0,
txBytes: 0,
rxTotalBytes: lastSample.rxTotalBytes,
txTotalBytes: lastSample.txTotalBytes};
data.push(sample);
}
data.push(aNewSample);
this._saveStats(aTxn, aStore, data);
data.push(newSample);
this._saveStats(txn, store, data);
return;
}
if (diff == 0 || diff < 0) {
@ -268,163 +266,91 @@ NetworkStatsDB.prototype = {
// If diff < 0, clock or timezone changed back. Place data in the last sample.
lastSample.rxBytes += aNewSample.rxBytes;
lastSample.txBytes += aNewSample.txBytes;
lastSample.rxBytes += newSample.rxBytes;
lastSample.txBytes += newSample.txBytes;
// If incoming data is obtained from netd, last |rxTotalBytes|/|txTotalBytes|
// needs to get updated by replacing the new |rxTotalBytes|/|txTotalBytes|.
if (aNewSample.appId == 0) {
lastSample.rxTotalBytes = aNewSample.rxTotalBytes;
lastSample.txTotalBytes = aNewSample.txTotalBytes;
if (newSample.appId == 0) {
lastSample.rxTotalBytes = newSample.rxTotalBytes;
lastSample.txTotalBytes = newSample.txTotalBytes;
} else {
// Else, the incoming data is per-app data, old |rxTotalBytes|/
// |txTotalBytes| needs to get updated by adding the new
// |rxBytes|/|txBytes| to last |rxTotalBytes|/|txTotalBytes|.
lastSample.rxTotalBytes += aNewSample.rxBytes;
lastSample.txTotalBytes += aNewSample.txBytes;
lastSample.rxTotalBytes += newSample.rxBytes;
lastSample.txTotalBytes += newSample.txBytes;
}
if (DEBUG) {
debug("Update: " + JSON.stringify(lastSample));
}
let req = aLastSampleCursor.update(lastSample);
let req = lastSampleCursor.update(lastSample);
}
},
_saveStats: function _saveStats(aTxn, aStore, aNetworkStats) {
_saveStats: function _saveStats(txn, store, networkStats) {
if (DEBUG) {
debug("_saveStats: " + JSON.stringify(aNetworkStats));
debug("_saveStats: " + JSON.stringify(networkStats));
}
if (Array.isArray(aNetworkStats)) {
let len = aNetworkStats.length - 1;
if (Array.isArray(networkStats)) {
let len = networkStats.length - 1;
for (let i = 0; i <= len; i++) {
aStore.put(aNetworkStats[i]);
store.put(networkStats[i]);
}
} else {
aStore.put(aNetworkStats);
store.put(networkStats);
}
},
_removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aNetwork, aDate) {
_removeOldStats: function _removeOldStats(txn, store, appId, connType, date) {
// Callback function to remove old items when new ones are added.
let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1);
let lowerFilter = [aAppId, aNetwork, 0];
let upperFilter = [aAppId, aNetwork, filterDate];
let filterDate = date - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1);
let lowerFilter = [appId, connType, 0];
let upperFilter = [appId, connType, filterDate];
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
let lastSample = null;
let self = this;
aStore.openCursor(range).onsuccess = function(event) {
store.openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
lastSample = cursor.value;
cursor.delete();
cursor.continue();
return;
}
// If all samples for a network are removed, an empty sample
// has to be saved to keep the totalBytes in order to compute
// future samples because system counters are not set to 0.
// Thus, if there are no samples left, the last sample removed
// will be saved again after setting its bytes to 0.
let request = aStore.index("network").openCursor(aNetwork);
request.onsuccess = function onsuccess(event) {
let cursor = event.target.result;
if (!cursor && lastSample != null) {
let timestamp = new Date();
timestamp = self.normalizeDate(timestamp);
lastSample.timestamp = timestamp;
lastSample.rxBytes = 0;
lastSample.txBytes = 0;
self._saveStats(aTxn, aStore, lastSample);
}
};
};
}.bind(this);
},
clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) {
let network = [aNetwork.id, aNetwork.type];
let self = this;
// Clear and save an empty sample to keep sync with system counters
this.dbNewTxn("readwrite", function(aTxn, aStore) {
let sample = null;
let request = aStore.index("network").openCursor(network, "prev");
request.onsuccess = function onsuccess(event) {
let cursor = event.target.result;
if (cursor) {
if (!sample) {
sample = cursor.value;
}
cursor.delete();
cursor.continue();
return;
}
if (sample) {
let timestamp = new Date();
timestamp = self.normalizeDate(timestamp);
sample.timestamp = timestamp;
sample.appId = 0;
sample.rxBytes = 0;
sample.txBytes = 0;
self._saveStats(aTxn, aStore, sample);
}
};
clear: function clear(aResultCb) {
this.dbNewTxn("readwrite", function(txn, store) {
if (DEBUG) {
debug("Going to clear all!");
}
store.clear();
}, aResultCb);
},
clearStats: function clearStats(aNetworks, aResultCb) {
let index = 0;
let stats = [];
let self = this;
let callback = function(aError, aResult) {
index++;
if (!aError && index < aNetworks.length) {
self.clearInterfaceStats(aNetworks[index], callback);
return;
}
aResultCb(aError, aResult);
};
if (!aNetworks[index]) {
aResultCb(null, true);
return;
}
this.clearInterfaceStats(aNetworks[index], callback);
},
find: function find(aResultCb, aNetwork, aStart, aEnd, aAppId, aManifestURL) {
find: function find(aResultCb, aOptions) {
let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
let start = this.normalizeDate(aStart);
let end = this.normalizeDate(aEnd);
let start = this.normalizeDate(aOptions.start);
let end = this.normalizeDate(aOptions.end);
if (DEBUG) {
debug("Find samples for appId: " + aAppId + " network " +
JSON.stringify(aNetwork) + " from " + start + " until " + end);
debug("Find: appId: " + aOptions.appId + " connectionType:" +
aOptions.connectionType + " start: " + start + " end: " + end);
debug("Start time: " + new Date(start));
debug("End time: " + new Date(end));
}
this.dbNewTxn("readonly", function(aTxn, aStore) {
let network = [aNetwork.id, aNetwork.type];
let lowerFilter = [aAppId, network, start];
let upperFilter = [aAppId, network, end];
this.dbNewTxn("readonly", function(txn, store) {
let lowerFilter = [aOptions.appId, aOptions.connectionType, start];
let upperFilter = [aOptions.appId, aOptions.connectionType, end];
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
let data = [];
if (!aTxn.result) {
aTxn.result = {};
if (!txn.result) {
txn.result = {};
}
let request = aStore.openCursor(range).onsuccess = function(event) {
let request = store.openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor){
data.push({ rxBytes: cursor.value.rxBytes,
@ -438,11 +364,66 @@ NetworkStatsDB.prototype = {
// now - VALUES_MAX_LENGTH, fill with empty samples.
this.fillResultSamples(start + offset, end + offset, data);
aTxn.result.manifestURL = aManifestURL;
aTxn.result.network = aNetwork;
aTxn.result.start = aStart;
aTxn.result.end = aEnd;
aTxn.result.data = data;
txn.result.manifestURL = aOptions.manifestURL;
txn.result.connectionType = aOptions.connectionType;
txn.result.start = aOptions.start;
txn.result.end = aOptions.end;
txn.result.data = data;
}.bind(this);
}.bind(this), aResultCb);
},
findAll: function findAll(aResultCb, aOptions) {
let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
let start = this.normalizeDate(aOptions.start);
let end = this.normalizeDate(aOptions.end);
if (DEBUG) {
debug("FindAll: appId: " + aOptions.appId +
" start: " + start + " end: " + end + "\n");
}
let self = this;
this.dbNewTxn("readonly", function(txn, store) {
let lowerFilter = start;
let upperFilter = end;
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
let data = [];
if (!txn.result) {
txn.result = {};
}
let request = store.index("timestamp").openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
if (cursor) {
if (cursor.value.appId != aOptions.appId) {
cursor.continue();
return;
}
if (data.length > 0 &&
data[data.length - 1].date.getTime() == cursor.value.timestamp + offset) {
// Time is the same, so add values.
data[data.length - 1].rxBytes += cursor.value.rxBytes;
data[data.length - 1].txBytes += cursor.value.txBytes;
} else {
data.push({ rxBytes: cursor.value.rxBytes,
txBytes: cursor.value.txBytes,
date: new Date(cursor.value.timestamp + offset) });
}
cursor.continue();
return;
}
this.fillResultSamples(start + offset, end + offset, data);
txn.result.manifestURL = aOptions.manifestURL;
txn.result.connectionType = aOptions.connectionType;
txn.result.start = aOptions.start;
txn.result.end = aOptions.end;
txn.result.data = data;
}.bind(this);
}.bind(this), aResultCb);
},
@ -480,9 +461,9 @@ NetworkStatsDB.prototype = {
},
logAllRecords: function logAllRecords(aResultCb) {
this.dbNewTxn("readonly", function(aTxn, aStore) {
aStore.mozGetAll().onsuccess = function onsuccess(event) {
aTxn.result = event.target.result;
this.dbNewTxn("readonly", function(txn, store) {
store.mozGetAll().onsuccess = function onsuccess(event) {
txn.result = event.target.result;
};
}, aResultCb);
},

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

@ -55,38 +55,9 @@ NetworkStatsData.prototype = {
QueryInterface : XPCOMUtils.generateQI([nsIDOMMozNetworkStatsData])
};
// NetworkStatsInterface
const NETWORKSTATSINTERFACE_CONTRACTID = "@mozilla.org/networkstatsinterface;1";
const NETWORKSTATSINTERFACE_CID = Components.ID("{f540615b-d803-43ff-8200-2a9d145a5645}");
const nsIDOMMozNetworkStatsInterface = Components.interfaces.nsIDOMMozNetworkStatsInterface;
function NetworkStatsInterface(aNetwork) {
if (DEBUG) {
debug("NetworkStatsInterface Constructor");
}
this.type = aNetwork.type;
this.id = aNetwork.id;
}
NetworkStatsInterface.prototype = {
__exposedProps__: {
id: 'r',
type: 'r',
},
classID : NETWORKSTATSINTERFACE_CID,
classInfo : XPCOMUtils.generateCI({classID: NETWORKSTATSINTERFACE_CID,
contractID: NETWORKSTATSINTERFACE_CONTRACTID,
classDescription: "NetworkStatsInterface",
interfaces: [nsIDOMMozNetworkStatsInterface],
flags: nsIClassInfo.DOM_OBJECT}),
QueryInterface : XPCOMUtils.generateQI([nsIDOMMozNetworkStatsInterface])
}
// NetworkStats
const NETWORKSTATS_CONTRACTID = "@mozilla.org/networkstats;1";
const NETWORKSTATS_CID = Components.ID("{b6fc4b14-628d-4c99-bf4e-e4ed56916cbe}");
const NETWORKSTATS_CID = Components.ID("{6613ea55-b99c-44f9-91bf-d07da10b9b74}");
const nsIDOMMozNetworkStats = Components.interfaces.nsIDOMMozNetworkStats;
function NetworkStats(aWindow, aStats) {
@ -94,7 +65,7 @@ function NetworkStats(aWindow, aStats) {
debug("NetworkStats Constructor");
}
this.manifestURL = aStats.manifestURL || null;
this.network = new NetworkStatsInterface(aStats.network);
this.connectionType = aStats.connectionType || null;
this.start = aStats.start || null;
this.end = aStats.end || null;
@ -107,7 +78,7 @@ function NetworkStats(aWindow, aStats) {
NetworkStats.prototype = {
__exposedProps__: {
manifestURL: 'r',
network: 'r',
connectionType: 'r',
start: 'r',
end: 'r',
data: 'r',
@ -121,14 +92,13 @@ NetworkStats.prototype = {
flags: nsIClassInfo.DOM_OBJECT}),
QueryInterface : XPCOMUtils.generateQI([nsIDOMMozNetworkStats,
nsIDOMMozNetworkStatsData,
nsIDOMMozNetworkStatsInterface])
nsIDOMMozNetworkStatsData])
}
// NetworkStatsManager
const NETWORKSTATSMANAGER_CONTRACTID = "@mozilla.org/networkStatsManager;1";
const NETWORKSTATSMANAGER_CID = Components.ID("{5fbdcae6-a2cd-47b3-929f-83ac75bd4881}");
const NETWORKSTATSMANAGER_CID = Components.ID("{87529a6c-aef6-11e1-a595-4f034275cfa6}");
const nsIDOMMozNetworkStatsManager = Components.interfaces.nsIDOMMozNetworkStatsManager;
function NetworkStatsManager() {
@ -146,64 +116,42 @@ NetworkStatsManager.prototype = {
}
},
getSamples: function getSamples(aNetwork, aStart, aEnd, aManifestURL) {
getNetworkStats: function getNetworkStats(aOptions) {
this.checkPrivileges();
if (aStart.constructor.name !== "Date" ||
aEnd.constructor.name !== "Date" ||
aStart > aEnd) {
if (!aOptions.start || !aOptions.end ||
aOptions.start > aOptions.end) {
throw Components.results.NS_ERROR_INVALID_ARG;
}
let request = this.createRequest();
cpmm.sendAsyncMessage("NetworkStats:Get",
{ network: aNetwork,
start: aStart,
end: aEnd,
manifestURL: aManifestURL,
id: this.getRequestId(request) });
{data: aOptions, id: this.getRequestId(request)});
return request;
},
clearStats: function clearStats(aNetwork) {
clearAllData: function clearAllData() {
this.checkPrivileges();
let request = this.createRequest();
cpmm.sendAsyncMessage("NetworkStats:Clear",
{ network: aNetwork,
id: this.getRequestId(request) });
return request;
},
clearAllStats: function clearAllStats() {
this.checkPrivileges();
let request = this.createRequest();
cpmm.sendAsyncMessage("NetworkStats:ClearAll",
{id: this.getRequestId(request)});
return request;
},
get availableNetworks() {
get connectionTypes() {
this.checkPrivileges();
let result = ObjectWrapper.wrap(cpmm.sendSyncMessage("NetworkStats:Networks")[0], this._window);
let networks = this.data = Cu.createArrayIn(this._window);
for (let i = 0; i < result.length; i++) {
networks.push(new NetworkStatsInterface(result[i]));
}
return networks;
return ObjectWrapper.wrap(cpmm.sendSyncMessage("NetworkStats:Types")[0], this._window);
},
get sampleRate() {
this.checkPrivileges();
return cpmm.sendSyncMessage("NetworkStats:SampleRate")[0];
return cpmm.sendSyncMessage("NetworkStats:SampleRate")[0] / 1000;
},
get maxStorageAge() {
get maxStorageSamples() {
this.checkPrivileges();
return cpmm.sendSyncMessage("NetworkStats:MaxStorageAge")[0];
return cpmm.sendSyncMessage("NetworkStats:MaxStorageSamples")[0];
},
receiveMessage: function(aMessage) {
@ -235,7 +183,6 @@ NetworkStatsManager.prototype = {
break;
case "NetworkStats:Clear:Return":
case "NetworkStats:ClearAll:Return":
if (msg.error) {
Services.DOMRequest.fireError(req, msg.error);
return;
@ -275,8 +222,7 @@ NetworkStatsManager.prototype = {
}
this.initDOMRequestHelper(aWindow, ["NetworkStats:Get:Return",
"NetworkStats:Clear:Return",
"NetworkStats:ClearAll:Return"]);
"NetworkStats:Clear:Return"]);
},
// Called from DOMRequestIpcHelper
@ -299,6 +245,5 @@ NetworkStatsManager.prototype = {
}
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsData,
NetworkStatsInterface,
NetworkStats,
NetworkStatsManager]);

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

@ -1,12 +1,9 @@
component {3b16fe17-5583-483a-b486-b64a3243221c} NetworkStatsManager.js
contract @mozilla.org/networkStatsdata;1 {3b16fe17-5583-483a-b486-b64a3243221c}
component {b6fc4b14-628d-4c99-bf4e-e4ed56916cbe} NetworkStatsManager.js
contract @mozilla.org/networkStats;1 {b6fc4b14-628d-4c99-bf4e-e4ed56916cbe}
component {6613ea55-b99c-44f9-91bf-d07da10b9b74} NetworkStatsManager.js
contract @mozilla.org/networkStats;1 {6613ea55-b99c-44f9-91bf-d07da10b9b74}
component {f540615b-d803-43ff-8200-2a9d145a5645} NetworkStatsManager.js
contract @mozilla.org/networkstatsinterface;1 {f540615b-d803-43ff-8200-2a9d145a5645}
component {5fbdcae6-a2cd-47b3-929f-83ac75bd4881} NetworkStatsManager.js
contract @mozilla.org/networkStatsManager;1 {5fbdcae6-a2cd-47b3-929f-83ac75bd4881}
component {87529a6c-aef6-11e1-a595-4f034275cfa6} NetworkStatsManager.js
contract @mozilla.org/networkStatsManager;1 {87529a6c-aef6-11e1-a595-4f034275cfa6}
category JavaScript-navigator-property mozNetworkStats @mozilla.org/networkStatsManager;1

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

@ -5,11 +5,7 @@
"use strict";
const DEBUG = false;
function debug(s) {
if (DEBUG) {
dump("-*- NetworkStatsService: " + s + "\n");
}
}
function debug(s) { dump("-*- NetworkStatsService: " + s + "\n"); }
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
@ -26,6 +22,7 @@ const TOPIC_INTERFACE_REGISTERED = "network-interface-registered";
const TOPIC_INTERFACE_UNREGISTERED = "network-interface-unregistered";
const NET_TYPE_WIFI = Ci.nsINetworkInterface.NETWORK_TYPE_WIFI;
const NET_TYPE_MOBILE = Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE;
const NET_TYPE_UNKNOWN = Ci.nsINetworkInterface.NETWORK_TYPE_UNKNOWN;
// The maximum traffic amount can be saved in the |cachedAppStats|.
const MAX_CACHED_TRAFFIC = 500 * 1000 * 1000; // 500 MB
@ -42,19 +39,11 @@ XPCOMUtils.defineLazyServiceGetter(this, "appsService",
"@mozilla.org/AppsService;1",
"nsIAppsService");
XPCOMUtils.defineLazyServiceGetter(this, "gSettingsService",
"@mozilla.org/settingsService;1",
"nsISettingsService");
XPCOMUtils.defineLazyGetter(this, "gRadioInterface", function () {
let ril = Cc["@mozilla.org/ril;1"].getService(Ci["nsIRadioInterfaceLayer"]);
// TODO: Bug 923382 - B2G Multi-SIM: support multiple SIM cards for network metering.
return ril.getRadioInterface(0);
});
this.NetworkStatsService = {
init: function() {
debug("Service started");
if (DEBUG) {
debug("Service started");
}
Services.obs.addObserver(this, "xpcom-shutdown", false);
Services.obs.addObserver(this, TOPIC_INTERFACE_REGISTERED, false);
@ -63,41 +52,24 @@ this.NetworkStatsService = {
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
// Object to store network interfaces, each network interface is composed
// by a network object (network type and network Id) and a interfaceName
// that contains the name of the physical interface (wlan0, rmnet0, etc.).
// The network type can be 0 for wifi or 1 for mobile. On the other hand,
// the network id is '0' for wifi or the iccid for mobile (SIM).
// Each networkInterface is placed in the _networks object by the index of
// 'networkId + networkType'.
//
// _networks object allows to map available network interfaces at low level
// (wlan0, rmnet0, etc.) to a network. It's not mandatory to have a
// networkInterface per network but can't exist a networkInterface not
// being mapped to a network.
this._connectionTypes = Object.create(null);
this._connectionTypes[NET_TYPE_WIFI] = { name: "wifi",
network: Object.create(null) };
this._connectionTypes[NET_TYPE_MOBILE] = { name: "mobile",
network: Object.create(null) };
this._networks = Object.create(null);
// There is no way to know a priori if wifi connection is available,
// just when the wifi driver is loaded, but it is unloaded when
// wifi is switched off. So wifi connection is hardcoded
let netId = this.getNetworkId('0', NET_TYPE_WIFI);
this._networks[netId] = { network: { id: '0',
type: NET_TYPE_WIFI },
interfaceName: null };
this.messages = ["NetworkStats:Get",
"NetworkStats:Clear",
"NetworkStats:ClearAll",
"NetworkStats:Networks",
"NetworkStats:Types",
"NetworkStats:SampleRate",
"NetworkStats:MaxStorageAge"];
"NetworkStats:MaxStorageSamples"];
this.messages.forEach(function(aMsgName) {
ppmm.addMessageListener(aMsgName, this);
this.messages.forEach(function(msgName) {
ppmm.addMessageListener(msgName, this);
}, this);
this._db = new NetworkStatsDB();
this._db = new NetworkStatsDB(this._connectionTypes);
// Stats for all interfaces are updated periodically
this.timer.initWithCallback(this, this._db.sampleRate,
@ -116,56 +88,58 @@ this.NetworkStatsService = {
return;
}
debug("receiveMessage " + aMessage.name);
if (DEBUG) {
debug("receiveMessage " + aMessage.name);
}
let mm = aMessage.target;
let msg = aMessage.json;
switch (aMessage.name) {
case "NetworkStats:Get":
this.getSamples(mm, msg);
this.getStats(mm, msg);
break;
case "NetworkStats:Clear":
this.clearInterfaceStats(mm, msg);
break;
case "NetworkStats:ClearAll":
this.clearDB(mm, msg);
break;
case "NetworkStats:Networks":
return this.availableNetworks();
case "NetworkStats:Types":
// This message is sync.
let types = [];
for (let i in this._connectionTypes) {
types.push(this._connectionTypes[i].name);
}
return types;
case "NetworkStats:SampleRate":
// This message is sync.
return this._db.sampleRate;
case "NetworkStats:MaxStorageAge":
case "NetworkStats:MaxStorageSamples":
// This message is sync.
return this._db.maxStorageSamples * this._db.sampleRate;
return this._db.maxStorageSamples;
}
},
observe: function observe(aSubject, aTopic, aData) {
switch (aTopic) {
observe: function observe(subject, topic, data) {
switch (topic) {
case TOPIC_INTERFACE_REGISTERED:
case TOPIC_INTERFACE_UNREGISTERED:
// If new interface is registered (notified from NetworkManager),
// the stats are updated for the new interface without waiting to
// complete the updating period.
let network = aSubject.QueryInterface(Ci.nsINetworkInterface);
debug("Network " + network.name + " of type " + network.type + " status change");
let netId = this.convertNetworkInterface(network);
if (!netId) {
break;
// complete the updating period
let network = subject.QueryInterface(Ci.nsINetworkInterface);
if (DEBUG) {
debug("Network " + network.name + " of type " + network.type + " status change");
}
if (this._connectionTypes[network.type]) {
this._connectionTypes[network.type].network = network;
this.updateStats(network.type);
}
this.updateStats(netId);
break;
case "xpcom-shutdown":
debug("Service shutdown");
if (DEBUG) {
debug("Service shutdown");
}
this.messages.forEach(function(aMsgName) {
ppmm.removeMessageListener(aMsgName, this);
this.messages.forEach(function(msgName) {
ppmm.removeMessageListener(msgName, this);
}, this);
Services.obs.removeObserver(this, "xpcom-shutdown");
@ -186,53 +160,10 @@ this.NetworkStatsService = {
* nsITimerCallback
* Timer triggers the update of all stats
*/
notify: function(aTimer) {
notify: function(timer) {
this.updateAllStats();
},
/*
* nsINetworkStatsService
*/
convertNetworkInterface: function(aNetwork) {
if (aNetwork.type != NET_TYPE_MOBILE &&
aNetwork.type != NET_TYPE_WIFI) {
return null;
}
let id = '0';
if (aNetwork.type == NET_TYPE_MOBILE) {
// Bug 904542 will provide the serviceId to map the iccId with the
// nsINetworkInterface of the NetworkManager. Now, lets assume that
// network is mapped with the current iccId of the single SIM.
id = gRadioInterface.rilContext.iccInfo.iccid;
}
let netId = this.getNetworkId(id, aNetwork.type);
if (!this._networks[netId]) {
this._networks[netId] = Object.create(null);
this._networks[netId].network = { id: id,
type: aNetwork.type };
}
this._networks[netId].interfaceName = aNetwork.name;
return netId;
},
getNetworkId: function getNetworkId(aIccId, aNetworkType) {
return aIccId + '' + aNetworkType;
},
availableNetworks: function availableNetworks() {
let result = [];
for (let netId in this._networks) {
result.push(this._networks[netId].network);
}
return result;
},
/*
* Function called from manager to get stats from database.
* In order to return updated stats, first is performed a call to
@ -241,71 +172,69 @@ this.NetworkStatsService = {
* Then, depending on the request (stats per appId or total stats)
* it retrieve them from database and return to the manager.
*/
getSamples: function getSamples(mm, msg) {
let self = this;
let network = msg.network;
let netId = this.getNetworkId(network.id, network.type);
getStats: function getStats(mm, msg) {
this.updateAllStats(function onStatsUpdated(aResult, aMessage) {
if (!this._networks[netId]) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: "Invalid connectionType", result: null });
return;
}
let data = msg.data;
let appId = 0;
let manifestURL = msg.manifestURL;
if (manifestURL) {
appId = appsService.getAppLocalIdByManifestURL(manifestURL);
let options = { appId: 0,
connectionType: data.connectionType,
start: data.start,
end: data.end };
if (!appId) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: "Invalid manifestURL", result: null });
let manifestURL = data.manifestURL;
if (manifestURL) {
let appId = appsService.getAppLocalIdByManifestURL(manifestURL);
if (DEBUG) {
debug("get appId: " + appId + " from manifestURL: " + manifestURL);
}
if (!appId) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: "Invalid manifestURL", result: null });
return;
}
options.appId = appId;
options.manifestURL = manifestURL;
}
if (DEBUG) {
debug("getStats for options: " + JSON.stringify(options));
}
if (!options.connectionType || options.connectionType.length == 0) {
this._db.findAll(function onStatsFound(error, result) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: error, result: result });
}, options);
return;
}
}
let start = new Date(msg.start);
let end = new Date(msg.end);
for (let i in this._connectionTypes) {
if (this._connectionTypes[i].name == options.connectionType) {
this._db.find(function onStatsFound(error, result) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: error, result: result });
}, options);
return;
}
}
this.updateStats(netId, function onStatsUpdated(aResult, aMessage) {
debug("getstats for network " + network.id + " of type " + network.type);
debug("appId: " + appId + " from manifestURL: " + manifestURL);
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: "Invalid connectionType", result: null });
self._db.find(function onStatsFound(aError, aResult) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: aError, result: aResult });
}, network, start, end, appId, manifestURL);
});
},
clearInterfaceStats: function clearInterfaceStats(mm, msg) {
let network = msg.network;
let netId = this.getNetworkId(network.id, network.type);
debug("clear stats for network " + network.id + " of type " + network.type);
if (!this._networks[netId]) {
mm.sendAsyncMessage("NetworkStats:Clear:Return",
{ id: msg.id, error: "Invalid networkType", result: null });
return;
}
this._db.clearInterfaceStats(network, function onDBCleared(aError, aResult) {
mm.sendAsyncMessage("NetworkStats:Clear:Return",
{ id: msg.id, error: aError, result: aResult });
});
}.bind(this));
},
clearDB: function clearDB(mm, msg) {
let networks = this.availableNetworks();
this._db.clearStats(networks, function onDBCleared(aError, aResult) {
mm.sendAsyncMessage("NetworkStats:ClearAll:Return",
{ id: msg.id, error: aError, result: aResult });
this._db.clear(function onDBCleared(error, result) {
mm.sendAsyncMessage("NetworkStats:Clear:Return",
{ id: msg.id, error: error, result: result });
});
},
updateAllStats: function updateAllStats(aCallback) {
updateAllStats: function updateAllStats(callback) {
// Update |cachedAppStats|.
this.updateCachedAppStats();
@ -318,19 +247,18 @@ this.NetworkStatsService = {
// the connection type is already in the queue it is not appended again,
// else it is pushed in 'elements' array, which later will be pushed to
// the queue array.
for (let netId in this._networks) {
lastElement = { netId: netId,
queueIndex: this.updateQueueIndex(netId)};
for (let i in this._connectionTypes) {
lastElement = { type: i,
queueIndex: this.updateQueueIndex(i)};
if (lastElement.queueIndex == -1) {
elements.push({netId: lastElement.netId, callbacks: []});
elements.push({type: lastElement.type, callbacks: []});
}
}
if (elements.length > 0) {
// If length of elements is greater than 0, callback is set to
// the last element.
elements[elements.length - 1].callbacks.push(aCallback);
elements[elements.length - 1].callbacks.push(callback);
this.updateQueue = this.updateQueue.concat(elements);
} else {
// Else, it means that all connection types are already in the queue to
@ -338,14 +266,16 @@ this.NetworkStatsService = {
// the element in the main queue with the index of the last 'lastElement'.
// But before is checked that element is still in the queue because it can
// be processed while generating 'elements' array.
let element = this.updateQueue[lastElement.queueIndex];
if (aCallback &&
(!element || element.netId != lastElement.netId)) {
aCallback();
if (!this.updateQueue[lastElement.queueIndex] ||
this.updateQueue[lastElement.queueIndex].type != lastElement.queueIndex) {
if (callback) {
callback();
}
return;
}
this.updateQueue[lastElement.queueIndex].callbacks.push(aCallback);
this.updateQueue[lastElement.queueIndex].callbacks.push(callback);
}
// Call the function that process the elements of the queue.
@ -356,14 +286,14 @@ this.NetworkStatsService = {
}
},
updateStats: function updateStats(aNetId, aCallback) {
// Check if the connection is in the main queue, push a new element
updateStats: function updateStats(connectionType, callback) {
// Check if the connection type is in the main queue, push a new element
// if it is not being processed or add a callback if it is.
let index = this.updateQueueIndex(aNetId);
let index = this.updateQueueIndex(connectionType);
if (index == -1) {
this.updateQueue.push({netId: aNetId, callbacks: [aCallback]});
this.updateQueue.push({type: connectionType, callbacks: [callback]});
} else {
this.updateQueue[index].callbacks.push(aCallback);
this.updateQueue[index].callbacks.push(callback);
}
// Call the function that process the elements of the queue.
@ -371,11 +301,16 @@ this.NetworkStatsService = {
},
/*
* Find if a connection is in the main queue array and return its
* Find if a connection type is in the main queue array and return its
* index, if it is not in the array return -1.
*/
updateQueueIndex: function updateQueueIndex(aNetId) {
return this.updateQueue.map(function(e) { return e.netId; }).indexOf(aNetId);
updateQueueIndex: function updateQueueIndex(type) {
for (let i in this.updateQueue) {
if (this.updateQueue[i].type == type) {
return i;
}
}
return -1;
},
/*
@ -412,64 +347,64 @@ this.NetworkStatsService = {
}
// Call the update function for the next element.
this.update(this.updateQueue[0].netId, this.processQueue.bind(this));
this.update(this.updateQueue[0].type, this.processQueue.bind(this));
},
update: function update(aNetId, aCallback) {
update: function update(connectionType, callback) {
// Check if connection type is valid.
if (!this._networks[aNetId]) {
if (aCallback) {
aCallback(false, "Invalid network " + aNetId);
if (!this._connectionTypes[connectionType]) {
if (callback) {
callback(false, "Invalid network type " + connectionType);
}
return;
}
let interfaceName = this._networks[aNetId].interfaceName;
debug("Update stats for " + interfaceName);
if (DEBUG) {
debug("Update stats for " + this._connectionTypes[connectionType].name);
}
// Request stats to NetworkManager, which will get stats from netd, passing
// 'networkStatsAvailable' as a callback.
if (interfaceName) {
networkManager.getNetworkInterfaceStats(interfaceName,
this.networkStatsAvailable.bind(this, aCallback, aNetId));
let networkName = this._connectionTypes[connectionType].network.name;
if (networkName) {
networkManager.getNetworkInterfaceStats(networkName,
this.networkStatsAvailable.bind(this, callback, connectionType));
return;
}
if (aCallback) {
aCallback(true, "ok");
if (callback) {
callback(true, "ok");
}
},
/*
* Callback of request stats. Store stats in database.
*/
networkStatsAvailable: function networkStatsAvailable(aCallback, aNetId,
aResult, aRxBytes,
aTxBytes, aDate) {
if (!aResult) {
if (aCallback) {
aCallback(false, "Netd IPC error");
networkStatsAvailable: function networkStatsAvailable(callback, connType, result, rxBytes, txBytes, date) {
if (!result) {
if (callback) {
callback(false, "Netd IPC error");
}
return;
}
let stats = { appId: 0,
networkId: this._networks[aNetId].network.id,
networkType: this._networks[aNetId].network.type,
date: aDate,
rxBytes: aTxBytes,
txBytes: aRxBytes };
let stats = { appId: 0,
connectionType: this._connectionTypes[connType].name,
date: date,
rxBytes: rxBytes,
txBytes: txBytes };
debug("Update stats for: " + JSON.stringify(stats));
this._db.saveStats(stats, function onSavedStats(aError, aResult) {
if (aCallback) {
if (aError) {
aCallback(false, aError);
if (DEBUG) {
debug("Update stats for " + stats.connectionType + ": rx=" + stats.rxBytes +
" tx=" + stats.txBytes + " timestamp=" + stats.date);
}
this._db.saveStats(stats, function onSavedStats(error, result) {
if (callback) {
if (error) {
callback(false, error);
return;
}
aCallback(true, "OK");
callback(true, "OK");
}
});
},
@ -477,34 +412,26 @@ this.NetworkStatsService = {
/*
* Function responsible for receiving per-app stats.
*/
saveAppStats: function saveAppStats(aAppId, aNetwork, aTimeStamp, aRxBytes, aTxBytes, aCallback) {
let netId = this.convertNetworkInterface(aNetwork);
if (!netId) {
if (aCallback) {
aCallback.notify(false, "Invalid network type");
}
return;
saveAppStats: function saveAppStats(aAppId, aConnectionType, aTimeStamp, aRxBytes, aTxBytes, aCallback) {
if (DEBUG) {
debug("saveAppStats: " + aAppId + " " + aConnectionType + " " +
aTimeStamp + " " + aRxBytes + " " + aTxBytes);
}
debug("saveAppStats: " + aAppId + " " + netId + " " +
aTimeStamp + " " + aRxBytes + " " + aTxBytes);
// Check if |aAppId| and |aConnectionType| are valid.
if (!aAppId || !this._networks[netId]) {
debug("Invalid appId or network interface");
if (!aAppId || aConnectionType == NET_TYPE_UNKNOWN) {
return;
}
let stats = { appId: aAppId,
networkId: this._networks[netId].network.id,
networkType: this._networks[netId].network.type,
connectionType: this._connectionTypes[aConnectionType].name,
date: new Date(aTimeStamp),
rxBytes: aRxBytes,
txBytes: aTxBytes };
// Generate an unique key from |appId| and |connectionType|,
// which is used to retrieve data in |cachedAppStats|.
let key = stats.appId + "" + netId;
let key = stats.appId + stats.connectionType;
// |cachedAppStats| only keeps the data with the same date.
// If the incoming date is different from |cachedAppStatsDate|,
@ -551,15 +478,19 @@ this.NetworkStatsService = {
appStats.txBytes > MAX_CACHED_TRAFFIC) {
this._db.saveStats(appStats,
function (error, result) {
debug("Application stats inserted in indexedDB");
if (DEBUG) {
debug("Application stats inserted in indexedDB");
}
}
);
delete this.cachedAppStats[key];
}
},
updateCachedAppStats: function updateCachedAppStats(aCallback) {
debug("updateCachedAppStats: " + this.cachedAppStatsDate);
updateCachedAppStats: function updateCachedAppStats(callback) {
if (DEBUG) {
debug("updateCachedAppStats: " + this.cachedAppStatsDate);
}
let stats = Object.keys(this.cachedAppStats);
if (stats.length == 0) {
@ -578,16 +509,16 @@ this.NetworkStatsService = {
if (index == stats.length - 1) {
this.cachedAppStats = Object.create(null);
if (!aCallback) {
if (!callback) {
return;
}
if (error) {
aCallback(false, error);
callback(false, error);
return;
}
aCallback(true, "ok");
callback(true, "ok");
return;
}
@ -603,17 +534,17 @@ this.NetworkStatsService = {
},
logAllRecords: function logAllRecords() {
this._db.logAllRecords(function onResult(aError, aResult) {
if (aError) {
debug("Error: " + aError);
this._db.logAllRecords(function onResult(error, result) {
if (error) {
debug("Error: " + error);
return;
}
debug("===== LOG =====");
debug("There are " + aResult.length + " items");
debug(JSON.stringify(aResult));
debug("There are " + result.length + " items");
debug(JSON.stringify(result));
});
},
}
};
NetworkStatsService.init();

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

@ -29,15 +29,15 @@ NetworkStatsServiceProxy.prototype = {
* Function called in the protocol layer (HTTP, FTP, WebSocket ...etc)
* to pass the per-app stats to NetworkStatsService.
*/
saveAppStats: function saveAppStats(aAppId, aNetwork, aTimeStamp,
saveAppStats: function saveAppStats(aAppId, aConnectionType, aTimeStamp,
aRxBytes, aTxBytes, aCallback) {
if (DEBUG) {
debug("saveAppStats: " + aAppId + " connectionType " + aNetwork.type +
" " + aTimeStamp + " " + aRxBytes + " " + aTxBytes);
debug("saveAppStats: " + aAppId + " " + aConnectionType + " " +
aTimeStamp + " " + aRxBytes + " " + aTxBytes);
}
NetworkStatsService.saveAppStats(aAppId, aNetwork, aTimeStamp,
aRxBytes, aTxBytes, aCallback);
NetworkStatsService.saveAppStats(aAppId, aConnectionType, aTimeStamp,
aRxBytes, aTxBytes, aCallback);
},
classID : NETWORKSTATSSERVICEPROXY_CID,

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

@ -167,7 +167,7 @@ TCPSocket.prototype = {
_txBytes: 0,
_rxBytes: 0,
_appId: Ci.nsIScriptSecurityManager.NO_APP_ID,
_activeNetwork: null,
_connectionType: Ci.nsINetworkInterface.NETWORK_TYPE_UNKNOWN,
#endif
// Public accessors.
@ -347,7 +347,7 @@ TCPSocket.prototype = {
LOG("Error: Ci.nsINetworkStatsServiceProxy service is not available.");
return;
}
nssProxy.saveAppStats(this._appId, this._activeNetwork, Date.now(),
nssProxy.saveAppStats(this._appId, this._connectionType, Date.now(),
this._rxBytes, this._txBytes);
// Reset the counters once the statistics is saved to NetworkStatsServiceProxy.
@ -530,12 +530,12 @@ TCPSocket.prototype = {
that._initStream(that._binaryType);
#ifdef MOZ_WIDGET_GONK
// Set _activeNetwork, which is only required for network statistics.
// Set _connectionType, which is only required for network statistics.
// Note that nsINetworkManager, as well as nsINetworkStatsServiceProxy, is
// Gonk-specific.
let networkManager = Cc["@mozilla.org/network/manager;1"].getService(Ci.nsINetworkManager);
if (networkManager) {
that._activeNetwork = networkManager.active;
if (networkManager && networkManager.active) {
that._connectionType = networkManager.active.type;
}
#endif

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

@ -12,34 +12,45 @@
<pre id="test">
<script type="application/javascript">
// Test for NetworkStats
function checkInterface(aInterface) {
ok(!(aInterface in window), aInterface + " should be prefixed");
ok(("Moz" + aInterface) in window, aInterface + " should be prefixed");
}
function test() {
// Test interfaces
checkInterface("NetworkStatsManager");
checkInterface("NetworkStats");
checkInterface("NetworkStatsData");
ok('mozNetworkStats' in navigator, "navigator.mozMozNetworkStats should exist");
ok(navigator.mozNetworkStats, "navigator.mozNetworkStats returns an object");
netStats = navigator.mozNetworkStats;
// Test IDL attributes
ok('availableNetworks' in netStats,
"availableNetworks should be a NetworkStats attribute");
ok(Array.isArray(netStats.availableNetworks) && netStats.availableNetworks.length > 0,
"availableNetworks is an array not empty.");
ok('connectionTypes' in netStats,
"connectionTypes should be a NetworkStats attribute");
ok(Array.isArray(netStats.connectionTypes) && netStats.connectionTypes.length > 0,
"connectionTypes is an array not empty.");
ok('sampleRate' in netStats,
"sampleRate should be a NetworkStats attribute");
ok(netStats.sampleRate > 0,
"sampleRate is greater than 0.");
ok('maxStorageAge' in netStats,
"maxStorageAge should be a NetworkStats attribute");
ok(netStats.maxStorageAge > 0,
"maxStorageAge is greater than 0.");
ok('maxStorageSamples' in netStats,
"maxStorageSamples should be a NetworkStats attribute");
ok(netStats.maxStorageSamples > 0,
"maxStorageSamples is greater than 0.");
// Test IDL methods
next();
return;
}
function checkDataDates(data, start, end, sampleRate) {
function checkDataDates(data, start, end, sampleRate){
var offset = (new Date()).getTimezoneOffset() * 60 * 1000;
start = Math.floor((start.getTime() - offset) / sampleRate) * sampleRate + offset;
end = Math.floor((end.getTime() - offset) / sampleRate) * sampleRate + offset;
@ -60,114 +71,84 @@ function checkDataDates(data, start, end, sampleRate) {
ok(success, "data result has correct dates");
}
function compareNetworks(networkA, networkB) {
return (networkA.id == networkB.id &&
networkA.type == networkB.type);
}
var req;
var index = -1;
var netStats = null;
var steps = [
function () {
// Test clearAllStats
req = netStats.clearAllStats();
// Test clearAlldata
req = netStats.clearAllData();
req.onsuccess = function () {
ok(true, "clearAllStats deleted the database");
ok(true, "clearAllData deleted the database");
next();
};
req.onerror = function () {
ok(false, "clearAllStats deleted the database");
ok(false, "clearAllData deleted the database");
}
},
function () {
// Check if getSamples launch exception when start is greather than end
// Check if getNetworkStats launch exception when start is greather than end
// Prepare get params
var network = netStats.availableNetworks[0];
var type = netStats.connectionTypes[0];
// Get dates
var endDate = new Date();
var startDate = new Date(endDate.getTime() + 1000);
try {
netStats.getSamples(network, startDate, endDate);
netStats.getNetworkStats({start: startDate, end: endDate});
} catch(ex) {
ok(true, "getSamples launch exception when start is greater than end");
ok(true, "getNetworkStats launch exception when start is greater than end");
next();
return;
}
ok(false, "getSamples launch exception when start is greater than end");
ok(false, "getNetworkStats launch exceptionwhen start is greater than end");
next();
return;
},
function () {
// Test if call getSamples with network of type different than
// nsIDOMMozNetworkStatsInterface launch an exception
// Test if call getNetworkStats with undefined start param launch an exception
// Prepare get params
var network = "wifi";
var endDate = new Date();
var startDate = new Date(endDate.getTime() - 1000);
var type = netStats.connectionTypes[0];
setTimeout(function() {
try {
netStats.getNetworkStats({end: new Date()});
} catch(ex) {
ok(true, "getNetworkStats launch exception when start param does not exist");
next();
return;
}
try {
netStats.getSamples(network, new Date(), new Date());
} catch(ex) {
ok(true, "getSamples launch exception if network is not " +
"a nsIDOMMozNetworkStatsInterface");
next();
return;
}
ok(false, "getSamples launch exception if network is not " +
"a nsIDOMMozNetworkStatsInterface");
ok(false, "getNetworkStats launch exception when start param does not exist");
}, 1000);
},
function () {
// Test if call getSamples with start parameter type different than Date launch an exception
// Test if call getNetworkStats with undefined end param launch an exception
// Prepare get params
var network = netStats.availableNetworks[0];
var endDate = new Date();
var startDate = new Date(endDate.getTime() - 1000);
startDate = startDate.toString();
var type = netStats.connectionTypes[0];
setTimeout(function() {
try {
netStats.getNetworkStats({start: new Date()});
} catch(ex) {
ok(true, "getNetworkStats launch exception when end param does not exist");
next();
return;
}
try {
netStats.getSamples(network, startDate, endDate);
} catch(ex) {
ok(true, "getSamples launch exception when start param is not a Date");
next();
return;
}
ok(false, "getSamples launch exception when start param is not a Date");
ok(false, "getNetworkStats launch exception when end param does not exist");
}, 1000);
},
function () {
// Test if call getSamples with end parameter type different than Date launch an exception
ok(true, "Get system stats for a connectionType and dates adapted to samplerate");
// Prepare get params
var network = netStats.availableNetworks[0];
var endDate = new Date();
var startDate = new Date(endDate.getTime() - 1000);
endDate = startDate.toString();
try {
netStats.getSamples(network, startDate, endDate);
} catch(ex) {
ok(true, "getSamples launch exception when end param is not a Date");
next();
return;
}
ok(false, "getSamples launch exception when end param is not a Date");
},
function () {
ok(true, "Get stats for a network and dates adapted to samplerate");
// Prepare get params
var network = netStats.availableNetworks[0];
var type = netStats.connectionTypes[0];
var diff = 2;
// Get samplerate in millis
var sampleRate = netStats.sampleRate;
var sampleRate = netStats.sampleRate * 1000;
// Get date with samplerate's precision
var offset = new Date().getTimezoneOffset() * 60 * 1000;
var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
@ -178,11 +159,11 @@ var steps = [
var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
// Launch request
req = netStats.getSamples(network, startDate, endDate);
req = netStats.getNetworkStats({start: startDate, end: endDate, connectionType: type});
req.onsuccess = function () {
ok(true, "Get system stats request ok");
ok(req.result.manifestURL == null, "manifestURL should be null");
ok(compareNetworks(req.result.network, network), "networks should be equals");
ok(req.result.connectionType == type, "connectionTypes should be equals");
ok(req.result.start.getTime() == startDate.getTime(), "starts should be equals");
ok(req.result.end.getTime() == endDate.getTime(), "ends should be equals");
var data = req.result.data;
@ -192,16 +173,123 @@ var steps = [
next();
};
req.onerror = function () {
ok(false, "Get stats failure!");
ok(false, "Get system stats for a connectionType failure!");
}
},
function () {
ok(true, "Get system stats for a network and dates not adapted to samplerate");
ok(true, "Get system stats for all connectionTypes and dates adapted to samplerate");
// Prepare get params
var network = netStats.availableNetworks[0];
var diff = 2;
// Get samplerate in millis
var sampleRate = netStats.sampleRate;
var sampleRate = netStats.sampleRate * 1000;
// Get date with samplerate's precision
var offset = new Date().getTimezoneOffset() * 60 * 1000;
var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
* sampleRate + offset);
var startDate = new Date(endDate.getTime() - (sampleRate * diff));
// Calculate the number of samples that should be returned based on the
// the samplerate and including final and initial samples.
var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
// Launch request
req = netStats.getNetworkStats({start: startDate, end: endDate});
req.onsuccess = function () {
ok(true, "Get stats request ok");
ok(req.result.manifestURL == null, "manifestURL should be null");
ok(req.result.connectionType == null, "connectionTypes should be null");
ok(req.result.start.getTime() == startDate.getTime(), "starts should be equals");
ok(req.result.end.getTime() == endDate.getTime(), "ends should be equals");
var data = req.result.data;
ok(Array.isArray(data) && data.length == samples,
"data is an array of length " + samples);
checkDataDates(data, startDate, endDate, sampleRate);
next();
};
req.onerror = function () {
ok(false, "Get system stats for all connectionTypes failure!");
}
},
function () {
ok(true, "Get app stats for a connectionType and dates adapted to samplerate");
// Prepare get params
var url = 'app://browser.gaiamobile.org/manifest.webapp';
var type = netStats.connectionTypes[0];
var diff = 2;
// Get samplerate in millis
var sampleRate = netStats.sampleRate * 1000;
// Get date with samplerate's precision
var offset = new Date().getTimezoneOffset() * 60 * 1000;
var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
* sampleRate + offset);
var startDate = new Date(endDate.getTime() - (sampleRate * diff));
// Calculate the number of samples that should be returned based on the
// the samplerate and including final and initial samples.
var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
// Launch request
req = netStats.getNetworkStats({start: startDate,
end: endDate,
connectionType: type,
manifestURL: url});
req.onsuccess = function () {
ok(true, "Get app stats request ok");
ok(req.result.manifestURL == url, "manifestURL should be equals");
ok(req.result.connectionType == type, "connectionTypes should be equals");
ok(req.result.start.getTime() == startDate.getTime(), "starts should be equals");
ok(req.result.end.getTime() == endDate.getTime(), "ends should be equals");
var data = req.result.data;
ok(Array.isArray(data) && data.length == samples,
"data is an array of length " + samples);
checkDataDates(data, startDate, endDate, sampleRate);
next();
};
req.onerror = function () {
ok(false, "Get app stats for a connectionType failure!");
}
},
function () {
ok(true, "Get app stats for all connectionTypes and dates adapted to samplerate");
// Prepare get params
var url = 'app://browser.gaiamobile.org/manifest.webapp';
var diff = 2;
// Get samplerate in millis
var sampleRate = netStats.sampleRate * 1000;
// Get date with samplerate's precision
var offset = new Date().getTimezoneOffset() * 60 * 1000;
var endDate = new Date(Math.floor((new Date().getTime() - offset) / sampleRate)
* sampleRate + offset);
var startDate = new Date(endDate.getTime() - (sampleRate * diff));
// Calculate the number of samples that should be returned based on the
// the samplerate and including final and initial samples.
var samples = (endDate.getTime() - startDate.getTime()) / sampleRate + 1;
// Launch request
req = netStats.getNetworkStats({start: startDate,
end: endDate,
manifestURL: url});
req.onsuccess = function () {
ok(true, "Get app stats request ok");
ok(req.result.manifestURL == url, "manifestURL should be equals");
ok(req.result.connectionType == null, "connectionTypes should be null");
ok(req.result.start.getTime() == startDate.getTime(), "starts should be equals");
ok(req.result.end.getTime() == endDate.getTime(), "ends should be equals");
var data = req.result.data;
ok(Array.isArray(data) && data.length == samples,
"data is an array of length " + samples);
checkDataDates(data, startDate, endDate, sampleRate);
next();
};
req.onerror = function () {
ok(false, "Get app stats for all connectionTypes failure!");
}
},
function () {
ok(true, "Get system stats for a connectionType and dates not adapted to samplerate");
// Prepare get params
var type = netStats.connectionTypes[0];
var diff = 2;
// Get samplerate in millis
var sampleRate = netStats.sampleRate * 1000;
var endDate = new Date();
var startDate = new Date(endDate.getTime() - (sampleRate * diff));
// Calculate the number of samples that should be returned based on the
@ -211,11 +299,11 @@ var steps = [
Math.floor(startDate.getTime() / (sampleRate)) * sampleRate) / sampleRate + 1;
// Launch request
req = netStats.getSamples(network, startDate, endDate);
req = netStats.getNetworkStats({start: startDate, end: endDate, connectionType: type});
req.onsuccess = function () {
ok(true, "Get stats request ok");
ok(true, "Get system stats request ok");
ok(req.result.manifestURL == null, "manifestURL should be null");
ok(compareNetworks(req.result.network, network), "networks should be equals");
ok(req.result.connectionType == type, "connectionTypes should be equals");
ok(req.result.start.getTime() == startDate.getTime(), "starts should be equals");
ok(req.result.end.getTime() == endDate.getTime(), "ends should be equals");
var data = req.result.data;
@ -225,20 +313,40 @@ var steps = [
next();
};
req.onerror = function () {
ok(false, "Get stats failure!");
ok(false, "Get system stats for a connectionType failure!");
}
},
function () {
// Test clearStats
var network = netStats.availableNetworks[0];
ok(true, "Get system stats for all connectionTypes and dates not adapted to samplerate");
// Prepare get params
var diff = 2;
// Get samplerate in millis
var sampleRate = netStats.sampleRate * 1000;
// Get date with samplerate's precision
var endDate = new Date();
var startDate = new Date(endDate.getTime() - (sampleRate * diff));
// Calculate the number of samples that should be returned based on the
// the samplerate, including final and initial samples and taking into
// account that these will be filtered according to precision.
var samples = (Math.floor(endDate.getTime() / (sampleRate)) * sampleRate -
Math.floor(startDate.getTime() / (sampleRate)) * sampleRate) / sampleRate + 1;
req = netStats.clearStats(network);
// Launch request
req = netStats.getNetworkStats({start: startDate, end: endDate});
req.onsuccess = function () {
ok(true, "clearStats deleted the database");
ok(true, "Get stats request ok");
ok(req.result.manifestURL == null, "manifestURL should be null");
ok(req.result.connectionType == null, "connectionTypes should be null");
ok(req.result.start.getTime() == startDate.getTime(), "starts should be equals");
ok(req.result.end.getTime() == endDate.getTime(), "ends should be equals");
var data = req.result.data;
ok(Array.isArray(data) && data.length == samples,
"data is an array of length " + samples);
checkDataDates(data, startDate, endDate, sampleRate);
next();
};
req.onerror = function () {
ok(false, "clearStats deleted the database");
ok(false, "Get system stats for all connectionType failure!");
}
},
function () {

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

@ -12,7 +12,7 @@
<pre id="test">
<script type="application/javascript">
// Test to ensure NetworkStats is enabled but mozNetworkStats.availableNetworks
// Test to ensure NetworkStats is enabled but mozNetworkStats.connectionTypes
// does not work in content.
SpecialPowers.setBoolPref("dom.mozNetworkStats.enabled", true);
@ -22,12 +22,12 @@ ok('mozNetworkStats' in navigator, "navigator.mozNetworkStats should be accessib
var error;
try {
navigator.mozNetworkStats.availableNetworks;
ok(false, "Accessing navigator.mozNetworkStats.availableNetworks should have thrown!");
navigator.mozNetworkStats.connectionTypes;
ok(false, "Accessing navigator.mozNetworkStats.connectionTypes should have thrown!");
} catch (ex) {
error = ex;
}
ok(error, "Got an exception accessing navigator.mozNetworkStats.availableNetworks");
ok(error, "Got an exception accessing navigator.mozNetworkStats.connectionTypes");
</script>
</pre>

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

@ -5,7 +5,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/NetworkStatsDB.jsm");
const netStatsDb = new NetworkStatsDB();
const netStatsDb = new NetworkStatsDB(this);
function filterTimestamp(date) {
var sampleRate = netStatsDb.sampleRate;
@ -13,15 +13,6 @@ function filterTimestamp(date) {
return Math.floor((date.getTime() - offset) / sampleRate) * sampleRate;
}
function getNetworks() {
return [{ id: '0', type: Ci.nsIDOMMozNetworkStatsManager.WIFI },
{ id: '1234', type: Ci.nsIDOMMozNetworkStatsManager.MOBILE }];
}
function compareNetworks(networkA, networkB) {
return (networkA[0] == networkB[0] && networkA[1] == networkB[1]);
}
add_test(function test_sampleRate() {
var sampleRate = netStatsDb.sampleRate;
do_check_true(sampleRate > 0);
@ -98,31 +89,20 @@ add_test(function test_fillResultSamples_noEmptyData() {
});
add_test(function test_clear() {
var networks = getNetworks();
netStatsDb.clearStats(networks, function (error, result) {
do_check_eq(error, null);
run_next_test();
});
});
add_test(function test_clear_interface() {
var networks = getNetworks();
netStatsDb.clearInterfaceStats(networks[0], function (error, result) {
netStatsDb.clear(function (error, result) {
do_check_eq(error, null);
run_next_test();
});
});
add_test(function test_internalSaveStats_singleSample() {
var networks = getNetworks();
var stats = { appId: 0,
network: [networks[0].id, networks[0].type],
timestamp: Date.now(),
rxBytes: 0,
txBytes: 0,
rxTotalBytes: 1234,
txTotalBytes: 1234 };
var stats = {appId: 0,
connectionType: "wifi",
timestamp: Date.now(),
rxBytes: 0,
txBytes: 0,
rxTotalBytes: 1234,
txTotalBytes: 1234};
netStatsDb.dbNewTxn("readwrite", function(txn, store) {
netStatsDb._saveStats(txn, store, stats);
@ -133,7 +113,7 @@ add_test(function test_internalSaveStats_singleSample() {
do_check_eq(error, null);
do_check_eq(result.length, 1);
do_check_eq(result[0].appId, stats.appId);
do_check_true(compareNetworks(result[0].network, stats.network));
do_check_eq(result[0].connectionType, stats.connectionType);
do_check_eq(result[0].timestamp, stats.timestamp);
do_check_eq(result[0].rxBytes, stats.rxBytes);
do_check_eq(result[0].txBytes, stats.txBytes);
@ -145,23 +125,19 @@ add_test(function test_internalSaveStats_singleSample() {
});
add_test(function test_internalSaveStats_arraySamples() {
var networks = getNetworks();
netStatsDb.clearStats(networks, function (error, result) {
netStatsDb.clear(function (error, result) {
do_check_eq(error, null);
var network = [networks[0].id, networks[0].type];
var samples = 2;
var stats = [];
for (var i = 0; i < samples; i++) {
stats.push({ appId: 0,
network: network,
timestamp: Date.now() + (10 * i),
rxBytes: 0,
txBytes: 0,
rxTotalBytes: 1234,
txTotalBytes: 1234 });
stats.push({appId: 0,
connectionType: "wifi",
timestamp: Date.now() + (10 * i),
rxBytes: 0,
txBytes: 0,
rxTotalBytes: 1234,
txTotalBytes: 1234});
}
netStatsDb.dbNewTxn("readwrite", function(txn, store) {
@ -171,16 +147,12 @@ add_test(function test_internalSaveStats_arraySamples() {
netStatsDb.logAllRecords(function(error, result) {
do_check_eq(error, null);
// Result has one sample more than samples because clear inserts
// an empty sample to keep totalBytes synchronized with netd counters
result.shift();
do_check_eq(result.length, samples);
var success = true;
for (var i = 1; i < samples; i++) {
for (var i = 0; i < samples; i++) {
if (result[i].appId != stats[i].appId ||
!compareNetworks(result[i].network, stats[i].network) ||
result[i].connectionType != stats[i].connectionType ||
result[i].timestamp != stats[i].timestamp ||
result[i].rxBytes != stats[i].rxBytes ||
result[i].txBytes != stats[i].txBytes ||
@ -198,31 +170,28 @@ add_test(function test_internalSaveStats_arraySamples() {
});
add_test(function test_internalRemoveOldStats() {
var networks = getNetworks();
netStatsDb.clearStats(networks, function (error, result) {
netStatsDb.clear(function (error, result) {
do_check_eq(error, null);
var network = [networks[0].id, networks[0].type];
var samples = 10;
var stats = [];
for (var i = 0; i < samples - 1; i++) {
stats.push({ appId: 0,
network: network, timestamp: Date.now() + (10 * i),
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234 });
stats.push({appId: 0,
connectionType: "wifi", timestamp: Date.now() + (10 * i),
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234});
}
stats.push({ appId: 0,
network: network, timestamp: Date.now() + (10 * samples),
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234 });
stats.push({appId: 0,
connectionType: "wifi", timestamp: Date.now() + (10 * samples),
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234});
netStatsDb.dbNewTxn("readwrite", function(txn, store) {
netStatsDb._saveStats(txn, store, stats);
var date = stats[stats.length - 1].timestamp
var date = stats[stats.length -1].timestamp
+ (netStatsDb.sampleRate * netStatsDb.maxStorageSamples - 1) - 1;
netStatsDb._removeOldStats(txn, store, 0, network, date);
netStatsDb._removeOldStats(txn, store, 0, "wifi", date);
}, function(error, result) {
do_check_eq(error, null);
@ -236,14 +205,14 @@ add_test(function test_internalRemoveOldStats() {
});
});
function processSamplesDiff(networks, lastStat, newStat, callback) {
netStatsDb.clearStats(networks, function (error, result){
function processSamplesDiff(lastStat, newStat, callback) {
netStatsDb.clear(function (error, result){
do_check_eq(error, null);
netStatsDb.dbNewTxn("readwrite", function(txn, store) {
netStatsDb._saveStats(txn, store, lastStat);
}, function(error, result) {
netStatsDb.dbNewTxn("readwrite", function(txn, store) {
let request = store.index("network").openCursor(newStat.network, "prev");
let request = store.index("connectionType").openCursor(newStat.connectionType, "prev");
request.onsuccess = function onsuccess(event) {
let cursor = event.target.result;
do_check_neq(cursor, null);
@ -261,26 +230,22 @@ function processSamplesDiff(networks, lastStat, newStat, callback) {
}
add_test(function test_processSamplesDiffSameSample() {
var networks = getNetworks();
var network = [networks[0].id, networks[0].type];
var sampleRate = netStatsDb.sampleRate;
var date = filterTimestamp(new Date());
var lastStat = {appId: 0,
connectionType: "wifi", timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234};
var lastStat = { appId: 0,
network: network, timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234 };
var newStat = {appId: 0,
connectionType: "wifi", timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 2234, txTotalBytes: 2234};
var newStat = { appId: 0,
network: network, timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 2234, txTotalBytes: 2234 };
processSamplesDiff(networks, lastStat, newStat, function(result) {
processSamplesDiff(lastStat, newStat, function(result) {
do_check_eq(result.length, 1);
do_check_eq(result[0].appId, newStat.appId);
do_check_true(compareNetworks(result[0].network, newStat.network));
do_check_eq(result[0].connectionType, newStat.connectionType);
do_check_eq(result[0].timestamp, newStat.timestamp);
do_check_eq(result[0].rxBytes, newStat.rxTotalBytes - lastStat.rxTotalBytes);
do_check_eq(result[0].txBytes, newStat.txTotalBytes - lastStat.txTotalBytes);
@ -291,26 +256,22 @@ add_test(function test_processSamplesDiffSameSample() {
});
add_test(function test_processSamplesDiffNextSample() {
var networks = getNetworks();
var network = [networks[0].id, networks[0].type];
var sampleRate = netStatsDb.sampleRate;
var date = filterTimestamp(new Date());
var lastStat = {appId: 0,
connectionType: "wifi", timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234};
var lastStat = { appId: 0,
network: network, timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234 };
var newStat = {appId: 0,
connectionType: "wifi", timestamp: date + sampleRate,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 500, txTotalBytes: 500};
var newStat = { appId: 0,
network: network, timestamp: date + sampleRate,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 500, txTotalBytes: 500 };
processSamplesDiff(networks, lastStat, newStat, function(result) {
processSamplesDiff(lastStat, newStat, function(result) {
do_check_eq(result.length, 2);
do_check_eq(result[1].appId, newStat.appId);
do_check_true(compareNetworks(result[1].network, newStat.network));
do_check_eq(result[1].connectionType, newStat.connectionType);
do_check_eq(result[1].timestamp, newStat.timestamp);
do_check_eq(result[1].rxBytes, newStat.rxTotalBytes);
do_check_eq(result[1].txBytes, newStat.txTotalBytes);
@ -321,25 +282,23 @@ add_test(function test_processSamplesDiffNextSample() {
});
add_test(function test_processSamplesDiffSamplesLost() {
var networks = getNetworks();
var network = [networks[0].id, networks[0].type];
var samples = 5;
var sampleRate = netStatsDb.sampleRate;
var date = filterTimestamp(new Date());
var lastStat = { appId: 0,
network: network, timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234 };
var lastStat = {appId: 0,
connectionType: "wifi", timestamp: date,
rxBytes: 0, txBytes: 0,
rxTotalBytes: 1234, txTotalBytes: 1234};
var newStat = { appId: 0,
network: network, timestamp: date + (sampleRate * samples),
rxBytes: 0, txBytes: 0,
rxTotalBytes: 2234, txTotalBytes: 2234 };
var newStat = {appId: 0,
connectionType: "wifi", timestamp: date + (sampleRate * samples),
rxBytes: 0, txBytes: 0,
rxTotalBytes: 2234, txTotalBytes: 2234};
processSamplesDiff(networks, lastStat, newStat, function(result) {
processSamplesDiff(lastStat, newStat, function(result) {
do_check_eq(result.length, samples + 1);
do_check_eq(result[0].appId, newStat.appId);
do_check_true(compareNetworks(result[samples].network, newStat.network));
do_check_eq(result[samples].connectionType, newStat.connectionType);
do_check_eq(result[samples].timestamp, newStat.timestamp);
do_check_eq(result[samples].rxBytes, newStat.rxTotalBytes - lastStat.rxTotalBytes);
do_check_eq(result[samples].txBytes, newStat.txTotalBytes - lastStat.txTotalBytes);
@ -350,17 +309,13 @@ add_test(function test_processSamplesDiffSamplesLost() {
});
add_test(function test_saveStats() {
var networks = getNetworks();
var network = [networks[0].id, networks[0].type];
var stats = {appId: 0,
connectionType: "wifi",
date: new Date(),
rxBytes: 2234,
txBytes: 2234};
var stats = { appId: 0,
networkId: networks[0].id,
networkType: networks[0].type,
date: new Date(),
rxBytes: 2234,
txBytes: 2234};
netStatsDb.clearStats(networks, function (error, result) {
netStatsDb.clear(function (error, result) {
do_check_eq(error, null);
netStatsDb.saveStats(stats, function(error, result) {
do_check_eq(error, null);
@ -368,7 +323,7 @@ add_test(function test_saveStats() {
do_check_eq(error, null);
do_check_eq(result.length, 1);
do_check_eq(result[0].appId, stats.appId);
do_check_true(compareNetworks(result[0].network, network));
do_check_eq(result[0].connectionType, stats.connectionType);
let timestamp = filterTimestamp(stats.date);
do_check_eq(result[0].timestamp, timestamp);
do_check_eq(result[0].rxBytes, 0);
@ -382,44 +337,35 @@ add_test(function test_saveStats() {
});
add_test(function test_saveAppStats() {
var networks = getNetworks();
var network = [networks[0].id, networks[0].type];
var stats = {appId: 1,
connectionType: "wifi",
date: new Date(),
rxBytes: 2234,
txBytes: 2234};
var stats = { appId: 1,
networkId: networks[0].id,
networkType: networks[0].type,
date: new Date(),
rxBytes: 2234,
txBytes: 2234};
netStatsDb.clearStats(networks, function (error, result) {
netStatsDb.clear(function (error, result) {
do_check_eq(error, null);
netStatsDb.saveStats(stats, function(error, result) {
do_check_eq(error, null);
netStatsDb.logAllRecords(function(error, result) {
do_check_eq(error, null);
// The clear function clears all records of the datbase but
// inserts a new element for each [appId, connectionId, connectionType]
// record to keep the track of rxTotalBytes / txTotalBytes.
// So at this point, we have two records, one for the appId 0 used in
// past tests and the new one for appId 1
do_check_eq(result.length, 2);
do_check_eq(result[1].appId, stats.appId);
do_check_true(compareNetworks(result[1].network, network));
do_check_eq(result.length, 1);
do_check_eq(result[0].appId, stats.appId);
do_check_eq(result[0].connectionType, stats.connectionType);
let timestamp = filterTimestamp(stats.date);
do_check_eq(result[1].timestamp, timestamp);
do_check_eq(result[1].rxBytes, stats.rxBytes);
do_check_eq(result[1].txBytes, stats.txBytes);
do_check_eq(result[1].rxTotalBytes, 0);
do_check_eq(result[1].txTotalBytes, 0);
do_check_eq(result[0].timestamp, timestamp);
do_check_eq(result[0].rxBytes, stats.rxBytes);
do_check_eq(result[0].txBytes, stats.txBytes);
do_check_eq(result[0].rxTotalBytes, 0);
do_check_eq(result[0].txTotalBytes, 0);
run_next_test();
});
});
});
});
function prepareFind(network, stats, callback) {
netStatsDb.clearStats(network, function (error, result) {
function prepareFind(stats, callback) {
netStatsDb.clear(function (error, result) {
do_check_eq(error, null);
netStatsDb.dbNewTxn("readwrite", function(txn, store) {
netStatsDb._saveStats(txn, store, stats);
@ -430,11 +376,6 @@ function prepareFind(network, stats, callback) {
}
add_test(function test_find () {
var networks = getNetworks();
var networkWifi = [networks[0].id, networks[0].type];
var networkMobile = [networks[1].id, networks[1].type]; // Fake mobile interface
var appId = 0;
var samples = 5;
var sampleRate = netStatsDb.sampleRate;
var start = Date.now();
@ -443,39 +384,46 @@ add_test(function test_find () {
start = new Date(start - sampleRate);
var stats = [];
for (var i = 0; i < samples; i++) {
stats.push({ appId: appId,
network: networkWifi, timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0 });
stats.push({appId: 0,
connectionType: "wifi", timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0});
stats.push({ appId: appId,
network: networkMobile, timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0 });
stats.push({appId: 0,
connectionType: "mobile", timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0});
}
prepareFind(networks[0], stats, function(error, result) {
prepareFind(stats, function(error, result) {
do_check_eq(error, null);
netStatsDb.find(function (error, result) {
do_check_eq(error, null);
do_check_eq(result.network.id, networks[0].id);
do_check_eq(result.network.type, networks[0].type);
do_check_eq(result.connectionType, "wifi");
do_check_eq(result.start.getTime(), start.getTime());
do_check_eq(result.end.getTime(), end.getTime());
do_check_eq(result.data.length, samples + 1);
do_check_eq(result.data[0].rxBytes, null);
do_check_eq(result.data[1].rxBytes, 0);
do_check_eq(result.data[samples].rxBytes, 0);
run_next_test();
}, networks[0], start, end, appId);
netStatsDb.findAll(function (error, result) {
do_check_eq(error, null);
do_check_eq(result.connectionType, null);
do_check_eq(result.start.getTime(), start.getTime());
do_check_eq(result.end.getTime(), end.getTime());
do_check_eq(result.data.length, samples + 1);
do_check_eq(result.data[0].rxBytes, null);
do_check_eq(result.data[1].rxBytes, 0);
do_check_eq(result.data[1].txBytes, 20);
do_check_eq(result.data[samples].rxBytes, 0);
run_next_test();
}, {appId: 0, start: start, end: end});
}, {start: start, end: end, connectionType: "wifi", appId: 0});
});
});
add_test(function test_findAppStats () {
var networks = getNetworks();
var networkWifi = [networks[0].id, networks[0].type];
var networkMobile = [networks[1].id, networks[1].type]; // Fake mobile interface
var samples = 5;
var sampleRate = netStatsDb.sampleRate;
var start = Date.now();
@ -484,63 +432,69 @@ add_test(function test_findAppStats () {
start = new Date(start - sampleRate);
var stats = [];
for (var i = 0; i < samples; i++) {
stats.push({ appId: 1,
network: networkWifi, timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0 });
stats.push({appId: 1,
connectionType: "wifi", timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0});
stats.push({ appId: 1,
network: networkMobile, timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0 });
stats.push({appId: 1,
connectionType: "mobile", timestamp: saveDate + (sampleRate * i),
rxBytes: 0, txBytes: 10,
rxTotalBytes: 0, txTotalBytes: 0});
}
prepareFind(networks[0], stats, function(error, result) {
prepareFind(stats, function(error, result) {
do_check_eq(error, null);
netStatsDb.find(function (error, result) {
do_check_eq(error, null);
do_check_eq(result.network.id, networks[0].id);
do_check_eq(result.network.type, networks[0].type);
do_check_eq(result.connectionType, "wifi");
do_check_eq(result.start.getTime(), start.getTime());
do_check_eq(result.end.getTime(), end.getTime());
do_check_eq(result.data.length, samples + 1);
do_check_eq(result.data[0].rxBytes, null);
do_check_eq(result.data[1].rxBytes, 0);
do_check_eq(result.data[samples].rxBytes, 0);
run_next_test();
}, networks[0], start, end, 1);
netStatsDb.findAll(function (error, result) {
do_check_eq(error, null);
do_check_eq(result.connectionType, null);
do_check_eq(result.start.getTime(), start.getTime());
do_check_eq(result.end.getTime(), end.getTime());
do_check_eq(result.data.length, samples + 1);
do_check_eq(result.data[0].rxBytes, null);
do_check_eq(result.data[1].rxBytes, 0);
do_check_eq(result.data[1].txBytes, 20);
do_check_eq(result.data[samples].rxBytes, 0);
run_next_test();
}, {start: start, end: end, appId: 1});
}, {start: start, end: end, connectionType: "wifi", appId: 1});
});
});
add_test(function test_saveMultipleAppStats () {
var networks = getNetworks();
var networkWifi = networks[0];
var networkMobile = networks[1]; // Fake mobile interface
var saveDate = filterTimestamp(new Date());
var cached = Object.create(null);
cached['1wifi'] = { appId: 1, date: new Date(),
networkId: networkWifi.id, networkType: networkWifi.type,
rxBytes: 0, txBytes: 10 };
cached['1wifi'] = {appId: 1,
connectionType: "wifi", date: new Date(),
rxBytes: 0, txBytes: 10};
cached['1mobile'] = { appId: 1, date: new Date(),
networkId: networkMobile.id, networkType: networkMobile.type,
rxBytes: 0, txBytes: 10 };
cached['1mobile'] = {appId: 1,
connectionType: "mobile", date: new Date(),
rxBytes: 0, txBytes: 10};
cached['2wifi'] = { appId: 2, date: new Date(),
networkId: networkWifi.id, networkType: networkWifi.type,
rxBytes: 0, txBytes: 10 };
cached['2wifi'] = {appId: 2,
connectionType: "wifi", date: new Date(),
rxBytes: 0, txBytes: 10};
cached['2mobile'] = { appId: 2, date: new Date(),
networkId: networkMobile.id, networkType: networkMobile.type,
rxBytes: 0, txBytes: 10 };
cached['2mobile'] = {appId: 2,
connectionType: "mobile", date: new Date(),
rxBytes: 0, txBytes: 10};
let keys = Object.keys(cached);
let index = 0;
networks.push(networkMobile);
netStatsDb.clearStats(networks, function (error, result) {
netStatsDb.clear(function (error, result) {
do_check_eq(error, null);
netStatsDb.saveStats(cached[keys[index]],
function callback(error, result) {
@ -548,17 +502,10 @@ add_test(function test_saveMultipleAppStats () {
if (index == keys.length - 1) {
netStatsDb.logAllRecords(function(error, result) {
// Again, result has two samples more than expected samples because
// clear inserts one empty sample for each network to keep totalBytes
// synchronized with netd counters. so the first two samples have to
// be discarted.
result.shift();
result.shift();
do_check_eq(error, null);
do_check_eq(result.length, 4);
do_check_eq(result[0].appId, 1);
do_check_true(compareNetworks(result[0].network,[networkWifi.id, networkWifi.type]));
do_check_eq(result[0].connectionType, 'mobile');
do_check_eq(result[0].rxBytes, 0);
do_check_eq(result[0].txBytes, 10);
run_next_test();

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

@ -4,56 +4,48 @@
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
add_test(function test_clearDB() {
var networks = NetworkStatsService.availableNetworks();
NetworkStatsService._db.clearStats(networks, function onDBCleared(error, result) {
NetworkStatsService._db.clear(function onDBCleared(error, result) {
do_check_eq(result, null);
run_next_test();
});
});
function getNetworkId() {
var network = (NetworkStatsService.availableNetworks())[0];
return NetworkStatsService.getNetworkId(network.id, network.type);
}
add_test(function test_networkStatsAvailable_ok() {
var netId = getNetworkId();
NetworkStatsService.networkStatsAvailable(function (success, msg) {
do_check_eq(success, true);
run_next_test();
}, netId, true, 1234, 4321, new Date());
}, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, true, 1234, 4321, new Date());
});
add_test(function test_networkStatsAvailable_failure() {
var netId = getNetworkId();
NetworkStatsService.networkStatsAvailable(function (success, msg) {
do_check_eq(success, false);
run_next_test();
}, netId, false, 1234, 4321, new Date());
}, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, false, 1234, 4321, new Date());
});
add_test(function test_update_invalidNetwork() {
add_test(function test_update_invalidConnection() {
NetworkStatsService.update(-1, function (success, msg) {
do_check_eq(success, false);
do_check_eq(msg, "Invalid network -1");
do_check_eq(msg, "Invalid network type -1");
run_next_test();
});
});
add_test(function test_update() {
var netId = getNetworkId();
NetworkStatsService.update(netId, function (success, msg) {
NetworkStatsService.update(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, function (success, msg) {
do_check_eq(success, true);
run_next_test();
});
});
add_test(function test_updateQueueIndex() {
NetworkStatsService.updateQueue = [{netId: 0, callbacks: null},
{netId: 1, callbacks: null},
{netId: 2, callbacks: null},
{netId: 3, callbacks: null},
{netId: 4, callbacks: null}];
NetworkStatsService.updateQueue = [{type: 0, callbacks: null},
{type: 1, callbacks: null},
{type: 2, callbacks: null},
{type: 3, callbacks: null},
{type: 4, callbacks: null}];
var index = NetworkStatsService.updateQueueIndex(3);
do_check_eq(index, 3);
index = NetworkStatsService.updateQueueIndex(10);
@ -71,8 +63,7 @@ add_test(function test_updateAllStats() {
});
add_test(function test_updateStats_ok() {
var netId = getNetworkId();
NetworkStatsService.updateStats(netId, function(success, msg){
NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, function(success, msg){
do_check_eq(success, true);
run_next_test();
});
@ -86,20 +77,15 @@ add_test(function test_updateStats_failure() {
});
add_test(function test_queue() {
// Fill networks with fake network interfaces
// Fill connections with fake network interfaces (wlan0 and rmnet0)
// to enable netd async requests
var network = {id: "1234", type: Ci.nsIDOMMozNetworkStatsManager.MOBILE};
var netId1 = NetworkStatsService.getNetworkId(network.id, network.type);
NetworkStatsService._networks[netId1] = { network: network,
interfaceName: "net1" };
NetworkStatsService._connectionTypes[Ci.nsINetworkInterface.NETWORK_TYPE_WIFI]
.network.name = 'wlan0';
NetworkStatsService._connectionTypes[Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE]
.network.name = 'rmnet0';
network = {id: "5678", type: Ci.nsIDOMMozNetworkStatsManager.MOBILE};
var netId2 = NetworkStatsService.getNetworkId(network.id, network.type);
NetworkStatsService._networks[netId2] = { network: network,
interfaceName: "net2" };
NetworkStatsService.updateStats(netId1);
NetworkStatsService.updateStats(netId2);
NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI);
NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE);
do_check_eq(NetworkStatsService.updateQueue.length, 2);
do_check_eq(NetworkStatsService.updateQueue[0].callbacks.length, 1);
@ -107,8 +93,8 @@ add_test(function test_queue() {
return;
};
NetworkStatsService.updateStats(netId1, callback);
NetworkStatsService.updateStats(netId2, callback);
NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, callback);
NetworkStatsService.updateStats(Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, callback);
do_check_eq(NetworkStatsService.updateQueue.length, 2);
do_check_eq(NetworkStatsService.updateQueue[0].callbacks.length, 2);

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

@ -9,66 +9,33 @@ XPCOMUtils.defineLazyServiceGetter(this, "nssProxy",
"@mozilla.org/networkstatsServiceProxy;1",
"nsINetworkStatsServiceProxy");
function mokConvertNetworkInterface() {
NetworkStatsService.convertNetworkInterface = function(aNetwork) {
if (aNetwork.type != Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE &&
aNetwork.type != Ci.nsINetworkInterface.NETWORK_TYPE_WIFI) {
return null;
}
let id = '0';
if (aNetwork.type == Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE) {
id = '1234'
}
let netId = this.getNetworkId(id, aNetwork.type);
if (!this._networks[netId]) {
this._networks[netId] = Object.create(null);
this._networks[netId].network = { id: id,
type: aNetwork.type };
}
return netId;
};
}
add_test(function test_saveAppStats() {
var cachedAppStats = NetworkStatsService.cachedAppStats;
var timestamp = NetworkStatsService.cachedAppStatsDate.getTime();
var samples = 5;
// Create to fake nsINetworkInterfaces. As nsINetworkInterface can not
// be instantiated, these two vars will emulate it by filling the properties
// that will be used.
var wifi = {type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, id: "0"};
var mobile = {type: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, id: "1234"};
// Insert fake mobile network interface in NetworkStatsService
var mobileNetId = NetworkStatsService.getNetworkId(mobile.id, mobile.type);
do_check_eq(Object.keys(cachedAppStats).length, 0);
for (var i = 0; i < samples; i++) {
nssProxy.saveAppStats(1, wifi, timestamp, 10, 20);
nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
timestamp, 10, 20);
nssProxy.saveAppStats(1, mobile, timestamp, 10, 20);
nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
timestamp, 10, 20);
}
var key1 = 1 + NetworkStatsService.getNetworkId(wifi.id, wifi.type);
var key2 = 1 + mobileNetId;
var key1 = 1 + 'wifi';
var key2 = 1 + 'mobile';
do_check_eq(Object.keys(cachedAppStats).length, 2);
do_check_eq(cachedAppStats[key1].appId, 1);
do_check_eq(cachedAppStats[key1].networkId, wifi.id);
do_check_eq(cachedAppStats[key1].networkType, wifi.type);
do_check_eq(cachedAppStats[key1].connectionType, 'wifi');
do_check_eq(new Date(cachedAppStats[key1].date).getTime() / 1000,
Math.floor(timestamp / 1000));
do_check_eq(cachedAppStats[key1].rxBytes, 50);
do_check_eq(cachedAppStats[key1].txBytes, 100);
do_check_eq(cachedAppStats[key2].appId, 1);
do_check_eq(cachedAppStats[key2].networkId, mobile.id);
do_check_eq(cachedAppStats[key2].networkType, mobile.type);
do_check_eq(cachedAppStats[key2].connectionType, 'mobile');
do_check_eq(new Date(cachedAppStats[key2].date).getTime() / 1000,
Math.floor(timestamp / 1000));
do_check_eq(cachedAppStats[key2].rxBytes, 50);
@ -80,11 +47,7 @@ add_test(function test_saveAppStats() {
add_test(function test_saveAppStatsWithDifferentDates() {
var today = NetworkStatsService.cachedAppStatsDate;
var tomorrow = new Date(today.getTime() + (24 * 60 * 60 * 1000));
var wifi = {type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, id: "0"};
var mobile = {type: Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE, id: "1234"};
var key = 1 + NetworkStatsService.getNetworkId(wifi.id, wifi.type);
var key = 1 + 'wifi';
NetworkStatsService.updateCachedAppStats(
function (success, msg) {
@ -92,20 +55,21 @@ add_test(function test_saveAppStatsWithDifferentDates() {
do_check_eq(Object.keys(NetworkStatsService.cachedAppStats).length, 0);
nssProxy.saveAppStats(1, wifi, today.getTime(), 10, 20);
nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
today.getTime(), 10, 20);
nssProxy.saveAppStats(1, mobile, today.getTime(), 10, 20);
nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
today.getTime(), 10, 20);
var saveAppStatsCb = {
notify: function notify(success, message) {
do_check_eq(success, true);
var cachedAppStats = NetworkStatsService.cachedAppStats;
var key = 2 + NetworkStatsService.getNetworkId(mobile.id, mobile.type);
var key = 2 + 'mobile';
do_check_eq(Object.keys(cachedAppStats).length, 1);
do_check_eq(cachedAppStats[key].appId, 2);
do_check_eq(cachedAppStats[key].networkId, mobile.id);
do_check_eq(cachedAppStats[key].networkType, mobile.type);
do_check_eq(cachedAppStats[key].connectionType, 'mobile');
do_check_eq(new Date(cachedAppStats[key].date).getTime() / 1000,
Math.floor(tomorrow.getTime() / 1000));
do_check_eq(cachedAppStats[key].rxBytes, 30);
@ -115,7 +79,8 @@ add_test(function test_saveAppStatsWithDifferentDates() {
}
};
nssProxy.saveAppStats(2, mobile, tomorrow.getTime(), 30, 40, saveAppStatsCb);
nssProxy.saveAppStats(2, Ci.nsINetworkInterface.NETWORK_TYPE_MOBILE,
tomorrow.getTime(), 30, 40, saveAppStatsCb);
}
);
});
@ -123,7 +88,6 @@ add_test(function test_saveAppStatsWithDifferentDates() {
add_test(function test_saveAppStatsWithMaxCachedTraffic() {
var timestamp = NetworkStatsService.cachedAppStatsDate.getTime();
var maxtraffic = NetworkStatsService.maxCachedTraffic;
var wifi = {type: Ci.nsINetworkInterface.NETWORK_TYPE_WIFI, id: "0"};
NetworkStatsService.updateCachedAppStats(
function (success, msg) {
@ -132,11 +96,13 @@ add_test(function test_saveAppStatsWithMaxCachedTraffic() {
var cachedAppStats = NetworkStatsService.cachedAppStats;
do_check_eq(Object.keys(cachedAppStats).length, 0);
nssProxy.saveAppStats(1, wifi, timestamp, 10, 20);
nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
timestamp, 10, 20);
do_check_eq(Object.keys(cachedAppStats).length, 1);
nssProxy.saveAppStats(1, wifi, timestamp, maxtraffic, 20);
nssProxy.saveAppStats(1, Ci.nsINetworkInterface.NETWORK_TYPE_WIFI,
timestamp, maxtraffic, 20);
do_check_eq(Object.keys(cachedAppStats).length, 0);
@ -149,9 +115,5 @@ function run_test() {
Cu.import("resource://gre/modules/NetworkStatsService.jsm");
// Function convertNetworkInterface of NetworkStatsService causes errors when dealing
// with RIL to get the iccid, so overwrite it.
mokConvertNetworkInterface();
run_next_test();
}

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

@ -8,6 +8,7 @@
ifndef INCLUDED_JAVA_BUILD_MK #{
ifdef ANDROID_RESFILES #{
ifndef IGNORE_ANDROID_RESFILES #{
res-dep := .deps-copy-java-res
GENERATED_DIRS += res
@ -25,6 +26,7 @@ res-dep-preqs := \
$(res-dep): $(res-dep-preqs)
$(call copy_dir,$(srcdir)/res,$(CURDIR)/res)
@$(TOUCH) $@
endif #} IGNORE_ANDROID_RESFILES
endif #} ANDROID_RESFILES

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

@ -21,6 +21,8 @@ INCLUDED_RULES_MK = 1
# present. If they are, this is a violation of the separation of
# responsibility between Makefile.in and mozbuild files.
_MOZBUILD_EXTERNAL_VARIABLES := \
ANDROID_GENERATED_RESFILES \
ANDROID_RESFILES \
CMMSRCS \
CPP_UNIT_TESTS \
DIRS \

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

@ -505,7 +505,7 @@ abstract public class BrowserApp extends GeckoApp
registerEventListener("Updater:Launch");
registerEventListener("Reader:GoToReadingList");
Distribution.init(this, getPackageResourcePath());
Distribution.init(this);
JavaAddonManager.getInstance().init(getApplicationContext());
mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());

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

@ -1,11 +1,7 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* ***** BEGIN LICENSE BLOCK *****
*
* 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/.
*
* ***** END LICENSE BLOCK ***** */
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
@ -13,113 +9,224 @@ import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class Distribution {
private static final String LOGTAG = "GeckoDistribution";
private static final String DEFAULT_PREFS = GeckoApp.PREFS_NAME;
private static final int STATE_UNKNOWN = 0;
private static final int STATE_NONE = 1;
private static final int STATE_SET = 2;
public static class DistributionDescriptor {
public final boolean valid;
public final String id;
public final String version; // Example uses a float, but that's a crazy idea.
// Default UI-visible description of the distribution.
public final String about;
// Each distribution file can include multiple localized versions of
// the 'about' string. These are represented as, e.g., "about.en-US"
// keys in the Global object.
// Here we map locale to description.
public final Map<String, String> localizedAbout;
@SuppressWarnings("unchecked")
public DistributionDescriptor(JSONObject obj) {
this.id = obj.optString("id");
this.version = obj.optString("version");
this.about = obj.optString("about");
Map<String, String> loc = new HashMap<String, String>();
try {
Iterator<String> keys = obj.keys();
while (keys.hasNext()) {
String key = keys.next();
if (key.startsWith("about.")) {
String locale = key.substring(6);
if (!obj.isNull(locale)) {
loc.put(locale, obj.getString(key));
}
}
}
} catch (JSONException ex) {
Log.w(LOGTAG, "Unable to completely process distribution JSON.", ex);
}
this.localizedAbout = Collections.unmodifiableMap(loc);
this.valid = (null != this.id) &&
(null != this.version) &&
(null != this.about);
}
}
/**
* Initializes distribution if it hasn't already been initalized.
* Initializes distribution if it hasn't already been initalized. Sends
* messages to Gecko as appropriate.
*
* @param packagePath specifies where to look for the distribution directory.
* @param packagePath where to look for the distribution directory.
*/
public static void init(final Context context, final String packagePath) {
public static void init(final Context context, final String packagePath, final String prefsPath) {
// Read/write preferences and files on the background thread.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
// Bail if we've already initialized the distribution.
SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE);
String keyName = context.getPackageName() + ".distribution_state";
int state = settings.getInt(keyName, STATE_UNKNOWN);
if (state == STATE_NONE) {
return;
}
// Send a message to Gecko if we've set a distribution.
if (state == STATE_SET) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
return;
}
boolean distributionSet = false;
try {
// First, try copying distribution files out of the APK.
distributionSet = copyFiles(context, packagePath);
} catch (IOException e) {
Log.e(LOGTAG, "Error copying distribution files", e);
}
if (!distributionSet) {
// If there aren't any distribution files in the APK, look in the /system directory.
File distDir = new File("/system/" + context.getPackageName() + "/distribution");
if (distDir.exists()) {
distributionSet = true;
}
}
Distribution dist = new Distribution(context, packagePath, prefsPath);
boolean distributionSet = dist.doInit();
if (distributionSet) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
settings.edit().putInt(keyName, STATE_SET).commit();
} else {
settings.edit().putInt(keyName, STATE_NONE).commit();
}
}
});
}
/**
* Use <code>Context.getPackageResourcePath</code> to find an implicit
* package path.
*/
public static void init(final Context context) {
Distribution.init(context, context.getPackageResourcePath(), DEFAULT_PREFS);
}
/**
* Returns parsed contents of bookmarks.json.
* This method should only be called from a background thread.
*/
public static JSONArray getBookmarks(final Context context) {
Distribution dist = new Distribution(context);
return dist.getBookmarks();
}
private final Context context;
private final String packagePath;
private final String prefsBranch;
private int state = STATE_UNKNOWN;
private File distributionDir = null;
/**
* @param packagePath where to look for the distribution directory.
*/
public Distribution(final Context context, final String packagePath, final String prefsBranch) {
this.context = context;
this.packagePath = packagePath;
this.prefsBranch = prefsBranch;
}
public Distribution(final Context context) {
this(context, context.getPackageResourcePath(), DEFAULT_PREFS);
}
/**
* Don't call from the main thread.
*
* @return true if we've set a distribution.
*/
private boolean doInit() {
// Bail if we've already tried to initialize the distribution, and
// there wasn't one.
SharedPreferences settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
String keyName = context.getPackageName() + ".distribution_state";
this.state = settings.getInt(keyName, STATE_UNKNOWN);
if (this.state == STATE_NONE) {
return false;
}
// We've done the work once; don't do it again.
if (this.state == STATE_SET) {
// Note that we don't compute the distribution directory.
// Call `ensureDistributionDir` if you need it.
return true;
}
boolean distributionSet = false;
try {
// First, try copying distribution files out of the APK.
distributionSet = copyFiles();
if (distributionSet) {
// We always copy to the data dir, and we only copy files from
// a 'distribution' subdirectory. Track our dist dir now that
// we know it.
this.distributionDir = new File(getDataDir(), "distribution/");
}
} catch (IOException e) {
Log.e(LOGTAG, "Error copying distribution files", e);
}
if (!distributionSet) {
// If there aren't any distribution files in the APK, look in the /system directory.
File distDir = getSystemDistributionDir();
if (distDir.exists()) {
distributionSet = true;
this.distributionDir = distDir;
}
}
this.state = distributionSet ? STATE_SET : STATE_NONE;
settings.edit().putInt(keyName, this.state).commit();
return distributionSet;
}
/**
* Copies the /distribution folder out of the APK and into the app's data directory.
* Returns true if distribution files were found and copied.
*/
private static boolean copyFiles(Context context, String packagePath) throws IOException {
private boolean copyFiles() throws IOException {
File applicationPackage = new File(packagePath);
ZipFile zip = new ZipFile(applicationPackage);
boolean distributionSet = false;
Enumeration<? extends ZipEntry> zipEntries = zip.entries();
byte[] buffer = new byte[1024];
while (zipEntries.hasMoreElements()) {
ZipEntry fileEntry = zipEntries.nextElement();
String name = fileEntry.getName();
if (!name.startsWith("distribution/"))
if (!name.startsWith("distribution/")) {
continue;
}
distributionSet = true;
File dataDir = new File(context.getApplicationInfo().dataDir);
File outFile = new File(dataDir, name);
File outFile = new File(getDataDir(), name);
File dir = outFile.getParentFile();
if (!dir.exists())
dir.mkdirs();
if (!dir.exists()) {
if (!dir.mkdirs()) {
Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
continue;
}
}
InputStream fileStream = zip.getInputStream(fileEntry);
OutputStream outStream = new FileOutputStream(outFile);
int b;
while ((b = fileStream.read()) != -1)
outStream.write(b);
int count;
while ((count = fileStream.read(buffer)) != -1) {
outStream.write(buffer, 0, count);
}
fileStream.close();
outStream.close();
@ -132,77 +239,125 @@ public final class Distribution {
}
/**
* Returns parsed contents of bookmarks.json.
* This method should only be called from a background thread.
* After calling this method, either <code>distributionDir</code>
* will be set, or there is no distribution in use.
*
* Only call after init.
*/
public static JSONArray getBookmarks(Context context) {
SharedPreferences settings = context.getSharedPreferences(GeckoApp.PREFS_NAME, Activity.MODE_PRIVATE);
String keyName = context.getPackageName() + ".distribution_state";
int state = settings.getInt(keyName, STATE_UNKNOWN);
if (state == STATE_NONE) {
private File ensureDistributionDir() {
if (this.distributionDir != null) {
return this.distributionDir;
}
if (this.state != STATE_SET) {
return null;
}
// After init, we know that either we've copied a distribution out of
// the APK, or it exists in /system/.
// Look in each location in turn.
// (This could be optimized by caching the path in shared prefs.)
File copied = new File(getDataDir(), "distribution/");
if (copied.exists()) {
return this.distributionDir = copied;
}
File system = getSystemDistributionDir();
if (system.exists()) {
return this.distributionDir = system;
}
return null;
}
/**
* Helper to grab a file in the distribution directory.
*
* Returns null if there is no distribution directory or the file
* doesn't exist. Ensures init first.
*/
private File getDistributionFile(String name) {
Log.i(LOGTAG, "Getting file from distribution.");
if (this.state == STATE_UNKNOWN) {
if (!this.doInit()) {
return null;
}
}
File dist = ensureDistributionDir();
if (dist == null) {
return null;
}
File descFile = new File(dist, name);
if (!descFile.exists()) {
Log.e(LOGTAG, "Distribution directory exists, but no file named " + name);
return null;
}
return descFile;
}
public DistributionDescriptor getDescriptor() {
File descFile = getDistributionFile("preferences.json");
if (descFile == null) {
// Logging and existence checks are handled in getDistributionFile.
return null;
}
ZipFile zip = null;
InputStream inputStream = null;
try {
if (state == STATE_UNKNOWN) {
// If the distribution hasn't been set yet, first look for bookmarks.json in the APK.
File applicationPackage = new File(context.getPackageResourcePath());
zip = new ZipFile(applicationPackage);
ZipEntry zipEntry = zip.getEntry("distribution/bookmarks.json");
if (zipEntry != null) {
inputStream = zip.getInputStream(zipEntry);
} else {
// If there's no bookmarks.json in the APK, but there is a preferences.json,
// don't create any distribution bookmarks.
zipEntry = zip.getEntry("distribution/preferences.json");
if (zipEntry != null) {
return null;
}
// Otherwise, look for bookmarks.json in the /system directory.
File systemFile = new File("/system/" + context.getPackageName() + "/distribution/bookmarks.json");
if (!systemFile.exists()) {
return null;
}
inputStream = new FileInputStream(systemFile);
}
} else {
// Otherwise, first look for the distribution in the data directory.
File distDir = new File(context.getApplicationInfo().dataDir, "distribution");
if (!distDir.exists()) {
// If that doesn't exist, then we must be using a distribution from the system directory.
distDir = new File("/system/" + context.getPackageName() + "/distribution");
}
JSONObject all = new JSONObject(getFileContents(descFile));
File file = new File(distDir, "bookmarks.json");
inputStream = new FileInputStream(file);
if (!all.has("Global")) {
Log.e(LOGTAG, "Distribution preferences.json has no Global entry!");
return null;
}
// Convert input stream to JSONArray
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder stringBuilder = new StringBuilder();
String s;
while ((s = reader.readLine()) != null) {
stringBuilder.append(s);
}
return new JSONArray(stringBuilder.toString());
return new DistributionDescriptor(all.getJSONObject("Global"));
} catch (IOException e) {
Log.e(LOGTAG, "Error getting distribution descriptor file.", e);
return null;
} catch (JSONException e) {
Log.e(LOGTAG, "Error parsing preferences.json", e);
return null;
}
}
public JSONArray getBookmarks() {
File bookmarks = getDistributionFile("bookmarks.json");
if (bookmarks == null) {
// Logging and existence checks are handled in getDistributionFile.
return null;
}
try {
return new JSONArray(getFileContents(bookmarks));
} catch (IOException e) {
Log.e(LOGTAG, "Error getting bookmarks", e);
} catch (JSONException e) {
Log.e(LOGTAG, "Error parsing bookmarks.json", e);
} finally {
try {
if (zip != null) {
zip.close();
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
Log.e(LOGTAG, "Error closing streams", e);
}
}
return null;
}
// Shortcut to slurp a file without messing around with streams.
private String getFileContents(File file) throws IOException {
Scanner scanner = null;
try {
scanner = new Scanner(file, "UTF-8");
return scanner.useDelimiter("\\A").next();
} finally {
if (scanner != null) {
scanner.close();
}
}
}
private String getDataDir() {
return context.getApplicationInfo().dataDir;
}
private File getSystemDistributionDir() {
return new File("/system/" + context.getPackageName() + "/distribution");
}
}

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

@ -117,6 +117,7 @@ import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
@ -1291,7 +1292,16 @@ abstract public class GeckoApp
final String profilePath = getProfile().getDir().getAbsolutePath();
final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
Log.i(LOGTAG, "Creating BrowserHealthRecorder.");
mHealthRecorder = new BrowserHealthRecorder(GeckoApp.this, profilePath, dispatcher,
final String osLocale = Locale.getDefault().toString();
Log.d(LOGTAG, "Locale is " + osLocale);
// Replace the duplicate `osLocale` argument when we support switchable
// application locales.
mHealthRecorder = new BrowserHealthRecorder(GeckoApp.this,
profilePath,
dispatcher,
osLocale,
osLocale, // Placeholder.
previousSession);
}
});
@ -1555,8 +1565,15 @@ abstract public class GeckoApp
GeckoPreferences.broadcastHealthReportUploadPref(context);
/*
XXXX see bug 635342
We want to disable this code if possible. It is about 145ms in runtime
XXXX see Bug 635342.
We want to disable this code if possible. It is about 145ms in runtime.
If this code ever becomes live again, you'll need to chain the
new locale into BrowserHealthRecorder correctly. See
GeckoAppShell.setSelectedLocale.
We pass the OS locale into the BHR constructor: we need to grab
that *before* we modify the current locale!
SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
String localeCode = settings.getString(getPackageName() + ".locale", "");
if (localeCode != null && localeCode.length() > 0)

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

@ -1525,6 +1525,12 @@ public class GeckoAppShell
Gecko resets the locale to en-US by calling this function with an empty string.
This affects GeckoPreferences activity in multi-locale builds.
N.B., if this code ever becomes live again, you need to hook it up to locale
recording in BrowserHealthRecorder: we track the current app and OS locales
as part of the recorded environment.
See similar note in GeckoApp.java for the startup path.
//We're not using this, not need to save it (see bug 635342)
SharedPreferences settings =
getContext().getPreferences(Activity.MODE_PRIVATE);

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

@ -313,8 +313,13 @@ public final class GeckoProfile {
}
public synchronized File getDir() {
forceCreate();
return mDir;
}
public synchronized GeckoProfile forceCreate() {
if (mDir != null) {
return mDir;
return this;
}
try {
@ -330,7 +335,7 @@ public final class GeckoProfile {
} catch (IOException ioe) {
Log.e(LOGTAG, "Error getting profile dir", ioe);
}
return mDir;
return this;
}
public File getFile(String aFile) {

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

@ -55,6 +55,7 @@ public class GeckoView extends LayerView
Clipboard.init(context);
HardwareUtils.init(context);
GeckoNetworkManager.getInstance().init(context);
GeckoLoader.loadMozGlue();
BrowserDB.setEnableContentProviders(false);
@ -75,7 +76,7 @@ public class GeckoView extends LayerView
ThreadUtils.setUiThread(Thread.currentThread(), new Handler());
initializeView(GeckoAppShell.getEventDispatcher());
GeckoProfile profile = GeckoProfile.get(context);
GeckoProfile profile = GeckoProfile.get(context).forceCreate();
BrowserDB.initialize(profile.getName());
if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.Launched)) {

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

@ -416,789 +416,12 @@ ICON_PATH = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon48.png
ICON_PATH_HDPI = $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)/content/icon64.png
endif
RES_LAYOUT = \
$(SYNC_RES_LAYOUT) \
res/layout/arrow_popup.xml \
res/layout/autocomplete_list.xml \
res/layout/autocomplete_list_item.xml \
res/layout/bookmark_edit.xml \
res/layout/bookmark_folder_row.xml \
res/layout/bookmark_item_row.xml \
res/layout/browser_search.xml \
res/layout/browser_toolbar.xml \
res/layout/datetime_picker.xml \
res/layout/doorhanger.xml \
res/layout/doorhanger_button.xml \
res/layout/find_in_page_content.xml \
res/layout/font_size_preference.xml \
res/layout/gecko_app.xml \
res/layout/home_bookmarks_page.xml \
res/layout/home_empty_page.xml \
res/layout/home_empty_reading_page.xml \
res/layout/home_item_row.xml \
res/layout/home_header_row.xml \
res/layout/home_history_page.xml \
res/layout/home_history_tabs_indicator.xml \
res/layout/home_last_tabs_page.xml \
res/layout/home_history_list.xml \
res/layout/home_most_recent_page.xml \
res/layout/home_pager.xml \
res/layout/home_reading_list_page.xml \
res/layout/home_search_item_row.xml \
res/layout/home_banner.xml \
res/layout/home_suggestion_prompt.xml \
res/layout/home_top_sites_page.xml \
res/layout/icon_grid.xml \
res/layout/icon_grid_item.xml \
res/layout/web_app.xml \
res/layout/launch_app_list.xml \
res/layout/launch_app_listitem.xml \
res/layout/menu_action_bar.xml \
res/layout/menu_item_action_view.xml \
res/layout/menu_popup.xml \
res/layout/notification_icon_text.xml \
res/layout/notification_progress.xml \
res/layout/notification_progress_text.xml \
res/layout/pin_site_dialog.xml \
res/layout/preference_rightalign_icon.xml \
res/layout/preference_search_engine.xml \
res/layout/preference_search_tip.xml \
res/layout/site_setting_item.xml \
res/layout/site_setting_title.xml \
res/layout/shared_ui_components.xml \
res/layout/site_identity.xml \
res/layout/remote_tabs_child.xml \
res/layout/remote_tabs_group.xml \
res/layout/search_engine_row.xml \
res/layout/tab_menu_strip.xml \
res/layout/tabs_panel.xml \
res/layout/tabs_counter.xml \
res/layout/tabs_panel_header.xml \
res/layout/tabs_panel_indicator.xml \
res/layout/tabs_item_cell.xml \
res/layout/tabs_item_row.xml \
res/layout/text_selection_handles.xml \
res/layout/top_sites_grid_item_view.xml \
res/layout/two_line_page_row.xml \
res/layout/list_item_header.xml \
res/layout/select_dialog_list.xml \
res/layout/select_dialog_multichoice.xml \
res/layout/select_dialog_singlechoice.xml \
res/layout/simple_dropdown_item_1line.xml \
res/layout/suggestion_item.xml \
res/layout/validation_message.xml \
res/layout/videoplayer.xml \
$(NULL)
RES_LAYOUT_LARGE_V11 = \
res/layout-large-v11/browser_toolbar.xml \
res/layout-large-v11/home_pager.xml \
$(NULL)
RES_LAYOUT_LARGE_LAND_V11 = \
res/layout-large-land-v11/home_history_page.xml \
res/layout-large-land-v11/home_history_tabs_indicator.xml \
res/layout-large-land-v11/home_history_list.xml \
res/layout-large-land-v11/tabs_panel.xml \
res/layout-large-land-v11/tabs_panel_header.xml \
res/layout-large-land-v11/tabs_panel_footer.xml \
$(NULL)
RES_LAYOUT_XLARGE_V11 = \
res/layout-xlarge-v11/font_size_preference.xml \
res/layout-xlarge-v11/home_history_page.xml \
res/layout-xlarge-v11/home_history_tabs_indicator.xml \
res/layout-xlarge-v11/home_history_list.xml \
res/layout-xlarge-v11/remote_tabs_child.xml \
res/layout-xlarge-v11/remote_tabs_group.xml \
$(NULL)
RES_VALUES = \
$(SYNC_RES_VALUES) \
res/values/attrs.xml \
res/values/arrays.xml \
res/values/colors.xml \
res/values/dimens.xml \
res/values/integers.xml \
res/values/layout.xml \
res/values/styles.xml \
res/values/themes.xml \
$(NULL)
RES_VALUES_LAND = \
res/values-land/integers.xml \
res/values-land/layout.xml \
res/values-land/styles.xml \
$(NULL)
RES_VALUES_V11 = \
$(SYNC_RES_VALUES_V11) \
res/values-v11/colors.xml \
res/values-v11/dimens.xml \
res/values-v11/styles.xml \
res/values-v11/themes.xml \
$(NULL)
RES_VALUES_LARGE_V11 = \
$(SYNC_RES_VALUES_LARGE_V11) \
res/values-large-v11/dimens.xml \
res/values-large-v11/layout.xml \
res/values-large-v11/styles.xml \
res/values-large-v11/themes.xml \
$(NULL)
RES_VALUES_LARGE_LAND_V11 = \
res/values-large-land-v11/dimens.xml \
res/values-large-land-v11/styles.xml \
$(NULL)
RES_VALUES_XLARGE_V11 = \
res/values-xlarge-v11/dimens.xml \
res/values-xlarge-v11/integers.xml \
res/values-xlarge-v11/styles.xml \
$(NULL)
RES_VALUES_XLARGE_LAND_V11 = \
res/values-xlarge-land-v11/dimens.xml \
res/values-xlarge-land-v11/styles.xml \
$(NULL)
RES_VALUES_V14 = \
res/values-v14/styles.xml \
$(NULL)
RES_VALUES_V16 = \
res/values-v16/styles.xml \
$(NULL)
RES_XML = \
res/xml/preferences.xml \
res/xml/preferences_customize.xml \
res/xml/preferences_display.xml \
res/xml/preferences_search.xml \
res/xml/preferences_privacy.xml \
res/xml/preferences_vendor.xml \
res/xml/preferences_devtools.xml \
res/xml/searchable.xml \
$(SYNC_RES_XML) \
$(NULL)
RES_XML_V11 = \
res/xml-v11/preferences_customize.xml \
res/xml-v11/preference_headers.xml \
res/xml-v11/preferences_customize_tablet.xml \
res/xml-v11/preferences.xml \
$(NULL)
RES_ANIM = \
res/anim/popup_show.xml \
res/anim/popup_hide.xml \
res/anim/grow_fade_in.xml \
res/anim/grow_fade_in_center.xml \
res/anim/progress_spinner.xml \
res/anim/shrink_fade_out.xml \
$(NULL)
RES_DRAWABLE_MDPI = \
$(SYNC_RES_DRAWABLE_MDPI) \
res/drawable-mdpi/blank.png \
res/drawable-mdpi/favicon.png \
res/drawable-mdpi/folder.png \
res/drawable-mdpi/abouthome_thumbnail.png \
res/drawable-mdpi/alert_addon.png \
res/drawable-mdpi/alert_app.png \
res/drawable-mdpi/alert_download.png \
res/drawable-mdpi/alert_camera.png \
res/drawable-mdpi/alert_mic.png \
res/drawable-mdpi/alert_mic_camera.png \
res/drawable-mdpi/arrow_popup_bg.9.png \
res/drawable-mdpi/autocomplete_list_bg.9.png \
res/drawable-mdpi/bookmark_folder_closed.png \
res/drawable-mdpi/bookmark_folder_opened.png \
res/drawable-mdpi/desktop_notification.png \
res/drawable-mdpi/grid_icon_bg_activated.9.png \
res/drawable-mdpi/grid_icon_bg_focused.9.png \
res/drawable-mdpi/home_tab_menu_strip.9.png \
res/drawable-mdpi/ic_menu_addons_filler.png \
res/drawable-mdpi/ic_menu_bookmark_add.png \
res/drawable-mdpi/ic_menu_bookmark_remove.png \
res/drawable-mdpi/ic_menu_character_encoding.png \
res/drawable-mdpi/close.png \
res/drawable-mdpi/ic_menu_forward.png \
res/drawable-mdpi/ic_menu_guest.png \
res/drawable-mdpi/ic_menu_new_private_tab.png \
res/drawable-mdpi/ic_menu_new_tab.png \
res/drawable-mdpi/ic_menu_reload.png \
res/drawable-mdpi/ic_status_logo.png \
res/drawable-mdpi/ic_url_bar_go.png \
res/drawable-mdpi/ic_url_bar_reader.png \
res/drawable-mdpi/ic_url_bar_search.png \
res/drawable-mdpi/ic_url_bar_star.png \
res/drawable-mdpi/ic_url_bar_tab.png \
res/drawable-mdpi/icon_bookmarks_empty.png \
res/drawable-mdpi/icon_last_tabs.png \
res/drawable-mdpi/icon_last_tabs_empty.png \
res/drawable-mdpi/icon_most_recent.png \
res/drawable-mdpi/icon_most_recent_empty.png \
res/drawable-mdpi/icon_most_visited.png \
res/drawable-mdpi/icon_openinapp.png \
res/drawable-mdpi/icon_pageaction.png \
res/drawable-mdpi/icon_reading_list_empty.png \
res/drawable-mdpi/progress_spinner.png \
res/drawable-mdpi/play.png \
res/drawable-mdpi/pause.png \
res/drawable-mdpi/tab_indicator_divider.9.png \
res/drawable-mdpi/tab_indicator_selected.9.png \
res/drawable-mdpi/tab_indicator_selected_focused.9.png \
res/drawable-mdpi/spinner_default.9.png \
res/drawable-mdpi/spinner_focused.9.png \
res/drawable-mdpi/spinner_pressed.9.png \
res/drawable-mdpi/tab_new.png \
res/drawable-mdpi/tab_new_pb.png \
res/drawable-mdpi/tab_close.png \
res/drawable-mdpi/tab_thumbnail_default.png \
res/drawable-mdpi/tab_thumbnail_shadow.png \
res/drawable-mdpi/tabs_count.png \
res/drawable-mdpi/tabs_count_foreground.png \
res/drawable-mdpi/url_bar_entry_default.9.png \
res/drawable-mdpi/url_bar_entry_default_pb.9.png \
res/drawable-mdpi/url_bar_entry_pressed.9.png \
res/drawable-mdpi/url_bar_entry_pressed_pb.9.png \
res/drawable-mdpi/tip_addsearch.png \
res/drawable-mdpi/toast.9.png \
res/drawable-mdpi/toast_button_focused.9.png \
res/drawable-mdpi/toast_button_pressed.9.png \
res/drawable-mdpi/toast_divider.9.png \
res/drawable-mdpi/find_close.png \
res/drawable-mdpi/find_next.png \
res/drawable-mdpi/find_prev.png \
res/drawable-mdpi/larry.png \
res/drawable-mdpi/lock_identified.png \
res/drawable-mdpi/lock_verified.png \
res/drawable-mdpi/menu.png \
res/drawable-mdpi/menu_pb.png \
res/drawable-mdpi/menu_panel_bg.9.png \
res/drawable-mdpi/menu_popup_bg.9.png \
res/drawable-mdpi/menu_popup_arrow_bottom.png \
res/drawable-mdpi/menu_popup_arrow_top.png \
res/drawable-mdpi/menu_item_check.png \
res/drawable-mdpi/menu_item_more.png \
res/drawable-mdpi/menu_item_uncheck.png \
res/drawable-mdpi/pin.png \
res/drawable-mdpi/shield.png \
res/drawable-mdpi/shield_doorhanger.png \
res/drawable-mdpi/tabs_normal.png \
res/drawable-mdpi/tabs_private.png \
res/drawable-mdpi/tabs_synced.png \
res/drawable-mdpi/top_site_add.png \
res/drawable-mdpi/urlbar_stop.png \
res/drawable-mdpi/reader.png \
res/drawable-mdpi/reader_cropped.png \
res/drawable-mdpi/reader_active.png \
res/drawable-mdpi/reading_list.png \
res/drawable-mdpi/validation_arrow.png \
res/drawable-mdpi/validation_arrow_inverted.png \
res/drawable-mdpi/validation_bg.9.png \
res/drawable-mdpi/bookmarkdefaults_favicon_support.png \
res/drawable-mdpi/bookmarkdefaults_favicon_addons.png \
res/drawable-mdpi/handle_end.png \
res/drawable-mdpi/handle_middle.png \
res/drawable-mdpi/handle_start.png \
res/drawable-mdpi/scrollbar.png \
res/drawable-mdpi/shadow.png \
res/drawable-mdpi/start.png \
res/drawable-mdpi/marketplace.png \
res/drawable-mdpi/history_tabs_indicator_selected.9.png \
res/drawable-mdpi/warning.png \
res/drawable-mdpi/warning_doorhanger.png \
$(NULL)
RES_DRAWABLE_LDPI = \
$(SYNC_RES_DRAWABLE_LDPI) \
$(NULL)
RES_DRAWABLE_HDPI = \
$(SYNC_RES_DRAWABLE_HDPI) \
res/drawable-hdpi/blank.png \
res/drawable-hdpi/favicon.png \
res/drawable-hdpi/folder.png \
res/drawable-hdpi/home_bg.png \
res/drawable-hdpi/home_star.png \
res/drawable-hdpi/grid_icon_bg_activated.9.png \
res/drawable-hdpi/grid_icon_bg_focused.9.png \
res/drawable-hdpi/abouthome_thumbnail.png \
res/drawable-hdpi/alert_addon.png \
res/drawable-hdpi/alert_app.png \
res/drawable-hdpi/alert_download.png \
res/drawable-hdpi/bookmark_folder_closed.png \
res/drawable-hdpi/bookmark_folder_opened.png \
res/drawable-hdpi/alert_camera.png \
res/drawable-hdpi/alert_mic.png \
res/drawable-hdpi/alert_mic_camera.png \
res/drawable-hdpi/arrow_popup_bg.9.png \
res/drawable-hdpi/home_tab_menu_strip.9.png \
res/drawable-hdpi/ic_menu_addons_filler.png \
res/drawable-hdpi/ic_menu_bookmark_add.png \
res/drawable-hdpi/ic_menu_bookmark_remove.png \
res/drawable-hdpi/ic_menu_character_encoding.png \
res/drawable-hdpi/close.png \
res/drawable-hdpi/ic_menu_forward.png \
res/drawable-hdpi/ic_menu_guest.png \
res/drawable-hdpi/ic_menu_new_private_tab.png \
res/drawable-hdpi/ic_menu_new_tab.png \
res/drawable-hdpi/ic_menu_reload.png \
res/drawable-hdpi/ic_status_logo.png \
res/drawable-hdpi/ic_url_bar_go.png \
res/drawable-hdpi/ic_url_bar_reader.png \
res/drawable-hdpi/ic_url_bar_search.png \
res/drawable-hdpi/ic_url_bar_star.png \
res/drawable-hdpi/ic_url_bar_tab.png \
res/drawable-hdpi/icon_bookmarks_empty.png \
res/drawable-hdpi/icon_last_tabs.png \
res/drawable-hdpi/icon_last_tabs_empty.png \
res/drawable-hdpi/icon_most_recent.png \
res/drawable-hdpi/icon_most_recent_empty.png \
res/drawable-hdpi/icon_most_visited.png \
res/drawable-hdpi/icon_openinapp.png \
res/drawable-hdpi/icon_pageaction.png \
res/drawable-hdpi/icon_reading_list_empty.png \
res/drawable-hdpi/tab_indicator_divider.9.png \
res/drawable-hdpi/tab_indicator_selected.9.png \
res/drawable-hdpi/tab_indicator_selected_focused.9.png \
res/drawable-hdpi/spinner_default.9.png \
res/drawable-hdpi/spinner_focused.9.png \
res/drawable-hdpi/spinner_pressed.9.png \
res/drawable-hdpi/tab_new.png \
res/drawable-hdpi/tab_new_pb.png \
res/drawable-hdpi/tab_close.png \
res/drawable-hdpi/tab_thumbnail_default.png \
res/drawable-hdpi/tab_thumbnail_shadow.png \
res/drawable-hdpi/tabs_count.png \
res/drawable-hdpi/tabs_count_foreground.png \
res/drawable-hdpi/url_bar_entry_default.9.png \
res/drawable-hdpi/url_bar_entry_default_pb.9.png \
res/drawable-hdpi/url_bar_entry_pressed.9.png \
res/drawable-hdpi/url_bar_entry_pressed_pb.9.png \
res/drawable-hdpi/tip_addsearch.png \
res/drawable-hdpi/find_close.png \
res/drawable-hdpi/find_next.png \
res/drawable-hdpi/find_prev.png \
res/drawable-hdpi/larry.png \
res/drawable-hdpi/lock_identified.png \
res/drawable-hdpi/lock_verified.png \
res/drawable-hdpi/menu.png \
res/drawable-hdpi/menu_pb.png \
res/drawable-hdpi/menu_panel_bg.9.png \
res/drawable-hdpi/menu_popup_bg.9.png \
res/drawable-hdpi/menu_popup_arrow_bottom.png \
res/drawable-hdpi/menu_popup_arrow_top.png \
res/drawable-hdpi/menu_item_check.png \
res/drawable-hdpi/menu_item_more.png \
res/drawable-hdpi/menu_item_uncheck.png \
res/drawable-hdpi/pin.png \
res/drawable-hdpi/play.png \
res/drawable-hdpi/pause.png \
res/drawable-hdpi/shield.png \
res/drawable-hdpi/shield_doorhanger.png \
res/drawable-hdpi/tabs_normal.png \
res/drawable-hdpi/tabs_private.png \
res/drawable-hdpi/tabs_synced.png \
res/drawable-hdpi/top_site_add.png \
res/drawable-hdpi/urlbar_stop.png \
res/drawable-hdpi/reader.png \
res/drawable-hdpi/reader_cropped.png \
res/drawable-hdpi/reader_active.png \
res/drawable-hdpi/reading_list.png \
res/drawable-hdpi/validation_arrow.png \
res/drawable-hdpi/validation_arrow_inverted.png \
res/drawable-hdpi/validation_bg.9.png \
res/drawable-hdpi/handle_end.png \
res/drawable-hdpi/handle_middle.png \
res/drawable-hdpi/handle_start.png \
res/drawable-hdpi/history_tabs_indicator_selected.9.png \
res/drawable-hdpi/warning.png \
res/drawable-hdpi/warning_doorhanger.png \
$(NULL)
RES_DRAWABLE_XHDPI = \
res/drawable-xhdpi/blank.png \
res/drawable-xhdpi/favicon.png \
res/drawable-xhdpi/folder.png \
res/drawable-xhdpi/abouthome_thumbnail.png \
res/drawable-xhdpi/url_bar_entry_default.9.png \
res/drawable-xhdpi/url_bar_entry_default_pb.9.png \
res/drawable-xhdpi/url_bar_entry_pressed.9.png \
res/drawable-xhdpi/url_bar_entry_pressed_pb.9.png \
res/drawable-xhdpi/alert_addon.png \
res/drawable-xhdpi/alert_app.png \
res/drawable-xhdpi/alert_download.png \
res/drawable-xhdpi/bookmark_folder_closed.png \
res/drawable-xhdpi/bookmark_folder_opened.png \
res/drawable-xhdpi/alert_camera.png \
res/drawable-xhdpi/alert_mic.png \
res/drawable-xhdpi/alert_mic_camera.png \
res/drawable-xhdpi/arrow_popup_bg.9.png \
res/drawable-xhdpi/home_tab_menu_strip.9.png \
res/drawable-xhdpi/grid_icon_bg_activated.9.png \
res/drawable-xhdpi/grid_icon_bg_focused.9.png \
res/drawable-xhdpi/ic_menu_addons_filler.png \
res/drawable-xhdpi/ic_menu_bookmark_add.png \
res/drawable-xhdpi/ic_menu_bookmark_remove.png \
res/drawable-xhdpi/close.png \
res/drawable-xhdpi/ic_menu_character_encoding.png \
res/drawable-xhdpi/ic_menu_forward.png \
res/drawable-xhdpi/ic_menu_guest.png \
res/drawable-xhdpi/ic_menu_new_private_tab.png \
res/drawable-xhdpi/ic_menu_new_tab.png \
res/drawable-xhdpi/ic_menu_reload.png \
res/drawable-xhdpi/ic_status_logo.png \
res/drawable-xhdpi/ic_url_bar_go.png \
res/drawable-xhdpi/ic_url_bar_reader.png \
res/drawable-xhdpi/ic_url_bar_search.png \
res/drawable-xhdpi/ic_url_bar_star.png \
res/drawable-xhdpi/ic_url_bar_tab.png \
res/drawable-xhdpi/icon_bookmarks_empty.png \
res/drawable-xhdpi/icon_last_tabs.png \
res/drawable-xhdpi/icon_last_tabs_empty.png \
res/drawable-xhdpi/icon_most_recent.png \
res/drawable-xhdpi/icon_most_recent_empty.png \
res/drawable-xhdpi/icon_most_visited.png \
res/drawable-xhdpi/icon_openinapp.png \
res/drawable-xhdpi/icon_pageaction.png \
res/drawable-xhdpi/icon_reading_list_empty.png \
res/drawable-xhdpi/spinner_default.9.png \
res/drawable-xhdpi/spinner_focused.9.png \
res/drawable-xhdpi/spinner_pressed.9.png \
res/drawable-xhdpi/tab_new.png \
res/drawable-xhdpi/tab_new_pb.png \
res/drawable-xhdpi/tab_close.png \
res/drawable-xhdpi/tab_thumbnail_default.png \
res/drawable-xhdpi/tab_thumbnail_shadow.png \
res/drawable-xhdpi/tabs_count.png \
res/drawable-xhdpi/tabs_count_foreground.png \
res/drawable-xhdpi/tip_addsearch.png \
res/drawable-xhdpi/find_close.png \
res/drawable-xhdpi/find_next.png \
res/drawable-xhdpi/find_prev.png \
res/drawable-xhdpi/top_site_add.png \
res/drawable-xhdpi/urlbar_stop.png \
res/drawable-xhdpi/reader.png \
res/drawable-xhdpi/reader_cropped.png \
res/drawable-xhdpi/reader_active.png \
res/drawable-xhdpi/reading_list.png \
res/drawable-xhdpi/larry.png \
res/drawable-xhdpi/lock_identified.png \
res/drawable-xhdpi/lock_verified.png \
res/drawable-xhdpi/menu.png \
res/drawable-xhdpi/menu_pb.png \
res/drawable-xhdpi/menu_panel_bg.9.png \
res/drawable-xhdpi/menu_popup_bg.9.png \
res/drawable-xhdpi/menu_popup_arrow_bottom.png \
res/drawable-xhdpi/menu_popup_arrow_top.png \
res/drawable-xhdpi/menu_item_check.png \
res/drawable-xhdpi/menu_item_more.png \
res/drawable-xhdpi/menu_item_uncheck.png \
res/drawable-xhdpi/pin.png \
res/drawable-xhdpi/play.png \
res/drawable-xhdpi/pause.png \
res/drawable-xhdpi/shield.png \
res/drawable-xhdpi/shield_doorhanger.png \
res/drawable-xhdpi/tab_indicator_divider.9.png \
res/drawable-xhdpi/tab_indicator_selected.9.png \
res/drawable-xhdpi/tab_indicator_selected_focused.9.png \
res/drawable-xhdpi/tabs_normal.png \
res/drawable-xhdpi/tabs_private.png \
res/drawable-xhdpi/tabs_synced.png \
res/drawable-xhdpi/validation_arrow.png \
res/drawable-xhdpi/validation_arrow_inverted.png \
res/drawable-xhdpi/validation_bg.9.png \
res/drawable-xhdpi/handle_end.png \
res/drawable-xhdpi/handle_middle.png \
res/drawable-xhdpi/handle_start.png \
res/drawable-xhdpi/history_tabs_indicator_selected.9.png \
res/drawable-xhdpi/warning.png \
res/drawable-xhdpi/warning_doorhanger.png \
$(NULL)
RES_DRAWABLE_MDPI_V11 = \
res/drawable-mdpi-v11/alert_addon.png \
res/drawable-mdpi-v11/alert_app.png \
res/drawable-mdpi-v11/alert_download.png \
res/drawable-mdpi-v11/alert_camera.png \
res/drawable-mdpi-v11/alert_mic.png \
res/drawable-mdpi-v11/alert_mic_camera.png \
res/drawable-mdpi-v11/firefox_settings_alert.png \
res/drawable-mdpi-v11/ic_menu_addons.png \
res/drawable-mdpi-v11/ic_menu_apps.png \
res/drawable-mdpi-v11/ic_menu_back.png \
res/drawable-mdpi-v11/ic_menu_bookmark_add.png \
res/drawable-mdpi-v11/ic_menu_bookmark_remove.png \
res/drawable-mdpi-v11/ic_menu_desktop_mode_off.png \
res/drawable-mdpi-v11/ic_menu_desktop_mode_on.png \
res/drawable-mdpi-v11/ic_menu_downloads.png \
res/drawable-mdpi-v11/ic_menu_find_in_page.png \
res/drawable-mdpi-v11/ic_menu_forward.png \
res/drawable-mdpi-v11/ic_menu_new_private_tab.png \
res/drawable-mdpi-v11/ic_menu_new_tab.png \
res/drawable-mdpi-v11/ic_menu_reload.png \
res/drawable-mdpi-v11/ic_menu_save_as_pdf.png \
res/drawable-mdpi-v11/ic_menu_settings.png \
res/drawable-mdpi-v11/ic_menu_share.png \
res/drawable-mdpi-v11/ic_menu_tools.png \
res/drawable-mdpi-v11/ic_menu_quit.png \
res/drawable-mdpi-v11/ic_status_logo.png \
$(NULL)
RES_DRAWABLE_HDPI_V11 = \
res/drawable-hdpi-v11/alert_addon.png \
res/drawable-hdpi-v11/alert_app.png \
res/drawable-hdpi-v11/alert_download.png \
res/drawable-hdpi-v11/alert_camera.png \
res/drawable-hdpi-v11/alert_mic.png \
res/drawable-hdpi-v11/alert_mic_camera.png \
res/drawable-hdpi-v11/firefox_settings_alert.png \
res/drawable-hdpi-v11/ic_menu_addons.png \
res/drawable-hdpi-v11/ic_menu_apps.png \
res/drawable-hdpi-v11/ic_menu_back.png \
res/drawable-hdpi-v11/ic_menu_bookmark_add.png \
res/drawable-hdpi-v11/ic_menu_bookmark_remove.png \
res/drawable-hdpi-v11/ic_menu_desktop_mode_off.png \
res/drawable-hdpi-v11/ic_menu_desktop_mode_on.png \
res/drawable-hdpi-v11/ic_menu_downloads.png \
res/drawable-hdpi-v11/ic_menu_find_in_page.png \
res/drawable-hdpi-v11/ic_menu_forward.png \
res/drawable-hdpi-v11/ic_menu_new_private_tab.png \
res/drawable-hdpi-v11/ic_menu_new_tab.png \
res/drawable-hdpi-v11/ic_menu_reload.png \
res/drawable-hdpi-v11/ic_menu_save_as_pdf.png \
res/drawable-hdpi-v11/ic_menu_settings.png \
res/drawable-hdpi-v11/ic_menu_share.png \
res/drawable-hdpi-v11/ic_menu_tools.png \
res/drawable-hdpi-v11/ic_menu_quit.png \
res/drawable-hdpi-v11/ic_status_logo.png \
$(NULL)
RES_DRAWABLE_XHDPI_V11 = \
res/drawable-xhdpi-v11/alert_addon.png \
res/drawable-xhdpi-v11/alert_app.png \
res/drawable-xhdpi-v11/alert_download.png \
res/drawable-xhdpi-v11/alert_camera.png \
res/drawable-xhdpi-v11/alert_mic.png \
res/drawable-xhdpi-v11/alert_mic_camera.png \
res/drawable-xhdpi-v11/firefox_settings_alert.png \
res/drawable-xhdpi-v11/ic_menu_addons.png \
res/drawable-xhdpi-v11/ic_menu_apps.png \
res/drawable-xhdpi-v11/ic_menu_back.png \
res/drawable-xhdpi-v11/ic_menu_bookmark_add.png \
res/drawable-xhdpi-v11/ic_menu_bookmark_remove.png \
res/drawable-xhdpi-v11/ic_menu_desktop_mode_off.png \
res/drawable-xhdpi-v11/ic_menu_desktop_mode_on.png \
res/drawable-xhdpi-v11/ic_menu_downloads.png \
res/drawable-xhdpi-v11/ic_menu_find_in_page.png \
res/drawable-xhdpi-v11/ic_menu_forward.png \
res/drawable-xhdpi-v11/ic_menu_new_private_tab.png \
res/drawable-xhdpi-v11/ic_menu_new_tab.png \
res/drawable-xhdpi-v11/ic_menu_reload.png \
res/drawable-xhdpi-v11/ic_menu_save_as_pdf.png \
res/drawable-xhdpi-v11/ic_menu_settings.png \
res/drawable-xhdpi-v11/ic_menu_share.png \
res/drawable-xhdpi-v11/ic_menu_tools.png \
res/drawable-xhdpi-v11/ic_menu_quit.png \
res/drawable-xhdpi-v11/ic_status_logo.png \
$(NULL)
RES_DRAWABLE_LARGE_LAND_V11 = \
res/drawable-large-land-v11/home_history_tabs_indicator.xml \
$(NULL)
RES_DRAWABLE_LARGE_MDPI_V11 = \
res/drawable-large-mdpi-v11/arrow_popup_bg.9.png \
res/drawable-large-mdpi-v11/ic_menu_reload.png \
res/drawable-large-mdpi-v11/ic_menu_forward.png \
res/drawable-large-mdpi-v11/menu.png \
$(NULL)
RES_DRAWABLE_LARGE_HDPI_V11 = \
res/drawable-large-hdpi-v11/arrow_popup_bg.9.png \
res/drawable-large-hdpi-v11/ic_menu_reload.png \
res/drawable-large-hdpi-v11/ic_menu_forward.png \
res/drawable-large-hdpi-v11/menu.png \
$(NULL)
RES_DRAWABLE_LARGE_XHDPI_V11 = \
res/drawable-large-xhdpi-v11/arrow_popup_bg.9.png \
res/drawable-large-xhdpi-v11/ic_menu_reload.png \
res/drawable-large-xhdpi-v11/ic_menu_forward.png \
res/drawable-large-xhdpi-v11/menu.png \
$(NULL)
RES_DRAWABLE_XLARGE_V11 = \
res/drawable-xlarge-v11/home_history_tabs_indicator.xml \
$(NULL)
RES_DRAWABLE_XLARGE_MDPI_V11 = \
res/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png \
res/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png \
$(NULL)
RES_DRAWABLE_XLARGE_HDPI_V11 = \
res/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png \
res/drawable-xlarge-hdpi-v11/ic_menu_bookmark_remove.png \
$(NULL)
RES_DRAWABLE_XLARGE_XHDPI_V11 = \
res/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png \
res/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png \
$(NULL)
RES_COLOR = \
res/color/primary_text.xml \
res/color/primary_text_inverse.xml \
res/color/secondary_text.xml \
res/color/secondary_text_inverse.xml \
res/color/select_item_multichoice.xml \
res/color/tertiary_text.xml \
res/color/tertiary_text_inverse.xml \
res/color/top_sites_grid_item_title.xml \
res/color/url_bar_title.xml \
res/color/url_bar_title_hint.xml \
$(NULL)
RES_MENU = \
res/menu/browser_app_menu.xml \
res/menu/gecko_app_menu.xml \
res/menu/home_contextmenu.xml \
res/menu/titlebar_contextmenu.xml \
res/menu/top_sites_contextmenu.xml \
res/menu-large-v11/browser_app_menu.xml \
res/menu-v11/browser_app_menu.xml \
res/menu-xlarge-v11/browser_app_menu.xml \
$(NULL)
JAVA_CLASSPATH = $(ANDROID_SDK)/android.jar
ifdef MOZ_CRASHREPORTER
FENNEC_JAVA_FILES += CrashReporter.java
RES_DRAWABLE_MDPI += res/drawable-mdpi/crash_reporter.png
RES_LAYOUT += res/layout/crash_reporter.xml
endif
RES_DRAWABLE += \
$(SYNC_RES_DRAWABLE) \
res/drawable/action_bar_button.xml \
res/drawable/action_bar_button_inverse.xml \
res/drawable/top_sites_thumbnail_bg.xml \
res/drawable/url_bar_bg.xml \
res/drawable/url_bar_entry.xml \
res/drawable/url_bar_nav_button.xml \
res/drawable/icon_grid_item_bg.xml \
res/drawable/url_bar_right_edge.xml \
res/drawable/bookmark_folder.xml \
res/drawable/divider_horizontal.xml \
res/drawable/divider_vertical.xml \
res/drawable/favicon_bg.xml \
res/drawable/handle_end_level.xml \
res/drawable/handle_start_level.xml \
res/drawable/home_history_tabs_indicator.xml \
res/drawable/home_page_title_background.xml \
res/drawable/home_banner.xml \
res/drawable/ic_menu_back.xml \
res/drawable/ic_menu_desktop_mode_off.xml \
res/drawable/ic_menu_desktop_mode_on.xml \
res/drawable/ic_menu_quit.xml \
res/drawable/menu_item_state.xml \
res/drawable/menu_level.xml \
res/drawable/remote_tabs_child_divider.xml \
res/drawable/shaped_button.xml \
res/drawable/site_security_level.xml \
res/drawable/spinner.xml \
res/drawable/suggestion_selector.xml \
res/drawable/tab_new_level.xml \
res/drawable/tab_row.xml \
res/drawable/tab_thumbnail.xml \
res/drawable/tabs_panel_indicator.xml \
res/drawable/textbox_bg.xml \
res/drawable/toast_button.xml \
res/drawable/webapp_titlebar_bg.xml \
$(NULL)
RESOURCES = \
$(RES_ANIM) \
$(RES_COLOR) \
$(RES_DRAWABLE) \
$(RES_DRAWABLE_HDPI) \
$(RES_DRAWABLE_HDPI_V11) \
$(RES_DRAWABLE_LARGE_LAND_V11) \
$(RES_DRAWABLE_LARGE_HDPI_V11) \
$(RES_DRAWABLE_LARGE_MDPI_V11) \
$(RES_DRAWABLE_LARGE_XHDPI_V11) \
$(RES_DRAWABLE_LDPI) \
$(RES_DRAWABLE_MDPI) \
$(RES_DRAWABLE_MDPI_V11) \
$(RES_DRAWABLE_XHDPI) \
$(RES_DRAWABLE_XHDPI_V11) \
$(RES_DRAWABLE_XLARGE_V11) \
$(RES_DRAWABLE_XLARGE_HDPI_V11) \
$(RES_DRAWABLE_XLARGE_MDPI_V11) \
$(RES_DRAWABLE_XLARGE_XHDPI_V11) \
$(RES_LAYOUT) \
$(RES_LAYOUT_LARGE_LAND_V11) \
$(RES_LAYOUT_LARGE_V11) \
$(RES_LAYOUT_XLARGE_LAND_V11) \
$(RES_LAYOUT_XLARGE_V11) \
$(RES_MENU) \
$(RES_VALUES) \
$(RES_VALUES_LAND) \
$(RES_VALUES_LAND_V14) \
$(RES_VALUES_LARGE_LAND_V11) \
$(RES_VALUES_LARGE_V11) \
$(RES_VALUES_V11) \
$(RES_VALUES_V14) \
$(RES_VALUES_V16) \
$(RES_VALUES_XLARGE_LAND_V11) \
$(RES_VALUES_XLARGE_V11) \
$(RES_XML) \
$(RES_XML_V11) \
$(NULL)
RES_DIRS= \
res/layout \
res/layout-large-v11 \
res/layout-large-land-v11 \
res/layout-xlarge-v11 \
res/values \
res/values-v11 \
res/values-large-v11 \
res/values-xlarge-land-v11 \
res/values-xlarge-v11 \
res/values-v14 \
res/values-v16 \
res/xml \
res/xml-v11 \
res/anim \
res/drawable-ldpi \
res/drawable-mdpi \
res/drawable-hdpi \
res/drawable-xhdpi \
res/drawable \
res/drawable-mdpi-v11 \
res/drawable-hdpi-v11 \
res/drawable-xhdpi-v11 \
res/drawable-large-land-v11 \
res/drawable-large-mdpi-v11 \
res/drawable-large-hdpi-v11 \
res/drawable-large-xhdpi-v11 \
res/drawable-xlarge-v11 \
res/drawable-xlarge-mdpi-v11 \
res/drawable-xlarge-hdpi-v11 \
res/drawable-xlarge-xhdpi-v11 \
res/color \
res/menu \
res/menu-v11 \
res/menu-large-v11 \
res/menu-xlarge-v11 \
$(NULL)
ALL_JARS = \
jars/gecko-browser.jar \
jars/gecko-mozglue.jar \
@ -1280,6 +503,10 @@ endif
include $(topsrcdir)/config/makefiles/java-build.mk
# We process ANDROID_RESFILES specially for now; the following flag
# disables the default processing.
IGNORE_ANDROID_RESFILES=1
include $(topsrcdir)/config/rules.mk
# Override the Java settings with some specific android settings
@ -1360,12 +587,14 @@ res/drawable-xxhdpi/icon.png: $(ICON_PATH_XXHDPI)
$(NSINSTALL) -D res/drawable-xxhdpi
cp $(ICON_PATH_XXHDPI) $@
$(call mkdir_deps,$(RES_DIRS)): $(subst res/,$(srcdir)/resources/,$(RESOURCES)) Makefile
ANDROID_RESDIRS := $(subst resources/,res/,$(sort $(dir $(ANDROID_RESFILES))))
$(call mkdir_deps,$(ANDROID_RESDIRS)): $(ANDROID_RESFILES) Makefile
$(RM) -r $(@D)
$(NSINSTALL) -D $(@D)
$(TOUCH) $@
$(RESOURCES): $(call mkdir_deps,$(RES_DIRS)) $(subst res/,$(srcdir)/resources/,$(RESOURCES))
$(subst resources/,res/,$(ANDROID_RESFILES)): $(call mkdir_deps,$(ANDROID_RESDIRS)) $(ANDROID_RESFILES)
@echo "creating $@"
$(NSINSTALL) $(subst res/,$(srcdir)/resources/,$@) $(dir $@)
@ -1376,14 +605,10 @@ res/values/strings.xml: $(call mkdir_deps,res/values)
# rebuild gecko.ap_ if any of them change.
MULTILOCALE_STRINGS_XML_FILES := $(wildcard res/values-*/strings.xml)
all_resources = \
res/drawable-mdpi/icon.png \
res/drawable-hdpi/icon.png \
res/drawable-xhdpi/icon.png \
res/drawable-xxhdpi/icon.png \
res/values/strings.xml \
$(MULTILOCALE_STRINGS_XML_FILES) \
AndroidManifest.xml \
$(RESOURCES) \
$(subst resources/,res/,$(ANDROID_RESFILES)) \
$(ANDROID_GENERATED_RESFILES) \
$(NULL)
R.java: $(all_resources)

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

@ -40,6 +40,7 @@ SYNC_JAVA_FILES := \
background/db/Tab.java \
background/healthreport/Environment.java \
background/healthreport/EnvironmentBuilder.java \
background/healthreport/EnvironmentV1.java \
background/healthreport/HealthReportBroadcastReceiver.java \
background/healthreport/HealthReportBroadcastService.java \
background/healthreport/HealthReportDatabases.java \
@ -301,53 +302,6 @@ SYNC_JAVA_FILES := \
sync/Utils.java \
$(NULL)
SYNC_RES_DRAWABLE := \
res/drawable/pin_background.xml \
$(NULL)
SYNC_RES_DRAWABLE_LDPI := \
$(NULL)
SYNC_RES_DRAWABLE_MDPI := \
res/drawable-mdpi/desktop.png \
res/drawable-mdpi/mobile.png \
$(NULL)
SYNC_RES_DRAWABLE_HDPI := \
$(NULL)
SYNC_RES_LAYOUT := \
res/layout/sync_account.xml \
res/layout/sync_list_item.xml \
res/layout/sync_redirect_to_setup.xml \
res/layout/sync_send_tab.xml \
res/layout/sync_setup.xml \
res/layout/sync_setup_failure.xml \
res/layout/sync_setup_jpake_waiting.xml \
res/layout/sync_setup_nointernet.xml \
res/layout/sync_setup_pair.xml \
res/layout/sync_setup_success.xml \
res/layout/sync_setup_webview.xml \
$(NULL)
SYNC_RES_VALUES := \
res/values/sync_styles.xml \
$(NULL)
SYNC_RES_VALUES_V11 := \
res/values-v11/sync_styles.xml \
$(NULL)
SYNC_RES_VALUES_LARGE_V11 := \
res/values-large-v11/sync_styles.xml \
$(NULL)
SYNC_RES_XML := \
res/xml/sync_authenticator.xml \
res/xml/sync_syncadapter.xml \
res/xml/sync_options.xml \
$(NULL)
SYNC_THIRDPARTY_JAVA_FILES := \
httpclientandroidlib/androidextra/HttpClientAndroidLog.java \
httpclientandroidlib/annotation/GuardedBy.java \

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

@ -0,0 +1,28 @@
# -*- 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/.
ANDROID_RESFILES += [
'resources/drawable-mdpi/desktop.png',
'resources/drawable-mdpi/mobile.png',
'resources/drawable/pin_background.xml',
'resources/layout/sync_account.xml',
'resources/layout/sync_list_item.xml',
'resources/layout/sync_redirect_to_setup.xml',
'resources/layout/sync_send_tab.xml',
'resources/layout/sync_setup.xml',
'resources/layout/sync_setup_failure.xml',
'resources/layout/sync_setup_jpake_waiting.xml',
'resources/layout/sync_setup_nointernet.xml',
'resources/layout/sync_setup_pair.xml',
'resources/layout/sync_setup_success.xml',
'resources/layout/sync_setup_webview.xml',
'resources/values-large-v11/sync_styles.xml',
'resources/values-v11/sync_styles.xml',
'resources/values/sync_styles.xml',
'resources/xml/sync_authenticator.xml',
'resources/xml/sync_options.xml',
'resources/xml/sync_syncadapter.xml',
]

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

@ -4,17 +4,6 @@
package org.mozilla.gecko.background.healthreport;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.SortedSet;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.background.common.log.Logger;
/**
* This captures all of the details that define an 'environment' for FHR's purposes.
* Whenever this format changes, it'll be changing with a build ID, so no migration
@ -29,246 +18,32 @@ import org.mozilla.gecko.background.common.log.Logger;
* registered an <code>Environment</code>, don't do so again; start from scratch.
*
*/
public abstract class Environment {
private static final String LOG_TAG = "GeckoEnvironment";
public abstract class Environment extends EnvironmentV1 {
// Version 2 adds osLocale, appLocale, acceptLangSet, and distribution.
public static final int CURRENT_VERSION = 2;
public static int VERSION = 1;
protected final Class<? extends EnvironmentAppender> appenderClass;
protected volatile String hash = null;
protected volatile int id = -1;
// org.mozilla.profile.age.
public int profileCreation;
// org.mozilla.sysinfo.sysinfo.
public int cpuCount;
public int memoryMB;
public String architecture;
public String sysName;
public String sysVersion; // Kernel.
// geckoAppInfo. Not sure if we can/should provide this on Android.
public String vendor;
public String appName;
public String appID;
public String appVersion;
public String appBuildID;
public String platformVersion;
public String platformBuildID;
public String os;
public String xpcomabi;
public String updateChannel;
// appInfo.
public int isBlocklistEnabled;
public int isTelemetryEnabled;
// public int isDefaultBrowser; // This is meaningless on Android.
// org.mozilla.addons.active.
public JSONObject addons = null;
// org.mozilla.addons.counts.
public int extensionCount;
public int pluginCount;
public int themeCount;
public String osLocale; // The Android OS "Locale" value.
public String appLocale;
public int acceptLangSet;
public String distribution; // ID + version. Typically empty.
public Environment() {
this(Environment.HashAppender.class);
}
public Environment(Class<? extends EnvironmentAppender> appenderClass) {
this.appenderClass = appenderClass;
super(appenderClass);
version = CURRENT_VERSION;
}
public JSONObject getNonIgnoredAddons() {
if (addons == null) {
return null;
}
JSONObject out = new JSONObject();
@SuppressWarnings("unchecked")
Iterator<String> keys = addons.keys();
while (keys.hasNext()) {
try {
final String key = keys.next();
final Object obj = addons.get(key);
if (obj != null && obj instanceof JSONObject && ((JSONObject) obj).optBoolean("ignore", false)) {
continue;
}
out.put(key, obj);
} catch (JSONException ex) {
// Do nothing.
}
}
return out;
@Override
protected void appendHash(EnvironmentAppender appender) {
super.appendHash(appender);
// v2.
appender.append(osLocale);
appender.append(appLocale);
appender.append(acceptLangSet);
appender.append(distribution);
}
/**
* We break out this interface in order to allow for testing -- pass in your
* own appender that just records strings, for example.
*/
public static abstract class EnvironmentAppender {
public abstract void append(String s);
public abstract void append(int v);
}
public static class HashAppender extends EnvironmentAppender {
final MessageDigest hasher;
public HashAppender() throws NoSuchAlgorithmException {
// Note to the security minded reader: we deliberately use SHA-1 here, not
// a stronger hash. These identifiers don't strictly need a cryptographic
// hash function, because there is negligible value in attacking the hash.
// We use SHA-1 because it's *shorter* -- the exact same reason that Git
// chose SHA-1.
hasher = MessageDigest.getInstance("SHA-1");
}
@Override
public void append(String s) {
try {
hasher.update(((s == null) ? "null" : s).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
// This can never occur. Thanks, Java.
}
}
@Override
public void append(int profileCreation) {
append(Integer.toString(profileCreation, 10));
}
@Override
public String toString() {
// We *could* use ASCII85 but the savings would be negated by the
// inclusion of JSON-unsafe characters like double-quote.
return new Base64(-1, null, false).encodeAsString(hasher.digest());
}
}
/**
* Compute the stable hash of the configured environment.
*
* @return the hash in base34, or null if there was a problem.
*/
public String getHash() {
// It's never unset, so we only care about partial reads. volatile is enough.
if (hash != null) {
return hash;
}
EnvironmentAppender appender;
try {
appender = appenderClass.newInstance();
} catch (InstantiationException ex) {
// Should never happen, but...
Logger.warn(LOG_TAG, "Could not compute hash.", ex);
return null;
} catch (IllegalAccessException ex) {
// Should never happen, but...
Logger.warn(LOG_TAG, "Could not compute hash.", ex);
return null;
}
appender.append(profileCreation);
appender.append(cpuCount);
appender.append(memoryMB);
appender.append(architecture);
appender.append(sysName);
appender.append(sysVersion);
appender.append(vendor);
appender.append(appName);
appender.append(appID);
appender.append(appVersion);
appender.append(appBuildID);
appender.append(platformVersion);
appender.append(platformBuildID);
appender.append(os);
appender.append(xpcomabi);
appender.append(updateChannel);
appender.append(isBlocklistEnabled);
appender.append(isTelemetryEnabled);
appender.append(extensionCount);
appender.append(pluginCount);
appender.append(themeCount);
// We need sorted values.
if (addons != null) {
appendSortedAddons(getNonIgnoredAddons(), appender);
}
return hash = appender.toString();
}
/**
* Take a collection of add-on descriptors, appending a consistent string
* to the provided builder.
*/
public static void appendSortedAddons(JSONObject addons,
final EnvironmentAppender builder) {
final SortedSet<String> keys = HealthReportUtils.sortedKeySet(addons);
// For each add-on, produce a consistent, sorted mapping of its descriptor.
for (String key : keys) {
try {
JSONObject addon = addons.getJSONObject(key);
// Now produce the output for this add-on.
builder.append(key);
builder.append("={");
for (String addonKey : HealthReportUtils.sortedKeySet(addon)) {
builder.append(addonKey);
builder.append("==");
try {
builder.append(addon.get(addonKey).toString());
} catch (JSONException e) {
builder.append("_e_");
}
}
builder.append("}");
} catch (Exception e) {
// Muffle.
Logger.warn(LOG_TAG, "Invalid add-on for ID " + key);
}
}
}
public void setJSONForAddons(byte[] json) throws Exception {
setJSONForAddons(new String(json, "UTF-8"));
}
public void setJSONForAddons(String json) throws Exception {
if (json == null || "null".equals(json)) {
addons = null;
return;
}
addons = new JSONObject(json);
}
public void setJSONForAddons(JSONObject json) {
addons = json;
}
/**
* Includes ignored add-ons.
*/
public String getNormalizedAddonsJSON() {
// We trust that our input will already be normalized. If that assumption
// is invalidated, then we'll be sorry.
return (addons == null) ? "null" : addons.toString();
}
/**
* Ensure that the {@link Environment} has been registered with its
* storage layer, and can be used to annotate events.
*
* It's safe to call this method more than once, and each time you'll
* get the same ID.
*
* @return the integer ID to use in subsequent DB insertions.
*/
public abstract int register();
}

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

@ -58,7 +58,13 @@ public class EnvironmentBuilder {
public static interface ProfileInformationProvider {
public boolean isBlocklistEnabled();
public boolean isTelemetryEnabled();
public boolean isAcceptLangUserSet();
public long getProfileCreationTime();
public String getDistributionString();
public String getOSLocale();
public String getAppLocale();
public JSONObject getAddonsJSON();
}
@ -124,6 +130,12 @@ public class EnvironmentBuilder {
}
e.addons = addons;
// v2 environment fields.
e.distribution = info.getDistributionString();
e.osLocale = info.getOSLocale();
e.appLocale = info.getAppLocale();
e.acceptLangSet = info.isAcceptLangUserSet() ? 1 : 0;
}
/**

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

@ -0,0 +1,267 @@
/* 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.background.healthreport;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.SortedSet;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.background.common.log.Logger;
public abstract class EnvironmentV1 {
private static final String LOG_TAG = "GeckoEnvironment";
private static final int VERSION = 1;
protected final Class<? extends EnvironmentAppender> appenderClass;
protected volatile String hash = null;
protected volatile int id = -1;
public int version = VERSION;
// org.mozilla.profile.age.
public int profileCreation;
// org.mozilla.sysinfo.sysinfo.
public int cpuCount;
public int memoryMB;
public String architecture;
public String sysName;
public String sysVersion; // Kernel.
// geckoAppInfo.
public String vendor;
public String appName;
public String appID;
public String appVersion;
public String appBuildID;
public String platformVersion;
public String platformBuildID;
public String os;
public String xpcomabi;
public String updateChannel;
// appinfo.
public int isBlocklistEnabled;
public int isTelemetryEnabled;
// org.mozilla.addons.active.
public JSONObject addons = null;
// org.mozilla.addons.counts.
public int extensionCount;
public int pluginCount;
public int themeCount;
/**
* We break out this interface in order to allow for testing -- pass in your
* own appender that just records strings, for example.
*/
public static abstract class EnvironmentAppender {
public abstract void append(String s);
public abstract void append(int v);
}
public static class HashAppender extends EnvironmentAppender {
final MessageDigest hasher;
public HashAppender() throws NoSuchAlgorithmException {
// Note to the security-minded reader: we deliberately use SHA-1 here, not
// a stronger hash. These identifiers don't strictly need a cryptographic
// hash function, because there is negligible value in attacking the hash.
// We use SHA-1 because it's *shorter* -- the exact same reason that Git
// chose SHA-1.
hasher = MessageDigest.getInstance("SHA-1");
}
@Override
public void append(String s) {
try {
hasher.update(((s == null) ? "null" : s).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
// This can never occur. Thanks, Java.
}
}
@Override
public void append(int profileCreation) {
append(Integer.toString(profileCreation, 10));
}
@Override
public String toString() {
// We *could* use ASCII85 but the savings would be negated by the
// inclusion of JSON-unsafe characters like double-quote.
return new Base64(-1, null, false).encodeAsString(hasher.digest());
}
}
/**
* Ensure that the {@link Environment} has been registered with its
* storage layer, and can be used to annotate events.
*
* It's safe to call this method more than once, and each time you'll
* get the same ID.
*
* @return the integer ID to use in subsequent DB insertions.
*/
public abstract int register();
protected EnvironmentAppender getAppender() {
EnvironmentAppender appender = null;
try {
appender = appenderClass.newInstance();
} catch (InstantiationException ex) {
// Should never happen, but...
Logger.warn(LOG_TAG, "Could not compute hash.", ex);
} catch (IllegalAccessException ex) {
// Should never happen, but...
Logger.warn(LOG_TAG, "Could not compute hash.", ex);
}
return appender;
}
protected void appendHash(EnvironmentAppender appender) {
appender.append(profileCreation);
appender.append(cpuCount);
appender.append(memoryMB);
appender.append(architecture);
appender.append(sysName);
appender.append(sysVersion);
appender.append(vendor);
appender.append(appName);
appender.append(appID);
appender.append(appVersion);
appender.append(appBuildID);
appender.append(platformVersion);
appender.append(platformBuildID);
appender.append(os);
appender.append(xpcomabi);
appender.append(updateChannel);
appender.append(isBlocklistEnabled);
appender.append(isTelemetryEnabled);
appender.append(extensionCount);
appender.append(pluginCount);
appender.append(themeCount);
// We need sorted values.
if (addons != null) {
appendSortedAddons(getNonIgnoredAddons(), appender);
}
}
/**
* Compute the stable hash of the configured environment.
*
* @return the hash in base34, or null if there was a problem.
*/
public String getHash() {
// It's never unset, so we only care about partial reads. volatile is enough.
if (hash != null) {
return hash;
}
EnvironmentAppender appender = getAppender();
if (appender == null) {
return null;
}
appendHash(appender);
return hash = appender.toString();
}
public EnvironmentV1(Class<? extends EnvironmentAppender> appenderClass) {
super();
this.appenderClass = appenderClass;
}
public JSONObject getNonIgnoredAddons() {
if (addons == null) {
return null;
}
JSONObject out = new JSONObject();
@SuppressWarnings("unchecked")
Iterator<String> keys = addons.keys();
while (keys.hasNext()) {
try {
final String key = keys.next();
final Object obj = addons.get(key);
if (obj != null &&
obj instanceof JSONObject &&
((JSONObject) obj).optBoolean("ignore", false)) {
continue;
}
out.put(key, obj);
} catch (JSONException ex) {
// Do nothing.
}
}
return out;
}
/**
* Take a collection of add-on descriptors, appending a consistent string
* to the provided builder.
*/
public static void appendSortedAddons(JSONObject addons, final EnvironmentAppender builder) {
final SortedSet<String> keys = HealthReportUtils.sortedKeySet(addons);
// For each add-on, produce a consistent, sorted mapping of its descriptor.
for (String key : keys) {
try {
JSONObject addon = addons.getJSONObject(key);
// Now produce the output for this add-on.
builder.append(key);
builder.append("={");
for (String addonKey : HealthReportUtils.sortedKeySet(addon)) {
builder.append(addonKey);
builder.append("==");
try {
builder.append(addon.get(addonKey).toString());
} catch (JSONException e) {
builder.append("_e_");
}
}
builder.append("}");
} catch (Exception e) {
// Muffle.
Logger.warn(LOG_TAG, "Invalid add-on for ID " + key);
}
}
}
public void setJSONForAddons(byte[] json) throws Exception {
setJSONForAddons(new String(json, "UTF-8"));
}
public void setJSONForAddons(String json) throws Exception {
if (json == null || "null".equals(json)) {
addons = null;
return;
}
addons = new JSONObject(json);
}
public void setJSONForAddons(JSONObject json) {
addons = json;
}
/**
* Includes ignored add-ons.
*/
public String getNormalizedAddonsJSON() {
// We trust that our input will already be normalized. If that assumption
// is invalidated, then we'll be sorry.
return (addons == null) ? "null" : addons.toString();
}
}

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

@ -128,7 +128,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
};
private static final String[] COLUMNS_ENVIRONMENT_DETAILS = new String[] {
"id", "hash",
"id", "version", "hash",
"profileCreation", "cpuCount", "memoryMB",
"isBlocklistEnabled", "isTelemetryEnabled", "extensionCount",
@ -138,6 +138,8 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
"appVersion", "appBuildID", "platformVersion", "platformBuildID", "os",
"xpcomabi", "updateChannel",
"distribution", "osLocale", "appLocale", "acceptLangSet",
// Joined to the add-ons table.
"addonsBody"
};
@ -188,7 +190,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
protected final HealthReportSQLiteOpenHelper helper;
public static class HealthReportSQLiteOpenHelper extends SQLiteOpenHelper {
public static final int CURRENT_VERSION = 5;
public static final int CURRENT_VERSION = 6;
public static final String LOG_TAG = "HealthReportSQL";
/**
@ -252,7 +254,10 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
" UNIQUE (body) " +
")");
// N.B., hash collisions can occur across versions. In that case, the system
// is likely to persist the original environment version.
db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
" version INTEGER, " +
" hash TEXT, " +
" profileCreation INTEGER, " +
" cpuCount INTEGER, " +
@ -275,6 +280,12 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
" os TEXT, " +
" xpcomabi TEXT, " +
" updateChannel TEXT, " +
" distribution TEXT, " +
" osLocale TEXT, " +
" appLocale TEXT, " +
" acceptLangSet INTEGER, " +
" addonsID INTEGER, " +
" FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
" UNIQUE (hash) " +
@ -357,6 +368,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
private void createAddonsEnvironmentsView(SQLiteDatabase db) {
db.execSQL("CREATE VIEW environments_with_addons AS " +
"SELECT e.id AS id, " +
" e.version AS version, " +
" e.hash AS hash, " +
" e.profileCreation AS profileCreation, " +
" e.cpuCount AS cpuCount, " +
@ -379,6 +391,10 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
" e.os AS os, " +
" e.xpcomabi AS xpcomabi, " +
" e.updateChannel AS updateChannel, " +
" e.distribution AS distribution, " +
" e.osLocale AS osLocale, " +
" e.appLocale AS appLocale, " +
" e.acceptLangSet AS acceptLangSet, " +
" addons.body AS addonsBody " +
"FROM environments AS e, addons " +
"WHERE e.addonsID = addons.id");
@ -417,6 +433,22 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
db.delete(EVENTS_TEXTUAL, "field NOT IN (SELECT id FROM fields)", null);
}
private void upgradeDatabaseFrom5to6(SQLiteDatabase db) {
db.execSQL("DROP VIEW environments_with_addons");
// Add version to environment (default to 1).
db.execSQL("ALTER TABLE environments ADD COLUMN version INTEGER DEFAULT 1");
// Add fields to environment (default to empty string).
db.execSQL("ALTER TABLE environments ADD COLUMN distribution TEXT DEFAULT ''");
db.execSQL("ALTER TABLE environments ADD COLUMN osLocale TEXT DEFAULT ''");
db.execSQL("ALTER TABLE environments ADD COLUMN appLocale TEXT DEFAULT ''");
db.execSQL("ALTER TABLE environments ADD COLUMN acceptLangSet INTEGER DEFAULT 0");
// Recreate view.
createAddonsEnvironmentsView(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion >= newVersion) {
@ -432,6 +464,8 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
upgradeDatabaseFrom3To4(db);
case 4:
upgradeDatabaseFrom4to5(db);
case 5:
upgradeDatabaseFrom5to6(db);
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Failure in onUpgrade.", e);
@ -536,6 +570,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
// Otherwise, add data and hash to the DB.
ContentValues v = new ContentValues();
v.put("version", version);
v.put("hash", h);
v.put("profileCreation", profileCreation);
v.put("cpuCount", cpuCount);
@ -558,6 +593,10 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
v.put("os", os);
v.put("xpcomabi", xpcomabi);
v.put("updateChannel", updateChannel);
v.put("distribution", distribution);
v.put("osLocale", osLocale);
v.put("appLocale", appLocale);
v.put("acceptLangSet", acceptLangSet);
final SQLiteDatabase db = storage.helper.getWritableDatabase();
@ -643,6 +682,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
}
public void init(ContentValues v) {
version = v.containsKey("version") ? v.getAsInteger("version") : Environment.CURRENT_VERSION;
profileCreation = v.getAsInteger("profileCreation");
cpuCount = v.getAsInteger("cpuCount");
memoryMB = v.getAsInteger("memoryMB");
@ -667,6 +707,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
xpcomabi = v.getAsString("xpcomabi");
updateChannel = v.getAsString("updateChannel");
distribution = v.getAsString("distribution");
osLocale = v.getAsString("osLocale");
appLocale = v.getAsString("appLocale");
acceptLangSet = v.getAsInteger("acceptLangSet");
try {
setJSONForAddons(v.getAsString("addonsBody"));
} catch (Exception e) {
@ -686,6 +731,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
public boolean init(Cursor cursor) {
int i = 0;
this.id = cursor.getInt(i++);
this.version = cursor.getInt(i++);
this.hash = cursor.getString(i++);
profileCreation = cursor.getInt(i++);
@ -712,6 +758,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
xpcomabi = cursor.getString(i++);
updateChannel = cursor.getString(i++);
distribution = cursor.getString(i++);
osLocale = cursor.getString(i++);
appLocale = cursor.getString(i++);
acceptLangSet = cursor.getInt(i++);
try {
setJSONForAddons(cursor.getBlob(i++));
} catch (Exception e) {
@ -1339,6 +1390,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
}
// Called internally only to ensure the same db instance is used.
@SuppressWarnings("static-method")
protected int deleteOrphanedEnv(final SQLiteDatabase db, final int curEnv) {
final String whereClause =
"id != ? AND " +
@ -1353,6 +1405,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
}
// Called internally only to ensure the same db instance is used.
@SuppressWarnings("static-method")
protected int deleteEventsBefore(final SQLiteDatabase db, final String dayString) {
final String whereClause = "date < ?";
final String[] whereArgs = new String[] {dayString};
@ -1377,6 +1430,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
}
// Called internally only to ensure the same db instance is used.
@SuppressWarnings("static-method")
protected int deleteOrphanedAddons(final SQLiteDatabase db) {
final String whereClause = "id NOT IN (SELECT addonsID FROM environments)";
return db.delete("addons", whereClause, null);

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

@ -388,24 +388,117 @@ public class HealthReportGenerator {
return gecko;
}
// Null-safe string comparison.
private static boolean stringsDiffer(final String a, final String b) {
if (a == null) {
return b != null;
}
return !a.equals(b);
}
private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException {
JSONObject appinfo = new JSONObject();
int changes = 0;
if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) {
appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled);
changes++;
Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash);
// Is the environment in question newer than the diff target, or is
// there no diff target?
final boolean outdated = current == null ||
e.version > current.version;
// Is the environment in question a different version (lower or higher),
// or is there no diff target?
final boolean differ = outdated || current.version > e.version;
// Always produce an output object if there's a version mismatch or this
// isn't a diff. Otherwise, track as we go if there's any difference.
boolean changed = differ;
switch (e.version) {
// There's a straightforward correspondence between environment versions
// and appinfo versions.
case 2:
appinfo.put("_v", 3);
break;
case 1:
appinfo.put("_v", 2);
break;
default:
Logger.warn(LOG_TAG, "Unknown environment version: " + e.version);
return appinfo;
}
if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) {
appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled);
changes++;
switch (e.version) {
case 2:
if (populateAppInfoV2(appinfo, e, current, outdated)) {
changed = true;
}
// Fall through.
case 1:
// There is no older version than v1, so don't check outdated.
if (populateAppInfoV1(e, current, appinfo)) {
changed = true;
}
}
if (current != null && changes == 0) {
if (!changed) {
return null;
}
appinfo.put("_v", 2);
return appinfo;
}
private static boolean populateAppInfoV1(Environment e,
Environment current,
JSONObject appinfo)
throws JSONException {
boolean changes = false;
if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) {
appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled);
changes = true;
}
if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) {
appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled);
changes = true;
}
return changes;
}
private static boolean populateAppInfoV2(JSONObject appinfo,
Environment e,
Environment current,
final boolean outdated)
throws JSONException {
boolean changes = false;
if (outdated ||
stringsDiffer(current.osLocale, e.osLocale)) {
appinfo.put("osLocale", e.osLocale);
changes = true;
}
if (outdated ||
stringsDiffer(current.appLocale, e.appLocale)) {
appinfo.put("appLocale", e.appLocale);
changes = true;
}
if (outdated ||
stringsDiffer(current.distribution, e.distribution)) {
appinfo.put("distribution", e.distribution);
changes = true;
}
if (outdated ||
current.acceptLangSet != e.acceptLangSet) {
appinfo.put("acceptLangIsUserSet", e.acceptLangSet);
changes = true;
}
return changes;
}
private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException {
JSONObject counts = new JSONObject();
int changes = 0;

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

@ -10,6 +10,7 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.Locale;
import java.util.Scanner;
import org.json.JSONException;
@ -32,8 +33,9 @@ public class ProfileInformationCache implements ProfileInformationProvider {
* -: No version number; implicit v1.
* 1: Add versioning (Bug 878670).
* 2: Bump to regenerate add-on set after landing Bug 900694 (Bug 901622).
* 3: Add distribution, osLocale, appLocale.
*/
public static final int FORMAT_VERSION = 2;
public static final int FORMAT_VERSION = 3;
protected boolean initialized = false;
protected boolean needsWrite = false;
@ -42,7 +44,29 @@ public class ProfileInformationCache implements ProfileInformationProvider {
private volatile boolean blocklistEnabled = true;
private volatile boolean telemetryEnabled = false;
private volatile boolean isAcceptLangUserSet = false;
private volatile long profileCreationTime = 0;
private volatile String distribution = "";
// There are really four kinds of locale in play:
//
// * The OS
// * The Android environment of the app (setDefault)
// * The Gecko locale
// * The requested content locale (Accept-Language).
//
// We track only the first two, assuming that the Gecko locale will typically
// be the same as the app locale.
//
// The app locale is fetched from the PIC because it can be modified at
// runtime -- it won't necessarily be what Locale.getDefaultLocale() returns
// in a fresh non-browser profile.
//
// We also track the OS locale here for the same reason -- we need to store
// the default (OS) value before the locale-switching code takes effect!
private volatile String osLocale = "";
private volatile String appLocale = "";
private volatile JSONObject addons = null;
@ -62,7 +86,11 @@ public class ProfileInformationCache implements ProfileInformationProvider {
object.put("version", FORMAT_VERSION);
object.put("blocklist", blocklistEnabled);
object.put("telemetry", telemetryEnabled);
object.put("isAcceptLangUserSet", isAcceptLangUserSet);
object.put("profileCreated", profileCreationTime);
object.put("osLocale", osLocale);
object.put("appLocale", appLocale);
object.put("distribution", distribution);
object.put("addons", addons);
} catch (JSONException e) {
// There isn't much we can do about this.
@ -86,8 +114,12 @@ public class ProfileInformationCache implements ProfileInformationProvider {
case FORMAT_VERSION:
blocklistEnabled = object.getBoolean("blocklist");
telemetryEnabled = object.getBoolean("telemetry");
isAcceptLangUserSet = object.getBoolean("isAcceptLangUserSet");
profileCreationTime = object.getLong("profileCreated");
addons = object.getJSONObject("addons");
distribution = object.getString("distribution");
osLocale = object.getString("osLocale");
appLocale = object.getString("appLocale");
return true;
default:
Logger.warn(LOG_TAG, "Unable to restore from version " + version + " PIC file: expecting " + FORMAT_VERSION);
@ -206,6 +238,18 @@ public class ProfileInformationCache implements ProfileInformationProvider {
needsWrite = true;
}
@Override
public boolean isAcceptLangUserSet() {
ensureInitialized();
return isAcceptLangUserSet;
}
public void setAcceptLangUserSet(boolean value) {
Logger.debug(LOG_TAG, "Setting accept-lang as user-set: " + value);
isAcceptLangUserSet = value;
needsWrite = true;
}
@Override
public long getProfileCreationTime() {
ensureInitialized();
@ -218,17 +262,83 @@ public class ProfileInformationCache implements ProfileInformationProvider {
needsWrite = true;
}
@Override
public String getDistributionString() {
ensureInitialized();
return distribution;
}
/**
* Ensure that your arguments are non-null.
*/
public void setDistributionString(String distributionID, String distributionVersion) {
Logger.debug(LOG_TAG, "Setting distribution: " + distributionID + ", " + distributionVersion);
distribution = distributionID + ":" + distributionVersion;
needsWrite = true;
}
@Override
public String getAppLocale() {
ensureInitialized();
return appLocale;
}
public void setAppLocale(String value) {
if (value.equalsIgnoreCase(appLocale)) {
return;
}
Logger.debug(LOG_TAG, "Setting app locale: " + value);
appLocale = value.toLowerCase(Locale.US);
needsWrite = true;
}
@Override
public String getOSLocale() {
ensureInitialized();
return osLocale;
}
public void setOSLocale(String value) {
if (value.equalsIgnoreCase(osLocale)) {
return;
}
Logger.debug(LOG_TAG, "Setting OS locale: " + value);
osLocale = value.toLowerCase(Locale.US);
needsWrite = true;
}
/**
* Update the PIC, if necessary, to match the current locale environment.
*
* @return true if the PIC needed to be updated.
*/
public boolean updateLocales(String osLocale, String appLocale) {
if (this.osLocale.equalsIgnoreCase(osLocale) &&
(appLocale == null || this.appLocale.equalsIgnoreCase(appLocale))) {
return false;
}
this.setOSLocale(osLocale);
if (appLocale != null) {
this.setAppLocale(appLocale);
}
return true;
}
@Override
public JSONObject getAddonsJSON() {
ensureInitialized();
return addons;
}
public void updateJSONForAddon(String id, String json) throws Exception {
addons.put(id, new JSONObject(json));
needsWrite = true;
}
public void removeAddon(String id) {
addons.remove(id);
if (null != addons.remove(id)) {
needsWrite = true;
}
}
/**
@ -240,6 +350,7 @@ public class ProfileInformationCache implements ProfileInformationProvider {
}
try {
addons.put(id, json);
needsWrite = true;
} catch (Exception e) {
// Why would this happen?
Logger.warn(LOG_TAG, "Unexpected failure updating JSON for add-on.", e);
@ -253,9 +364,11 @@ public class ProfileInformationCache implements ProfileInformationProvider {
*/
public void setJSONForAddons(String json) throws Exception {
addons = new JSONObject(json);
needsWrite = true;
}
public void setJSONForAddons(JSONObject json) {
addons = json;
needsWrite = true;
}
}

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

@ -13,11 +13,11 @@ import android.content.SharedPreferences;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Distribution;
import org.mozilla.gecko.Distribution.DistributionDescriptor;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.PrefsHelper.PrefHandler;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
@ -38,6 +38,7 @@ import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;
@ -50,8 +51,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* Keep an instance of this class around.
*
* Tell it when an environment attribute has changed: call {@link
* #onBlocklistPrefChanged(boolean)} or {@link
* #onTelemetryPrefChanged(boolean)}, followed by {@link
* #onAppLocaleChanged(String)} followed by {@link
* #onEnvironmentChanged()}.
*
* Use it to record events: {@link #recordSearch(String, String)}.
@ -60,8 +60,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
*/
public class BrowserHealthRecorder implements GeckoEventListener {
private static final String LOG_TAG = "GeckoHealthRec";
private static final String PREF_ACCEPT_LANG = "intl.accept_languages";
private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
private static final String EVENT_ADDONS_ALL = "Addons:All";
private static final String EVENT_SNAPSHOT = "HealthReport:Snapshot";
private static final String EVENT_ADDONS_CHANGE = "Addons:Change";
private static final String EVENT_ADDONS_UNINSTALLING = "Addons:Uninstalling";
private static final String EVENT_PREF_CHANGE = "Pref:Change";
@ -242,8 +243,15 @@ public class BrowserHealthRecorder implements GeckoEventListener {
/**
* This constructor does IO. Run it on a background thread.
*
* appLocale can be null, which indicates that it will be provided later.
*/
public BrowserHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher, SessionInformation previousSession) {
public BrowserHealthRecorder(final Context context,
final String profilePath,
final EventDispatcher dispatcher,
final String osLocale,
final String appLocale,
SessionInformation previousSession) {
Log.d(LOG_TAG, "Initializing. Dispatcher is " + dispatcher);
this.dispatcher = dispatcher;
this.previousSession = previousSession;
@ -263,9 +271,12 @@ public class BrowserHealthRecorder implements GeckoEventListener {
this.client = null;
}
// Note that the PIC is not necessarily fully initialized at this point:
// we haven't set the app locale. This must be done before an environment
// is recorded.
this.profileCache = new ProfileInformationCache(profilePath);
try {
this.initialize(context, profilePath);
this.initialize(context, profilePath, osLocale, appLocale);
} catch (Exception e) {
Log.e(LOG_TAG, "Exception initializing.", e);
}
@ -299,7 +310,7 @@ public class BrowserHealthRecorder implements GeckoEventListener {
}
private void unregisterEventListeners() {
this.dispatcher.unregisterEventListener(EVENT_ADDONS_ALL, this);
this.dispatcher.unregisterEventListener(EVENT_SNAPSHOT, this);
this.dispatcher.unregisterEventListener(EVENT_ADDONS_CHANGE, this);
this.dispatcher.unregisterEventListener(EVENT_ADDONS_UNINSTALLING, this);
this.dispatcher.unregisterEventListener(EVENT_PREF_CHANGE, this);
@ -307,14 +318,9 @@ public class BrowserHealthRecorder implements GeckoEventListener {
this.dispatcher.unregisterEventListener(EVENT_SEARCH, this);
}
public void onBlocklistPrefChanged(boolean to) {
public void onAppLocaleChanged(String to) {
this.profileCache.beginInitialization();
this.profileCache.setBlocklistEnabled(to);
}
public void onTelemetryPrefChanged(boolean to) {
this.profileCache.beginInitialization();
this.profileCache.setTelemetryEnabled(to);
this.profileCache.setAppLocale(to);
}
public void onAddonChanged(String id, JSONObject json) {
@ -340,8 +346,7 @@ public class BrowserHealthRecorder implements GeckoEventListener {
* environment, such that a new environment should be computed and prepared
* for use in future events.
*
* Invoke this method after calls that mutate the environment, such as
* {@link #onBlocklistPrefChanged(boolean)}.
* Invoke this method after calls that mutate the environment.
*
* If this change resulted in a transition between two environments, {@link
* #onEnvironmentTransition(int, int)} will be invoked on the background
@ -491,14 +496,36 @@ public class BrowserHealthRecorder implements GeckoEventListener {
return time;
}
private void handlePrefValue(final String pref, final boolean value) {
Log.d(LOG_TAG, "Incorporating environment: " + pref + " = " + value);
if (AppConstants.TELEMETRY_PREF_NAME.equals(pref)) {
profileCache.setTelemetryEnabled(value);
private void onPrefMessage(final String pref, final JSONObject message) {
Log.d(LOG_TAG, "Incorporating environment: " + pref);
if (PREF_ACCEPT_LANG.equals(pref)) {
// We only record whether this is user-set.
try {
this.profileCache.beginInitialization();
this.profileCache.setAcceptLangUserSet(message.getBoolean("isUserSet"));
} catch (JSONException ex) {
Log.w(LOG_TAG, "Unexpected JSONException fetching isUserSet for " + pref);
}
return;
}
if (PREF_BLOCKLIST_ENABLED.equals(pref)) {
profileCache.setBlocklistEnabled(value);
// (We only handle boolean prefs right now.)
try {
boolean value = message.getBoolean("value");
if (AppConstants.TELEMETRY_PREF_NAME.equals(pref)) {
this.profileCache.beginInitialization();
this.profileCache.setTelemetryEnabled(value);
return;
}
if (PREF_BLOCKLIST_ENABLED.equals(pref)) {
this.profileCache.beginInitialization();
this.profileCache.setBlocklistEnabled(value);
return;
}
} catch (JSONException ex) {
Log.w(LOG_TAG, "Unexpected JSONException fetching boolean value for " + pref);
return;
}
Log.w(LOG_TAG, "Unexpected pref: " + pref);
@ -571,7 +598,9 @@ public class BrowserHealthRecorder implements GeckoEventListener {
* Add provider-specific initialization in this method.
*/
private synchronized void initialize(final Context context,
final String profilePath)
final String profilePath,
final String osLocale,
final String appLocale)
throws java.io.IOException {
Log.d(LOG_TAG, "Initializing profile cache.");
@ -579,6 +608,9 @@ public class BrowserHealthRecorder implements GeckoEventListener {
// If we can restore state from last time, great.
if (this.profileCache.restoreUnlessInitialized()) {
this.profileCache.updateLocales(osLocale, appLocale);
this.profileCache.completeInitialization();
Log.d(LOG_TAG, "Successfully restored state. Initializing storage.");
initializeStorage();
return;
@ -587,31 +619,24 @@ public class BrowserHealthRecorder implements GeckoEventListener {
// Otherwise, let's initialize it from scratch.
this.profileCache.beginInitialization();
this.profileCache.setProfileCreationTime(getAndPersistProfileInitTime(context, profilePath));
this.profileCache.setOSLocale(osLocale);
this.profileCache.setAppLocale(appLocale);
final BrowserHealthRecorder self = this;
PrefHandler handler = new PrefsHelper.PrefHandlerBase() {
// Because the distribution lookup can take some time, do it at the end of
// our background startup work, along with the Gecko snapshot fetch.
final GeckoEventListener self = this;
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void prefValue(String pref, boolean value) {
handlePrefValue(pref, value);
public void run() {
final DistributionDescriptor desc = new Distribution(context).getDescriptor();
if (desc != null && desc.valid) {
profileCache.setDistributionString(desc.id, desc.version);
}
Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko.");
dispatcher.registerEventListener(EVENT_SNAPSHOT, self);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null));
}
@Override
public void finish() {
Log.d(LOG_TAG, "Requesting all add-ons from Gecko.");
dispatcher.registerEventListener(EVENT_ADDONS_ALL, self);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Addons:FetchAll", null));
// Wait for the broadcast event which completes our initialization.
}
};
// Oh, singletons.
PrefsHelper.getPrefs(new String[] {
AppConstants.TELEMETRY_PREF_NAME,
PREF_BLOCKLIST_ENABLED
},
handler);
Log.d(LOG_TAG, "Requested prefs.");
});
}
/**
@ -638,12 +663,22 @@ public class BrowserHealthRecorder implements GeckoEventListener {
@Override
public void handleMessage(String event, JSONObject message) {
try {
if (EVENT_ADDONS_ALL.equals(event)) {
Log.d(LOG_TAG, "Got all add-ons.");
if (EVENT_SNAPSHOT.equals(event)) {
Log.d(LOG_TAG, "Got all add-ons and prefs.");
try {
JSONObject addons = message.getJSONObject("json");
JSONObject json = message.getJSONObject("json");
JSONObject addons = json.getJSONObject("addons");
Log.i(LOG_TAG, "Persisting " + addons.length() + " add-ons.");
profileCache.setJSONForAddons(addons);
JSONObject prefs = json.getJSONObject("prefs");
Log.i(LOG_TAG, "Persisting prefs.");
Iterator<?> keys = prefs.keys();
while (keys.hasNext()) {
String pref = (String) keys.next();
this.onPrefMessage(pref, prefs.getJSONObject(pref));
}
profileCache.completeInitialization();
} catch (java.io.IOException e) {
Log.e(LOG_TAG, "Error completing profile cache initialization.", e);
@ -675,7 +710,7 @@ public class BrowserHealthRecorder implements GeckoEventListener {
if (EVENT_PREF_CHANGE.equals(event)) {
final String pref = message.getString("pref");
Log.d(LOG_TAG, "Pref changed: " + pref);
handlePrefValue(pref, message.getBoolean("value"));
this.onPrefMessage(pref, message);
this.onEnvironmentChanged();
return;
}

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

@ -5,3 +5,609 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ['locales']
include('android-services.mozbuild')
ANDROID_GENERATED_RESFILES += [
'res/drawable-hdpi/icon.png',
'res/drawable-mdpi/icon.png',
'res/drawable-xhdpi/icon.png',
'res/drawable-xxhdpi/icon.png',
'res/values/strings.xml',
]
ANDROID_RESFILES += [
'resources/anim/grow_fade_in.xml',
'resources/anim/grow_fade_in_center.xml',
'resources/anim/popup_hide.xml',
'resources/anim/popup_show.xml',
'resources/anim/progress_spinner.xml',
'resources/anim/shrink_fade_out.xml',
'resources/color/primary_text.xml',
'resources/color/primary_text_inverse.xml',
'resources/color/secondary_text.xml',
'resources/color/secondary_text_inverse.xml',
'resources/color/select_item_multichoice.xml',
'resources/color/tertiary_text.xml',
'resources/color/tertiary_text_inverse.xml',
'resources/color/top_sites_grid_item_title.xml',
'resources/color/url_bar_title.xml',
'resources/color/url_bar_title_hint.xml',
'resources/drawable-hdpi-v11/alert_addon.png',
'resources/drawable-hdpi-v11/alert_app.png',
'resources/drawable-hdpi-v11/alert_camera.png',
'resources/drawable-hdpi-v11/alert_download.png',
'resources/drawable-hdpi-v11/alert_mic.png',
'resources/drawable-hdpi-v11/alert_mic_camera.png',
'resources/drawable-hdpi-v11/firefox_settings_alert.png',
'resources/drawable-hdpi-v11/ic_menu_addons.png',
'resources/drawable-hdpi-v11/ic_menu_apps.png',
'resources/drawable-hdpi-v11/ic_menu_back.png',
'resources/drawable-hdpi-v11/ic_menu_bookmark_add.png',
'resources/drawable-hdpi-v11/ic_menu_bookmark_remove.png',
'resources/drawable-hdpi-v11/ic_menu_desktop_mode_off.png',
'resources/drawable-hdpi-v11/ic_menu_desktop_mode_on.png',
'resources/drawable-hdpi-v11/ic_menu_downloads.png',
'resources/drawable-hdpi-v11/ic_menu_find_in_page.png',
'resources/drawable-hdpi-v11/ic_menu_forward.png',
'resources/drawable-hdpi-v11/ic_menu_new_private_tab.png',
'resources/drawable-hdpi-v11/ic_menu_new_tab.png',
'resources/drawable-hdpi-v11/ic_menu_quit.png',
'resources/drawable-hdpi-v11/ic_menu_reload.png',
'resources/drawable-hdpi-v11/ic_menu_save_as_pdf.png',
'resources/drawable-hdpi-v11/ic_menu_settings.png',
'resources/drawable-hdpi-v11/ic_menu_share.png',
'resources/drawable-hdpi-v11/ic_menu_tools.png',
'resources/drawable-hdpi-v11/ic_status_logo.png',
'resources/drawable-hdpi/abouthome_thumbnail.png',
'resources/drawable-hdpi/alert_addon.png',
'resources/drawable-hdpi/alert_app.png',
'resources/drawable-hdpi/alert_camera.png',
'resources/drawable-hdpi/alert_download.png',
'resources/drawable-hdpi/alert_mic.png',
'resources/drawable-hdpi/alert_mic_camera.png',
'resources/drawable-hdpi/arrow_popup_bg.9.png',
'resources/drawable-hdpi/blank.png',
'resources/drawable-hdpi/bookmark_folder_closed.png',
'resources/drawable-hdpi/bookmark_folder_opened.png',
'resources/drawable-hdpi/close.png',
'resources/drawable-hdpi/favicon.png',
'resources/drawable-hdpi/find_close.png',
'resources/drawable-hdpi/find_next.png',
'resources/drawable-hdpi/find_prev.png',
'resources/drawable-hdpi/folder.png',
'resources/drawable-hdpi/grid_icon_bg_activated.9.png',
'resources/drawable-hdpi/grid_icon_bg_focused.9.png',
'resources/drawable-hdpi/handle_end.png',
'resources/drawable-hdpi/handle_middle.png',
'resources/drawable-hdpi/handle_start.png',
'resources/drawable-hdpi/history_tabs_indicator_selected.9.png',
'resources/drawable-hdpi/home_bg.png',
'resources/drawable-hdpi/home_star.png',
'resources/drawable-hdpi/home_tab_menu_strip.9.png',
'resources/drawable-hdpi/ic_menu_addons_filler.png',
'resources/drawable-hdpi/ic_menu_bookmark_add.png',
'resources/drawable-hdpi/ic_menu_bookmark_remove.png',
'resources/drawable-hdpi/ic_menu_character_encoding.png',
'resources/drawable-hdpi/ic_menu_forward.png',
'resources/drawable-hdpi/ic_menu_guest.png',
'resources/drawable-hdpi/ic_menu_new_private_tab.png',
'resources/drawable-hdpi/ic_menu_new_tab.png',
'resources/drawable-hdpi/ic_menu_reload.png',
'resources/drawable-hdpi/ic_status_logo.png',
'resources/drawable-hdpi/ic_url_bar_go.png',
'resources/drawable-hdpi/ic_url_bar_reader.png',
'resources/drawable-hdpi/ic_url_bar_search.png',
'resources/drawable-hdpi/ic_url_bar_star.png',
'resources/drawable-hdpi/ic_url_bar_tab.png',
'resources/drawable-hdpi/icon_bookmarks_empty.png',
'resources/drawable-hdpi/icon_last_tabs.png',
'resources/drawable-hdpi/icon_last_tabs_empty.png',
'resources/drawable-hdpi/icon_most_recent.png',
'resources/drawable-hdpi/icon_most_recent_empty.png',
'resources/drawable-hdpi/icon_most_visited.png',
'resources/drawable-hdpi/icon_openinapp.png',
'resources/drawable-hdpi/icon_pageaction.png',
'resources/drawable-hdpi/icon_reading_list_empty.png',
'resources/drawable-hdpi/larry.png',
'resources/drawable-hdpi/lock_identified.png',
'resources/drawable-hdpi/lock_verified.png',
'resources/drawable-hdpi/menu.png',
'resources/drawable-hdpi/menu_item_check.png',
'resources/drawable-hdpi/menu_item_more.png',
'resources/drawable-hdpi/menu_item_uncheck.png',
'resources/drawable-hdpi/menu_panel_bg.9.png',
'resources/drawable-hdpi/menu_pb.png',
'resources/drawable-hdpi/menu_popup_arrow_bottom.png',
'resources/drawable-hdpi/menu_popup_arrow_top.png',
'resources/drawable-hdpi/menu_popup_bg.9.png',
'resources/drawable-hdpi/pause.png',
'resources/drawable-hdpi/pin.png',
'resources/drawable-hdpi/play.png',
'resources/drawable-hdpi/reader.png',
'resources/drawable-hdpi/reader_active.png',
'resources/drawable-hdpi/reader_cropped.png',
'resources/drawable-hdpi/reading_list.png',
'resources/drawable-hdpi/shield.png',
'resources/drawable-hdpi/shield_doorhanger.png',
'resources/drawable-hdpi/spinner_default.9.png',
'resources/drawable-hdpi/spinner_focused.9.png',
'resources/drawable-hdpi/spinner_pressed.9.png',
'resources/drawable-hdpi/tab_close.png',
'resources/drawable-hdpi/tab_indicator_divider.9.png',
'resources/drawable-hdpi/tab_indicator_selected.9.png',
'resources/drawable-hdpi/tab_indicator_selected_focused.9.png',
'resources/drawable-hdpi/tab_new.png',
'resources/drawable-hdpi/tab_new_pb.png',
'resources/drawable-hdpi/tab_thumbnail_default.png',
'resources/drawable-hdpi/tab_thumbnail_shadow.png',
'resources/drawable-hdpi/tabs_count.png',
'resources/drawable-hdpi/tabs_count_foreground.png',
'resources/drawable-hdpi/tabs_normal.png',
'resources/drawable-hdpi/tabs_private.png',
'resources/drawable-hdpi/tabs_synced.png',
'resources/drawable-hdpi/tip_addsearch.png',
'resources/drawable-hdpi/top_site_add.png',
'resources/drawable-hdpi/url_bar_entry_default.9.png',
'resources/drawable-hdpi/url_bar_entry_default_pb.9.png',
'resources/drawable-hdpi/url_bar_entry_pressed.9.png',
'resources/drawable-hdpi/url_bar_entry_pressed_pb.9.png',
'resources/drawable-hdpi/urlbar_stop.png',
'resources/drawable-hdpi/validation_arrow.png',
'resources/drawable-hdpi/validation_arrow_inverted.png',
'resources/drawable-hdpi/validation_bg.9.png',
'resources/drawable-hdpi/warning.png',
'resources/drawable-hdpi/warning_doorhanger.png',
'resources/drawable-large-hdpi-v11/arrow_popup_bg.9.png',
'resources/drawable-large-hdpi-v11/ic_menu_forward.png',
'resources/drawable-large-hdpi-v11/ic_menu_reload.png',
'resources/drawable-large-hdpi-v11/menu.png',
'resources/drawable-large-land-v11/home_history_tabs_indicator.xml',
'resources/drawable-large-mdpi-v11/arrow_popup_bg.9.png',
'resources/drawable-large-mdpi-v11/ic_menu_forward.png',
'resources/drawable-large-mdpi-v11/ic_menu_reload.png',
'resources/drawable-large-mdpi-v11/menu.png',
'resources/drawable-large-xhdpi-v11/arrow_popup_bg.9.png',
'resources/drawable-large-xhdpi-v11/ic_menu_forward.png',
'resources/drawable-large-xhdpi-v11/ic_menu_reload.png',
'resources/drawable-large-xhdpi-v11/menu.png',
'resources/drawable-mdpi-v11/alert_addon.png',
'resources/drawable-mdpi-v11/alert_app.png',
'resources/drawable-mdpi-v11/alert_camera.png',
'resources/drawable-mdpi-v11/alert_download.png',
'resources/drawable-mdpi-v11/alert_mic.png',
'resources/drawable-mdpi-v11/alert_mic_camera.png',
'resources/drawable-mdpi-v11/firefox_settings_alert.png',
'resources/drawable-mdpi-v11/ic_menu_addons.png',
'resources/drawable-mdpi-v11/ic_menu_apps.png',
'resources/drawable-mdpi-v11/ic_menu_back.png',
'resources/drawable-mdpi-v11/ic_menu_bookmark_add.png',
'resources/drawable-mdpi-v11/ic_menu_bookmark_remove.png',
'resources/drawable-mdpi-v11/ic_menu_desktop_mode_off.png',
'resources/drawable-mdpi-v11/ic_menu_desktop_mode_on.png',
'resources/drawable-mdpi-v11/ic_menu_downloads.png',
'resources/drawable-mdpi-v11/ic_menu_find_in_page.png',
'resources/drawable-mdpi-v11/ic_menu_forward.png',
'resources/drawable-mdpi-v11/ic_menu_new_private_tab.png',
'resources/drawable-mdpi-v11/ic_menu_new_tab.png',
'resources/drawable-mdpi-v11/ic_menu_quit.png',
'resources/drawable-mdpi-v11/ic_menu_reload.png',
'resources/drawable-mdpi-v11/ic_menu_save_as_pdf.png',
'resources/drawable-mdpi-v11/ic_menu_settings.png',
'resources/drawable-mdpi-v11/ic_menu_share.png',
'resources/drawable-mdpi-v11/ic_menu_tools.png',
'resources/drawable-mdpi-v11/ic_status_logo.png',
'resources/drawable-mdpi/abouthome_thumbnail.png',
'resources/drawable-mdpi/alert_addon.png',
'resources/drawable-mdpi/alert_app.png',
'resources/drawable-mdpi/alert_camera.png',
'resources/drawable-mdpi/alert_download.png',
'resources/drawable-mdpi/alert_mic.png',
'resources/drawable-mdpi/alert_mic_camera.png',
'resources/drawable-mdpi/arrow_popup_bg.9.png',
'resources/drawable-mdpi/autocomplete_list_bg.9.png',
'resources/drawable-mdpi/blank.png',
'resources/drawable-mdpi/bookmark_folder_closed.png',
'resources/drawable-mdpi/bookmark_folder_opened.png',
'resources/drawable-mdpi/bookmarkdefaults_favicon_addons.png',
'resources/drawable-mdpi/bookmarkdefaults_favicon_support.png',
'resources/drawable-mdpi/close.png',
'resources/drawable-mdpi/desktop_notification.png',
'resources/drawable-mdpi/favicon.png',
'resources/drawable-mdpi/find_close.png',
'resources/drawable-mdpi/find_next.png',
'resources/drawable-mdpi/find_prev.png',
'resources/drawable-mdpi/folder.png',
'resources/drawable-mdpi/grid_icon_bg_activated.9.png',
'resources/drawable-mdpi/grid_icon_bg_focused.9.png',
'resources/drawable-mdpi/handle_end.png',
'resources/drawable-mdpi/handle_middle.png',
'resources/drawable-mdpi/handle_start.png',
'resources/drawable-mdpi/history_tabs_indicator_selected.9.png',
'resources/drawable-mdpi/home_tab_menu_strip.9.png',
'resources/drawable-mdpi/ic_menu_addons_filler.png',
'resources/drawable-mdpi/ic_menu_bookmark_add.png',
'resources/drawable-mdpi/ic_menu_bookmark_remove.png',
'resources/drawable-mdpi/ic_menu_character_encoding.png',
'resources/drawable-mdpi/ic_menu_forward.png',
'resources/drawable-mdpi/ic_menu_guest.png',
'resources/drawable-mdpi/ic_menu_new_private_tab.png',
'resources/drawable-mdpi/ic_menu_new_tab.png',
'resources/drawable-mdpi/ic_menu_reload.png',
'resources/drawable-mdpi/ic_status_logo.png',
'resources/drawable-mdpi/ic_url_bar_go.png',
'resources/drawable-mdpi/ic_url_bar_reader.png',
'resources/drawable-mdpi/ic_url_bar_search.png',
'resources/drawable-mdpi/ic_url_bar_star.png',
'resources/drawable-mdpi/ic_url_bar_tab.png',
'resources/drawable-mdpi/icon_bookmarks_empty.png',
'resources/drawable-mdpi/icon_last_tabs.png',
'resources/drawable-mdpi/icon_last_tabs_empty.png',
'resources/drawable-mdpi/icon_most_recent.png',
'resources/drawable-mdpi/icon_most_recent_empty.png',
'resources/drawable-mdpi/icon_most_visited.png',
'resources/drawable-mdpi/icon_openinapp.png',
'resources/drawable-mdpi/icon_pageaction.png',
'resources/drawable-mdpi/icon_reading_list_empty.png',
'resources/drawable-mdpi/larry.png',
'resources/drawable-mdpi/lock_identified.png',
'resources/drawable-mdpi/lock_verified.png',
'resources/drawable-mdpi/marketplace.png',
'resources/drawable-mdpi/menu.png',
'resources/drawable-mdpi/menu_item_check.png',
'resources/drawable-mdpi/menu_item_more.png',
'resources/drawable-mdpi/menu_item_uncheck.png',
'resources/drawable-mdpi/menu_panel_bg.9.png',
'resources/drawable-mdpi/menu_pb.png',
'resources/drawable-mdpi/menu_popup_arrow_bottom.png',
'resources/drawable-mdpi/menu_popup_arrow_top.png',
'resources/drawable-mdpi/menu_popup_bg.9.png',
'resources/drawable-mdpi/pause.png',
'resources/drawable-mdpi/pin.png',
'resources/drawable-mdpi/play.png',
'resources/drawable-mdpi/progress_spinner.png',
'resources/drawable-mdpi/reader.png',
'resources/drawable-mdpi/reader_active.png',
'resources/drawable-mdpi/reader_cropped.png',
'resources/drawable-mdpi/reading_list.png',
'resources/drawable-mdpi/scrollbar.png',
'resources/drawable-mdpi/shadow.png',
'resources/drawable-mdpi/shield.png',
'resources/drawable-mdpi/shield_doorhanger.png',
'resources/drawable-mdpi/spinner_default.9.png',
'resources/drawable-mdpi/spinner_focused.9.png',
'resources/drawable-mdpi/spinner_pressed.9.png',
'resources/drawable-mdpi/start.png',
'resources/drawable-mdpi/tab_close.png',
'resources/drawable-mdpi/tab_indicator_divider.9.png',
'resources/drawable-mdpi/tab_indicator_selected.9.png',
'resources/drawable-mdpi/tab_indicator_selected_focused.9.png',
'resources/drawable-mdpi/tab_new.png',
'resources/drawable-mdpi/tab_new_pb.png',
'resources/drawable-mdpi/tab_thumbnail_default.png',
'resources/drawable-mdpi/tab_thumbnail_shadow.png',
'resources/drawable-mdpi/tabs_count.png',
'resources/drawable-mdpi/tabs_count_foreground.png',
'resources/drawable-mdpi/tabs_normal.png',
'resources/drawable-mdpi/tabs_private.png',
'resources/drawable-mdpi/tabs_synced.png',
'resources/drawable-mdpi/tip_addsearch.png',
'resources/drawable-mdpi/toast.9.png',
'resources/drawable-mdpi/toast_button_focused.9.png',
'resources/drawable-mdpi/toast_button_pressed.9.png',
'resources/drawable-mdpi/toast_divider.9.png',
'resources/drawable-mdpi/top_site_add.png',
'resources/drawable-mdpi/url_bar_entry_default.9.png',
'resources/drawable-mdpi/url_bar_entry_default_pb.9.png',
'resources/drawable-mdpi/url_bar_entry_pressed.9.png',
'resources/drawable-mdpi/url_bar_entry_pressed_pb.9.png',
'resources/drawable-mdpi/urlbar_stop.png',
'resources/drawable-mdpi/validation_arrow.png',
'resources/drawable-mdpi/validation_arrow_inverted.png',
'resources/drawable-mdpi/validation_bg.9.png',
'resources/drawable-mdpi/warning.png',
'resources/drawable-mdpi/warning_doorhanger.png',
'resources/drawable-xhdpi-v11/alert_addon.png',
'resources/drawable-xhdpi-v11/alert_app.png',
'resources/drawable-xhdpi-v11/alert_camera.png',
'resources/drawable-xhdpi-v11/alert_download.png',
'resources/drawable-xhdpi-v11/alert_mic.png',
'resources/drawable-xhdpi-v11/alert_mic_camera.png',
'resources/drawable-xhdpi-v11/firefox_settings_alert.png',
'resources/drawable-xhdpi-v11/ic_menu_addons.png',
'resources/drawable-xhdpi-v11/ic_menu_apps.png',
'resources/drawable-xhdpi-v11/ic_menu_back.png',
'resources/drawable-xhdpi-v11/ic_menu_bookmark_add.png',
'resources/drawable-xhdpi-v11/ic_menu_bookmark_remove.png',
'resources/drawable-xhdpi-v11/ic_menu_desktop_mode_off.png',
'resources/drawable-xhdpi-v11/ic_menu_desktop_mode_on.png',
'resources/drawable-xhdpi-v11/ic_menu_downloads.png',
'resources/drawable-xhdpi-v11/ic_menu_find_in_page.png',
'resources/drawable-xhdpi-v11/ic_menu_forward.png',
'resources/drawable-xhdpi-v11/ic_menu_new_private_tab.png',
'resources/drawable-xhdpi-v11/ic_menu_new_tab.png',
'resources/drawable-xhdpi-v11/ic_menu_quit.png',
'resources/drawable-xhdpi-v11/ic_menu_reload.png',
'resources/drawable-xhdpi-v11/ic_menu_save_as_pdf.png',
'resources/drawable-xhdpi-v11/ic_menu_settings.png',
'resources/drawable-xhdpi-v11/ic_menu_share.png',
'resources/drawable-xhdpi-v11/ic_menu_tools.png',
'resources/drawable-xhdpi-v11/ic_status_logo.png',
'resources/drawable-xhdpi/abouthome_thumbnail.png',
'resources/drawable-xhdpi/alert_addon.png',
'resources/drawable-xhdpi/alert_app.png',
'resources/drawable-xhdpi/alert_camera.png',
'resources/drawable-xhdpi/alert_download.png',
'resources/drawable-xhdpi/alert_mic.png',
'resources/drawable-xhdpi/alert_mic_camera.png',
'resources/drawable-xhdpi/arrow_popup_bg.9.png',
'resources/drawable-xhdpi/blank.png',
'resources/drawable-xhdpi/bookmark_folder_closed.png',
'resources/drawable-xhdpi/bookmark_folder_opened.png',
'resources/drawable-xhdpi/close.png',
'resources/drawable-xhdpi/favicon.png',
'resources/drawable-xhdpi/find_close.png',
'resources/drawable-xhdpi/find_next.png',
'resources/drawable-xhdpi/find_prev.png',
'resources/drawable-xhdpi/folder.png',
'resources/drawable-xhdpi/grid_icon_bg_activated.9.png',
'resources/drawable-xhdpi/grid_icon_bg_focused.9.png',
'resources/drawable-xhdpi/handle_end.png',
'resources/drawable-xhdpi/handle_middle.png',
'resources/drawable-xhdpi/handle_start.png',
'resources/drawable-xhdpi/history_tabs_indicator_selected.9.png',
'resources/drawable-xhdpi/home_tab_menu_strip.9.png',
'resources/drawable-xhdpi/ic_menu_addons_filler.png',
'resources/drawable-xhdpi/ic_menu_bookmark_add.png',
'resources/drawable-xhdpi/ic_menu_bookmark_remove.png',
'resources/drawable-xhdpi/ic_menu_character_encoding.png',
'resources/drawable-xhdpi/ic_menu_forward.png',
'resources/drawable-xhdpi/ic_menu_guest.png',
'resources/drawable-xhdpi/ic_menu_new_private_tab.png',
'resources/drawable-xhdpi/ic_menu_new_tab.png',
'resources/drawable-xhdpi/ic_menu_reload.png',
'resources/drawable-xhdpi/ic_status_logo.png',
'resources/drawable-xhdpi/ic_url_bar_go.png',
'resources/drawable-xhdpi/ic_url_bar_reader.png',
'resources/drawable-xhdpi/ic_url_bar_search.png',
'resources/drawable-xhdpi/ic_url_bar_star.png',
'resources/drawable-xhdpi/ic_url_bar_tab.png',
'resources/drawable-xhdpi/icon_bookmarks_empty.png',
'resources/drawable-xhdpi/icon_last_tabs.png',
'resources/drawable-xhdpi/icon_last_tabs_empty.png',
'resources/drawable-xhdpi/icon_most_recent.png',
'resources/drawable-xhdpi/icon_most_recent_empty.png',
'resources/drawable-xhdpi/icon_most_visited.png',
'resources/drawable-xhdpi/icon_openinapp.png',
'resources/drawable-xhdpi/icon_pageaction.png',
'resources/drawable-xhdpi/icon_reading_list_empty.png',
'resources/drawable-xhdpi/larry.png',
'resources/drawable-xhdpi/lock_identified.png',
'resources/drawable-xhdpi/lock_verified.png',
'resources/drawable-xhdpi/menu.png',
'resources/drawable-xhdpi/menu_item_check.png',
'resources/drawable-xhdpi/menu_item_more.png',
'resources/drawable-xhdpi/menu_item_uncheck.png',
'resources/drawable-xhdpi/menu_panel_bg.9.png',
'resources/drawable-xhdpi/menu_pb.png',
'resources/drawable-xhdpi/menu_popup_arrow_bottom.png',
'resources/drawable-xhdpi/menu_popup_arrow_top.png',
'resources/drawable-xhdpi/menu_popup_bg.9.png',
'resources/drawable-xhdpi/pause.png',
'resources/drawable-xhdpi/pin.png',
'resources/drawable-xhdpi/play.png',
'resources/drawable-xhdpi/reader.png',
'resources/drawable-xhdpi/reader_active.png',
'resources/drawable-xhdpi/reader_cropped.png',
'resources/drawable-xhdpi/reading_list.png',
'resources/drawable-xhdpi/shield.png',
'resources/drawable-xhdpi/shield_doorhanger.png',
'resources/drawable-xhdpi/spinner_default.9.png',
'resources/drawable-xhdpi/spinner_focused.9.png',
'resources/drawable-xhdpi/spinner_pressed.9.png',
'resources/drawable-xhdpi/tab_close.png',
'resources/drawable-xhdpi/tab_indicator_divider.9.png',
'resources/drawable-xhdpi/tab_indicator_selected.9.png',
'resources/drawable-xhdpi/tab_indicator_selected_focused.9.png',
'resources/drawable-xhdpi/tab_new.png',
'resources/drawable-xhdpi/tab_new_pb.png',
'resources/drawable-xhdpi/tab_thumbnail_default.png',
'resources/drawable-xhdpi/tab_thumbnail_shadow.png',
'resources/drawable-xhdpi/tabs_count.png',
'resources/drawable-xhdpi/tabs_count_foreground.png',
'resources/drawable-xhdpi/tabs_normal.png',
'resources/drawable-xhdpi/tabs_private.png',
'resources/drawable-xhdpi/tabs_synced.png',
'resources/drawable-xhdpi/tip_addsearch.png',
'resources/drawable-xhdpi/top_site_add.png',
'resources/drawable-xhdpi/url_bar_entry_default.9.png',
'resources/drawable-xhdpi/url_bar_entry_default_pb.9.png',
'resources/drawable-xhdpi/url_bar_entry_pressed.9.png',
'resources/drawable-xhdpi/url_bar_entry_pressed_pb.9.png',
'resources/drawable-xhdpi/urlbar_stop.png',
'resources/drawable-xhdpi/validation_arrow.png',
'resources/drawable-xhdpi/validation_arrow_inverted.png',
'resources/drawable-xhdpi/validation_bg.9.png',
'resources/drawable-xhdpi/warning.png',
'resources/drawable-xhdpi/warning_doorhanger.png',
'resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png',
'resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_remove.png',
'resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png',
'resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png',
'resources/drawable-xlarge-v11/home_history_tabs_indicator.xml',
'resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png',
'resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png',
'resources/drawable/action_bar_button.xml',
'resources/drawable/action_bar_button_inverse.xml',
'resources/drawable/bookmark_folder.xml',
'resources/drawable/divider_horizontal.xml',
'resources/drawable/divider_vertical.xml',
'resources/drawable/favicon_bg.xml',
'resources/drawable/handle_end_level.xml',
'resources/drawable/handle_start_level.xml',
'resources/drawable/home_banner.xml',
'resources/drawable/home_history_tabs_indicator.xml',
'resources/drawable/home_page_title_background.xml',
'resources/drawable/ic_menu_back.xml',
'resources/drawable/ic_menu_desktop_mode_off.xml',
'resources/drawable/ic_menu_desktop_mode_on.xml',
'resources/drawable/ic_menu_quit.xml',
'resources/drawable/icon_grid_item_bg.xml',
'resources/drawable/menu_item_state.xml',
'resources/drawable/menu_level.xml',
'resources/drawable/remote_tabs_child_divider.xml',
'resources/drawable/shaped_button.xml',
'resources/drawable/site_security_level.xml',
'resources/drawable/spinner.xml',
'resources/drawable/suggestion_selector.xml',
'resources/drawable/tab_new_level.xml',
'resources/drawable/tab_row.xml',
'resources/drawable/tab_thumbnail.xml',
'resources/drawable/tabs_panel_indicator.xml',
'resources/drawable/textbox_bg.xml',
'resources/drawable/toast_button.xml',
'resources/drawable/top_sites_thumbnail_bg.xml',
'resources/drawable/url_bar_bg.xml',
'resources/drawable/url_bar_entry.xml',
'resources/drawable/url_bar_nav_button.xml',
'resources/drawable/url_bar_right_edge.xml',
'resources/drawable/webapp_titlebar_bg.xml',
'resources/layout-large-land-v11/home_history_list.xml',
'resources/layout-large-land-v11/home_history_page.xml',
'resources/layout-large-land-v11/home_history_tabs_indicator.xml',
'resources/layout-large-land-v11/tabs_panel.xml',
'resources/layout-large-land-v11/tabs_panel_footer.xml',
'resources/layout-large-land-v11/tabs_panel_header.xml',
'resources/layout-large-v11/browser_toolbar.xml',
'resources/layout-large-v11/home_pager.xml',
'resources/layout-xlarge-v11/font_size_preference.xml',
'resources/layout-xlarge-v11/home_history_list.xml',
'resources/layout-xlarge-v11/home_history_page.xml',
'resources/layout-xlarge-v11/home_history_tabs_indicator.xml',
'resources/layout-xlarge-v11/remote_tabs_child.xml',
'resources/layout-xlarge-v11/remote_tabs_group.xml',
'resources/layout/arrow_popup.xml',
'resources/layout/autocomplete_list.xml',
'resources/layout/autocomplete_list_item.xml',
'resources/layout/bookmark_edit.xml',
'resources/layout/bookmark_folder_row.xml',
'resources/layout/bookmark_item_row.xml',
'resources/layout/browser_search.xml',
'resources/layout/browser_toolbar.xml',
'resources/layout/datetime_picker.xml',
'resources/layout/doorhanger.xml',
'resources/layout/doorhanger_button.xml',
'resources/layout/find_in_page_content.xml',
'resources/layout/font_size_preference.xml',
'resources/layout/gecko_app.xml',
'resources/layout/home_banner.xml',
'resources/layout/home_bookmarks_page.xml',
'resources/layout/home_empty_page.xml',
'resources/layout/home_empty_reading_page.xml',
'resources/layout/home_header_row.xml',
'resources/layout/home_history_list.xml',
'resources/layout/home_history_page.xml',
'resources/layout/home_history_tabs_indicator.xml',
'resources/layout/home_item_row.xml',
'resources/layout/home_last_tabs_page.xml',
'resources/layout/home_most_recent_page.xml',
'resources/layout/home_pager.xml',
'resources/layout/home_reading_list_page.xml',
'resources/layout/home_search_item_row.xml',
'resources/layout/home_suggestion_prompt.xml',
'resources/layout/home_top_sites_page.xml',
'resources/layout/icon_grid.xml',
'resources/layout/icon_grid_item.xml',
'resources/layout/launch_app_list.xml',
'resources/layout/launch_app_listitem.xml',
'resources/layout/list_item_header.xml',
'resources/layout/menu_action_bar.xml',
'resources/layout/menu_item_action_view.xml',
'resources/layout/menu_popup.xml',
'resources/layout/notification_icon_text.xml',
'resources/layout/notification_progress.xml',
'resources/layout/notification_progress_text.xml',
'resources/layout/pin_site_dialog.xml',
'resources/layout/preference_rightalign_icon.xml',
'resources/layout/preference_search_engine.xml',
'resources/layout/preference_search_tip.xml',
'resources/layout/remote_tabs_child.xml',
'resources/layout/remote_tabs_group.xml',
'resources/layout/search_engine_row.xml',
'resources/layout/select_dialog_list.xml',
'resources/layout/select_dialog_multichoice.xml',
'resources/layout/select_dialog_singlechoice.xml',
'resources/layout/shared_ui_components.xml',
'resources/layout/simple_dropdown_item_1line.xml',
'resources/layout/site_identity.xml',
'resources/layout/site_setting_item.xml',
'resources/layout/site_setting_title.xml',
'resources/layout/suggestion_item.xml',
'resources/layout/tab_menu_strip.xml',
'resources/layout/tabs_counter.xml',
'resources/layout/tabs_item_cell.xml',
'resources/layout/tabs_item_row.xml',
'resources/layout/tabs_panel.xml',
'resources/layout/tabs_panel_header.xml',
'resources/layout/tabs_panel_indicator.xml',
'resources/layout/text_selection_handles.xml',
'resources/layout/top_sites_grid_item_view.xml',
'resources/layout/two_line_page_row.xml',
'resources/layout/validation_message.xml',
'resources/layout/videoplayer.xml',
'resources/layout/web_app.xml',
'resources/menu-large-v11/browser_app_menu.xml',
'resources/menu-v11/browser_app_menu.xml',
'resources/menu-xlarge-v11/browser_app_menu.xml',
'resources/menu/browser_app_menu.xml',
'resources/menu/gecko_app_menu.xml',
'resources/menu/home_contextmenu.xml',
'resources/menu/titlebar_contextmenu.xml',
'resources/menu/top_sites_contextmenu.xml',
'resources/values-land/integers.xml',
'resources/values-land/layout.xml',
'resources/values-land/styles.xml',
'resources/values-large-land-v11/dimens.xml',
'resources/values-large-land-v11/styles.xml',
'resources/values-large-v11/dimens.xml',
'resources/values-large-v11/layout.xml',
'resources/values-large-v11/styles.xml',
'resources/values-large-v11/themes.xml',
'resources/values-v11/colors.xml',
'resources/values-v11/dimens.xml',
'resources/values-v11/styles.xml',
'resources/values-v11/themes.xml',
'resources/values-v14/styles.xml',
'resources/values-v16/styles.xml',
'resources/values-xlarge-land-v11/dimens.xml',
'resources/values-xlarge-land-v11/styles.xml',
'resources/values-xlarge-v11/dimens.xml',
'resources/values-xlarge-v11/integers.xml',
'resources/values-xlarge-v11/styles.xml',
'resources/values/arrays.xml',
'resources/values/attrs.xml',
'resources/values/colors.xml',
'resources/values/dimens.xml',
'resources/values/integers.xml',
'resources/values/layout.xml',
'resources/values/styles.xml',
'resources/values/themes.xml',
'resources/xml-v11/preference_headers.xml',
'resources/xml-v11/preferences.xml',
'resources/xml-v11/preferences_customize.xml',
'resources/xml-v11/preferences_customize_tablet.xml',
'resources/xml/preferences.xml',
'resources/xml/preferences_customize.xml',
'resources/xml/preferences_devtools.xml',
'resources/xml/preferences_display.xml',
'resources/xml/preferences_privacy.xml',
'resources/xml/preferences_search.xml',
'resources/xml/preferences_vendor.xml',
'resources/xml/searchable.xml',
]
if CONFIG['MOZ_CRASHREPORTER']:
ANDROID_RESFILES += [
'resources/drawable-mdpi/crash_reporter.png',
'resources/layout/crash_reporter.xml',
]

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

@ -4,6 +4,7 @@ package @ANDROID_PACKAGE_NAME@.tests;
import @ANDROID_PACKAGE_NAME@.*;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import java.io.File;
import java.io.FileOutputStream;
@ -38,11 +39,50 @@ public class testDistribution extends ContentProviderTest {
return TEST_MOCHITEST;
}
/**
* This is a hack.
*
* Startup results in us writing prefs -- we fetch the Distribution, which
* caches its state. Our tests try to wipe those prefs, but apparently
* sometimes race with startup, which leads to us not getting one of our
* expected messages. The test fails.
*
* This hack waits for any existing background tasks -- such as the one that
* writes prefs -- to finish before we begin the test.
*/
private void waitForBackgroundHappiness() {
try {
ClassLoader classLoader = mActivity.getClassLoader();
Class threadUtilsClass = classLoader.loadClass("org.mozilla.gecko.util.ThreadUtils");
Method postToBackgroundThread = threadUtilsClass.getMethod("postToBackgroundThread", Runnable.class);
final Object signal = new Object();
final Runnable done = new Runnable() {
@Override
public void run() {
synchronized (signal) {
signal.notify();
}
}
};
synchronized (signal) {
postToBackgroundThread.invoke(null, done);
signal.wait();
}
} catch (Exception e) {
mAsserter.ok(false, "Exception waiting on background thread.", e.toString());
}
mAsserter.dumpLog("Background task completed. Proceeding.");
}
public void testDistribution() {
mActivity = getActivity();
String mockPackagePath = getMockPackagePath();
// Wait for any startup-related background distribution shenanigans to
// finish. This reduces the chance of us racing with startup pref writes.
waitForBackgroundHappiness();
// Pre-clear distribution pref, run basic preferences and en-US localized preferences Tests
clearDistributionPref();
setTestLocale("en-US");
@ -64,11 +104,10 @@ public class testDistribution extends ContentProviderTest {
// Call Distribution.init with the mock package.
ClassLoader classLoader = mActivity.getClassLoader();
Class distributionClass = classLoader.loadClass("org.mozilla.gecko.Distribution");
Class contextClass = classLoader.loadClass("android.content.Context");
Method init = distributionClass.getMethod("init", contextClass, String.class);
Method init = distributionClass.getMethod("init", Context.class, String.class, String.class);
Actions.EventExpecter distributionSetExpecter = mActions.expectGeckoEvent("Distribution:Set:OK");
init.invoke(null, mActivity, aPackagePath);
init.invoke(null, mActivity, aPackagePath, "prefs-" + System.currentTimeMillis());
distributionSetExpecter.blockForEvent();
distributionSetExpecter.unregisterListener();
} catch (Exception e) {
@ -268,6 +307,7 @@ public class testDistribution extends ContentProviderTest {
// Clears the distribution pref to return distribution state to STATE_UNKNOWN
private void clearDistributionPref() {
mAsserter.dumpLog("Clearing distribution pref.");
SharedPreferences settings = mActivity.getSharedPreferences("GeckoApp", Activity.MODE_PRIVATE);
String keyName = mActivity.getPackageName() + ".distribution_state";
settings.edit().remove(keyName).commit();

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

@ -5316,7 +5316,10 @@ var FormAssistant = {
* -- and reflect them back to Java.
*/
let HealthReportStatusListener = {
TELEMETRY_PREF:
PREF_ACCEPT_LANG: "intl.accept_languages",
PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled",
PREF_TELEMETRY_ENABLED:
#ifdef MOZ_TELEMETRY_REPORTING
// Telemetry pref differs based on build.
#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
@ -5335,18 +5338,21 @@ let HealthReportStatusListener = {
console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex);
}
Services.obs.addObserver(this, "Addons:FetchAll", false);
Services.prefs.addObserver("extensions.blocklist.enabled", this, false);
if (this.TELEMETRY_PREF) {
Services.prefs.addObserver(this.TELEMETRY_PREF, this, false);
console.log("Adding HealthReport:RequestSnapshot observer.");
Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false);
Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false);
Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false);
if (this.PREF_TELEMETRY_ENABLED) {
Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false);
}
},
uninit: function () {
Services.obs.removeObserver(this, "Addons:FetchAll");
Services.prefs.removeObserver("extensions.blocklist.enabled", this);
if (this.TELEMETRY_PREF) {
Services.prefs.removeObserver(this.TELEMETRY_PREF, this);
Services.obs.removeObserver(this, "HealthReport:RequestSnapshot");
Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this);
Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this);
if (this.PREF_TELEMETRY_ENABLED) {
Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this);
}
AddonManager.removeAddonListener(this);
@ -5354,11 +5360,30 @@ let HealthReportStatusListener = {
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "Addons:FetchAll":
HealthReportStatusListener.sendAllAddonsToJava();
case "HealthReport:RequestSnapshot":
HealthReportStatusListener.sendSnapshotToJava();
break;
case "nsPref:changed":
sendMessageToJava({ type: "Pref:Change", pref: aData, value: Services.prefs.getBoolPref(aData) });
let response = {
type: "Pref:Change",
pref: aData,
isUserSet: Services.prefs.prefHasUserValue(aData),
};
switch (aData) {
case this.PREF_ACCEPT_LANG:
response.value = Services.prefs.getCharPref(aData);
break;
case this.PREF_TELEMETRY_ENABLED:
case this.PREF_BLOCKLIST_ENABLED:
response.value = Services.prefs.getBoolPref(aData);
break;
default:
console.log("Unexpected pref in HealthReportStatusListener: " + aData);
return;
}
sendMessageToJava(response);
break;
}
},
@ -5440,9 +5465,9 @@ let HealthReportStatusListener = {
this.notifyJava(aAddon);
},
sendAllAddonsToJava: function () {
sendSnapshotToJava: function () {
AddonManager.getAllAddons(function (aAddons) {
let json = {};
let jsonA = {};
if (aAddons) {
for (let i = 0; i < aAddons.length; ++i) {
let addon = aAddons[i];
@ -5451,14 +5476,43 @@ let HealthReportStatusListener = {
if (HealthReportStatusListener._shouldIgnore(addon)) {
addonJSON.ignore = true;
}
json[addon.id] = addonJSON;
jsonA[addon.id] = addonJSON;
} catch (e) {
// Just skip this add-on.
}
}
}
sendMessageToJava({ type: "Addons:All", json: json });
});
// Now add prefs.
let jsonP = {};
for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) {
if (!pref) {
// This will be the case for PREF_TELEMETRY_ENABLED in developer builds.
continue;
}
jsonP[pref] = {
pref: pref,
value: Services.prefs.getBoolPref(pref),
isUserSet: Services.prefs.prefHasUserValue(pref),
};
}
for (let pref of [this.PREF_ACCEPT_LANG]) {
jsonP[pref] = {
pref: pref,
value: Services.prefs.getCharPref(pref),
isUserSet: Services.prefs.prefHasUserValue(pref),
};
}
console.log("Sending snapshot message.");
sendMessageToJava({
type: "HealthReport:Snapshot",
json: {
addons: jsonA,
prefs: jsonP,
},
});
}.bind(this));
},
};

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

@ -27,6 +27,7 @@ background/db/CursorDumper.java
background/db/Tab.java
background/healthreport/Environment.java
background/healthreport/EnvironmentBuilder.java
background/healthreport/EnvironmentV1.java
background/healthreport/HealthReportBroadcastReceiver.java
background/healthreport/HealthReportBroadcastService.java
background/healthreport/HealthReportDatabases.java

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

@ -21,7 +21,6 @@ include $(srcdir)/android-services-files.mk
# BACKGROUND_TESTS_{JAVA,RES}_FILES are defined in android-services-files.mk.
JAVAFILES := $(BACKGROUND_TESTS_JAVA_FILES)
ANDROID_RESFILES := $(BACKGROUND_TESTS_RES_FILES)
# The test APK needs to know the contents of the target APK while not
# being linked against them. This is a best effort to avoid getting

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

@ -100,11 +100,3 @@ BACKGROUND_TESTS_JAVA_FILES := \
src/testhelpers/WBORepository.java \
$(NULL)
BACKGROUND_TESTS_RES_FILES := \
res/drawable-hdpi/icon.png \
res/drawable-ldpi/icon.png \
res/drawable-mdpi/icon.png \
res/layout/main.xml \
res/values/strings.xml \
$(NULL)

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

@ -0,0 +1,7 @@
ANDROID_RESFILES += [
'res/drawable-hdpi/icon.png',
'res/drawable-ldpi/icon.png',
'res/drawable-mdpi/icon.png',
'res/layout/main.xml',
'res/values/strings.xml',
]

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

@ -3,3 +3,5 @@
# 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/.
include('android-services.mozbuild')

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

@ -38,7 +38,7 @@ public class MockDatabaseEnvironment extends DatabaseEnvironment {
}
}
public MockDatabaseEnvironment mockInit(String version) {
public MockDatabaseEnvironment mockInit(String appVersion) {
profileCreation = 1234;
cpuCount = 2;
memoryMB = 512;
@ -55,7 +55,7 @@ public class MockDatabaseEnvironment extends DatabaseEnvironment {
vendor = "";
appName = "";
appID = "";
appVersion = version;
this.appVersion = appVersion;
appBuildID = "";
platformVersion = "";
platformBuildID = "";
@ -63,6 +63,14 @@ public class MockDatabaseEnvironment extends DatabaseEnvironment {
xpcomabi = "";
updateChannel = "";
// v2 fields.
distribution = "";
appLocale = "";
osLocale = "";
acceptLangSet = 0;
version = Environment.CURRENT_VERSION;
return this;
}
}

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

@ -15,6 +15,10 @@ import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.util.SparseArray;
public class TestHealthReportGenerator extends FakeProfileTestCase {
@SuppressWarnings("static-method")
public void testOptObject() throws JSONException {
@ -57,9 +61,14 @@ public class TestHealthReportGenerator extends FakeProfileTestCase {
assertFalse(bar.has("b"));
}
// We don't initialize the env in testHashing, so these are just the default
// values for the Java types, in order.
private static final String EXPECTED_MOCK_BASE_HASH = "000nullnullnullnullnullnullnull"
+ "nullnullnullnullnullnull00000";
// v2 fields.
private static final String EXPECTED_MOCK_BASE_HASH_SUFFIX = "null" + "null" + 0 + "null";
public void testHashing() throws JSONException {
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
MockDatabaseEnvironment env = new MockDatabaseEnvironment(storage, MockDatabaseEnvironment.MockEnvironmentAppender.class);
@ -96,10 +105,10 @@ public class TestHealthReportGenerator extends FakeProfileTestCase {
"}");
env.addons.put("{addonA}", addonA1);
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash, env.getHash());
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash + EXPECTED_MOCK_BASE_HASH_SUFFIX, env.getHash());
env.addons.put("{addonA}", addonA1rev);
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash, env.getHash());
assertEquals(EXPECTED_MOCK_BASE_HASH + addonAHash + EXPECTED_MOCK_BASE_HASH_SUFFIX, env.getHash());
}
private void assertJSONDiff(JSONObject source, JSONObject diff) throws JSONException {
@ -406,4 +415,103 @@ public class TestHealthReportGenerator extends FakeProfileTestCase {
protected String getCacheSuffix() {
return File.separator + "health-" + System.currentTimeMillis() + ".profile";
}
public void testEnvironmentDiffing() throws JSONException {
// Manually insert a v1 environment.
final MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
final SQLiteDatabase db = storage.getDB();
storage.deleteEverything();
final MockDatabaseEnvironment v1env = storage.getEnvironment();
v1env.mockInit("27.0a1");
v1env.version = 1;
v1env.appLocale = "";
v1env.osLocale = "";
v1env.distribution = "";
v1env.acceptLangSet = 0;
final int v1ID = v1env.register();
// Verify.
final String[] cols = new String[] {
"id", "version", "hash",
"osLocale", "acceptLangSet", "appLocale", "distribution"
};
final Cursor c1 = db.query("environments", cols, "id = " + v1ID, null, null, null, null);
String v1envHash;
try {
assertTrue(c1.moveToFirst());
assertEquals(1, c1.getCount());
assertEquals(v1ID, c1.getInt(0));
assertEquals(1, c1.getInt(1));
v1envHash = c1.getString(2);
assertNotNull(v1envHash);
assertEquals("", c1.getString(3));
assertEquals(0, c1.getInt(4));
assertEquals("", c1.getString(5));
assertEquals("", c1.getString(6));
} finally {
c1.close();
}
// Insert a v2 environment.
final MockDatabaseEnvironment v2env = storage.getEnvironment();
v2env.mockInit("27.0a1");
v2env.appLocale = v2env.osLocale = "en_us";
v2env.acceptLangSet = 1;
final int v2ID = v2env.register();
assertFalse(v1ID == v2ID);
final Cursor c2 = db.query("environments", cols, "id = " + v2ID, null, null, null, null);
String v2envHash;
try {
assertTrue(c2.moveToFirst());
assertEquals(1, c2.getCount());
assertEquals(v2ID, c2.getInt(0));
assertEquals(2, c2.getInt(1));
v2envHash = c2.getString(2);
assertNotNull(v2envHash);
assertEquals("en_us", c2.getString(3));
assertEquals(1, c2.getInt(4));
assertEquals("en_us", c2.getString(5));
assertEquals("", c2.getString(6));
} finally {
c2.close();
}
assertFalse(v1envHash.equals(v2envHash));
// Now let's diff based on DB contents.
SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
JSONObject oldEnv = HealthReportGenerator.jsonify(envs.get(v1ID), null).getJSONObject("org.mozilla.appInfo.appinfo");
JSONObject newEnv = HealthReportGenerator.jsonify(envs.get(v2ID), null).getJSONObject("org.mozilla.appInfo.appinfo");
// Generate the new env as if the old were the current. This should rarely happen in practice.
// Fields supported by the new env but not the old will appear, even if the 'default' for the
// old implementation is equal to the new env's value.
JSONObject newVsOld = HealthReportGenerator.jsonify(envs.get(v2ID), envs.get(v1ID)).getJSONObject("org.mozilla.appInfo.appinfo");
// Generate the old env as if the new were the current. This is normal. Fields not supported by the old
// environment version should not appear in the output.
JSONObject oldVsNew = HealthReportGenerator.jsonify(envs.get(v1ID), envs.get(v2ID)).getJSONObject("org.mozilla.appInfo.appinfo");
assertEquals(2, oldEnv.getInt("_v"));
assertEquals(3, newEnv.getInt("_v"));
assertEquals(2, oldVsNew.getInt("_v"));
assertEquals(3, newVsOld.getInt("_v"));
assertFalse(oldVsNew.has("osLocale"));
assertFalse(oldVsNew.has("appLocale"));
assertFalse(oldVsNew.has("distribution"));
assertFalse(oldVsNew.has("acceptLangIsUserSet"));
assertTrue(newVsOld.has("osLocale"));
assertTrue(newVsOld.has("appLocale"));
assertTrue(newVsOld.has("distribution"));
assertTrue(newVsOld.has("acceptLangIsUserSet"));
}
}

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

@ -146,7 +146,7 @@ public class TestHealthReportProvider extends DBProviderTestCase<HealthReportPro
Cursor envCursor = resolver.query(envURI, null, null, null, null);
try {
assertTrue(envCursor.moveToFirst());
envHash = envCursor.getString(1);
envHash = envCursor.getString(2); // id, version, hash, ...
} finally {
envCursor.close();
}
@ -249,6 +249,13 @@ public class TestHealthReportProvider extends DBProviderTestCase<HealthReportPro
v.put("os", "");
v.put("xpcomabi", "");
v.put("updateChannel", "");
// v2.
v.put("distribution", "");
v.put("osLocale", "en_us");
v.put("appLocale", "en_us");
v.put("acceptLangSet", 0);
return v;
}
}

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

@ -53,6 +53,7 @@
#include <algorithm>
#ifdef MOZ_WIDGET_GONK
#include "nsINetworkManager.h"
#include "nsINetworkStatsServiceProxy.h"
#endif
@ -966,6 +967,7 @@ WebSocketChannel::WebSocketChannel() :
mCountRecv(0),
mCountSent(0),
mAppId(0),
mConnectionType(NETWORK_NO_TYPE),
mIsInBrowser(false)
{
NS_ABORT_IF_FALSE(NS_IsMainThread(), "not main thread");
@ -1080,9 +1082,9 @@ WebSocketChannel::BeginOpen()
NS_GetAppInfo(localChannel, &mAppId, &mIsInBrowser);
}
// obtain active network
// obtain active connection type
if (mAppId != NECKO_NO_APP_ID) {
GetActiveNetwork();
GetConnectionType(&mConnectionType);
}
rv = localChannel->AsyncOpen(this, mHttpChannel);
@ -3272,7 +3274,7 @@ WebSocketChannel::OnDataAvailable(nsIRequest *aRequest,
}
nsresult
WebSocketChannel::GetActiveNetwork()
WebSocketChannel::GetConnectionType(int32_t *type)
{
#ifdef MOZ_WIDGET_GONK
MOZ_ASSERT(NS_IsMainThread());
@ -3281,11 +3283,15 @@ WebSocketChannel::GetActiveNetwork()
nsCOMPtr<nsINetworkManager> networkManager = do_GetService("@mozilla.org/network/manager;1", &result);
if (NS_FAILED(result) || !networkManager) {
mActiveNetwork = nullptr;
return NS_ERROR_UNEXPECTED;
*type = NETWORK_NO_TYPE;
}
result = networkManager->GetActive(getter_AddRefs(mActiveNetwork));
nsCOMPtr<nsINetworkInterface> networkInterface;
result = networkManager->GetActive(getter_AddRefs(networkInterface));
if (networkInterface) {
result = networkInterface->GetType(type);
}
return NS_OK;
#else
@ -3297,8 +3303,9 @@ nsresult
WebSocketChannel::SaveNetworkStats(bool enforce)
{
#ifdef MOZ_WIDGET_GONK
// Check if the active network and app id are valid.
if(!mActiveNetwork || mAppId == NECKO_NO_APP_ID) {
// Check if the connection type and app id are valid.
if(mConnectionType == NETWORK_NO_TYPE ||
mAppId == NECKO_NO_APP_ID) {
return NS_OK;
}
@ -3322,7 +3329,7 @@ WebSocketChannel::SaveNetworkStats(bool enforce)
return rv;
}
mNetworkStatsServiceProxy->SaveAppStats(mAppId, mActiveNetwork, PR_Now() / 1000,
mNetworkStatsServiceProxy->SaveAppStats(mAppId, mConnectionType, PR_Now() / 1000,
mCountRecv, mCountSent, nullptr);
// Reset the counters after saving.

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

@ -18,10 +18,6 @@
#include "nsIHttpChannelInternal.h"
#include "BaseWebSocketChannel.h"
#ifdef MOZ_WIDGET_GONK
#include "nsINetworkManager.h"
#endif
#include "nsCOMPtr.h"
#include "nsString.h"
#include "nsDeque.h"
@ -258,17 +254,16 @@ private:
// These members are used for network per-app metering (bug 855949)
// Currently, they are only available on gonk.
public:
const static int32_t NETWORK_NO_TYPE = -1; // default conntection type
const static uint64_t NETWORK_STATS_THRESHOLD = 65536;
private:
uint64_t mCountRecv;
uint64_t mCountSent;
uint32_t mAppId;
int32_t mConnectionType;
bool mIsInBrowser;
#ifdef MOZ_WIDGET_GONK
nsCOMPtr<nsINetworkInterface> mActiveNetwork;
#endif
nsresult GetActiveNetwork();
nsresult GetConnectionType(int32_t *);
nsresult SaveNetworkStats(bool);
void CountRecvBytes(uint64_t recvBytes)
{

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

@ -139,6 +139,8 @@ class TreeMetadataEmitter(LoggingMixin):
passthru = VariablePassthru(sandbox)
varmap = dict(
# Makefile.in : moz.build
ANDROID_GENERATED_RESFILES='ANDROID_GENERATED_RESFILES',
ANDROID_RESFILES='ANDROID_RESFILES',
ASFILES='ASFILES',
CMMSRCS='CMMSRCS',
CPPSRCS='CPP_SOURCES',

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

@ -43,6 +43,22 @@ from mozbuild.util import (
VARIABLES = {
# Variables controlling reading of other frontend files.
'ANDROID_GENERATED_RESFILES': (StrictOrderingOnAppendList, list, [],
"""Android resource files generated as part of the build.
This variable contains a list of files that are expected to be
generated (often by preprocessing) into a 'res' directory as
part of the build process, and subsequently merged into an APK
file.
""", 'export'),
'ANDROID_RESFILES': (StrictOrderingOnAppendList, list, [],
"""Android resource files.
This variable contains a list of files to package into a 'res'
directory and merge into an APK file.
""", 'export'),
'ASFILES': (StrictOrderingOnAppendList, list, [],
"""Assembly file sources.

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

@ -0,0 +1,6 @@
MARIONETTE_TIMEOUT = 60000;
MARIONETTE_CONTEXT = "chrome";
ok(true);
(function () {
finish();
})();

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

@ -93,4 +93,5 @@ b2g = false
[test_implicit_waits.py]
[test_date_time_value.py]
[test_getactiveframe_oop.py]
[test_submit.py]
[test_submit.py]
[test_chrome_async_finish.js]

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

@ -844,7 +844,7 @@ MarionetteServerConnection.prototype = {
aRequest.newSandbox = true;
}
if (this.context == "chrome") {
if (aRequest.async) {
if (aRequest.parameters.async) {
this.executeWithCallback(aRequest, aRequest.parameters.async);
}
else {