Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2016-05-03 16:19:09 +02:00
commit 359203771f
120 changed files with 3573 additions and 1382 deletions

View File

@ -33,7 +33,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0'
classpath 'com.android.tools.build:gradle:2.1.0'
classpath('com.stanfy.spoon:spoon-gradle-plugin:1.0.4') {
// Without these, we get errors linting.
exclude module: 'guava'

View File

@ -44,46 +44,74 @@
label="&inspectorHTMLEdit.label;"
accesskey="&inspectorHTMLEdit.accesskey;"
oncommand="inspector.editHTML()"/>
<menuitem id="node-menu-copyinner"
label="&inspectorHTMLCopyInner.label;"
accesskey="&inspectorHTMLCopyInner.accesskey;"
oncommand="inspector.copyInnerHTML()"/>
<menuitem id="node-menu-copyouter"
label="&inspectorHTMLCopyOuter.label;"
accesskey="&inspectorHTMLCopyOuter.accesskey;"
oncommand="inspector.copyOuterHTML()"/>
<menuitem id="node-menu-copyuniqueselector"
label="&inspectorCopyUniqueSelector.label;"
accesskey="&inspectorCopyUniqueSelector.accesskey;"
oncommand="inspector.copyUniqueSelector()"/>
<menuitem id="node-menu-copyimagedatauri"
label="&inspectorCopyImageDataUri.label;"
oncommand="inspector.copyImageDataUri()"/>
<menuitem id="node-menu-showdomproperties"
label="&inspectorShowDOMProperties.label;"
oncommand="inspector.showDOMProperties()"/>
<menuitem id="node-menu-useinconsole"
label="&inspectorUseInConsole.label;"
oncommand="inspector.useInConsole()"/>
<menuitem id="node-menu-expand"
label="&inspectorExpandNode.label;"
oncommand="inspector.expandNode()"/>
<menuitem id="node-menu-collapse"
label="&inspectorCollapseNode.label;"
oncommand="inspector.collapseNode()"/>
<menuseparator/>
<menuitem id="node-menu-pasteinnerhtml"
label="&inspectorHTMLPasteInner.label;"
accesskey="&inspectorHTMLPasteInner.accesskey;"
oncommand="inspector.pasteInnerHTML()"/>
<menuitem id="node-menu-pasteouterhtml"
label="&inspectorHTMLPasteOuter.label;"
accesskey="&inspectorHTMLPasteOuter.accesskey;"
oncommand="inspector.pasteOuterHTML()"/>
<menu id="node-menu-paste-extra-submenu"
label="&inspectorHTMLPasteExtraSubmenu.label;"
accesskey="&inspectorHTMLPasteExtraSubmenu.accesskey;">
<menuitem id="node-menu-add"
label="&inspectorAddNode.label;"
accesskey="&inspectorAddNode.accesskey;"
oncommand="inspector.addNode()"/>
<menuitem id="node-menu-duplicatenode"
label="&inspectorDuplicateNode.label;"
oncommand="inspector.duplicateNode()"/>
<menuitem id="node-menu-delete"
label="&inspectorHTMLDelete.label;"
accesskey="&inspectorHTMLDelete.accesskey;"
oncommand="inspector.deleteNode()"/>
<menu label="&inspectorAttributesSubmenu.label;"
accesskey="&inspectorAttributesSubmenu.accesskey;">
<menupopup>
<menuitem id="node-menu-add-attribute"
label="&inspectorAddAttribute.label;"
accesskey="&inspectorAddAttribute.accesskey;"
oncommand="inspector.onAddAttribute()"/>
<menuitem id="node-menu-edit-attribute"
label="&inspectorEditAttribute.label;"
accesskey="&inspectorEditAttribute.accesskey;"
oncommand="inspector.onEditAttribute()"/>
<menuitem id="node-menu-remove-attribute"
label="&inspectorRemoveAttribute.label;"
accesskey="&inspectorRemoveAttribute.accesskey;"
oncommand="inspector.onRemoveAttribute()"/>
</menupopup>
</menu>
<menuseparator/>
<menuitem id="node-menu-pseudo-hover"
label=":hover" type="checkbox"
oncommand="inspector.togglePseudoClass(':hover')"/>
<menuitem id="node-menu-pseudo-active"
label=":active" type="checkbox"
oncommand="inspector.togglePseudoClass(':active')"/>
<menuitem id="node-menu-pseudo-focus"
label=":focus" type="checkbox"
oncommand="inspector.togglePseudoClass(':focus')"/>
<menuseparator/>
<menu label="&inspectorCopyHTMLSubmenu.label;">
<menupopup>
<menuitem id="node-menu-copyinner"
label="&inspectorCopyInnerHTML.label;"
accesskey="&inspectorCopyInnerHTML.accesskey;"
oncommand="inspector.copyInnerHTML()"/>
<menuitem id="node-menu-copyouter"
label="&inspectorCopyOuterHTML.label;"
accesskey="&inspectorCopyOuterHTML.accesskey;"
oncommand="inspector.copyOuterHTML()"/>
<menuitem id="node-menu-copyuniqueselector"
label="&inspectorCopyCSSSelector.label;"
accesskey="&inspectorCopyCSSSelector.accesskey;"
oncommand="inspector.copyUniqueSelector()"/>
<menuitem id="node-menu-copyimagedatauri"
label="&inspectorImageDataUri.label;"
oncommand="inspector.copyImageDataUri()"/>
</menupopup>
</menu>
<menu label="&inspectorPasteHTMLSubmenu.label;">
<menupopup>
<menuitem id="node-menu-pasteinnerhtml"
label="&inspectorPasteInnerHTML.label;"
accesskey="&inspectorPasteInnerHTML.accesskey;"
oncommand="inspector.pasteInnerHTML()"/>
<menuitem id="node-menu-pasteouterhtml"
label="&inspectorPasteOuterHTML.label;"
accesskey="&inspectorPasteOuterHTML.accesskey;"
oncommand="inspector.pasteOuterHTML()"/>
<menuitem id="node-menu-pastebefore"
label="&inspectorHTMLPasteBefore.label;"
accesskey="&inspectorHTMLPasteBefore.accesskey;"
@ -103,6 +131,13 @@
</menupopup>
</menu>
<menuseparator/>
<menuitem id="node-menu-expand"
label="&inspectorExpandNode.label;"
oncommand="inspector.expandNode()"/>
<menuitem id="node-menu-collapse"
label="&inspectorCollapseNode.label;"
oncommand="inspector.collapseNode()"/>
<menuseparator/>
<menuitem id="node-menu-scrollnodeintoview"
label="&inspectorScrollNodeIntoView.label;"
accesskey="&inspectorScrollNodeIntoView.accesskey;"
@ -110,49 +145,17 @@
<menuitem id="node-menu-screenshotnode"
label="&inspectorScreenshotNode.label;"
oncommand="inspector.screenshotNode()" />
<menuitem id="node-menu-add"
label="&inspectorAddNode.label;"
accesskey="&inspectorAddNode.accesskey;"
oncommand="inspector.addNode()"/>
<menuitem id="node-menu-duplicatenode"
label="&inspectorDuplicateNode.label;"
oncommand="inspector.duplicateNode()"/>
<menuitem id="node-menu-delete"
label="&inspectorHTMLDelete.label;"
accesskey="&inspectorHTMLDelete.accesskey;"
oncommand="inspector.deleteNode()"/>
<menu label="&inspectorAttributeSubmenu.label;"
accesskey="&inspectorAttributeSubmenu.accesskey;">
<menupopup>
<menuitem id="node-menu-add-attribute"
label="&inspectorAddAttribute.label;"
accesskey="&inspectorAddAttribute.accesskey;"
oncommand="inspector.onAddAttribute()"/>
<menuitem id="node-menu-edit-attribute"
label="&inspectorEditAttribute.label;"
accesskey="&inspectorEditAttribute.accesskey;"
oncommand="inspector.onEditAttribute()"/>
<menuitem id="node-menu-remove-attribute"
label="&inspectorRemoveAttribute.label;"
accesskey="&inspectorRemoveAttribute.accesskey;"
oncommand="inspector.onRemoveAttribute()"/>
</menupopup>
</menu>
<menuitem id="node-menu-useinconsole"
label="&inspectorUseInConsole.label;"
oncommand="inspector.useInConsole()"/>
<menuitem id="node-menu-showdomproperties"
label="&inspectorShowDOMProperties.label;"
oncommand="inspector.showDOMProperties()"/>
<menuseparator id="node-menu-link-separator"/>
<menuitem id="node-menu-link-follow"
oncommand="inspector.onFollowLink()"/>
<menuitem id="node-menu-link-copy"
oncommand="inspector.onCopyLink()"/>
<menuseparator/>
<menuitem id="node-menu-pseudo-hover"
label=":hover" type="checkbox"
oncommand="inspector.togglePseudoClass(':hover')"/>
<menuitem id="node-menu-pseudo-active"
label=":active" type="checkbox"
oncommand="inspector.togglePseudoClass(':active')"/>
<menuitem id="node-menu-pseudo-focus"
label=":focus" type="checkbox"
oncommand="inspector.togglePseudoClass(':focus')"/>
</menupopup>
</popupset>

View File

@ -59,6 +59,9 @@ loader.lazyGetter(this, "AutocompletePopup", () => {
return require("devtools/client/shared/autocomplete-popup").AutocompletePopup;
});
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
/**
* Vocabulary for the purposes of this file:
*
@ -1548,11 +1551,12 @@ MarkupView.prototype = {
}
if (!(children.hasFirst && children.hasLast)) {
let nodesCount = container.node.numChildren;
let showAllString = PluralForm.get(nodesCount,
this.strings.GetStringFromName("markupView.more.showAll2"));
let data = {
showing: this.strings.GetStringFromName("markupView.more.showing"),
showAll: this.strings.formatStringFromName(
"markupView.more.showAll",
[container.node.numChildren.toString()], 1),
showAll: showAllString.replace("#1", nodesCount),
allButtonClick: () => {
container.maxChildren = -1;
container.childrenDirty = true;

View File

@ -53,6 +53,7 @@ support-files =
[browser_rules_add-rule_04.js]
[browser_rules_add-rule_05.js]
[browser_rules_add-rule_pseudo_class.js]
[browser_rules_add-rule_iframes.js]
[browser_rules_authored.js]
[browser_rules_authored_color.js]
[browser_rules_authored_override.js]

View File

@ -0,0 +1,85 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Tests adding a rule on elements nested in iframes.
const TEST_URI =
`<div>outer</div>
<iframe id="frame1" src="data:text/html;charset=utf-8,<div>inner1</div>">
</iframe>
<iframe id="frame2" src="data:text/html;charset=utf-8,<div>inner2</div>">
</iframe>`;
add_task(function* () {
yield addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
let {inspector, view} = yield openRuleView();
yield selectNode("div", inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, "div", 1);
yield addNewProperty(view, 1, "color", "red");
let innerFrameDiv1 = yield getNodeFrontInFrame("div", "#frame1", inspector);
yield selectNode(innerFrameDiv1, inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, "div", 1);
yield addNewProperty(view, 1, "color", "blue");
let innerFrameDiv2 = yield getNodeFrontInFrame("div", "#frame2", inspector);
yield selectNode(innerFrameDiv2, inspector);
yield addNewRule(inspector, view);
yield testNewRule(view, "div", 1);
yield addNewProperty(view, 1, "color", "green");
});
function* addNewRule(inspector, view) {
info("Adding the new rule using the button");
view.addRuleButton.click();
info("Waiting for rule view to change");
let onRuleViewChanged = once(view, "ruleview-changed");
yield onRuleViewChanged;
}
/**
* Check the newly created rule has the expected selector and submit the
* selector editor.
*/
function* testNewRule(view, expected, index) {
let idRuleEditor = getRuleViewRuleEditor(view, index);
let editor = idRuleEditor.selectorText.ownerDocument.activeElement;
is(editor.value, expected,
"Selector editor value is as expected: " + expected);
info("Entering the escape key");
EventUtils.synthesizeKey("VK_ESCAPE", {});
is(idRuleEditor.selectorText.textContent, expected,
"Selector text value is as expected: " + expected);
}
/**
* Add a new property in the rule at the provided index in the rule view.
*
* @param {RuleView} view
* @param {Number} index
* The index of the rule in which we should add a new property.
* @param {String} name
* The name of the new property.
* @param {String} value
* The value of the new property.
*/
function* addNewProperty(view, index, name, value) {
let idRuleEditor = getRuleViewRuleEditor(view, index);
info(`Adding new property "${name}: ${value};"`);
let onRuleViewChanged = view.once("ruleview-changed");
idRuleEditor.addProperty(name, value, "");
yield onRuleViewChanged;
let textProps = idRuleEditor.rule.textProps;
let lastProperty = textProps[textProps.length - 1];
is(lastProperty.name, name, "Last property has the expected name");
is(lastProperty.value, value, "Last property has the expected value");
}

View File

@ -4,35 +4,35 @@
<!ENTITY inspectorHTMLEdit.label "Edit As HTML">
<!ENTITY inspectorHTMLEdit.accesskey "E">
<!-- LOCALIZATION NOTE (inspectorHTMLCopyInner.label): This is the label shown
<!-- LOCALIZATION NOTE (inspectorCopyInnerHTML.label): This is the label shown
in the inspector contextual-menu for the item that lets users copy the
inner HTML of the current node -->
<!ENTITY inspectorHTMLCopyInner.label "Copy Inner HTML">
<!ENTITY inspectorHTMLCopyInner.accesskey "I">
<!ENTITY inspectorCopyInnerHTML.label "Inner HTML">
<!ENTITY inspectorCopyInnerHTML.accesskey "I">
<!-- LOCALIZATION NOTE (inspectorHTMLCopyOuter.label): This is the label shown
<!-- LOCALIZATION NOTE (inspectorCopyOuterHTML.label): This is the label shown
in the inspector contextual-menu for the item that lets users copy the
outer HTML of the current node -->
<!ENTITY inspectorHTMLCopyOuter.label "Copy Outer HTML">
<!ENTITY inspectorHTMLCopyOuter.accesskey "O">
<!ENTITY inspectorCopyOuterHTML.label "Outer HTML">
<!ENTITY inspectorCopyOuterHTML.accesskey "O">
<!-- LOCALIZATION NOTE (inspectorCopyUniqueSelector.label): This is the label
<!-- LOCALIZATION NOTE (inspectorCopyCSSSelector.label): This is the label
shown in the inspector contextual-menu for the item that lets users copy
the CSS Selector of the current node -->
<!ENTITY inspectorCopyUniqueSelector.label "Copy Unique Selector">
<!ENTITY inspectorCopyUniqueSelector.accesskey "U">
<!ENTITY inspectorCopyCSSSelector.label "CSS Selector">
<!ENTITY inspectorCopyCSSSelector.accesskey "S">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteOuter.label): This is the label shown
<!-- LOCALIZATION NOTE (inspectorPasteOuterHTML.label): This is the label shown
in the inspector contextual-menu for the item that lets users paste outer
HTML in the current node -->
<!ENTITY inspectorHTMLPasteOuter.label "Paste Outer HTML">
<!ENTITY inspectorHTMLPasteOuter.accesskey "P">
<!ENTITY inspectorPasteOuterHTML.label "Outer HTML">
<!ENTITY inspectorPasteOuterHTML.accesskey "O">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteInner.label): This is the label shown
<!-- LOCALIZATION NOTE (inspectorPasteInnerHTML.label): This is the label shown
in the inspector contextual-menu for the item that lets users paste inner
HTML in the current node -->
<!ENTITY inspectorHTMLPasteInner.label "Paste Inner HTML">
<!ENTITY inspectorHTMLPasteInner.accesskey "N">
<!ENTITY inspectorPasteInnerHTML.label "Inner HTML">
<!ENTITY inspectorPasteInnerHTML.accesskey "I">
<!-- LOCALIZATION NOTE (inspectorHTMLPasteExtraSubmenu.label): This is the label
shown in the inspector contextual-menu for the sub-menu of the other Paste
@ -79,14 +79,15 @@
current node -->
<!ENTITY inspectorHTMLDelete.label "Delete Node">
<!ENTITY inspectorHTMLDelete.accesskey "D">
<!-- LOCALIZATION NOTE (inspectorAttributeSubmenu.label): This is the label
<!-- LOCALIZATION NOTE (inspectorAttributesSubmenu.label): This is the label
shown in the inspector contextual-menu for the sub-menu of the other
attribute items, which allow to:
- add new attribute
- edit attribute
- remove attribute -->
<!ENTITY inspectorAttributeSubmenu.label "Attribute">
<!ENTITY inspectorAttributeSubmenu.accesskey "A">
<!ENTITY inspectorAttributesSubmenu.label "Attributes">
<!ENTITY inspectorAttributesSubmenu.accesskey "A">
<!-- LOCALIZATION NOTE (inspectorAddAttribute.label): This is the label shown in
the inspector contextual-menu for the item that lets users add attribute
@ -117,12 +118,12 @@
shown as the placeholder for the markup view search in the inspector. -->
<!ENTITY inspectorSearchHTML.label3 "Search HTML">
<!-- LOCALIZATION NOTE (inspectorCopyImageDataUri.label): This is the label
<!-- LOCALIZATION NOTE (inspectorImageDataUri.label): This is the label
shown in the inspector contextual-menu for the item that lets users copy
the URL embedding the image data encoded in Base 64 (what we name
here Image Data URL). For more information:
https://developer.mozilla.org/en-US/docs/Web/HTTP/data_URIs -->
<!ENTITY inspectorCopyImageDataUri.label "Copy Image Data-URL">
<!ENTITY inspectorImageDataUri.label "Image Data-URL">
<!-- LOCALIZATION NOTE (inspectorShowDOMProperties.label): This is the label
shown in the inspector contextual-menu for the item that lets users see
@ -161,3 +162,23 @@
DOM (as children of the currently selected element). -->
<!ENTITY inspectorAddNode.label "Create New Node">
<!ENTITY inspectorAddNode.accesskey "C">
<!-- LOCALIZATION NOTE (inspectorCopyHTMLSubmenu.label): This is the label
shown in the inspector contextual-menu for the sub-menu of the other
copy items, which allow to:
- Copy Inner HTML
- Copy Outer HTML
- Copy Unique selector
- Copy Image data URI -->
<!ENTITY inspectorCopyHTMLSubmenu.label "Copy">
<!-- LOCALIZATION NOTE (inspectorPasteHTMLSubmenu.label): This is the label
shown in the inspector contextual-menu for the sub-menu of the other
paste items, which allow to:
- Paste Inner HTML
- Paste Outer HTML
- Before
- After
- As First Child
- As Last Child -->
<!ENTITY inspectorPasteHTMLSubmenu.label "Paste">

View File

@ -42,12 +42,17 @@ inspector.accesskey=I
inspector.panelLabel=Inspector Panel
inspector.panelLabel.markupView=Markup View
# LOCALIZATION NOTE (markupView.more.*)
# LOCALIZATION NOTE (markupView.more.showing)
# When there are too many nodes to load at once, we will offer to
# show all the nodes.
# Keyboard shortcut for DOM and Style Inspector will shown inside brackets.
markupView.more.showing=Some nodes were hidden.
markupView.more.showAll=Show All %S Nodes
# LOCALIZATION NOTE (markupView.more.showAll2): Semi-colon list of plural forms.
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
markupView.more.showAll2=Show one more node;Show all #1 nodes
# LOCALIZATION NOTE (inspector.tooltip2)
# Keyboard shortcut for DOM and Style Inspector will be shown inside brackets.
inspector.tooltip2=DOM and Style Inspector (%S)
#LOCALIZATION NOTE: Used in the image preview tooltip when the image could not be loaded

View File

@ -296,6 +296,9 @@ pref("devtools.webconsole.timestampMessages", false);
// to automatically trigger multiline editing (equivalent to shift + enter).
pref("devtools.webconsole.autoMultiline", true);
// Enable the experimental webconsole frontend (work in progress)
pref("devtools.webconsole.new-frontend-enabled", false);
// The number of lines that are displayed in the web console for the Net,
// CSS, JS and Web Developer categories. These defaults should be kept in sync
// with DEFAULT_LOG_LIMIT in the webconsole frontend.

View File

@ -75,10 +75,10 @@ module.exports = createClass({
}
let { lastClientX, lastClientY, ignoreX, ignoreY } = this.state;
// we are resizing a centered viewport, so dragging a mouse resizes
// twice as much - also on opposite side.
// the viewport is centered horizontally, so horizontal resize resizes
// by twice the distance the mouse was dragged - on left and right side.
let deltaX = 2 * (clientX - lastClientX);
let deltaY = 2 * (clientY - lastClientY);
let deltaY = (clientY - lastClientY);
if (ignoreX) {
deltaX = 0;

View File

@ -5,34 +5,55 @@ http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URL = "about:logo";
function dragElementBy(selector, x, y, win) {
function getElRect(selector, win) {
let el = win.document.querySelector(selector);
let rect = el.getBoundingClientRect();
return el.getBoundingClientRect();
}
/**
* Drag an element identified by 'selector' by [x,y] amount. Returns
* the rect of the dragged element as it was before drag.
*/
function dragElementBy(selector, x, y, win) {
let rect = getElRect(selector, win);
let startPoint = [ rect.left + rect.width / 2, rect.top + rect.height / 2 ];
let endPoint = [ startPoint[0] + x, startPoint[1] + y ];
EventUtils.synthesizeMouseAtPoint(...startPoint, { type: "mousedown" }, win);
EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mousemove" }, win);
EventUtils.synthesizeMouseAtPoint(...endPoint, { type: "mouseup" }, win);
return rect;
}
function* testViewportResize(ui, selector, moveBy,
expectedViewportSize, expectedHandleMove) {
let win = ui.toolWindow;
let resized = waitForViewportResizeTo(ui, ...expectedViewportSize);
let startRect = dragElementBy(selector, ...moveBy, win);
yield resized;
let endRect = getElRect(selector, win);
is(endRect.left - startRect.left, expectedHandleMove[0],
`The x move of ${selector} is as expected`);
is(endRect.top - startRect.top, expectedHandleMove[1],
`The y move of ${selector} is as expected`);
}
addRDMTask(TEST_URL, function* ({ ui, manager }) {
ok(ui, "An instance of the RDM should be attached to the tab.");
yield setViewportSize(ui, manager, 300, 300);
let win = ui.toolWindow;
// Do horizontal + vertical resize
let resized = waitForViewportResizeTo(ui, 320, 320);
dragElementBy(".viewport-resize-handle", 10, 10, win);
yield resized;
yield testViewportResize(ui, ".viewport-resize-handle",
[10, 10], [320, 310], [10, 10]);
// Do horizontal resize
let hResized = waitForViewportResizeTo(ui, 300, 320);
dragElementBy(".viewport-horizontal-resize-handle", -10, -10, win);
yield hResized;
yield testViewportResize(ui, ".viewport-horizontal-resize-handle",
[-10, 10], [300, 310], [-10, 0]);
// Do vertical resize
let vResized = waitForViewportResizeTo(ui, 300, 300);
dragElementBy(".viewport-vertical-resize-handle", -10, -10, win);
yield vResized;
yield testViewportResize(ui, ".viewport-vertical-resize-handle",
[-10, -10], [300, 300], [0, -10], ui);
});

View File

@ -372,6 +372,11 @@ JSTerm.prototype = {
return;
}
if (this.hud.NEW_CONSOLE_OUTPUT_ENABLED) {
this.hud.newConsoleOutput.dispatchMessageAdd(response);
// @TODO figure out what to do about the callback.
return;
}
let msg = new Messages.JavaScriptEvalOutput(response,
errorMessage, errorDocLink);
this.hud.output.addMessage(msg);
@ -939,6 +944,10 @@ JSTerm.prototype = {
this._sidebarDestroy();
if (hud.NEW_CONSOLE_OUTPUT_ENABLED) {
hud.newConsoleOutput.dispatchMessagesClear();
}
this.emit("messages-cleared");
},

View File

@ -7,7 +7,8 @@
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
DIRS += [
'net'
'net',
'new-console-output',
]
DevToolsModules(

View File

@ -0,0 +1,33 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
prepareMessage
} = require("devtools/client/webconsole/new-console-output/utils/messages");
const {
MESSAGE_ADD,
MESSAGES_CLEAR,
} = require("../constants");
function messageAdd(packet) {
let message = prepareMessage(packet);
return {
type: MESSAGE_ADD,
message
};
}
function messagesClear() {
return {
type: MESSAGES_CLEAR
};
}
exports.messageAdd = messageAdd;
exports.messagesClear = messagesClear;

View File

@ -0,0 +1,8 @@
# 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/.
DevToolsModules(
'messages.js',
)

View File

@ -0,0 +1,68 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
createClass,
createFactory,
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const MessageContainer = createFactory(require("devtools/client/webconsole/new-console-output/components/message-container").MessageContainer);
const ConsoleOutput = createClass({
propTypes: {
jsterm: PropTypes.object.isRequired,
// This function is created in mergeProps
openVariablesView: PropTypes.func.isRequired,
messages: PropTypes.array.isRequired
},
displayName: "ConsoleOutput",
componentWillUpdate() {
// @TODO Move this to a parent component.
let node = ReactDOM.findDOMNode(this).parentNode.parentNode.parentNode;
if (node.lastChild) {
this.shouldScrollBottom = isScrolledToBottom(node.lastChild, node);
}
},
componentDidUpdate() {
if (this.shouldScrollBottom) {
let node = ReactDOM.findDOMNode(this).parentNode.parentNode.parentNode;
node.scrollTop = node.scrollHeight;
}
},
render() {
let messageNodes = this.props.messages.map(function(message) {
return (
MessageContainer({ message })
);
});
return (
dom.div({}, messageNodes)
);
}
});
function isScrolledToBottom(outputNode, scrollNode) {
let lastNodeHeight = outputNode.lastChild ?
outputNode.lastChild.clientHeight : 0;
return scrollNode.scrollTop + scrollNode.clientHeight >=
scrollNode.scrollHeight - lastNodeHeight / 2;
}
function mapStateToProps(state) {
return {
messages: state.messages
};
}
module.exports = connect(mapStateToProps)(ConsoleOutput);

View File

@ -0,0 +1,50 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
createClass,
createFactory,
PropTypes
} = require("devtools/client/shared/vendor/react");
const MessageContainer = createClass({
propTypes: {
message: PropTypes.object.isRequired
},
displayName: "MessageContainer",
render() {
const { message } = this.props;
let MessageComponent = getMessageComponent(message.messageType);
return MessageComponent({ message });
}
});
function getMessageComponent(messageType) {
let MessageComponent;
switch (messageType) {
case "ConsoleApiCall":
MessageComponent = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call").ConsoleApiCall;
break;
case "EvaluationResult":
MessageComponent = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result").EvaluationResult;
break;
case "PageError":
MessageComponent = require("devtools/client/webconsole/new-console-output/components/message-types/page-error").PageError;
break;
}
return createFactory(MessageComponent);
}
module.exports.MessageContainer = MessageContainer;
// Exported so we can test it with unit tests.
module.exports.getMessageComponent = getMessageComponent;

View File

@ -0,0 +1,32 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const {l10n} = require("devtools/client/webconsole/new-console-output/utils/messages");
MessageIcon.displayName = "MessageIcon";
MessageIcon.propTypes = {
severity: PropTypes.string.isRequired,
};
function MessageIcon(props) {
const { severity } = props;
const title = l10n.getStr("severity." + severity);
return dom.div({
className: "icon",
title
});
}
module.exports.MessageIcon = MessageIcon;

View File

@ -0,0 +1,28 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
MessageRepeat.displayName = "MessageRepeat";
MessageRepeat.propTypes = {
repeat: PropTypes.number.isRequired
};
function MessageRepeat(props) {
const { repeat } = props;
const visibility = repeat > 1 ? "visible" : "hidden";
return dom.span({className: "message-repeats", style: {visibility}}, repeat);
}
exports.MessageRepeat = MessageRepeat;

View File

@ -0,0 +1,66 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
createFactory,
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
ConsoleApiCall.displayName = "ConsoleApiCall";
ConsoleApiCall.propTypes = {
message: PropTypes.object.isRequired,
};
function ConsoleApiCall(props) {
const { message } = props;
const messageBody =
dom.span({className: "message-body devtools-monospace"},
formatTextContent(message.data.arguments));
const icon = MessageIcon({severity: message.severity});
const repeat = MessageRepeat({repeat: message.repeat});
const children = [
messageBody,
repeat
];
// @TODO Use of "is" is a temporary hack to get the category and severity
// attributes to be applied. There are targeted in webconsole's CSS rules,
// so if we remove this hack, we have to modify the CSS rules accordingly.
return dom.div({
class: "message cm-s-mozilla",
is: "fdt-message",
category: message.category,
severity: message.severity
},
icon,
dom.span({className: "message-body-wrapper"},
dom.span({},
dom.span({className: "message-flex-body"},
children
)
)
)
);
}
function formatTextContent(args) {
return args.map(function(arg, i, arr) {
const str = dom.span({className: "console-string"}, arg);
if (i < arr.length - 1) {
return [str, " "];
}
return str;
});
}
module.exports.ConsoleApiCall = ConsoleApiCall;

View File

@ -0,0 +1,60 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
createFactory,
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const VariablesViewLink = createFactory(require("devtools/client/webconsole/new-console-output/components/variables-view-link").VariablesViewLink);
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
DatePreview.displayName = "DatePreview";
DatePreview.propTypes = {
data: PropTypes.object.isRequired,
};
function DatePreview(props) {
const { data, category, severity } = props;
const { preview } = data;
const dateString = new Date(preview.timestamp).toISOString();
const textNodes = [
VariablesViewLink({
objectActor: data,
label: "Date"
}),
dom.span({ className: "cm-string-2" }, ` ${dateString}`)
];
const icon = MessageIcon({ severity });
// @TODO Use of "is" is a temporary hack to get the category and severity
// attributes to be applied. There are targeted in webconsole's CSS rules,
// so if we remove this hack, we have to modify the CSS rules accordingly.
return dom.div({
class: "message cm-s-mozilla",
is: "fdt-message",
category: category,
severity: severity
},
icon,
dom.span({
className: "message-body-wrapper message-body devtools-monospace"
}, dom.span({},
dom.span({ className: "class-Date" },
textNodes
)
)
)
);
}
module.exports.DatePreview = DatePreview;

View File

@ -0,0 +1,37 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
createFactory,
DOM: dom,
} = require("devtools/client/shared/vendor/react");
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
DefaultRenderer.displayName = "DefaultRenderer";
function DefaultRenderer(props) {
const { category, severity } = props;
const icon = MessageIcon({ severity });
// @TODO Use of "is" is a temporary hack to get the category and severity
// attributes to be applied. There are targeted in webconsole's CSS rules,
// so if we remove this hack, we have to modify the CSS rules accordingly.
return dom.div({
class: "message cm-s-mozilla",
is: "fdt-message",
category: category,
severity: severity
},
icon,
"This evaluation result type is not supported yet."
);
}
module.exports.DefaultRenderer = DefaultRenderer;

View File

@ -0,0 +1,42 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
createFactory,
PropTypes
} = require("devtools/client/shared/vendor/react");
EvaluationResult.displayName = "EvaluationResult";
EvaluationResult.propTypes = {
message: PropTypes.object.isRequired,
};
function EvaluationResult(props) {
const { message } = props;
let PreviewComponent = getPreviewComponent(message.data);
return PreviewComponent({
data: message.data,
category: message.category,
severity: message.severity
});
}
function getPreviewComponent(data) {
if (typeof data.class != "undefined") {
switch (data.class) {
case "Date":
return createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/date-preview").DatePreview);
}
}
return createFactory(require("devtools/client/webconsole/new-console-output/components/message-types/default-renderer").DefaultRenderer);
}
module.exports.EvaluationResult = EvaluationResult;

View File

@ -0,0 +1,12 @@
# 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/.
DevToolsModules(
'console-api-call.js',
'date-preview.js',
'default-renderer.js',
'evaluation-result.js',
'page-error.js',
)

View File

@ -0,0 +1,56 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
createFactory,
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const MessageRepeat = createFactory(require("devtools/client/webconsole/new-console-output/components/message-repeat").MessageRepeat);
const MessageIcon = createFactory(require("devtools/client/webconsole/new-console-output/components/message-icon").MessageIcon);
PageError.displayName = "PageError";
PageError.propTypes = {
message: PropTypes.object.isRequired,
};
function PageError(props) {
const { message } = props;
const messageBody =
dom.span({className: "message-body devtools-monospace"},
message.data.errorMessage);
const repeat = MessageRepeat({repeat: message.repeat});
const icon = MessageIcon({severity: message.severity});
const children = [
messageBody,
repeat
];
// @TODO Use of "is" is a temporary hack to get the category and severity
// attributes to be applied. There are targeted in webconsole's CSS rules,
// so if we remove this hack, we have to modify the CSS rules accordingly.
return dom.div({
class: "message cm-s-mozilla",
is: "fdt-message",
category: message.category,
severity: message.severity
},
icon,
dom.span({className: "message-body-wrapper"},
dom.span({},
dom.span({className: "message-flex-body"},
children
)
)
)
);
}
module.exports.PageError = PageError;

View File

@ -0,0 +1,16 @@
# 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/.
DIRS += [
'message-types'
]
DevToolsModules(
'console-output.js',
'message-container.js',
'message-icon.js',
'message-repeat.js',
'variables-view-link.js'
)

View File

@ -0,0 +1,34 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const {
DOM: dom,
PropTypes
} = require("devtools/client/shared/vendor/react");
const {openVariablesView} = require("devtools/client/webconsole/new-console-output/utils/variables-view");
VariablesViewLink.displayName = "VariablesViewLink";
VariablesViewLink.propTypes = {
objectActor: PropTypes.object.required,
label: PropTypes.string.label,
};
function VariablesViewLink(props) {
const { objectActor, label } = props;
return dom.a({
onClick: openVariablesView.bind(null, objectActor),
className: "cm-variable",
draggable: false,
href: "#"
}, label);
}
module.exports.VariablesViewLink = VariablesViewLink;

View File

@ -0,0 +1,77 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const actionTypes = {
MESSAGE_ADD: "MESSAGE_ADD",
MESSAGES_CLEAR: "MESSAGES_CLEAR",
};
const categories = {
CATEGORY_NETWORK: 0,
CATEGORY_CSS: 1,
CATEGORY_JS: 2,
CATEGORY_WEBDEV: 3,
CATEGORY_INPUT: 4,
CATEGORY_OUTPUT: 5,
CATEGORY_SECURITY: 6,
CATEGORY_SERVER: 7
};
const severities = {
SEVERITY_ERROR: 0,
SEVERITY_WARNING: 1,
SEVERITY_INFO: 2,
SEVERITY_LOG: 3
};
// The fragment of a CSS class name that identifies categories/severities.
const fragments = {
CATEGORY_CLASS_FRAGMENTS: [
"network",
"cssparser",
"exception",
"console",
"input",
"output",
"security",
"server",
],
SEVERITY_CLASS_FRAGMENTS: [
"error",
"warn",
"info",
"log",
]
};
// A mapping from the console API log event levels to the Web Console
// severities.
const levels = {
LEVELS: {
error: severities.SEVERITY_ERROR,
exception: severities.SEVERITY_ERROR,
assert: severities.SEVERITY_ERROR,
warn: severities.SEVERITY_WARNING,
info: severities.SEVERITY_INFO,
log: severities.SEVERITY_LOG,
trace: severities.SEVERITY_LOG,
table: severities.SEVERITY_LOG,
debug: severities.SEVERITY_LOG,
dir: severities.SEVERITY_LOG,
dirxml: severities.SEVERITY_LOG,
group: severities.SEVERITY_LOG,
groupCollapsed: severities.SEVERITY_LOG,
groupEnd: severities.SEVERITY_LOG,
time: severities.SEVERITY_LOG,
timeEnd: severities.SEVERITY_LOG,
count: severities.SEVERITY_LOG
}
};
// Combine into a single constants object
module.exports = Object.assign({}, actionTypes, categories, severities,
fragments, levels);

View File

@ -0,0 +1,24 @@
/* 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/. */
/* global BrowserLoader */
"use strict";
var { utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://devtools/client/shared/browser-loader.js");
// Initialize module loader and load all modules of the new inline
// preview feature. The entire code-base doesn't need any extra
// privileges and runs entirely in content scope.
const NewConsoleOutputWrapper = BrowserLoader({
baseURI: "resource://devtools/client/webconsole/new-console-output/",
window: this}).require("./new-console-output-wrapper");
this.NewConsoleOutput = function(parentNode, jsterm) {
console.log("Creating NewConsoleOutput", parentNode, NewConsoleOutputWrapper);
return new NewConsoleOutputWrapper(parentNode, jsterm);
};

View File

@ -0,0 +1,28 @@
# 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/.
DIRS += [
'actions',
'components',
'reducers',
'utils',
]
DevToolsModules(
'constants.js',
'main.js',
'new-console-output-wrapper.js',
'store.js',
)
MOCHITEST_CHROME_MANIFESTS += [
'test/components/chrome.ini',
'test/utils/chrome.ini'
]
XPCSHELL_TESTS_MANIFESTS += [
'test/actions/xpcshell.ini',
'test/store/xpcshell.ini'
]

View File

@ -0,0 +1,33 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// React & Redux
const React = require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
const { store } = require("devtools/client/webconsole/new-console-output/store");
const ConsoleOutput = React.createFactory(require("devtools/client/webconsole/new-console-output/components/console-output"));
function NewConsoleOutputWrapper(parentNode, jsterm) {
let childComponent = ConsoleOutput({ jsterm });
let provider = React.createElement(
Provider, { store: store }, childComponent);
this.body = ReactDOM.render(provider, parentNode);
}
NewConsoleOutputWrapper.prototype = {
dispatchMessageAdd: (message) => {
store.dispatch(actions.messageAdd(message));
},
dispatchMessagesClear: () => {
store.dispatch(actions.messagesClear());
}
};
// Exports from this module
module.exports = NewConsoleOutputWrapper;

View File

@ -0,0 +1,12 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { messages } = require("./messages");
exports.reducers = {
messages
};

View File

@ -0,0 +1,29 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const constants = require("devtools/client/webconsole/new-console-output/constants");
function messages(state = [], action) {
switch (action.type) {
case constants.MESSAGE_ADD:
let newMessage = action.message;
if (newMessage.allowRepeating && state.length > 0) {
let lastMessage = state[state.length - 1];
if (lastMessage.repeatId === newMessage.repeatId) {
newMessage.repeat = lastMessage.repeat + 1;
return state.slice(0, state.length - 1).concat(newMessage);
}
}
return state.concat([ newMessage ]);
case constants.MESSAGES_CLEAR:
return [];
}
return state;
}
exports.messages = messages;

View File

@ -0,0 +1,9 @@
# 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/.
DevToolsModules(
'index.js',
'messages.js',
)

View File

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { combineReducers, createStore } = require("devtools/client/shared/vendor/redux");
const { reducers } = require("./reducers/index");
function storeFactory(initialState = {}) {
return createStore(combineReducers(reducers), initialState);
}
// Provide the single store instance for app code.
module.exports.store = storeFactory();
// Provide the store factory for test code so that each test is working with
// its own instance.
module.exports.storeFactory = storeFactory;

View File

@ -0,0 +1,3 @@
{
"extends": ["../../../../.eslintrc.xpcshell"]
}

View File

@ -0,0 +1,38 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var { utils: Cu } = Components;
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
DevToolsUtils.testing = true;
DevToolsUtils.dumpn.wantLogging = true;
DevToolsUtils.dumpv.wantVerbose = false;
// @TODO consolidate once we have a shared head. See #16
const testPackets = new Map();
testPackets.set("console.log", {
"from": "server1.conn4.child1/consoleActor2",
"type": "consoleAPICall",
"message": {
"arguments": [
"foobar",
"test"
],
"columnNumber": 1,
"counter": null,
"filename": "file:///test.html",
"functionName": "",
"groupName": "",
"level": "log",
"lineNumber": 1,
"private": false,
"styles": [],
"timeStamp": 1455064271115,
"timer": null,
"workerType": "none",
"category": "webdev"
}
});

View File

@ -0,0 +1,37 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const {
messageAdd,
messagesClear
} = require("devtools/client/webconsole/new-console-output/actions/messages");
const {
prepareMessage
} = require("devtools/client/webconsole/new-console-output/utils/messages");
const constants = require("devtools/client/webconsole/new-console-output/constants");
function run_test() {
run_next_test();
}
add_task(function*() {
const packet = testPackets.get("console.log");
const action = messageAdd(packet);
const expected = {
type: constants.MESSAGE_ADD,
// Prepare message is tested independently.
message: prepareMessage(packet)
};
deepEqual(action, expected,
"messageAdd action creator returns expected action object");
});
add_task(function*() {
const action = messagesClear();
const expected = {
type: constants.MESSAGES_CLEAR,
};
deepEqual(action, expected,
"messagesClear action creator returns expected action object");
});

View File

@ -0,0 +1,7 @@
[DEFAULT]
tags = devtools devtools-webconsole
head = head.js
tail =
firefox-appdir = browser
[test_messages.js]

View File

@ -0,0 +1,13 @@
[DEFAULT]
support-files =
head.js
[test_console-api-call.html]
[test_console-api-call_repeat.html]
[test_date-preview.html]
[test_evaluation-result.html]
[test_message-icon.html]
[test_message-container.html]
[test_message-repeat.html]
[test_page-error.html]

View File

@ -0,0 +1,263 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* exported getPacket, renderComponent, shallowRenderComponent,
cleanActualHTML, cleanExpectedHTML */
"use strict";
var { utils: Cu } = Components;
Cu.import("resource://testing-common/Assert.jsm");
Cu.import("resource://gre/modules/Task.jsm");
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
var {DebuggerServer} = require("devtools/server/main");
var {DebuggerClient} = require("devtools/shared/client/main");
const Services = require("Services");
DevToolsUtils.testing = true;
var { require: browserRequire } = BrowserLoader({
baseURI: "resource://devtools/client/webconsole/",
window: this
});
let ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
let React = browserRequire("devtools/client/shared/vendor/react");
var TestUtils = React.addons.TestUtils;
let testCommands = new Map();
testCommands.set("console.log()", {
command: "console.log('foobar', 'test')",
commandType: "consoleAPICall",
expectedText: "foobar test"
});
testCommands.set("new Date()", {
command: "new Date(448156800000)",
commandType: "evaluationResult",
expectedText: "Date 1984-03-15T00:00:00.000Z"
});
testCommands.set("pageError", {
command: null,
commandType: "pageError",
expectedText: "ReferenceError: asdf is not defined"
});
function* getPacket(command, type = "evaluationResult") {
try {
// Attach the console to the tab.
let state = yield new Promise(function(resolve) {
attachConsoleToTab(["ConsoleAPI"], resolve);
});
// Run the command and get the packet.
let packet;
switch (type) {
case "consoleAPICall":
packet = yield new Promise((resolve) => {
function onConsoleApiCall(apiCallType, apiCallPacket) {
state.dbgClient.removeListener("consoleAPICall", onConsoleApiCall);
resolve(apiCallPacket);
}
state.dbgClient.addListener("consoleAPICall", onConsoleApiCall);
state.client.evaluateJS(`top.${command}`);
});
break;
case "evaluationResult":
packet = yield new Promise(resolve => {
state.client.evaluateJS(command, resolve);
});
break;
case "pageError":
// @TODO: get packet with RDP
packet = {
"from": "server1.conn1.child1/consoleActor2",
"type": "pageError",
"pageError": {
"errorMessage": "ReferenceError: asdf is not defined",
"sourceName": "data:text/html,<script>asdf</script>",
"lineText": "",
"lineNumber": 1,
"columnNumber": 1,
"category": "content javascript",
"timeStamp": 1455735574091,
"warning": false,
"error": false,
"exception": true,
"strict": false,
"info": false,
"private": false,
"stacktrace": [{
"columnNumber": 68,
"filename": "test.html",
"functionName": "baz",
"language": 2,
"lineNumber": 1
}, {
"columnNumber": 43,
"filename": "test.html",
"functionName": "bar",
"language": 2,
"lineNumber": 2
}, {
"columnNumber": 18,
"filename": "test.html",
"functionName": "foo",
"language": 2,
"lineNumber": 3
}, {
"columnNumber": 150,
"filename": "test.html",
"functionName": "",
"language": 2,
"lineNumber": 4
}]
}
};
break;
}
closeDebugger(state);
return packet;
} catch (e) {
ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e));
}
}
function renderComponent(component, props) {
const el = React.createElement(component, props, {});
// By default, renderIntoDocument() won't work for stateless components, but
// it will work if the stateless component is wrapped in a stateful one.
// See https://github.com/facebook/react/issues/4839
const wrappedEl = React.DOM.span({}, [el]);
const renderedComponent = TestUtils.renderIntoDocument(wrappedEl);
return ReactDOM.findDOMNode(renderedComponent).children[0];
}
function shallowRenderComponent(component, props) {
const el = React.createElement(component, props);
const renderer = TestUtils.createRenderer();
renderer.render(el, {});
return renderer.getRenderOutput();
}
function cleanActualHTML(htmlString) {
return htmlString.replace(/ data-reactid=\".*?\"/g, "");
}
function cleanExpectedHTML(htmlString) {
return htmlString.replace(/(?:\r\n|\r|\n)\s*/g, "");
}
// Helpers copied in from shared/webconsole/test/common.js
function initCommon()
{
//Services.prefs.setBoolPref("devtools.debugger.log", true);
}
function initDebuggerServer()
{
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
DebuggerServer.allowChromeProcess = true;
}
function connectToDebugger(aCallback)
{
initCommon();
initDebuggerServer();
let transport = DebuggerServer.connectPipe();
let client = new DebuggerClient(transport);
let dbgState = { dbgClient: client };
client.connect().then(response => aCallback(dbgState, response));
}
function closeDebugger(aState, aCallback)
{
aState.dbgClient.close(aCallback);
aState.dbgClient = null;
aState.client = null;
}
function attachConsole(aListeners, aCallback) {
_attachConsole(aListeners, aCallback);
}
function attachConsoleToTab(aListeners, aCallback) {
_attachConsole(aListeners, aCallback, true);
}
function attachConsoleToWorker(aListeners, aCallback) {
_attachConsole(aListeners, aCallback, true, true);
}
function _attachConsole(aListeners, aCallback, aAttachToTab, aAttachToWorker)
{
function _onAttachConsole(aState, aResponse, aWebConsoleClient)
{
if (aResponse.error) {
Cu.reportError("attachConsole failed: " + aResponse.error + " " +
aResponse.message);
}
aState.client = aWebConsoleClient;
aCallback(aState, aResponse);
}
connectToDebugger(function _onConnect(aState, aResponse) {
if (aResponse.error) {
Cu.reportError("client.connect() failed: " + aResponse.error + " " +
aResponse.message);
aCallback(aState, aResponse);
return;
}
if (aAttachToTab) {
aState.dbgClient.listTabs(function _onListTabs(aResponse) {
if (aResponse.error) {
Cu.reportError("listTabs failed: " + aResponse.error + " " +
aResponse.message);
aCallback(aState, aResponse);
return;
}
let tab = aResponse.tabs[aResponse.selected];
aState.dbgClient.attachTab(tab.actor, function (response, tabClient) {
if (aAttachToWorker) {
var worker = new Worker("console-test-worker.js");
worker.addEventListener("message", function listener() {
worker.removeEventListener("message", listener);
tabClient.listWorkers(function (response) {
tabClient.attachWorker(response.workers[0].actor, function (response, workerClient) {
workerClient.attachThread({}, function(aResponse) {
aState.actor = workerClient.consoleActor;
aState.dbgClient.attachConsole(workerClient.consoleActor, aListeners,
_onAttachConsole.bind(null, aState));
});
});
});
});
} else {
aState.actor = tab.consoleActor;
aState.dbgClient.attachConsole(tab.consoleActor, aListeners,
_onAttachConsole.bind(null, aState));
}
});
});
} else {
aState.dbgClient.getProcess().then(response => {
aState.dbgClient.attachTab(response.form.actor, function () {
let consoleActor = response.form.consoleActor;
aState.actor = consoleActor;
aState.dbgClient.attachConsole(consoleActor, aListeners,
_onAttachConsole.bind(null, aState));
});
});
}
});
}

View File

@ -0,0 +1,33 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for ConsoleApiCall component</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for ConsoleApiCall component</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
const packet = yield getPacket("console.log('foobar', 'test')", "consoleAPICall");
const message = prepareMessage(packet);
const rendered = renderComponent(ConsoleApiCall, {message});
const queryPath = "div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace";
const messageBody = rendered.querySelectorAll(queryPath);
const consoleStringNodes = messageBody[0].querySelectorAll("span.console-string");
is(consoleStringNodes.length, 2, "ConsoleApiCall outputs expected HTML structure");
is(messageBody[0].textContent, "foobar test", "ConsoleApiCall outputs expected text");
SimpleTest.finish()
});
</script>
</body>
</html>

View File

@ -0,0 +1,36 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for ConsoleApiCall component with repeats</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for ConsoleApiCall component with repeats</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
const packet = yield getPacket("console.log('foobar', 'test')", "consoleAPICall");
const message = prepareMessage(packet);
message.repeat = 107;
const rendered = renderComponent(ConsoleApiCall, {message});
const messageBodyPath = "span > span.message-flex-body > span.message-body.devtools-monospace";
const messageBody = rendered.querySelectorAll(messageBodyPath);
is(messageBody[0].textContent, "foobar test", "ConsoleApiCall outputs expected text for repeated message");
const repeatPath = "span > span.message-flex-body > span.message-body.devtools-monospace + span.message-repeats";
const repeat = rendered.querySelectorAll(repeatPath);
is(repeat[0].textContent, `${message.repeat}`, "ConsoleApiCall outputs correct repeat count");
SimpleTest.finish()
});
</script>
</body>
</html>

View File

@ -0,0 +1,41 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for DatePreview component</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for DatePreview component</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
const { DatePreview } = require("devtools/client/webconsole/new-console-output/components/message-types/date-preview");
const testCommand = testCommands.get("new Date()");
const packet = yield getPacket(testCommand.command, testCommand.commandType);
const message = prepareMessage(packet);
const props = {
data: message.data,
severity: message.severity,
category: message.category,
};
const rendered = renderComponent(DatePreview, props);
const queryPathBase = "div.message.cm-s-mozilla span.message-body-wrapper.message-body.devtools-monospace span span.class-Date";
const preview = rendered.querySelectorAll(queryPathBase);
is(preview[0].textContent, testCommand.expectedText, "DatePreview outputs expected text");
const link = rendered.querySelectorAll(`${queryPathBase} a[draggable=false][href="#"].cm-variable`);
is(link.length, 1, "DatePreview outputs the variables view link");
SimpleTest.finish()
});
</script>
</body>
</html>

View File

@ -0,0 +1,70 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for EvaluationResult component</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for EvaluationResult component</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
const {
EvaluationResult,
getPreviewComponent
} = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
yield testFullRender();
yield testGetPreviewComponent();
SimpleTest.finish()
/**
* Test that passing in a message correctly wires up all the children.
*
* The different combinations of children are tested in separate per-component
* tests. This test just ensures that this component pipes data to its children.
*/
function testFullRender() {
const testValue = testCommands.get("new Date()");
const packet = yield getPacket(testValue.command, testValue.commandType);
const message = prepareMessage(packet);
const props = {
message
};
const rendered = renderComponent(EvaluationResult, props);
ok(rendered.textContent.includes(testValue.expectedText),
"EvaluationResult pipes data to its children as expected");
}
/**
* Test that getPreviewComponent() returns correct component for each object type.
*/
function testGetPreviewComponent() {
const testValues = [
{
commandObj: testCommands.get("new Date()"),
expectedComponent: require("devtools/client/webconsole/new-console-output/components/message-types/date-preview").DatePreview
}
];
for (let testValue of testValues) {
const { commandObj, expectedComponent } = testValue;
const packet = yield getPacket(commandObj.command, commandObj.commandType);
const message = prepareMessage(packet);
const rendered = shallowRenderComponent(EvaluationResult, {message});
is(rendered.type, expectedComponent,
`EvaluationResult nests ${expectedComponent} based on command: ${commandObj.command}`);
}
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,78 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for MessageContainer component</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for MessageContainer component</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
const { MessageContainer } = require("devtools/client/webconsole/new-console-output/components/message-container");
const { ConsoleApiCall } = require("devtools/client/webconsole/new-console-output/components/message-types/console-api-call");
const { EvaluationResult } = require("devtools/client/webconsole/new-console-output/components/message-types/evaluation-result");
const { PageError } = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
yield testFullRender();
yield testGetMessageComponent();
SimpleTest.finish();
/**
* Test that passing in a message correctly wires up all the children.
*
* The different combinations of children are tested in separate per-component
* tests. This test just ensures that this component pipes data to its children.
*/
function testFullRender() {
const testValue = testCommands.get("console.log()");
const packet = yield getPacket(testValue.command, testValue.commandType);
const message = prepareMessage(packet);
const props = {
message
};
const rendered = renderComponent(MessageContainer, props);
ok(rendered.textContent.includes(testValue.expectedText),
"MessageContainer pipes data to its children as expected");
}
/**
* Test that getMessageComponent() returns correct component for each message type.
*/
function testGetMessageComponent() {
const testValues = [
{
commandObj: testCommands.get("console.log()"),
expectedComponent: ConsoleApiCall
},
{
commandObj: testCommands.get("new Date()"),
expectedComponent: EvaluationResult
},
{
commandObj: testCommands.get("pageError"),
expectedComponent: PageError
}
];
for (let testValue of testValues) {
const { commandObj, expectedComponent } = testValue;
const packet = yield getPacket(commandObj.command, commandObj.commandType);
const message = prepareMessage(packet);
const rendered = shallowRenderComponent(MessageContainer, {message});
is(rendered.type, expectedComponent,
`MessageContainer nests ${expectedComponent} based on command: ${commandObj.command}`);
}
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for MessageRepeat component</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for MessageIcon component</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const {
SEVERITY_CLASS_FRAGMENTS,
SEVERITY_ERROR,
} = require("devtools/client/webconsole/new-console-output/constants");
const { MessageIcon } = require("devtools/client/webconsole/new-console-output/components/message-icon");
let severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_ERROR];
const iconRendered = renderComponent(MessageIcon, { severity });
ok(iconRendered.classList.contains("icon"), "MessageIcon has expected class");
is(iconRendered.getAttribute("title"), "Error",
"MessageIcon shows correct title attribute");
SimpleTest.finish();
});
</script>
</body>
</html>

View File

@ -0,0 +1,31 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for MessageRepeat component</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for MessageRepeat component</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const { MessageRepeat } = require("devtools/client/webconsole/new-console-output/components/message-repeat");
const repeatRendered = renderComponent(MessageRepeat, { repeat: 99 });
ok(repeatRendered.classList.contains("message-repeats"), "MessageRepeat has expected class");
is(repeatRendered.style.visibility, "visible", "MessageRepeat with 2+ repeats is visible");
is(repeatRendered.textContent, "99", "MessageRepeat shows correct number of repeats");
const noRepeatRendered = renderComponent(MessageRepeat, { repeat: 1 });
is(noRepeatRendered.style.visibility, "hidden", "MessageRepeat with 1 repeat is hidden");
is(noRepeatRendered.textContent, "1", "MessageRepeat with 1 repeat shows correct number of repeats")
SimpleTest.finish();
});
</script>
</body>
</html>

View File

@ -0,0 +1,32 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for PageError component</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for PageError component</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const { prepareMessage } = require("devtools/client/webconsole/new-console-output/utils/messages");
const { PageError } = require("devtools/client/webconsole/new-console-output/components/message-types/page-error");
const packet = yield getPacket(null, "pageError");
const message = prepareMessage(packet);
const rendered = renderComponent(PageError, {message});
const queryPath = "div.message.cm-s-mozilla span span.message-flex-body span.message-body.devtools-monospace";
const messageBody = rendered.querySelectorAll(queryPath);
is(messageBody.length, 1, "PageError outputs expected HTML structure");
is(messageBody[0].textContent, testCommands.get("pageError").expectedText, "PageError outputs expected text");
SimpleTest.finish()
});
</script>
</body>
</html>

View File

@ -0,0 +1,41 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* exported storeFactory */
"use strict";
var { utils: Cu } = Components;
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
var DevToolsUtils = require("devtools/shared/DevToolsUtils");
DevToolsUtils.testing = true;
DevToolsUtils.dumpn.wantLogging = true;
DevToolsUtils.dumpv.wantVerbose = false;
const { storeFactory } = require("devtools/client/webconsole/new-console-output/store");
const testPackets = new Map();
testPackets.set("console.log", {
"from": "server1.conn4.child1/consoleActor2",
"type": "consoleAPICall",
"message": {
"arguments": [
"foobar",
"test"
],
"columnNumber": 1,
"counter": null,
"filename": "file:///test.html",
"functionName": "",
"groupName": "",
"level": "log",
"lineNumber": 1,
"private": false,
"styles": [],
"timeStamp": 1455064271115,
"timer": null,
"workerType": "none",
"category": "webdev"
}
});

View File

@ -0,0 +1,59 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const actions = require("devtools/client/webconsole/new-console-output/actions/messages");
const packet = testPackets.get("console.log");
const {
getRepeatId,
prepareMessage
} = require("devtools/client/webconsole/new-console-output/utils/messages");
function run_test() {
run_next_test();
}
/**
* Test adding a message to the store.
*/
add_task(function*() {
const { getState, dispatch } = storeFactory();
dispatch(actions.messageAdd(packet));
const expectedMessage = prepareMessage(packet);
deepEqual(getState().messages, [expectedMessage],
"MESSAGE_ADD action adds a message");
});
/**
* Test repeating messages in the store.
*/
add_task(function*() {
const { getState, dispatch } = storeFactory();
dispatch(actions.messageAdd(packet));
dispatch(actions.messageAdd(packet));
dispatch(actions.messageAdd(packet));
const expectedMessage = prepareMessage(packet);
expectedMessage.repeat = 3;
deepEqual(getState().messages, [expectedMessage],
"Adding same message to the store twice results in repeated message");
});
/**
* Test getRepeatId().
*/
add_task(function*() {
const message1 = prepareMessage(packet);
const message2 = prepareMessage(packet);
equal(getRepeatId(message1), getRepeatId(message2),
"getRepeatId() returns same repeat id for objects with the same values");
message2.data.arguments = ["new args"];
notEqual(getRepeatId(message1), getRepeatId(message2),
"getRepeatId() returns different repeat ids for different values");
});

View File

@ -0,0 +1,7 @@
[DEFAULT]
tags = devtools devtools-webconsole
head = head.js
tail =
firefox-appdir = browser
[test_messages.js]

View File

@ -0,0 +1,6 @@
[DEFAULT]
support-files =
../components/head.js
[test_getRepeatId.html]

View File

@ -0,0 +1,108 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf8">
<title>Test for getRepeatId()</title>
<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<!-- Any copyright is dedicated to the Public Domain.
- http://creativecommons.org/publicdomain/zero/1.0/ -->
</head>
<body>
<p>Test for getRepeatId()</p>
<script type="text/javascript;version=1.8">
window.onload = Task.async(function* () {
const {
prepareMessage,
getRepeatId
} = require("devtools/client/webconsole/new-console-output/utils/messages");
yield testDuplicateValues();
yield testDifferentValues();
yield testDifferentSeverities();
yield testFalsyValues();
yield testConsoleVsJSTerm();
SimpleTest.finish();
function testDuplicateValues() {
const packet1 = yield getPacket("console.log('same')", "consoleAPICall");
const packet2 = yield getPacket("console.log('same')", "consoleAPICall");
const message1 = prepareMessage(packet1);
const message2 = prepareMessage(packet2);
is(getRepeatId(message1.data), getRepeatId(message2.data),
"getRepeatId() returns same repeat id for objects with the same values");
}
function testDifferentValues() {
const packet1 = yield getPacket("console.log('same')", "consoleAPICall");
const packet2 = yield getPacket("console.log('diff')", "consoleAPICall");
const message1 = prepareMessage(packet1);
const message2 = prepareMessage(packet2);
isnot(getRepeatId(message1.data), getRepeatId(message2.data),
"getRepeatId() returns different repeat ids for different values");
}
function testDifferentSeverities() {
const packet1 = yield getPacket("console.log('test')", "consoleAPICall");
const packet2 = yield getPacket("console.warn('test')", "consoleAPICall");
const message1 = prepareMessage(packet1);
const message2 = prepareMessage(packet2);
isnot(getRepeatId(message1.data), getRepeatId(message2.data),
"getRepeatId() returns different repeat ids for different severities");
}
function testFalsyValues() {
const packetNaN = yield getPacket("console.log(NaN)", "consoleAPICall");
const packetUnd = yield getPacket("console.log(undefined)", "consoleAPICall");
const packetNul = yield getPacket("console.log(null)", "consoleAPICall");
const messageNaN = prepareMessage(packetNaN);
const messageUnd = prepareMessage(packetUnd);
const messageNul = prepareMessage(packetNul);
const repeatIds = new Set([
getRepeatId(messageNaN.data),
getRepeatId(messageUnd.data),
getRepeatId(messageNul.data)]
);
is(repeatIds.size, 3,
"getRepeatId() handles falsy values distinctly");
const packetNaN2 = yield getPacket("console.log(NaN)", "consoleAPICall");
const packetUnd2 = yield getPacket("console.log(undefined)", "consoleAPICall");
const packetNul2 = yield getPacket("console.log(null)", "consoleAPICall");
const messageNaN2 = prepareMessage(packetNaN2);
const messageUnd2 = prepareMessage(packetUnd2);
const messageNul2 = prepareMessage(packetNul2);
is(getRepeatId(messageNaN.data), getRepeatId(messageNaN2.data),
"getRepeatId() handles NaN values");
is(getRepeatId(messageUnd.data), getRepeatId(messageUnd2.data),
"getRepeatId() handles undefined values");
is(getRepeatId(messageNul.data), getRepeatId(messageNul2.data),
"getRepeatId() handles null values");
}
function testConsoleVsJSTerm() {
const packet1 = yield getPacket("console.log(undefined)", "consoleAPICall");
const packet2 = yield getPacket("undefined");
const message1 = prepareMessage(packet1);
const message2 = prepareMessage(packet2);
isnot(getRepeatId(message1.data), getRepeatId(message2.data),
"getRepeatId() returns different repeat ids for console vs JSTerm");
}
});
</script>
</body>
</html>

View File

@ -0,0 +1,93 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
CATEGORY_CLASS_FRAGMENTS,
CATEGORY_JS,
CATEGORY_WEBDEV,
CATEGORY_OUTPUT,
LEVELS,
SEVERITY_CLASS_FRAGMENTS,
SEVERITY_ERROR,
SEVERITY_WARNING,
SEVERITY_LOG,
} = require("../constants");
const WebConsoleUtils = require("devtools/shared/webconsole/utils").Utils;
const STRINGS_URI = "chrome://devtools/locale/webconsole.properties";
const l10n = new WebConsoleUtils.L10n(STRINGS_URI);
function prepareMessage(packet) {
// @TODO turn this into an Immutable Record.
let allowRepeating;
let category;
let data;
let messageType;
let repeat;
let repeatId;
let severity;
switch (packet.type) {
case "consoleAPICall":
data = Object.assign({}, packet.message);
allowRepeating = true;
category = CATEGORY_CLASS_FRAGMENTS[CATEGORY_WEBDEV];
messageType = "ConsoleApiCall";
repeat = 1;
repeatId = getRepeatId(data);
severity = SEVERITY_CLASS_FRAGMENTS[LEVELS[data.level]];
break;
case "pageError":
data = Object.assign({}, packet.pageError);
allowRepeating = true;
category = CATEGORY_CLASS_FRAGMENTS[CATEGORY_JS];
messageType = "PageError";
repeat = 1;
repeatId = getRepeatId(data);
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_ERROR];
if (data.warning || data.strict) {
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_WARNING];
} else if (data.info) {
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_LOG];
}
break;
case "evaluationResult":
default:
data = Object.assign({}, packet.result);
allowRepeating = true;
category = CATEGORY_CLASS_FRAGMENTS[CATEGORY_OUTPUT];
messageType = "EvaluationResult";
repeat = 1;
repeatId = getRepeatId(data);
severity = SEVERITY_CLASS_FRAGMENTS[SEVERITY_LOG];
break;
}
return {
allowRepeating,
category,
data,
messageType,
repeat,
repeatId,
severity
};
}
function getRepeatId(message) {
let clonedMessage = JSON.parse(JSON.stringify(message));
delete clonedMessage.timeStamp;
delete clonedMessage.uniqueID;
return JSON.stringify(clonedMessage);
}
exports.prepareMessage = prepareMessage;
// Export for use in testing.
exports.getRepeatId = getRepeatId;
exports.l10n = l10n;

View File

@ -0,0 +1,9 @@
# 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/.
DevToolsModules(
'messages.js',
'variables-view.js',
)

View File

@ -0,0 +1,17 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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/. */
/* global window */
"use strict";
/**
* @TODO Remove this.
*
* Once JSTerm is also written in React/Redux, these will be actions.
*/
exports.openVariablesView = (objectActor) => {
window.jsterm.openVariablesView({objectActor});
};

View File

@ -198,6 +198,7 @@ const MIN_FONT_SIZE = 10;
const PREF_CONNECTION_TIMEOUT = "devtools.debugger.remote-timeout";
const PREF_PERSISTLOG = "devtools.webconsole.persistlog";
const PREF_MESSAGE_TIMESTAMP = "devtools.webconsole.timestampMessages";
const PREF_NEW_FRONTEND_ENABLED = "devtools.webconsole.new-frontend-enabled";
/**
* A WebConsoleFrame instance is an interactive console initialized *per target*
@ -508,6 +509,8 @@ WebConsoleFrame.prototype = {
_initUI: function() {
this.document = this.window.document;
this.rootElement = this.document.documentElement;
this.NEW_CONSOLE_OUTPUT_ENABLED = !this.owner._browserConsole &&
Services.prefs.getBoolPref(PREF_NEW_FRONTEND_ENABLED);
this._initDefaultFilterPrefs();
@ -568,6 +571,22 @@ WebConsoleFrame.prototype = {
this.jsterm = new JSTerm(this);
this.jsterm.init();
if (this.NEW_CONSOLE_OUTPUT_ENABLED) {
// @TODO Remove this once JSTerm is handled with React/Redux.
this.window.jsterm = this.jsterm;
console.log("Entering experimental mode for console frontend");
// XXX: We should actually stop output from happening on old output
// panel, but for now let's just hide it.
this.experimentalOutputNode = this.outputNode.cloneNode();
this.outputNode.hidden = true;
this.outputNode.parentNode.appendChild(this.experimentalOutputNode);
// @TODO Once the toolbox has been converted to React, see if passing
// in JSTerm is still necessary.
this.newConsoleOutput = new this.window.NewConsoleOutput(this.experimentalOutputNode, this.jsterm);
console.log("Created newConsoleOutput", this.newConsoleOutput);
}
this.resize();
this.window.addEventListener("resize", this.resize, true);
this.jsterm.on("sidebar-opened", this.resize);
@ -3332,6 +3351,10 @@ WebConsoleConnectionProxy.prototype = {
*/
_onPageError: function(type, packet) {
if (this.webConsoleFrame && packet.from == this._consoleActor) {
if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet);
return;
}
this.webConsoleFrame.handlePageError(packet.pageError);
}
},
@ -3364,7 +3387,11 @@ WebConsoleConnectionProxy.prototype = {
*/
_onConsoleAPICall: function(type, packet) {
if (this.webConsoleFrame && packet.from == this._consoleActor) {
this.webConsoleFrame.handleConsoleAPICall(packet.message);
if (this.webConsoleFrame.NEW_CONSOLE_OUTPUT_ENABLED) {
this.webConsoleFrame.newConsoleOutput.dispatchMessageAdd(packet);
} else {
this.webConsoleFrame.handleConsoleAPICall(packet.message);
}
}
},

View File

@ -26,6 +26,8 @@
<script type="application/javascript;version=1.8"
src="chrome://devtools/content/shared/theme-switching.js"/>
<script type="application/javascript;version=1.8"
src="resource://devtools/client/webconsole/new-console-output/main.js"/>
<script type="text/javascript" src="chrome://global/content/globalOverlay.js"/>
<script type="text/javascript" src="resource://devtools/client/webconsole/net/main.js"/>
<script type="text/javascript"><![CDATA[

View File

@ -152,6 +152,9 @@ var PageStyleActor = protocol.ActorClass({
// Stores the association of DOM objects -> actors
this.refMap = new Map();
// Maps document elements to style elements, used to add new rules.
this.styleElements = new WeakMap();
this.onFrameUnload = this.onFrameUnload.bind(this);
events.on(this.inspector.tabActor, "will-navigate", this.onFrameUnload);
@ -169,7 +172,7 @@ var PageStyleActor = protocol.ActorClass({
this.walker = null;
this.refMap = null;
this.cssLogic = null;
this._styleElement = null;
this.styleElements = null;
for (let sheet of this._watchedSheets) {
sheet.off("style-applied", this._styleApplied);
@ -971,23 +974,26 @@ var PageStyleActor = protocol.ActorClass({
* On page navigation, tidy up remaining objects.
*/
onFrameUnload: function() {
this._styleElement = null;
this.styleElements = new WeakMap();
},
/**
* Helper function to addNewRule to construct a new style tag in the document.
* Helper function to addNewRule to get or create a style tag in the provided
* document.
*
* @param {Document} document
* The document in which the style element should be appended.
* @returns DOMElement of the style tag
*/
get styleElement() {
if (!this._styleElement) {
let document = this.inspector.window.document;
getStyleElement: function(document) {
if (!this.styleElements.has(document)) {
let style = document.createElementNS(XHTML_NS, "style");
style.setAttribute("type", "text/css");
document.documentElement.appendChild(style);
this._styleElement = style;
this.styleElements.set(document, style);
}
return this._styleElement;
return this.styleElements.get(document);
},
/**
@ -1016,7 +1022,7 @@ var PageStyleActor = protocol.ActorClass({
*/
addNewRule: method(Task.async(function* (node, pseudoClasses,
editAuthored = false) {
let style = this.styleElement;
let style = this.getStyleElement(node.rawNode.ownerDocument);
let sheet = style.sheet;
let cssRules = sheet.cssRules;
let rawNode = node.rawNode;

View File

@ -189,7 +189,12 @@
<issue id="UnlocalizedSms" severity="error" />
<issue id="UnusedNamespace" severity="error" />
<issue id="UnusedQuantity" severity="error" />
<issue id="UnusedResources" severity="error" />
<issue id="UnusedResources" severity="error">
<!-- The moz.build based build system leaves a .mkdir.done file lying around in the
preprocessed_resources res/raw folder. Lint reports it as unused. We should get
rid of the file eventually. See bug 1268948. -->
<ignore path="**/raw/.mkdir.done" />
</issue>
<issue id="Usability" severity="error" />
<issue id="UseCheckPermission" severity="error" />
<issue id="UseCompoundDrawables" severity="error" />

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<!-- Crash Reporter Colors -->
<color name="textbox_background">#FFF</color>
<color name="textbox_background_disabled">#DDD</color>
<color name="textbox_stroke">#000</color>
<color name="textbox_stroke_disabled">#666</color>
</resources>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
<!-- Crash Reporter Styles -->
<style name="CrashReporter" />
<style name="CrashReporter.EditText">
<item name="android:background">@drawable/textbox_bg</item>
<item name="android:padding">10dp</item>
<item name="android:textAppearance">@style/TextAppearance</item>
</style>
</resources>

View File

@ -11,7 +11,6 @@ import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import org.json.JSONArray;
import org.mozilla.gecko.adjust.AdjustHelperInterface;
@ -59,7 +58,8 @@ import org.mozilla.gecko.permissions.Permissions;
import org.mozilla.gecko.preferences.ClearOnShutdownPref;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
import org.mozilla.gecko.promotion.SimpleHelperUI;
import org.mozilla.gecko.promotion.BookmarkStateChangeDelegate;
import org.mozilla.gecko.promotion.ReaderViewBookmarkPromotion;
import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.prompts.PromptListItem;
import org.mozilla.gecko.reader.SavedReaderViewHelper;
@ -132,7 +132,6 @@ import android.support.design.widget.Snackbar;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.MenuItemCompat;
import android.text.TextUtils;
import android.util.AttributeSet;
@ -214,9 +213,12 @@ public class BrowserApp extends GeckoApp
// Request ID for startActivityForResult.
private static final int ACTIVITY_REQUEST_PREFERENCES = 1001;
private static final int ACTIVITY_REQUEST_TAB_QUEUE = 2001;
private static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
private static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
private static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
public static final int ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK = 3001;
public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS = 3002;
public static final int ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE = 3003;
public static final int ACTIVITY_REQUEST_TRIPLE_READERVIEW = 4001;
public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK = 4002;
public static final int ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE = 4003;
public static final String ACTION_VIEW_MULTIPLE = AppConstants.ANDROID_PACKAGE_NAME + ".action.VIEW_MULTIPLE";
@ -307,7 +309,9 @@ public class BrowserApp extends GeckoApp
private final List<BrowserAppDelegate> delegates = Collections.unmodifiableList(Arrays.asList(
(BrowserAppDelegate) new AddToHomeScreenPromotion(),
(BrowserAppDelegate) new ScreenshotDelegate()
(BrowserAppDelegate) new ScreenshotDelegate(),
(BrowserAppDelegate) new BookmarkStateChangeDelegate(),
(BrowserAppDelegate) new ReaderViewBookmarkPromotion()
));
@NonNull
@ -378,39 +382,6 @@ public class BrowserApp extends GeckoApp
case PAGE_SHOW:
tab.loadFavicon();
break;
case BOOKMARK_ADDED:
// We always show the special offline snackbar whenever we bookmark a reader page.
// It's possible that the page is already stored offline, however this is highly
// unlikely, and even so it is probably nicer to show the same offline notification
// every time we bookmark an about:reader page.
if (!AboutPages.isAboutReader(tab.getURL())) {
showBookmarkAddedSnackbar();
} else {
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(this);
final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false);
if (hasFirstReaderViewPromptBeenShownBefore) {
showReaderModeBookmarkAddedSnackbar();
} else {
SimpleHelperUI.show(this,
SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA,
ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK,
R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message,
R.drawable.helper_first_readerview_bookmark, R.string.helper_first_offline_bookmark_button,
ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS,
ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE);
GeckoSharedPrefs.forProfile(this)
.edit()
.putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
.apply();
}
}
break;
case BOOKMARK_REMOVED:
showBookmarkRemovedSnackbar();
break;
case UNSELECTED:
// We receive UNSELECTED immediately after the SELECTED listeners run
@ -466,49 +437,6 @@ public class BrowserApp extends GeckoApp
}
}
private void showBookmarkAddedSnackbar() {
// This flow is from the option menu which has check to see if a bookmark was already added.
// So, it is safe here to show the snackbar that bookmark_added without any checks.
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
@Override
public void onClick(View v) {
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options");
showBookmarkDialog();
}
};
SnackbarHelper.showSnackbarWithAction(this,
getResources().getString(R.string.bookmark_added),
Snackbar.LENGTH_LONG,
getResources().getString(R.string.bookmark_options),
callback);
}
private void showReaderModeBookmarkAddedSnackbar() {
final Drawable iconDownloaded = DrawableUtil.tintDrawable(getContext(), R.drawable.status_icon_readercache, Color.WHITE);
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
@Override
public void onClick(View v) {
openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(PanelType.BOOKMARKS));
}
};
SnackbarHelper.showSnackbarWithActionAndColors(this,
getResources().getString(R.string.reader_saved_offline),
Snackbar.LENGTH_LONG,
getResources().getString(R.string.reader_switch_to_bookmarks),
callback,
iconDownloaded,
ContextCompat.getColor(this, R.color.link_blue),
Color.WHITE);
}
private void showBookmarkRemovedSnackbar() {
SnackbarHelper.showSnackbar(this, getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (AndroidGamepadManager.handleKeyEvent(event)) {
@ -1086,7 +1014,7 @@ public class BrowserApp extends GeckoApp
}
EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
"Prompt:ShowTop");
"Prompt:ShowTop");
processTabQueue();
@ -1265,57 +1193,6 @@ public class BrowserApp extends GeckoApp
mBrowserToolbar.setOnKeyListener(this);
}
private void showBookmarkDialog() {
final Resources res = getResources();
final Tab tab = Tabs.getInstance().getSelectedTab();
final Prompt ps = new Prompt(this, new Prompt.PromptCallback() {
@Override
public void onPromptFinished(String result) {
int itemId = -1;
try {
itemId = new JSONObject(result).getInt("button");
} catch (JSONException ex) {
Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
}
if (tab == null) {
return;
}
if (itemId == 0) {
final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
TelemetryContract.Method.DIALOG, extrasId);
new EditBookmarkDialog(BrowserApp.this).show(tab.getURL());
} else if (itemId == 1) {
final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
TelemetryContract.Method.DIALOG, extrasId);
final String url = tab.getURL();
final String title = tab.getDisplayTitle();
if (url != null && title != null) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
GeckoAppShell.createShortcut(title, url);
}
});
}
}
}
});
final PromptListItem[] items = new PromptListItem[2];
items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
ps.show("", "", items, ListView.CHOICE_MODE_NONE);
}
private void setDynamicToolbarEnabled(boolean enabled) {
ThreadUtils.assertOnUiThread();
@ -2341,7 +2218,7 @@ public class BrowserApp extends GeckoApp
return true;
}
private void openUrlAndStopEditing(String url) {
public void openUrlAndStopEditing(String url) {
openUrlAndStopEditing(url, null, false);
}
@ -2693,15 +2570,11 @@ public class BrowserApp extends GeckoApp
TabQueueHelper.processTabQueuePromptResponse(resultCode, this);
break;
case ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK:
if (resultCode == ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) {
openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(PanelType.BOOKMARKS));
} else if (resultCode == ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) {
showReaderModeBookmarkAddedSnackbar();
}
break;
default:
for (final BrowserAppDelegate delegate : delegates) {
delegate.onActivityResult(this, requestCode, resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
}
@ -3170,12 +3043,10 @@ public class BrowserApp extends GeckoApp
// Action providers are available only ICS+.
if (Versions.feature14Plus) {
GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);
final GeckoMenuItem quickShare = (GeckoMenuItem) mMenu.findItem(R.id.quickshare);
GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this);
share.setActionProvider(provider);
quickShare.setActionProvider(provider);
}
return true;
@ -3272,7 +3143,6 @@ public class BrowserApp extends GeckoApp
final MenuItem back = aMenu.findItem(R.id.back);
final MenuItem forward = aMenu.findItem(R.id.forward);
final MenuItem share = aMenu.findItem(R.id.share);
final MenuItem quickShare = aMenu.findItem(R.id.quickshare);
final MenuItem bookmarksList = aMenu.findItem(R.id.bookmarks_list);
final MenuItem historyList = aMenu.findItem(R.id.history_list);
final MenuItem saveAsPDF = aMenu.findItem(R.id.save_as_pdf);
@ -3300,7 +3170,6 @@ public class BrowserApp extends GeckoApp
back.setEnabled(false);
forward.setEnabled(false);
share.setEnabled(false);
quickShare.setEnabled(false);
saveAsPDF.setEnabled(false);
print.setEnabled(false);
findInPage.setEnabled(false);
@ -3400,9 +3269,6 @@ public class BrowserApp extends GeckoApp
// Action providers are available only ICS+.
if (Versions.feature14Plus) {
quickShare.setVisible(shareVisible);
quickShare.setEnabled(shareEnabled);
// This provider also applies to the quick share menu item.
final GeckoActionProvider provider = ((GeckoMenuItem) share).getGeckoActionProvider();
if (provider != null) {

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko;
import android.content.Intent;
import android.os.Bundle;
import org.mozilla.gecko.tabs.TabsPanel;
@ -58,4 +59,13 @@ public abstract class BrowserAppDelegate {
* Called when the tabs tray is closed.
*/
public void onTabsTrayHidden(BrowserApp browserApp, TabsPanel tabsPanel) {}
/**
* Called when an activity started using startActivityForResult() returns.
*
* Delegates should only use request and result codes declared in BrowserApp itself (as opposed
* to declarations in the delegate), in order to avoid conflicts.
*/
public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {}
}

View File

@ -5,6 +5,30 @@
package org.mozilla.gecko;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.db.StubBrowserDB;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
import org.mozilla.gecko.mozglue.ContextUtils;
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
import org.mozilla.gecko.util.INIParser;
import org.mozilla.gecko.util.INISection;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
@ -20,31 +44,6 @@ import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.json.JSONException;
import org.json.JSONArray;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.db.StubBrowserDB;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.mozglue.ContextUtils;
import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
import org.mozilla.gecko.util.INIParser;
import org.mozilla.gecko.util.INISection;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.SharedPreferences;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
public final class GeckoProfile {
private static final String LOGTAG = "GeckoProfile";
@ -719,7 +718,7 @@ public final class GeckoProfile {
return getProfileCreationDateFromTimesFile();
} catch (final IOException e) {
Log.d(LOGTAG, "Unable to retrieve profile creation date from times.json. Getting from system...");
final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getPackageInstallTime(context);
final long packageInstallMillis = org.mozilla.gecko.util.ContextUtils.getCurrentPackageInfo(context).firstInstallTime;
try {
persistProfileCreationDateToTimesFile(packageInstallMillis);
} catch (final IOException ioEx) {

View File

@ -1,246 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import android.support.v4.content.ContextCompat;
import android.text.format.DateUtils;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.RemoteTab;
import org.mozilla.gecko.home.TwoLinePageRow;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
/**
* An adapter that populates group and child views with remote client and tab
* data maintained in a monolithic static array.
* <p>
* The group and child view resources are parameters to allow future
* specialization to home fragment styles.
*/
public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter {
/**
* If a device claims to have synced before this date, we will assume it has never synced.
*/
private static final Date EARLIEST_VALID_SYNCED_DATE;
static {
final Calendar c = GregorianCalendar.getInstance();
c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
EARLIEST_VALID_SYNCED_DATE = c.getTime();
}
protected final ArrayList<RemoteClient> clients;
private final boolean showGroupIndicator;
protected int groupLayoutId;
protected int childLayoutId;
public static class GroupViewHolder {
final TextView nameView;
final TextView lastModifiedView;
final ImageView deviceTypeView;
final ImageView deviceExpandedView;
public GroupViewHolder(View view) {
nameView = (TextView) view.findViewById(R.id.client);
lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
deviceExpandedView = (ImageView) view.findViewById(R.id.device_expanded);
}
}
/**
* Construct a new adapter.
* <p>
* It's fine to create with clients to be null, and then to use
* {@link RemoteTabsExpandableListAdapter#replaceClients(List)} to
* update this list of clients.
*
* @param groupLayoutId
* @param childLayoutId
* @param clients
* initial list of clients; can be null.
* @param showGroupIndicator
*/
public RemoteTabsExpandableListAdapter(int groupLayoutId, int childLayoutId, List<RemoteClient> clients, boolean showGroupIndicator) {
this.groupLayoutId = groupLayoutId;
this.childLayoutId = childLayoutId;
this.clients = new ArrayList<>();
if (clients != null) {
this.clients.addAll(clients);
}
this.showGroupIndicator = showGroupIndicator;
}
public void replaceClients(List<RemoteClient> clients) {
this.clients.clear();
if (clients != null) {
this.clients.addAll(clients);
this.notifyDataSetChanged();
} else {
this.notifyDataSetInvalidated();
}
}
@Override
public boolean hasStableIds() {
return false; // Client GUIDs are stable, but tab hashes are not.
}
@Override
public long getGroupId(int groupPosition) {
return clients.get(groupPosition).guid.hashCode();
}
@Override
public int getGroupCount() {
return clients.size();
}
@Override
public Object getGroup(int groupPosition) {
return clients.get(groupPosition);
}
@Override
public int getChildrenCount(int groupPosition) {
return clients.get(groupPosition).tabs.size();
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
final View view;
if (convertView != null) {
view = convertView;
} else {
final LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(groupLayoutId, parent, false);
final GroupViewHolder holder = new GroupViewHolder(view);
view.setTag(holder);
}
final RemoteClient client = clients.get(groupPosition);
updateClientsItemView(isExpanded, context, view, client);
return view;
}
public void updateClientsItemView(final boolean isExpanded, final Context context, final View view, final RemoteClient client) {
final GroupViewHolder holder = (GroupViewHolder) view.getTag();
// UI elements whose state depends on isExpanded, roughly from left to
// right: device type icon; client name text color; expanded state
// indicator.
final int deviceTypeResId;
final int textColorResId;
final int deviceExpandedResId;
if (isExpanded && !client.tabs.isEmpty()) {
deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop : R.drawable.sync_mobile;
textColorResId = R.color.placeholder_active_grey;
deviceExpandedResId = showGroupIndicator ? R.drawable.arrow_down : R.drawable.home_group_collapsed;
} else {
deviceTypeResId = "desktop".equals(client.deviceType) ? R.drawable.sync_desktop_inactive : R.drawable.sync_mobile_inactive;
textColorResId = R.color.tabs_tray_icon_grey;
deviceExpandedResId = showGroupIndicator ? R.drawable.home_group_collapsed : 0;
}
// Now update the UI.
holder.nameView.setText(client.name);
holder.nameView.setTextColor(ContextCompat.getColor(context, textColorResId));
final long now = System.currentTimeMillis();
// It's OK to access the DB on the main thread here, as we're just
// getting a string.
final GeckoProfile profile = GeckoProfile.get(context);
holder.lastModifiedView.setText(getLastSyncedString(context, now, client.lastModified));
// These views exists only in some of our group views: they are present
// for the home panel groups and not for the tabs panel groups.
// Therefore, we must handle null.
if (holder.deviceTypeView != null) {
holder.deviceTypeView.setImageResource(deviceTypeResId);
}
if (holder.deviceExpandedView != null) {
// If there are no tabs to display, don't show an indicator at all.
holder.deviceExpandedView.setImageResource(client.tabs.isEmpty() ? 0 : deviceExpandedResId);
}
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return clients.get(groupPosition).tabs.get(childPosition);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return clients.get(groupPosition).tabs.get(childPosition).hashCode();
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
final View view;
if (convertView != null) {
view = convertView;
} else {
final LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(childLayoutId, parent, false);
}
final RemoteClient client = clients.get(groupPosition);
final RemoteTab tab = client.tabs.get(childPosition);
// The view is a TwoLinePageRow only for some of our child views: it's
// present for the home panel children and not for the tabs panel
// children. Therefore, we must handle one case manually.
if (view instanceof TwoLinePageRow) {
((TwoLinePageRow) view).update(tab.title, tab.url);
} else {
final TextView titleView = (TextView) view.findViewById(R.id.title);
titleView.setText(TextUtils.isEmpty(tab.title) ? tab.url : tab.title);
final TextView urlView = (TextView) view.findViewById(R.id.url);
urlView.setText(tab.url);
}
return view;
}
/**
* Return a relative "Last synced" time span for the given tab record.
*
* @param now local time.
* @param time to format string for.
* @return string describing time span
*/
public static String getLastSyncedString(Context context, long now, long time) {
if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
return context.getString(R.string.remote_tabs_never_synced);
}
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
}
}

View File

@ -26,7 +26,7 @@ public class RemoteClient implements Parcelable {
this.name = name;
this.lastModified = lastModified;
this.deviceType = deviceType;
this.tabs = new ArrayList<RemoteTab>();
this.tabs = new ArrayList<>();
}
@Override
@ -62,4 +62,8 @@ public class RemoteClient implements Parcelable {
return new RemoteClient[size];
}
};
public boolean isDesktop() {
return "desktop".equals(deviceType);
}
}

View File

@ -4,17 +4,13 @@
package org.mozilla.gecko.db;
import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
import android.os.Parcel;
import android.os.Parcelable;
/**
* A thin representation of a remote tab.
* <p>
* We use the hash of the tab as the ID in
* {@link RemoteTabsExpandableListAdapter#getClientId(int)}, and therefore we
* must implement equality as well. These are generated functions.
* These are generated functions.
*/
public class RemoteTab implements Parcelable {
public final String title;

View File

@ -6,13 +6,13 @@
package org.mozilla.gecko.dlc;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import org.mozilla.gecko.dlc.catalog.DownloadContent;
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
import org.mozilla.gecko.util.ContextUtils;
/**
* Study: Scan the catalog for "new" content available for download.
@ -64,14 +64,10 @@ public class StudyAction extends BaseAction {
final String appVersionPattern = content.getAppVersionPattern();
if (!TextUtils.isEmpty(appVersionPattern)) {
try {
final String appVersion = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
if (!appVersion.matches(appVersionPattern)) {
Log.d(LOGTAG, String.format("App version (%s) does not match pattern: %s", appVersion, appVersionPattern));
return false;
}
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError("Should not happen: Can't get package info of own package");
final String appVersion = ContextUtils.getCurrentPackageInfo(context).versionName;
if (!appVersion.matches(appVersionPattern)) {
Log.d(LOGTAG, String.format("App version (%s) does not match pattern: %s", appVersion, appVersionPattern));
return false;
}
}

View File

@ -0,0 +1,323 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import android.content.Context;
import android.support.annotation.UiThread;
import android.support.v4.util.Pair;
import android.support.v7.widget.RecyclerView;
import android.text.format.DateUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.RemoteTab;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType.*;
public class ClientsAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
public static final String LOGTAG = "GeckoClientsAdapter";
/**
* If a device claims to have synced before this date, we will assume it has never synced.
*/
public static final Date EARLIEST_VALID_SYNCED_DATE;
static {
final Calendar c = GregorianCalendar.getInstance();
c.set(2000, Calendar.JANUARY, 1, 0, 0, 0);
EARLIEST_VALID_SYNCED_DATE = c.getTime();
}
List<Pair<String, Integer>> adapterList = new LinkedList<>();
// List of hidden remote clients.
// Only accessed from the UI thread.
protected final List<RemoteClient> hiddenClients = new ArrayList<>();
private Map<String, RemoteClient> visibleClients = new HashMap<>();
// Maintain group collapsed and hidden state. Only accessed from the UI thread.
protected static RemoteTabsExpandableListState sState;
private final Context context;
public ClientsAdapter(Context context) {
this.context = context;
// This races when multiple Fragments are created. That's okay: one
// will win, and thereafter, all will be okay. If we create and then
// drop an instance the shared SharedPreferences backing all the
// instances will maintain the state for us. Since everything happens on
// the UI thread, this doesn't even need to be volatile.
if (sState == null) {
sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
}
}
@Override
public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View view;
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
switch (itemType) {
case NAVIGATION_BACK:
view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
return new CombinedHistoryItem.HistoryItem(view);
case CLIENT:
view = inflater.inflate(R.layout.home_remote_tabs_group, parent, false);
return new CombinedHistoryItem.ClientItem(view);
case CHILD:
view = inflater.inflate(R.layout.home_item_row, parent, false);
return new CombinedHistoryItem.HistoryItem(view);
case HIDDEN_DEVICES:
view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, parent, false);
return new CombinedHistoryItem.BasicItem(view);
}
return null;
}
@Override
public void onBindViewHolder (CombinedHistoryItem holder, final int position){
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
switch (itemType) {
case CLIENT:
final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) holder;
final String clientGuid = adapterList.get(position).first;
final RemoteClient client = visibleClients.get(clientGuid);
clientItem.bind(context, client, sState.isClientCollapsed(clientGuid));
break;
case CHILD:
final Pair<String, Integer> pair = adapterList.get(position);
RemoteTab remoteTab = visibleClients.get(pair.first).tabs.get(pair.second);
((CombinedHistoryItem.HistoryItem) holder).bind(remoteTab);
break;
case HIDDEN_DEVICES:
final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
((TextView) holder.itemView).setText(hiddenDevicesLabel);
break;
}
}
@Override
public int getItemCount () {
return adapterList.size();
}
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
if (position == 0) {
return NAVIGATION_BACK;
}
final Pair<String, Integer> pair = adapterList.get(position);
if (pair == null) {
return HIDDEN_DEVICES;
} else if (pair.second == -1) {
return CLIENT;
} else {
return CHILD;
}
}
@Override
public int getItemViewType(int position) {
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
}
@UiThread
public void setClients(List<RemoteClient> clients) {
adapterList.clear();
adapterList.add(null);
hiddenClients.clear();
visibleClients.clear();
for (RemoteClient client : clients) {
final String guid = client.guid;
if (sState.isClientHidden(guid)) {
hiddenClients.add(client);
} else {
visibleClients.put(guid, client);
adapterList.addAll(getVisibleItems(client));
}
}
// Add item for unhiding clients.
if (!hiddenClients.isEmpty()) {
adapterList.add(null);
}
notifyDataSetChanged();
}
private static List<Pair<String, Integer>> getVisibleItems(RemoteClient client) {
List<Pair<String, Integer>> list = new LinkedList<>();
final String guid = client.guid;
list.add(new Pair<>(guid, -1));
if (!sState.isClientCollapsed(client.guid)) {
for (int i = 0; i < client.tabs.size(); i++) {
list.add(new Pair<>(guid, i));
}
}
return list;
}
public List<RemoteClient> getHiddenClients() {
return hiddenClients;
}
public void toggleClient(int position) {
final Pair<String, Integer> pair = adapterList.get(position);
if (pair.second != -1) {
return;
}
final String clientGuid = pair.first;
final RemoteClient client = visibleClients.get(clientGuid);
final boolean isCollapsed = sState.isClientCollapsed(clientGuid);
sState.setClientCollapsed(clientGuid, !isCollapsed);
notifyItemChanged(position);
if (isCollapsed) {
for (int i = client.tabs.size() - 1; i > -1; i--) {
// Insert child tabs at the index right after the client item that was clicked.
adapterList.add(position + 1, new Pair<>(clientGuid, i));
}
notifyItemRangeInserted(position + 1, client.tabs.size());
} else {
int i = client.tabs.size();
while (i > 0) {
adapterList.remove(position + 1);
i--;
}
notifyItemRangeRemoved(position + 1, client.tabs.size());
}
}
public void unhideClients(List<RemoteClient> selectedClients) {
final int numClients = selectedClients.size();
if (numClients == 0) {
return;
}
final int insertionIndex = adapterList.size() - 1;
int itemCount = numClients;
for (RemoteClient client : selectedClients) {
final String clientGuid = client.guid;
sState.setClientHidden(clientGuid, false);
hiddenClients.remove(client);
visibleClients.put(clientGuid, client);
sState.setClientCollapsed(clientGuid, false);
adapterList.addAll(adapterList.size() - 1, getVisibleItems(client));
itemCount += client.tabs.size();
}
notifyItemRangeInserted(insertionIndex, itemCount);
final int hiddenDevicesIndex = adapterList.size() - 1;
if (hiddenClients.isEmpty()) {
// No more hidden clients, remove "unhide" item.
adapterList.remove(hiddenDevicesIndex);
notifyItemRemoved(hiddenDevicesIndex);
} else {
// Update "hidden clients" item because number of hidden clients changed.
notifyItemChanged(hiddenDevicesIndex);
}
}
public void removeItem(int position) {
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
switch (itemType) {
case CLIENT:
final String clientGuid = adapterList.get(position).first;
final RemoteClient client = visibleClients.remove(clientGuid);
final boolean hadHiddenClients = !hiddenClients.isEmpty();
int removeCount = sState.isClientCollapsed(clientGuid) ? 1 : client.tabs.size() + 1;
int c = removeCount;
while (c > 0) {
adapterList.remove(position);
c--;
}
notifyItemRangeRemoved(position, removeCount);
sState.setClientHidden(clientGuid, true);
hiddenClients.add(client);
if (!hadHiddenClients) {
// Add item for unhiding clients;
adapterList.add(null);
notifyItemInserted(adapterList.size() - 1);
} else {
// Update "hidden clients" item because number of hidden clients changed.
notifyItemChanged(adapterList.size() - 1);
}
break;
}
}
@Override
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
HomeContextMenuInfo info;
final Pair<String, Integer> pair = adapterList.get(position);
switch (itemType) {
case CHILD:
info = new HomeContextMenuInfo(view, position, -1);
return populateChildInfoFromTab(info, visibleClients.get(pair.first).tabs.get(pair.second));
case CLIENT:
info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, visibleClients.get(pair.first));
return info;
}
return null;
}
protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) {
info.url = tab.url;
info.title = tab.title;
return info;
}
/**
* Return a relative "Last synced" time span for the given tab record.
*
* @param now local time.
* @param time to format string for.
* @return string describing time span
*/
public static String getLastSyncedString(Context context, long now, long time) {
if (new Date(time).before(EARLIEST_VALID_SYNCED_DATE)) {
return context.getString(R.string.remote_tabs_never_synced);
}
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
}
}

View File

@ -4,101 +4,49 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import android.content.res.Resources;
import android.support.v7.widget.RecyclerView;
import android.content.Context;
import android.database.Cursor;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.json.JSONArray;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.RemoteTab;
import org.mozilla.gecko.home.CombinedHistoryPanel.SectionHeader;
import java.util.Collections;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
private static final int SYNCED_DEVICES_SMARTFOLDER_INDEX = 0;
public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> {
private static final String LOGTAG = "GeckoCombinedHistAdapt";
// Array for the time ranges in milliseconds covered by each section.
static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length];
public enum ItemType {
CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD;
public static ItemType viewTypeToItemType(int viewType) {
if (viewType >= ItemType.values().length) {
Log.e(LOGTAG, "No corresponding ItemType!");
}
return ItemType.values()[viewType];
}
public static int itemTypeToViewType(ItemType itemType) {
return itemType.ordinal();
}
// Semantic names for the time covered by each section
public enum SectionHeader {
TODAY,
YESTERDAY,
WEEK,
THIS_MONTH,
MONTH_AGO,
TWO_MONTHS_AGO,
THREE_MONTHS_AGO,
FOUR_MONTHS_AGO,
FIVE_MONTHS_AGO,
OLDER_THAN_SIX_MONTHS
}
private List<RemoteClient> remoteClients = Collections.emptyList();
private List<RemoteTab> clientChildren;
private int remoteClientIndexOfParent = -1;
private Cursor historyCursor;
// Maintain group collapsed and hidden state. Only accessed from the UI thread.
protected static RemoteTabsExpandableListState sState;
// List of hidden remote clients.
// Only accessed from the UI thread.
protected final List<RemoteClient> hiddenClients = new ArrayList<>();
private DevicesUpdateHandler devicesUpdateHandler;
private int deviceCount = 0;
// We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
private final SparseArray<CombinedHistoryPanel.SectionHeader> sectionHeaders;
private final SparseArray<SectionHeader> sectionHeaders;
private final Context context;
public CombinedHistoryAdapter(Context context, int savedParentIndex) {
public CombinedHistoryAdapter(Resources resources) {
super();
this.context = context;
sectionHeaders = new SparseArray<>();
// This races when multiple Fragments are created. That's okay: one
// will win, and thereafter, all will be okay. If we create and then
// drop an instance the shared SharedPreferences backing all the
// instances will maintain the state for us. Since everything happens on
// the UI thread, this doesn't even need to be volatile.
if (sState == null) {
sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
}
remoteClientIndexOfParent = savedParentIndex;
}
public void setClients(List<RemoteClient> clients) {
hiddenClients.clear();
remoteClients.clear();
final Iterator<RemoteClient> it = clients.iterator();
while (it.hasNext()) {
final RemoteClient client = it.next();
if (sState.isClientHidden(client.guid)) {
hiddenClients.add(client);
it.remove();
}
}
remoteClients = clients;
// Add item for unhiding clients.
if (!hiddenClients.isEmpty()) {
remoteClients.add(null);
}
notifyItemRangeChanged(0, remoteClients.size());
HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray);
}
public void setHistory(Cursor history) {
@ -107,102 +55,70 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
notifyDataSetChanged();
}
public void removeItem(int position) {
final ItemType itemType = getItemTypeForPosition(position);
switch (itemType) {
case CLIENT:
final boolean hadHiddenClients = !hiddenClients.isEmpty();
final RemoteClient client = remoteClients.remove(transformAdapterPositionForDataStructure(ItemType.CLIENT, position));
notifyItemRemoved(position);
public interface DevicesUpdateHandler {
void onDeviceCountUpdated(int count);
}
sState.setClientHidden(client.guid, true);
hiddenClients.add(client);
if (!hadHiddenClients) {
// Add item for unhiding clients;
remoteClients.add(null);
} else {
// Update "hidden clients" item because number of hidden clients changed.
notifyItemChanged(remoteClients.size() - 1);
public DevicesUpdateHandler getDeviceUpdateHandler() {
if (devicesUpdateHandler == null) {
devicesUpdateHandler = new DevicesUpdateHandler() {
@Override
public void onDeviceCountUpdated(int count) {
deviceCount = count;
notifyItemChanged(0);
}
break;
};
}
return devicesUpdateHandler;
}
@Override
public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
final View view;
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
switch (itemType) {
case SYNCED_DEVICES:
view = inflater.inflate(R.layout.home_smartfolder, viewGroup, false);
return new CombinedHistoryItem.SmartFolder(view);
case SECTION_HEADER:
view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
return new CombinedHistoryItem.BasicItem(view);
case HISTORY:
notifyItemRemoved(position);
view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
return new CombinedHistoryItem.HistoryItem(view);
default:
throw new IllegalArgumentException("Unexpected Home Panel item type");
}
}
@Override
public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
switch (itemType) {
case SYNCED_DEVICES:
((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.cloud, R.string.home_synced_devices_smartfolder, R.string.home_synced_devices_number, deviceCount);
break;
case SECTION_HEADER:
((TextView) viewHolder.itemView).setText(getSectionHeaderTitle(sectionHeaders.get(localPosition)));
break;
case HISTORY:
if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
}
((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
break;
}
}
public void unhideClients(List<RemoteClient> selectedClients) {
if (selectedClients.size() == 0) {
return;
}
for (RemoteClient client : selectedClients) {
sState.setClientHidden(client.guid, false);
hiddenClients.remove(client);
}
final int insertIndex = remoteClients.size() - 1;
remoteClients.addAll(insertIndex, selectedClients);
notifyItemRangeInserted(insertIndex, selectedClients.size());
if (hiddenClients.isEmpty()) {
// No more hidden clients, remove "unhide" item.
remoteClients.remove(remoteClients.size() - 1);
} else {
// Update "hidden clients" item because number of hidden clients changed.
notifyItemChanged(remoteClients.size() - 1);
}
}
public List<RemoteClient> getHiddenClients() {
return hiddenClients;
}
public JSONArray getCurrentChildTabs() {
if (clientChildren != null) {
final JSONArray urls = new JSONArray();
for (int i = 1; i < clientChildren.size(); i++) {
urls.put(clientChildren.get(i).url);
}
return urls;
}
return null;
}
public int getParentIndex() {
return remoteClientIndexOfParent;
}
private boolean isInChildView() {
return remoteClientIndexOfParent != -1;
}
public void showChildView(int parentPosition) {
if (clientChildren == null) {
clientChildren = new ArrayList<>();
}
// Handle "back" view.
clientChildren.add(null);
remoteClientIndexOfParent = transformAdapterPositionForDataStructure(ItemType.CLIENT, parentPosition);
clientChildren.addAll(remoteClients.get(remoteClientIndexOfParent).tabs);
notifyDataSetChanged();
}
public boolean exitChildView() {
if (!isInChildView()) {
return false;
}
remoteClientIndexOfParent = -1;
clientChildren.clear();
notifyDataSetChanged();
return true;
}
private ItemType getItemTypeForPosition(int position) {
return ItemType.viewTypeToItemType(getItemViewType(position));
}
/**
* Transform an adapter position to the position for the data structure backing the item type.
*
@ -213,108 +129,36 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
* @param position position in the adapter
* @return position of the item in the data structure
*/
private int transformAdapterPositionForDataStructure(ItemType type, int position) {
if (type == ItemType.CLIENT) {
private int transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType type, int position) {
if (type == CombinedHistoryItem.ItemType.SECTION_HEADER) {
return position;
} else if (type == ItemType.SECTION_HEADER) {
return position - remoteClients.size();
} else if (type == ItemType.HISTORY) {
return position - remoteClients.size() - getHeadersBefore(position);
} else if (type == CombinedHistoryItem.ItemType.HISTORY){
return position - getHeadersBefore(position) - CombinedHistoryPanel.NUM_SMART_FOLDERS;
} else {
return position;
}
}
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
final ItemType itemType = getItemTypeForPosition(position);
HomeContextMenuInfo info;
switch (itemType) {
case CHILD:
info = new HomeContextMenuInfo(view, position, -1);
return CombinedHistoryPanel.populateChildInfoFromTab(info, clientChildren.get(position));
case HISTORY:
info = new HomeContextMenuInfo(view, position, -1);
historyCursor.moveToPosition(transformAdapterPositionForDataStructure(ItemType.HISTORY, position));
return CombinedHistoryPanel.populateHistoryInfoFromCursor(info, historyCursor);
case CLIENT:
final int clientPosition = transformAdapterPositionForDataStructure(ItemType.CLIENT, position);
info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position, -1, remoteClients.get(clientPosition));
return info;
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
if (position == SYNCED_DEVICES_SMARTFOLDER_INDEX) {
return CombinedHistoryItem.ItemType.SYNCED_DEVICES;
}
return null;
}
@Override
public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
final View view;
final ItemType itemType = ItemType.viewTypeToItemType(viewType);
switch (itemType) {
case CLIENT:
view = inflater.inflate(R.layout.home_remote_tabs_group, viewGroup, false);
return new CombinedHistoryItem.ClientItem(view);
case HIDDEN_DEVICES:
view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, viewGroup, false);
return new CombinedHistoryItem.BasicItem(view);
case NAVIGATION_BACK:
view = inflater.inflate(R.layout.home_combined_back_item, viewGroup, false);
return new CombinedHistoryItem.HistoryItem(view);
case SECTION_HEADER:
view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
return new CombinedHistoryItem.BasicItem(view);
case CHILD:
case HISTORY:
view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
return new CombinedHistoryItem.HistoryItem(view);
default:
throw new IllegalArgumentException("Unexpected Home Panel item type");
final int sectionPosition = transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.SECTION_HEADER, position);
if (sectionHeaders.get(sectionPosition) != null) {
return CombinedHistoryItem.ItemType.SECTION_HEADER;
}
return CombinedHistoryItem.ItemType.HISTORY;
}
@Override
public int getItemViewType(int position) {
if (isInChildView()) {
if (position == 0) {
return ItemType.itemTypeToViewType(ItemType.NAVIGATION_BACK);
}
return ItemType.itemTypeToViewType(ItemType.CHILD);
} else {
final int numClients = remoteClients.size();
if (position < numClients) {
if (!hiddenClients.isEmpty() && position == numClients - 1) {
return ItemType.itemTypeToViewType(ItemType.HIDDEN_DEVICES);
}
return ItemType.itemTypeToViewType(ItemType.CLIENT);
}
final int sectionPosition = transformAdapterPositionForDataStructure(ItemType.SECTION_HEADER, position);
if (sectionHeaders.get(sectionPosition) != null) {
return ItemType.itemTypeToViewType(ItemType.SECTION_HEADER);
}
return ItemType.itemTypeToViewType(ItemType.HISTORY);
}
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
}
@Override
public int getItemCount() {
if (isInChildView()) {
if (clientChildren == null) {
clientChildren = new ArrayList<>();
clientChildren.add(null);
clientChildren.addAll(remoteClients.get(remoteClientIndexOfParent).tabs);
}
return clientChildren.size();
} else {
final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
return remoteClients.size() + historySize + sectionHeaders.size();
}
final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
return historySize + sectionHeaders.size() + CombinedHistoryPanel.NUM_SMART_FOLDERS;
}
/**
@ -335,11 +179,11 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
do {
final int historyPosition = c.getPosition();
final long visitTime = c.getLong(c.getColumnIndexOrThrow(BrowserContract.History.DATE_LAST_VISITED));
final SectionHeader itemSection = CombinedHistoryPanel.getSectionFromTime(visitTime);
final SectionHeader itemSection = getSectionFromTime(visitTime);
if (section != itemSection) {
section = itemSection;
sparseArray.append(historyPosition + sparseArray.size(), section);
sparseArray.append(historyPosition + sparseArray.size() + CombinedHistoryPanel.NUM_SMART_FOLDERS, section);
}
if (section == SectionHeader.OLDER_THAN_SIX_MONTHS) {
@ -348,48 +192,18 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
} while (c.moveToNext());
}
public boolean containsHistory() {
if (historyCursor == null) {
return false;
}
return (historyCursor.getCount() > 0);
private static String getSectionHeaderTitle(SectionHeader section) {
return sectionDateRangeArray[section.ordinal()].displayName;
}
@Override
public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
final ItemType itemType = getItemTypeForPosition(position);
final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
switch (itemType) {
case CLIENT:
final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) viewHolder;
final RemoteClient client = remoteClients.get(localPosition);
clientItem.bind(client, context);
break;
case HIDDEN_DEVICES:
final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
((TextView) viewHolder.itemView).setText(hiddenDevicesLabel);
break;
case CHILD:
RemoteTab remoteTab = clientChildren.get(position);
((CombinedHistoryItem.HistoryItem) viewHolder).bind(remoteTab);
break;
case SECTION_HEADER:
((TextView) viewHolder.itemView).setText(CombinedHistoryPanel.getSectionHeaderTitle(sectionHeaders.get(localPosition)));
break;
case HISTORY:
if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
}
((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
break;
private static SectionHeader getSectionFromTime(long time) {
for (int i = 0; i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
if (time > sectionDateRangeArray[i].start) {
return SectionHeader.values()[i];
}
}
return SectionHeader.OLDER_THAN_SIX_MONTHS;
}
/**
@ -397,15 +211,42 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
* @param position position in the adapter
*/
private int getHeadersBefore(int position) {
final int adjustedPosition = position - remoteClients.size();
// Skip the first header case because there will always be a header.
for (int i = 1; i < sectionHeaders.size(); i++) {
// If the position of the header is greater than the history position,
// return the number of headers tested.
if (sectionHeaders.keyAt(i) > adjustedPosition) {
if (sectionHeaders.keyAt(i) > position) {
return i;
}
}
return sectionHeaders.size();
}
@Override
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
if (position == 0) {
// No context menu for smartfolders.
return null;
}
HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, -1);
historyCursor.moveToPosition(transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.HISTORY, position));
return populateHistoryInfoFromCursor(info, historyCursor);
}
protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) {
info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a
// bookmark, in which case the bookmarks ID column value will be null.
info.bookmarkId = -1;
} else {
info.bookmarkId = cursor.getInt(bookmarkIdCol);
}
return info;
}
}

View File

@ -8,25 +8,64 @@ import android.content.Context;
import android.database.Cursor;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.RemoteTab;
public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
private static final String LOGTAG = "CombinedHistoryItem";
public CombinedHistoryItem(View view) {
super(view);
}
public enum ItemType {
CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES;
public static ItemType viewTypeToItemType(int viewType) {
if (viewType >= ItemType.values().length) {
Log.e(LOGTAG, "No corresponding ItemType!");
}
return ItemType.values()[viewType];
}
public static int itemTypeToViewType(ItemType itemType) {
return itemType.ordinal();
}
}
public static class BasicItem extends CombinedHistoryItem {
public BasicItem(View view) {
super(view);
}
}
public static class SmartFolder extends CombinedHistoryItem {
final Context context;
final ImageView icon;
final TextView title;
final TextView subtext;
public SmartFolder(View view) {
super(view);
context = view.getContext();
icon = (ImageView) view.findViewById(R.id.device_type);
title = (TextView) view.findViewById(R.id.title);
subtext = (TextView) view.findViewById(R.id.subtext);
}
public void bind(int drawableRes, int titleRes, int subtitleRes, int numDevices) {
icon.setImageResource(drawableRes);
title.setText(titleRes);
subtext.setText(context.getString(subtitleRes, numDevices));
}
}
public static class HistoryItem extends CombinedHistoryItem {
public HistoryItem(View view) {
super(view);
@ -49,21 +88,31 @@ public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
final TextView nameView;
final ImageView deviceTypeView;
final TextView lastModifiedView;
final ImageView deviceExpanded;
public ClientItem(View view) {
super(view);
nameView = (TextView) view.findViewById(R.id.client);
deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
deviceExpanded = (ImageView) view.findViewById(R.id.device_expanded);
}
public void bind(RemoteClient client, Context context) {
this.nameView.setText(client.name);
this.nameView.setTextColor(ContextCompat.getColor(context, R.color.placeholder_active_grey));
this.deviceTypeView.setImageResource("desktop".equals(client.deviceType) ? R.drawable.sync_desktop : R.drawable.sync_mobile);
public void bind(Context context, RemoteClient client, boolean isCollapsed) {
this.nameView.setText(client.name);
final long now = System.currentTimeMillis();
this.lastModifiedView.setText(ClientsAdapter.getLastSyncedString(context, now, client.lastModified));
final long now = System.currentTimeMillis();
this.lastModifiedView.setText(RemoteTabsExpandableListAdapter.getLastSyncedString(context, now, client.lastModified));
if (client.isDesktop()) {
deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_desktop_inactive : R.drawable.sync_desktop);
} else {
deviceTypeView.setImageResource(isCollapsed ? R.drawable.sync_mobile_inactive : R.drawable.sync_mobile);
}
nameView.setTextColor(ContextCompat.getColor(context, isCollapsed ? R.color.tabs_tray_icon_grey : R.color.placeholder_active_grey));
if (client.tabs.size() > 0) {
deviceExpanded.setImageResource(isCollapsed ? R.drawable.home_group_collapsed : R.drawable.arrow_down);
}
}
}
}

View File

@ -5,16 +5,19 @@
package org.mozilla.gecko.home;
import android.accounts.Account;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.RecyclerView;
import android.text.SpannableStringBuilder;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
@ -27,26 +30,24 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.RemoteClientsDialogFragment;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.SyncStatusListener;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.RemoteTab;
import org.mozilla.gecko.home.HistorySectionsHelper.SectionDateRange;
import org.mozilla.gecko.restrictions.Restrictable;
import org.mozilla.gecko.widget.DividerItemDecoration;
@ -56,46 +57,58 @@ import java.util.List;
public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsDialogFragment.RemoteClientsListener {
private static final String LOGTAG = "GeckoCombinedHistoryPnl";
private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "clients", "tabs" };
private final int LOADER_ID_HISTORY = 0;
private final int LOADER_ID_REMOTE = 1;
// Semantic names for the time covered by each section
public enum SectionHeader {
TODAY,
YESTERDAY,
WEEK,
THIS_MONTH,
MONTH_AGO,
TWO_MONTHS_AGO,
THREE_MONTHS_AGO,
FOUR_MONTHS_AGO,
FIVE_MONTHS_AGO,
OLDER_THAN_SIX_MONTHS
}
// Array for the time ranges in milliseconds covered by each section.
private static final SectionDateRange[] sectionDateRangeArray = new SectionDateRange[SectionHeader.values().length];
// String placeholders to mark formatting.
private final static String FORMAT_S1 = "%1$s";
private final static String FORMAT_S2 = "%2$s";
private CombinedHistoryRecyclerView mRecyclerView;
private CombinedHistoryAdapter mAdapter;
private CursorLoaderCallbacks mCursorLoaderCallbacks;
private int mSavedParentIndex = -1;
// Number of smart folders for determining practical empty state.
public static final int NUM_SMART_FOLDERS = 1;
private OnPanelLevelChangeListener.PanelLevel mPanelLevel = OnPanelLevelChangeListener.PanelLevel.PARENT;
private CombinedHistoryRecyclerView mRecyclerView;
private CombinedHistoryAdapter mHistoryAdapter;
private ClientsAdapter mClientsAdapter;
private CursorLoaderCallbacks mCursorLoaderCallbacks;
private OnPanelLevelChangeListener.PanelLevel mPanelLevel;
private Button mPanelFooterButton;
// Child refresh layout view.
protected SwipeRefreshLayout mRefreshLayout;
// Sync listener that stops refreshing when a sync is completed.
protected RemoteTabsSyncListener mSyncStatusListener;
// Reference to the View to display when there are no results.
private View mEmptyView;
private View mHistoryEmptyView;
private View mClientsEmptyView;
public interface OnPanelLevelChangeListener {
enum PanelLevel {
PARENT, CHILD
}
}
void onPanelLevelChange(PanelLevel level);
/**
* Propagates level changes.
* @param level
* @return true if level changed, false otherwise.
*/
boolean changeLevel(PanelLevel level);
}
@Override
public void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
mHistoryAdapter = new CombinedHistoryAdapter(getResources());
mClientsAdapter = new ClientsAdapter(getContext());
mSyncStatusListener = new RemoteTabsSyncListener();
FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
}
@Override
@ -108,17 +121,76 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
super.onViewCreated(view, savedInstanceState);
mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
mAdapter = new CombinedHistoryAdapter(getContext(), mSavedParentIndex);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setItemAnimator(new DefaultItemAnimator());
setUpRecyclerView();
mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
setUpRefreshLayout();
mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view);
mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view);
setUpEmptyViews();
mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
}
private void setUpRecyclerView() {
if (mPanelLevel == null) {
mPanelLevel = OnPanelLevelChangeListener.PanelLevel.PARENT;
}
mRecyclerView.setAdapter(mPanelLevel == OnPanelLevelChangeListener.PanelLevel.PARENT ? mHistoryAdapter : mClientsAdapter);
final RecyclerView.ItemAnimator animator = new DefaultItemAnimator();
animator.setAddDuration(100);
animator.setChangeDuration(100);
animator.setMoveDuration(100);
animator.setRemoveDuration(100);
mRecyclerView.setItemAnimator(animator);
mRecyclerView.addItemDecoration(new DividerItemDecoration(getContext()));
mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper());
registerForContextMenu(mRecyclerView);
}
mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
private void setUpRefreshLayout() {
mRefreshLayout.setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener());
}
private void setUpEmptyViews() {
// Set up history empty view.
final ImageView emptyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
emptyIcon.setVisibility(View.GONE);
final TextView emptyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
emptyText.setText(R.string.home_most_recent_empty);
final TextView emptyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
emptyHint.setVisibility(View.GONE);
} else {
final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
final SpannableStringBuilder hintBuilder = formatHintText(hintText);
if (hintBuilder != null) {
emptyHint.setText(hintBuilder);
emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
emptyHint.setVisibility(View.VISIBLE);
}
}
// Set up Clients empty view.
final Button syncSetupButton = (Button) mClientsEmptyView.findViewById(R.id.sync_setup_button);
syncSetupButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// This Activity will redirect to the correct Activity as needed.
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
startActivity(intent);
}
});
}
@Override
@ -127,17 +199,6 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (isVisible()) {
// The parent stack is saved just so that the folder state can be
// restored on rotation.
mSavedParentIndex = mAdapter.getParentIndex();
}
}
@Override
protected void load() {
getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
@ -171,25 +232,10 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
@Override
public Cursor loadCursor() {
final ContentResolver cr = getContext().getContentResolver();
HistorySectionsHelper.updateRecentSectionOffset(getContext().getResources(), sectionDateRangeArray);
return mDB.getRecentHistory(cr, HISTORY_LIMIT);
}
}
protected static String getSectionHeaderTitle(SectionHeader section) {
return sectionDateRangeArray[section.ordinal()].displayName;
}
protected static SectionHeader getSectionFromTime(long time) {
for (int i = 0; i < SectionHeader.OLDER_THAN_SIX_MONTHS.ordinal(); i++) {
if (time > sectionDateRangeArray[i].start) {
return SectionHeader.values()[i];
}
}
return SectionHeader.OLDER_THAN_SIX_MONTHS;
}
private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
private BrowserDB mDB; // Pseudo-final: set in onCreateLoader.
@ -215,140 +261,122 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
final int loaderId = loader.getId();
switch (loaderId) {
case LOADER_ID_HISTORY:
mAdapter.setHistory(c);
mHistoryAdapter.setHistory(c);
break;
case LOADER_ID_REMOTE:
final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
mAdapter.setClients(clients);
mHistoryAdapter.getDeviceUpdateHandler().onDeviceCountUpdated(clients.size());
mClientsAdapter.setClients(clients);
mRefreshLayout.setEnabled(clients.size() > 0);
break;
}
// Check and set empty state.
updateButtonFromLevel(mPanelLevel);
updateEmptyView(mAdapter.getItemCount() == 0);
updateEmptyView();
updateButtonFromLevel();
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.setClients(Collections.<RemoteClient>emptyList());
mAdapter.setHistory(null);
mClientsAdapter.setClients(Collections.<RemoteClient>emptyList());
mHistoryAdapter.setHistory(null);
}
}
protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
@Override
public void onPanelLevelChange(PanelLevel level) {
updateButtonFromLevel(level);
public boolean changeLevel(PanelLevel level) {
if (level == mPanelLevel) {
return false;
}
mPanelLevel = level;
switch (level) {
case PARENT:
mRecyclerView.swapAdapter(mHistoryAdapter, true);
break;
case CHILD:
mRecyclerView.swapAdapter(mClientsAdapter, true);
break;
}
updateEmptyView();
updateButtonFromLevel();
return true;
}
}
private void updateButtonFromLevel(OnPanelLevelChangeListener.PanelLevel level) {
mPanelLevel = level;
switch (level) {
case CHILD:
mPanelFooterButton.setVisibility(View.VISIBLE);
mPanelFooterButton.setText(R.string.home_open_all);
break;
private void updateButtonFromLevel() {
switch (mPanelLevel) {
case PARENT:
final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY);
if (historyRestricted || !mAdapter.containsHistory()) {
if (historyRestricted || mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS) {
mPanelFooterButton.setVisibility(View.GONE);
} else {
mPanelFooterButton.setVisibility(View.VISIBLE);
mPanelFooterButton.setText(R.string.home_clear_history_button);
}
break;
case CHILD:
mPanelFooterButton.setVisibility(View.GONE);
break;
}
}
private class OnFooterButtonClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
switch (mPanelLevel) {
case PARENT:
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
});
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
});
// Send message to Java to clear history.
final JSONObject json = new JSONObject();
try {
json.put("history", true);
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
}
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
}
});
dialogBuilder.show();
break;
case CHILD:
final JSONArray tabUrls = mAdapter.getCurrentChildTabs();
if (tabUrls != null) {
final JSONObject message = new JSONObject();
try {
message.put("urls", tabUrls);
message.put("shouldNotifyTabsOpenedToJava", false);
GeckoAppShell.notifyObservers("Tabs:OpenMultiple", message.toString());
} catch (JSONException e) {
Log.e(LOGTAG, "Error making JSON message to open tabs");
}
// Send message to Java to clear history.
final JSONObject json = new JSONObject();
try {
json.put("history", true);
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
}
break;
}
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
}
});
dialogBuilder.show();
}
}
private void updateEmptyView(boolean isEmpty) {
if (isEmpty) {
if (mEmptyView == null) {
// Set empty panel view if it needs to be shown and hasn't been inflated.
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
mEmptyView = emptyViewStub.inflate();
private void updateEmptyView() {
boolean showEmptyHistoryView = false;
boolean showEmptyClientsView = false;
switch (mPanelLevel) {
case PARENT:
showEmptyHistoryView = mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS;
break;
final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
emptyIcon.setImageResource(R.drawable.icon_most_recent_empty);
final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
emptyText.setText(R.string.home_most_recent_empty);
final TextView emptyHint = (TextView) mEmptyView.findViewById(R.id.home_empty_hint);
final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
final SpannableStringBuilder hintBuilder = formatHintText(hintText);
if (hintBuilder != null) {
emptyHint.setText(hintBuilder);
emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
emptyHint.setVisibility(View.VISIBLE);
}
if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
emptyHint.setVisibility(View.GONE);
}
}
mEmptyView.setVisibility(View.VISIBLE);
} else {
if (mEmptyView != null) {
mEmptyView.setVisibility(View.GONE);
}
case CHILD:
showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
break;
}
final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView;
mRecyclerView.setOverScrollMode(showEmptyView? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE);
mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE);
}
/**
* Make Span that is clickable, and underlined
* between the string markers <code>FORMAT_S1</code> and
@ -439,7 +467,7 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
final int itemId = item.getItemId();
if (itemId == R.id.home_remote_tabs_hide_client) {
mAdapter.removeItem(info.position);
mClientsAdapter.removeItem(info.position);
return true;
}
@ -453,20 +481,18 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
protected class HiddenClientsHelper implements DialogBuilder<RemoteClient> {
@Override
public void createAndShowDialog(List<RemoteClient> clientsList) {
final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance(
final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance(
getResources().getString(R.string.home_remote_tabs_hidden_devices_title),
getResources().getString(R.string.home_remote_tabs_unhide_selected_devices),
RemoteClientsDialogFragment.ChoiceMode.MULTIPLE, new ArrayList<>(clientsList));
dialog.setTargetFragment(CombinedHistoryPanel.this, 0);
dialog.show(getActivity().getSupportFragmentManager(), "show-clients");
}
}
@Override
public void onClients(List<RemoteClient> clients) {
mAdapter.unhideClients(clients);
mClientsAdapter.unhideClients(clients);
}
/**
@ -481,25 +507,46 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
}
}
protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) {
info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
if (cursor.isNull(bookmarkIdCol)) {
// If this is a combined cursor, we may get a history item without a
// bookmark, in which case the bookmarks ID column value will be null.
info.bookmarkId = -1;
} else {
info.bookmarkId = cursor.getInt(bookmarkIdCol);
protected class RemoteTabsRefreshListener implements SwipeRefreshLayout.OnRefreshListener {
@Override
public void onRefresh() {
if (FirefoxAccounts.firefoxAccountsExist(getActivity())) {
final Account account = FirefoxAccounts.getFirefoxAccount(getActivity());
FirefoxAccounts.requestImmediateSync(account, STAGES_TO_SYNC_ON_REFRESH, null);
} else {
Log.wtf(LOGTAG, "No Firefox Account found; this should never happen. Ignoring.");
mRefreshLayout.setRefreshing(false);
}
}
return info;
}
protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, RemoteTab tab) {
info.url = tab.url;
info.title = tab.title;
return info;
protected class RemoteTabsSyncListener implements SyncStatusListener {
@Override
public Context getContext() {
return getActivity();
}
@Override
public Account getAccount() {
return FirefoxAccounts.getFirefoxAccount(getContext());
}
@Override
public void onSyncStarted() {
}
@Override
public void onSyncFinished() {
mRefreshLayout.setRefreshing(false);
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mSyncStatusListener != null) {
FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
mSyncStatusListener = null;
}
}
}

View File

@ -13,17 +13,22 @@ import android.view.KeyEvent;
import android.view.View;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener;
import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.widget.RecyclerViewClickSupport;
import java.util.EnumSet;
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD;
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
public class CombinedHistoryRecyclerView extends RecyclerView
implements RecyclerViewClickSupport.OnItemClickListener, RecyclerViewClickSupport.OnItemLongClickListener {
public static String LOGTAG = "CombinedHistoryRecycView";
protected interface AdapterContextMenuBuilder {
HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position);
}
protected HomePager.OnUrlOpenListener mOnUrlOpenListener;
protected OnPanelLevelChangeListener mOnPanelLevelChangeListener;
@ -61,8 +66,7 @@ public class CombinedHistoryRecyclerView extends RecyclerView
// If the user hit the BACK key, try to move to the parent folder.
if (action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
mOnPanelLevelChangeListener.onPanelLevelChange(PARENT);
return ((CombinedHistoryAdapter) getAdapter()).exitChildView();
return mOnPanelLevelChangeListener.changeLevel(PARENT);
}
return false;
}
@ -84,23 +88,27 @@ public class CombinedHistoryRecyclerView extends RecyclerView
@Override
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
final int viewType = getAdapter().getItemViewType(position);
final CombinedHistoryAdapter.ItemType itemType = CombinedHistoryAdapter.ItemType.viewTypeToItemType(viewType);
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
switch (itemType) {
case CLIENT:
mOnPanelLevelChangeListener.onPanelLevelChange(PanelLevel.CHILD);
((CombinedHistoryAdapter) getAdapter()).showChildView(position);
case SYNCED_DEVICES:
mOnPanelLevelChangeListener.changeLevel(CHILD);
break;
case CLIENT:
((ClientsAdapter) getAdapter()).toggleClient(position);
break;
case HIDDEN_DEVICES:
if (mDialogBuilder != null) {
mDialogBuilder.createAndShowDialog(((CombinedHistoryAdapter) getAdapter()).getHiddenClients());
mDialogBuilder.createAndShowDialog(((ClientsAdapter) getAdapter()).getHiddenClients());
}
break;
case NAVIGATION_BACK:
mOnPanelLevelChangeListener.onPanelLevelChange(PARENT);
((CombinedHistoryAdapter) getAdapter()).exitChildView();
mOnPanelLevelChangeListener.changeLevel(PARENT);
break;
case CHILD:
case HISTORY:
if (mOnUrlOpenListener != null) {
@ -114,7 +122,7 @@ public class CombinedHistoryRecyclerView extends RecyclerView
@Override
public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
mContextMenuInfo = ((CombinedHistoryAdapter) getAdapter()).makeContextMenuInfoFromPosition(v, position);
mContextMenuInfo = ((AdapterContextMenuBuilder) getAdapter()).makeContextMenuInfoFromPosition(v, position);
return showContextMenuForChild(this);
}

View File

@ -7,12 +7,13 @@ package org.mozilla.gecko.home;
import android.content.res.Resources;
import org.mozilla.gecko.home.CombinedHistoryPanel.SectionHeader;
import org.mozilla.gecko.home.CombinedHistoryAdapter.SectionHeader;
import org.mozilla.gecko.R;
import java.util.Calendar;
import java.util.Locale;
public class HistorySectionsHelper {
// Constants for different time sections.

View File

@ -44,7 +44,6 @@ public class TwoLinePageRow extends LinearLayout
private int mSwitchToTabIconId;
private final FaviconView mFavicon;
private final View mReaderCached;
private boolean mShowIcons;
private int mLoadFaviconJobId = Favicons.NOT_LOADING;
@ -109,8 +108,6 @@ public class TwoLinePageRow extends LinearLayout
mFavicon = (FaviconView) findViewById(R.id.icon);
mFaviconListener = new UpdateViewFaviconLoadedListener(mFavicon);
mReaderCached = findViewById(R.id.is_reader_cached);
}
@Override
@ -201,9 +198,23 @@ public class TwoLinePageRow extends LinearLayout
mUrl.setCompoundDrawablesWithIntrinsicBounds(mSwitchToTabIconId, 0, 0, 0);
}
private void showBookmarkIcon(boolean toShow) {
final int visibility = toShow ? VISIBLE : GONE;
mStatusIcon.setVisibility(visibility);
private void updateStatusIcon(boolean isBookmark, boolean isReaderItem) {
if (isReaderItem) {
mStatusIcon.setImageResource(R.drawable.status_icon_readercache);
} else if (isBookmark) {
mStatusIcon.setImageResource(R.drawable.star_blue);
}
if (mShowIcons && (isBookmark || isReaderItem)) {
mStatusIcon.setVisibility(View.VISIBLE);
} else if (mShowIcons) {
// We use INVISIBLE to have consistent padding for our items. This means text/URLs
// fade consistently in the same location, regardless of them being bookmarked.
mStatusIcon.setVisibility(View.INVISIBLE);
} else {
mStatusIcon.setVisibility(View.GONE);
}
}
/**
@ -261,9 +272,10 @@ public class TwoLinePageRow extends LinearLayout
// The bookmark id will be 0 (null in database) when the url
// is not a bookmark.
final boolean isBookmark = bookmarkId != 0;
showBookmarkIcon(isBookmark);
updateStatusIcon(isBookmark, hasReaderCacheItem);
} else {
showBookmarkIcon(false);
updateStatusIcon(false, false);
}
// Use the URL instead of an empty title for consistency with the normal URL
@ -286,8 +298,6 @@ public class TwoLinePageRow extends LinearLayout
mLoadFaviconJobId = Favicons.getSizedFaviconForPageFromLocal(getContext(), pageURL, mFaviconListener);
updateDisplayedUrl(url, hasReaderCacheItem);
mReaderCached.setVisibility(hasReaderCacheItem ? View.VISIBLE : View.INVISIBLE);
}
/**

View File

@ -130,10 +130,7 @@ public class GeckoMenuInflater extends MenuInflater {
item.enabled = a.getBoolean(R.styleable.MenuItem_android_enabled, true);
item.hasSubMenu = false;
item.iconRes = a.getResourceId(R.styleable.MenuItem_android_icon, 0);
if (Versions.feature11Plus) {
item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0);
}
item.showAsAction = a.getInt(R.styleable.MenuItem_android_showAsAction, 0);
a.recycle();
}

View File

@ -17,8 +17,7 @@ import android.view.SubMenu;
import android.view.View;
public class GeckoMenuItem implements MenuItem {
private static final int SECONDARY_ACTION_BAR_HISTORY_SIZE = 0;
private static final int QUICK_SHARE_ACTION_BAR_HISTORY_SIZE = 3;
private static final int SHARE_BAR_HISTORY_SIZE = 2;
// These values mirror MenuItem values that are only available on API >= 11.
public static final int SHOW_AS_ACTION_NEVER = 0;
@ -135,13 +134,8 @@ public class GeckoMenuItem implements MenuItem {
@Override
public View getActionView() {
if (mActionProvider != null) {
if (getActionEnum() == MenuItem.SHOW_AS_ACTION_IF_ROOM) {
return mActionProvider.onCreateActionView(SECONDARY_ACTION_BAR_HISTORY_SIZE,
GeckoActionProvider.ActionViewType.DEFAULT);
} else {
return mActionProvider.onCreateActionView(QUICK_SHARE_ACTION_BAR_HISTORY_SIZE,
GeckoActionProvider.ActionViewType.QUICK_SHARE_ICON);
}
return mActionProvider.onCreateActionView(SHARE_BAR_HISTORY_SIZE,
GeckoActionProvider.ActionViewType.DEFAULT);
}
return mActionView;

View File

@ -8,7 +8,6 @@ package org.mozilla.gecko.menu;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.R;
import android.annotation.TargetApi;
@ -156,9 +155,9 @@ public class MenuItemSwitcherLayout extends LinearLayout
params.weight = 1.0f;
button.setLayoutParams(params);
// Fill in the action-buttons to the left of the actual menu button.
// Place action buttons to the right of the actual menu item
mActionButtons.add(button);
addView(button, count);
addView(button, count + 1);
}
}

View File

@ -1,36 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.menu;
import android.annotation.TargetApi;
import android.content.Context;
import android.util.AttributeSet;
import org.mozilla.gecko.R;
/**
* A MenuItemActionView without the default child views.
*
* A better implementation would have the non-child view implementation as the parent of
* MenuItemActionView, but this is simpler and faster to implement for something intended to be
* uplifted, and this implementation will soon be replaced with the old implementation for adding
* the share plane to the menu (see https://bug1122302.bugzilla.mozilla.org/attachment.cgi?id=8572126).
*/
public class QuickShareBarActionView extends MenuItemSwitcherLayout {
public QuickShareBarActionView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.menuItemSwitcherLayoutStyle);
}
@TargetApi(14)
public QuickShareBarActionView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// We remove the views so they are visible, but note that
// the child still does some computation on them.
removeAllViews();
}
}

View File

@ -0,0 +1,239 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.promotion;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.BrowserAppDelegate;
import org.mozilla.gecko.EditBookmarkDialog;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarHelper;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.home.HomeConfig;
import org.mozilla.gecko.prompts.Prompt;
import org.mozilla.gecko.prompts.PromptListItem;
import org.mozilla.gecko.util.DrawableUtil;
import org.mozilla.gecko.util.ThreadUtils;
import java.lang.ref.WeakReference;
/**
* Delegate to watch for bookmark state changes.
*
* This is responsible for showing snackbars and helper UIs related to the addition/removal
* of bookmarks, or reader view bookmarks.
*/
public class BookmarkStateChangeDelegate extends BrowserAppDelegate implements Tabs.OnTabsChangedListener {
private static final String LOGTAG = "BookmarkDelegate";
private WeakReference<BrowserApp> mBrowserApp;
@Override
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
mBrowserApp = new WeakReference<>(browserApp);
}
@Override
public void onResume(BrowserApp browserApp) {
Tabs.registerOnTabsChangedListener(this);
}
@Override
public void onPause(BrowserApp browserApp) {
Tabs.unregisterOnTabsChangedListener(this);
}
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
switch (msg) {
case BOOKMARK_ADDED:
// We always show the special offline snackbar whenever we bookmark a reader page.
// It's possible that the page is already stored offline, however this is highly
// unlikely, and even so it is probably nicer to show the same offline notification
// every time we bookmark an about:reader page.
if (!AboutPages.isAboutReader(tab.getURL())) {
showBookmarkAddedSnackbar();
} else {
if (!promoteReaderViewBookmarkAdded()) {
showReaderModeBookmarkAddedSnackbar();
}
}
break;
case BOOKMARK_REMOVED:
showBookmarkRemovedSnackbar();
break;
}
}
@Override
public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
if (requestCode == BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK) {
if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS) {
browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
} else if (resultCode == BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE) {
showReaderModeBookmarkAddedSnackbar();
}
}
}
private boolean promoteReaderViewBookmarkAdded() {
final BrowserApp browserApp = mBrowserApp.get();
if (browserApp == null) {
return false;
}
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp);
final boolean hasFirstReaderViewPromptBeenShownBefore = prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false);
if (hasFirstReaderViewPromptBeenShownBefore) {
return false;
}
SimpleHelperUI.show(browserApp,
SimpleHelperUI.FIRST_RVBP_SHOWN_TELEMETRYEXTRA,
BrowserApp.ACTIVITY_REQUEST_FIRST_READERVIEW_BOOKMARK,
R.string.helper_first_offline_bookmark_title, R.string.helper_first_offline_bookmark_message,
R.drawable.helper_readerview_bookmark, R.string.helper_first_offline_bookmark_button,
BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_GOTO_BOOKMARKS,
BrowserApp.ACTIVITY_RESULT_FIRST_READERVIEW_BOOKMARKS_IGNORE);
GeckoSharedPrefs.forProfile(browserApp)
.edit()
.putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
.apply();
return true;
}
private void showBookmarkAddedSnackbar() {
final BrowserApp browserApp = mBrowserApp.get();
if (browserApp == null) {
return;
}
// This flow is from the option menu which has check to see if a bookmark was already added.
// So, it is safe here to show the snackbar that bookmark_added without any checks.
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
@Override
public void onClick(View v) {
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.TOAST, "bookmark_options");
showBookmarkDialog(browserApp);
}
};
SnackbarHelper.showSnackbarWithAction(browserApp,
browserApp.getResources().getString(R.string.bookmark_added),
Snackbar.LENGTH_LONG,
browserApp.getResources().getString(R.string.bookmark_options),
callback);
}
private void showBookmarkRemovedSnackbar() {
final BrowserApp browserApp = mBrowserApp.get();
if (browserApp == null) {
return;
}
SnackbarHelper.showSnackbar(browserApp, browserApp.getResources().getString(R.string.bookmark_removed), Snackbar.LENGTH_LONG);
}
private static void showBookmarkDialog(final BrowserApp browserApp) {
final Resources res = browserApp.getResources();
final Tab tab = Tabs.getInstance().getSelectedTab();
final Prompt ps = new Prompt(browserApp, new Prompt.PromptCallback() {
@Override
public void onPromptFinished(String result) {
int itemId = -1;
try {
itemId = new JSONObject(result).getInt("button");
} catch (JSONException ex) {
Log.e(LOGTAG, "Exception reading bookmark prompt result", ex);
}
if (tab == null) {
return;
}
if (itemId == 0) {
final String extrasId = res.getResourceEntryName(R.string.contextmenu_edit_bookmark);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
TelemetryContract.Method.DIALOG, extrasId);
new EditBookmarkDialog(browserApp).show(tab.getURL());
} else if (itemId == 1) {
final String extrasId = res.getResourceEntryName(R.string.contextmenu_add_to_launcher);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION,
TelemetryContract.Method.DIALOG, extrasId);
final String url = tab.getURL();
final String title = tab.getDisplayTitle();
if (url != null && title != null) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
GeckoAppShell.createShortcut(title, url);
}
});
}
}
}
});
final PromptListItem[] items = new PromptListItem[2];
items[0] = new PromptListItem(res.getString(R.string.contextmenu_edit_bookmark));
items[1] = new PromptListItem(res.getString(R.string.contextmenu_add_to_launcher));
ps.show("", "", items, ListView.CHOICE_MODE_NONE);
}
private void showReaderModeBookmarkAddedSnackbar() {
final BrowserApp browserApp = mBrowserApp.get();
if (browserApp == null) {
return;
}
final Drawable iconDownloaded = DrawableUtil.tintDrawable(browserApp, R.drawable.status_icon_readercache, Color.WHITE);
final SnackbarHelper.SnackbarCallback callback = new SnackbarHelper.SnackbarCallback() {
@Override
public void onClick(View v) {
browserApp.openUrlAndStopEditing("about:home?panel=" + HomeConfig.getIdForBuiltinPanelType(HomeConfig.PanelType.BOOKMARKS));
}
};
SnackbarHelper.showSnackbarWithActionAndColors(browserApp,
browserApp.getResources().getString(R.string.reader_saved_offline),
Snackbar.LENGTH_LONG,
browserApp.getResources().getString(R.string.reader_switch_to_bookmarks),
callback,
iconDownloaded,
ContextCompat.getColor(browserApp, R.color.link_blue),
Color.WHITE);
}
}

View File

@ -0,0 +1,115 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.promotion;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import com.keepsafe.switchboard.SwitchBoard;
import org.mozilla.gecko.BrowserApp;
import org.mozilla.gecko.BrowserAppDelegate;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.reader.ReaderModeUtils;
import org.mozilla.gecko.util.Experiments;
import java.lang.ref.WeakReference;
public class ReaderViewBookmarkPromotion extends BrowserAppDelegate implements Tabs.OnTabsChangedListener {
private WeakReference<BrowserApp> mBrowserApp;
private int mTimesEnteredReaderMode;
private boolean mExperimentEnabled;
@Override
public void onCreate(BrowserApp browserApp, Bundle savedInstanceState) {
mBrowserApp = new WeakReference<>(browserApp);
mExperimentEnabled = SwitchBoard.isInExperiment(browserApp, Experiments.TRIPLE_READERVIEW_BOOKMARK_PROMPT);
}
@Override
public void onResume(BrowserApp browserApp) {
Tabs.registerOnTabsChangedListener(this);
}
@Override
public void onPause(BrowserApp browserApp) {
Tabs.unregisterOnTabsChangedListener(this);
}
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, String data) {
switch (msg) {
case LOCATION_CHANGE:
// old url: data
// new url: tab.getURL()
final boolean enteringReaderMode = ReaderModeUtils.isEnteringReaderMode(tab.getURL(), data);
if (mTimesEnteredReaderMode < 4 && enteringReaderMode) {
mTimesEnteredReaderMode++;
}
if (mTimesEnteredReaderMode == 3) {
promoteBookmarking();
}
break;
}
}
@Override
public void onActivityResult(BrowserApp browserApp, int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW:
if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK) {
final Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
tab.addBookmark();
}
} else if (resultCode == BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE) {
// Nothing to do: we won't show this promotion again either way.
}
break;
}
}
private void promoteBookmarking() {
final BrowserApp browserApp = mBrowserApp.get();
if (browserApp == null) {
return;
}
final SharedPreferences prefs = GeckoSharedPrefs.forProfile(browserApp);
// We reuse the same preference as for the first offline reader view bookmark
// as we only want to show one of the two UIs (they both explain the same
// functionality).
if (!mExperimentEnabled || prefs.getBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, false)) {
return;
}
SimpleHelperUI.show(browserApp,
SimpleHelperUI.TRIPLE_READERVIEW_OPEN_TELEMETRYEXTRA,
BrowserApp.ACTIVITY_REQUEST_TRIPLE_READERVIEW,
R.string.helper_triple_readerview_open_title,
R.string.helper_triple_readerview_open_message,
R.drawable.helper_readerview_bookmark, // We share the icon with the usual helper UI
R.string.helper_triple_readerview_open_button,
BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_ADD_BOOKMARK,
BrowserApp.ACTIVITY_RESULT_TRIPLE_READERVIEW_IGNORE);
GeckoSharedPrefs.forProfile(browserApp)
.edit()
.putBoolean(SimpleHelperUI.PREF_FIRST_RVBP_SHOWN, true)
.apply();
}
}

View File

@ -33,6 +33,7 @@ import org.mozilla.gecko.TelemetryContract;
public class SimpleHelperUI extends Locales.LocaleAwareActivity {
public static final String PREF_FIRST_RVBP_SHOWN = "first_reader_view_bookmark_prompt_shown";
public static final String FIRST_RVBP_SHOWN_TELEMETRYEXTRA = "first_readerview_bookmark_prompt";
public static final String TRIPLE_READERVIEW_OPEN_TELEMETRYEXTRA = "third_readerview_open_prompt";
private View containerView;

View File

@ -35,7 +35,7 @@ import android.widget.Spinner;
import android.widget.TextView;
import android.widget.TimePicker;
public class PromptInput {
public abstract class PromptInput {
protected final String mLabel;
protected final String mType;
protected final String mId;
@ -47,7 +47,7 @@ public class PromptInput {
public static final String LOGTAG = "GeckoPromptInput";
public interface OnChangeListener {
public void onChange(PromptInput input);
void onChange(PromptInput input);
}
public void setListener(OnChangeListener listener) {
@ -372,9 +372,7 @@ public class PromptInput {
return null;
}
public View getView(Context context) throws UnsupportedOperationException {
return null;
}
public abstract View getView(Context context) throws UnsupportedOperationException;
public String getId() {
return mId;

View File

@ -7,6 +7,7 @@
package org.mozilla.gecko.util;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
public class ContextUtils {
@ -17,11 +18,11 @@ public class ContextUtils {
* @throws PackageManager.NameNotFoundException Unexpected - we get the package name from the context so
* it's expected to be found.
*/
public static long getPackageInstallTime(final Context context) {
public static PackageInfo getCurrentPackageInfo(final Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0).firstInstallTime;
} catch (final PackageManager.NameNotFoundException e) {
throw new AssertionError("Should not happen: could not get package info for own package");
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
throw new AssertionError("Should not happen: Can't get package info of own package");
}
}
}

View File

@ -47,6 +47,9 @@ public class Experiments {
public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
// Promotion to bookmark reader-view items after entering reader view three times (Bug 1247689)
public static final String TRIPLE_READERVIEW_BOOKMARK_PROMPT = "triple-readerview-bookmark-prompt";
private static volatile Boolean disabled = null;
/**

View File

@ -33,7 +33,8 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration {
}
for (int i = 0; i < parent.getChildCount(); i++) {
final View child = parent.getChildAt(i);
c.drawRect(0, child.getBottom(), parent.getWidth(), child.getBottom() + mDividerHeight, mDividerPaint);
final float bottom = child.getBottom() + child.getTranslationY();
c.drawRect(0, bottom, parent.getWidth(), bottom + mDividerHeight, mDividerPaint);
}
}
}

View File

@ -16,7 +16,6 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.SnackbarHelper;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.menu.QuickShareBarActionView;
import org.mozilla.gecko.mozglue.ContextUtils;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.menu.MenuItemSwitcherLayout;
@ -119,10 +118,6 @@ public class GeckoActionProvider {
view = new MenuItemSwitcherLayout(mContext, null);
break;
case QUICK_SHARE_ICON:
view = new QuickShareBarActionView(mContext, null);
break;
case CONTEXT_MENU:
view = new MenuItemSwitcherLayout(mContext, null);
view.initContextMenuStyles();
@ -283,7 +278,6 @@ public class GeckoActionProvider {
public enum ActionViewType {
DEFAULT,
QUICK_SHARE_ICON,
CONTEXT_MENU,
}

View File

@ -536,7 +536,9 @@ size. -->
with the page title defined in aboutHome.dtd -->
<!ENTITY home_title "&brandShortName; Home">
<!ENTITY home_history_title "History">
<!ENTITY home_history_back_to "Go back to all history">
<!ENTITY home_synced_devices_smartfolder "Synced devices">
<!ENTITY home_synced_devices_number "&formatD; devices">
<!ENTITY home_history_back_to2 "Back to full History">
<!ENTITY home_clear_history_button "Clear browsing history">
<!ENTITY home_clear_history_confirm "Are you sure you want to clear your history?">
<!ENTITY home_bookmarks_empty "Bookmarks you save show up here.">
@ -778,3 +780,8 @@ just addresses the organization to follow, e.g. "This site is run by " -->
<!ENTITY helper_first_offline_bookmark_title "Read offline">
<!ENTITY helper_first_offline_bookmark_message "Find your Reader View items in Bookmarks, even offline.">
<!ENTITY helper_first_offline_bookmark_button "Go to Bookmarks">
<!ENTITY helper_triple_readerview_open_title "Available offline">
<!ENTITY helper_triple_readerview_open_message "Bookmark Reader View items to read them offline.">
<!ENTITY helper_triple_readerview_open_button "Add to Bookmarks">

View File

@ -385,6 +385,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'home/BookmarksListView.java',
'home/BookmarksPanel.java',
'home/BrowserSearch.java',
'home/ClientsAdapter.java',
'home/CombinedHistoryAdapter.java',
'home/CombinedHistoryItem.java',
'home/CombinedHistoryPanel.java',
@ -456,7 +457,6 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'menu/MenuItemSwitcherLayout.java',
'menu/MenuPanel.java',
'menu/MenuPopup.java',
'menu/QuickShareBarActionView.java',
'MotionEventInterceptor.java',
'NotificationClient.java',
'NotificationHandler.java',
@ -508,7 +508,9 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'PrintHelper.java',
'PrivateTab.java',
'promotion/AddToHomeScreenPromotion.java',
'promotion/BookmarkStateChangeDelegate.java',
'promotion/HomeScreenPrompt.java',
'promotion/ReaderViewBookmarkPromotion.java',
'promotion/SimpleHelperUI.java',
'prompts/ColorPickerInput.java',
'prompts/IconGridInput.java',
@ -524,7 +526,6 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'reader/ReadingListHelper.java',
'reader/SavedReaderViewHelper.java',
'RemoteClientsDialogFragment.java',
'RemoteTabsExpandableListAdapter.java',
'Restarter.java',
'restrictions/DefaultConfiguration.java',
'restrictions/GuestProfileConfiguration.java',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 754 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1000 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More