Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-11-25 16:47:36 -05:00
commit 3aa63e8b3e
90 changed files with 2593 additions and 673 deletions

View File

@ -22,4 +22,4 @@
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
# don't change CLOBBER for WebIDL changes any more.
Bug 1101170 - Linux desktop build changes libmozsandbox from shared to static.
Bug 1102488 - Changes to Android build system.

View File

@ -223,6 +223,8 @@ loop.conversation = (function(mozL10n) {
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
mixins: [sharedMixins.AudioMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
@ -230,7 +232,8 @@ loop.conversation = (function(mozL10n) {
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {
@ -302,6 +305,8 @@ loop.conversation = (function(mozL10n) {
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
sharedViews.FeedbackView({
feedbackStore: this.props.feedbackStore,
@ -552,7 +557,8 @@ loop.conversation = (function(mozL10n) {
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {

View File

@ -223,6 +223,8 @@ loop.conversation = (function(mozL10n) {
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
@ -230,7 +232,8 @@ loop.conversation = (function(mozL10n) {
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {
@ -302,6 +305,8 @@ loop.conversation = (function(mozL10n) {
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
<sharedViews.FeedbackView
feedbackStore={this.props.feedbackStore}
@ -552,7 +557,8 @@ loop.conversation = (function(mozL10n) {
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {

View File

@ -428,6 +428,8 @@ loop.conversationViews = (function(mozL10n) {
* the different views that need displaying.
*/
var OutgoingConversationView = React.createClass({displayName: 'OutgoingConversationView',
mixins: [sharedMixins.AudioMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
store: React.PropTypes.instanceOf(
@ -493,6 +495,7 @@ loop.conversationViews = (function(mozL10n) {
);
}
case CALL_STATES.FINISHED: {
this.play("terminated");
return this._renderFeedbackView();
}
case CALL_STATES.INIT: {

View File

@ -428,6 +428,8 @@ loop.conversationViews = (function(mozL10n) {
* the different views that need displaying.
*/
var OutgoingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
store: React.PropTypes.instanceOf(
@ -493,6 +495,7 @@ loop.conversationViews = (function(mozL10n) {
);
}
case CALL_STATES.FINISHED: {
this.play("terminated");
return this._renderFeedbackView();
}
case CALL_STATES.INIT: {

View File

@ -16,8 +16,6 @@ loop.roomViews = (function(mozL10n) {
var ROOM_STATES = loop.store.ROOM_STATES;
var sharedViews = loop.shared.views;
function noop() {}
/**
* ActiveRoomStore mixin.
* @type {Object}
@ -140,7 +138,9 @@ loop.roomViews = (function(mozL10n) {
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
},
_renderInvitationOverlay: function() {
@ -215,6 +215,13 @@ loop.roomViews = (function(mozL10n) {
return this.getDOMNode().querySelector(className);
},
/**
* User clicked on the "Leave" button.
*/
leaveRoom: function() {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
},
/**
* Closes the window if the cancel button is pressed in the generic failure view.
*/
@ -257,6 +264,12 @@ loop.roomViews = (function(mozL10n) {
cancelCall: this.closeWindow}
);
}
case ROOM_STATES.ENDED: {
return sharedViews.FeedbackView({
feedbackStore: this.props.feedbackStore,
onAfterFeedbackReceived: this.closeWindow}
);
}
default: {
return (
React.DOM.div({className: "room-conversation-wrapper"},
@ -273,7 +286,7 @@ loop.roomViews = (function(mozL10n) {
video: {enabled: !this.state.videoMuted, visible: true},
audio: {enabled: !this.state.audioMuted, visible: true},
publishStream: this.publishStream,
hangup: noop})
hangup: this.leaveRoom})
)
)
)

View File

@ -16,8 +16,6 @@ loop.roomViews = (function(mozL10n) {
var ROOM_STATES = loop.store.ROOM_STATES;
var sharedViews = loop.shared.views;
function noop() {}
/**
* ActiveRoomStore mixin.
* @type {Object}
@ -140,7 +138,9 @@ loop.roomViews = (function(mozL10n) {
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired,
},
_renderInvitationOverlay: function() {
@ -215,6 +215,13 @@ loop.roomViews = (function(mozL10n) {
return this.getDOMNode().querySelector(className);
},
/**
* User clicked on the "Leave" button.
*/
leaveRoom: function() {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
},
/**
* Closes the window if the cancel button is pressed in the generic failure view.
*/
@ -257,6 +264,12 @@ loop.roomViews = (function(mozL10n) {
cancelCall={this.closeWindow}
/>;
}
case ROOM_STATES.ENDED: {
return <sharedViews.FeedbackView
feedbackStore={this.props.feedbackStore}
onAfterFeedbackReceived={this.closeWindow}
/>;
}
default: {
return (
<div className="room-conversation-wrapper">
@ -273,7 +286,7 @@ loop.roomViews = (function(mozL10n) {
video={{enabled: !this.state.videoMuted, visible: true}}
audio={{enabled: !this.state.audioMuted, visible: true}}
publishStream={this.publishStream}
hangup={noop} />
hangup={this.leaveRoom} />
</div>
</div>
</div>

View File

@ -744,11 +744,8 @@ html, .fx-embedded, #main,
color: #555;
}
/**
* Hides the hangup button for room conversations.
*/
.room-conversation .conversation-toolbar .btn-hangup-entry {
display: none;
.fx-embedded .room-conversation .conversation-toolbar .btn-hangup {
background-image: url("../img/icons-16x16.svg#leave");
}
.room-invitation-overlay {

View File

@ -118,6 +118,10 @@ use[id$="-red"] {
<polyline fill="#FFFFFF" points="9.25,7.542 8.75,7.542 8.75,11.542 9.25,11.542 "/>
<rect x="6.75" y="7.542" fill="#FFFFFF" width="0.5" height="4"/>
</g>
<g id="leave-shape">
<polygon fill="#FFFFFF" points="2.08,11.52 2.08,4 8,4 8,2.24 0.32,2.24 0.32,13.28 8,13.28 8,11.52"/>
<polygon fill="#FFFFFF" points="15.66816,7.77344 9.6,2.27456 9.6,5.6 3.68,5.6 3.68,9.92 9.6,9.92 9.6,13.27232"/>
</g>
</defs>
<use id="audio" xlink:href="#audio-shape"/>
<use id="audio-hover" xlink:href="#audio-shape"/>
@ -137,6 +141,7 @@ use[id$="-red"] {
<use id="history" xlink:href="#history-shape"/>
<use id="history-hover" xlink:href="#history-shape"/>
<use id="history-active" xlink:href="#history-shape"/>
<use id="leave" xlink:href="#leave-shape"/>
<use id="precall" xlink:href="#precall-shape"/>
<use id="precall-hover" xlink:href="#precall-shape"/>
<use id="precall-active" xlink:href="#precall-shape"/>

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -37,7 +37,9 @@ loop.store.ActiveRoomStore = (function() {
// There was an issue with the room
FAILED: "room-failed",
// The room is full
FULL: "room-full"
FULL: "room-full",
// The room conversation has ended
ENDED: "room-ended"
};
/**
@ -424,7 +426,7 @@ loop.store.ActiveRoomStore = (function() {
}
this.setStoreState({
roomState: nextState ? nextState : ROOM_STATES.READY
roomState: nextState ? nextState : ROOM_STATES.ENDED
});
}
});

View File

@ -222,7 +222,7 @@ loop.shared.views.FeedbackView = (function(l10n) {
* Feedback view.
*/
var FeedbackView = React.createClass({displayName: 'FeedbackView',
mixins: [Backbone.Events, sharedMixins.AudioMixin],
mixins: [Backbone.Events],
propTypes: {
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
@ -242,10 +242,6 @@ loop.shared.views.FeedbackView = (function(l10n) {
this.listenTo(this.props.feedbackStore, "change", this._onStoreStateChanged);
},
componentDidMount: function() {
this.play("terminated");
},
componentWillUnmount: function() {
this.stopListening(this.props.feedbackStore);
},

View File

@ -222,7 +222,7 @@ loop.shared.views.FeedbackView = (function(l10n) {
* Feedback view.
*/
var FeedbackView = React.createClass({
mixins: [Backbone.Events, sharedMixins.AudioMixin],
mixins: [Backbone.Events],
propTypes: {
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
@ -242,10 +242,6 @@ loop.shared.views.FeedbackView = (function(l10n) {
this.listenTo(this.props.feedbackStore, "change", this._onStoreStateChanged);
},
componentDidMount: function() {
this.play("terminated");
},
componentWillUnmount: function() {
this.stopListening(this.props.feedbackStore);
},

View File

@ -238,17 +238,18 @@ loop.shared.mixins = (function() {
function isConnectedToRoom(state) {
return state === ROOM_STATES.HAS_PARTICIPANTS ||
state === ROOM_STATES.SESSION_CONNECTED;
state === ROOM_STATES.SESSION_CONNECTED;
}
function notConnectedToRoom(state) {
// Failed and full are states that the user is not
// really connected o the room, but we don't want to
// really connected to the room, but we don't want to
// catch those here, as they get their own sounds.
return state === ROOM_STATES.INIT ||
state === ROOM_STATES.GATHER ||
state === ROOM_STATES.READY ||
state === ROOM_STATES.JOINED;
state === ROOM_STATES.GATHER ||
state === ROOM_STATES.READY ||
state === ROOM_STATES.JOINED ||
state === ROOM_STATES.ENDED;
}
// Joining the room.

View File

@ -95,6 +95,13 @@ loop.shared.models = (function(l10n) {
if (selectedCallType) {
this.set("selectedCallType", selectedCallType);
}
this.trigger("call:outgoing:get-media-privs");
},
/**
* Used to indicate that media privileges have been accepted.
*/
gotMediaPrivs: function() {
this.trigger("call:outgoing:setup");
},

View File

@ -52,13 +52,15 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("rooms_unavailable_notification_message");
default:
return mozL10n.get("status_error");
};
}
},
_renderContent: function() {
switch(this.props.roomState) {
case ROOM_STATES.INIT:
case ROOM_STATES.READY: {
case ROOM_STATES.READY:
case ROOM_STATES.ENDED: {
// XXX: In ENDED state, we should rather display the feedback form.
return (
React.DOM.button({className: "btn btn-join btn-info",
onClick: this.props.joinRoom},
@ -219,13 +221,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
},
/**
* Watches for when we transition from READY to JOINED room state, so we can
* request user media access.
* Watches for when we transition to JOINED room state, so we can request
* user media access.
*
* @param {Object} nextProps (Unused)
* @param {Object} nextState Next state object.
*/
componentWillUpdate: function(nextProps, nextState) {
if (this.state.roomState === ROOM_STATES.READY &&
if (this.state.roomState !== ROOM_STATES.JOINED &&
nextState.roomState === ROOM_STATES.JOINED) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),

View File

@ -52,13 +52,15 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("rooms_unavailable_notification_message");
default:
return mozL10n.get("status_error");
};
}
},
_renderContent: function() {
switch(this.props.roomState) {
case ROOM_STATES.INIT:
case ROOM_STATES.READY: {
case ROOM_STATES.READY:
case ROOM_STATES.ENDED: {
// XXX: In ENDED state, we should rather display the feedback form.
return (
<button className="btn btn-join btn-info"
onClick={this.props.joinRoom}>
@ -219,13 +221,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
},
/**
* Watches for when we transition from READY to JOINED room state, so we can
* request user media access.
* Watches for when we transition to JOINED room state, so we can request
* user media access.
*
* @param {Object} nextProps (Unused)
* @param {Object} nextState Next state object.
*/
componentWillUpdate: function(nextProps, nextState) {
if (this.state.roomState === ROOM_STATES.READY &&
if (this.state.roomState !== ROOM_STATES.JOINED &&
nextState.roomState === ROOM_STATES.JOINED) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this._getPublisherConfig(),

View File

@ -270,7 +270,80 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
/**
* A view for when conversations are pending, displays any messages
* and an option cancel button.
*/
var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
propTypes: {
callState: React.PropTypes.string.isRequired,
// If not supplied, the cancel button is not displayed.
cancelCallback: React.PropTypes.func
},
render: function() {
var cancelButtonClasses = React.addons.classSet({
btn: true,
"btn-large": true,
"btn-cancel": true,
hide: !this.props.cancelCallback
});
return (
React.DOM.div({className: "container"},
React.DOM.div({className: "container-box"},
React.DOM.header({className: "pending-header header-box"},
ConversationBranding(null)
),
React.DOM.div({id: "cameraPreview"}),
React.DOM.div({id: "messages"}),
React.DOM.p({className: "standalone-btn-label"},
this.props.callState
),
React.DOM.div({className: "btn-pending-cancel-group btn-group"},
React.DOM.div({className: "flex-padding-1"}),
React.DOM.button({className: cancelButtonClasses,
onClick: this.props.cancelCallback},
React.DOM.span({className: "standalone-call-btn-text"},
mozL10n.get("initiate_call_cancel_button")
)
),
React.DOM.div({className: "flex-padding-1"})
)
),
ConversationFooter(null)
)
);
}
});
/**
* View displayed whilst the get user media prompt is being displayed. Indicates
* to the user to accept the prompt.
*/
var GumPromptConversationView = React.createClass({displayName: 'GumPromptConversationView',
render: function() {
var callState = mozL10n.get("call_progress_getting_media_description", {
clientShortname: mozL10n.get("clientShortname2")
});
document.title = mozL10n.get("standalone_title_with_status", {
clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("call_progress_getting_media_title")
});
return PendingConversationView({callState: callState});
}
});
/**
* View displayed waiting for a call to be connected. Updates the display
* once the websocket shows that the callee is being alerted.
*/
var WaitingConversationView = React.createClass({displayName: 'WaitingConversationView',
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
@ -306,33 +379,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get(callStateStringEntityName)});
return (
React.DOM.div({className: "container"},
React.DOM.div({className: "container-box"},
React.DOM.header({className: "pending-header header-box"},
ConversationBranding(null)
),
React.DOM.div({id: "cameraPreview"}),
React.DOM.div({id: "messages"}),
React.DOM.p({className: "standalone-btn-label"},
callState
),
React.DOM.div({className: "btn-pending-cancel-group btn-group"},
React.DOM.div({className: "flex-padding-1"}),
React.DOM.button({className: "btn btn-large btn-cancel",
onClick: this._cancelOutgoingCall},
React.DOM.span({className: "standalone-call-btn-text"},
mozL10n.get("initiate_call_cancel_button")
)
),
React.DOM.div({className: "flex-padding-1"})
)
),
ConversationFooter(null)
PendingConversationView({
callState: callState,
cancelCallback: this._cancelOutgoingCall}
)
);
}
@ -458,15 +509,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/
startCall: function(callType) {
return function() {
multiplexGum.getPermsAndCacheMedia({audio:true, video:true},
function(localStream) {
this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true});
}.bind(this),
function(errorCode) {
multiplexGum.reset();
}.bind(this)
);
this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true});
}.bind(this);
},
@ -627,6 +671,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
componentDidMount: function() {
this.props.conversation.on("call:outgoing", this.startCall, this);
this.props.conversation.on("call:outgoing:get-media-privs", this.getMediaPrivs, this);
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
@ -674,8 +719,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
)
);
}
case "gumPrompt": {
return GumPromptConversationView(null);
}
case "pending": {
return PendingConversationView({websocket: this._websocket});
return WaitingConversationView({websocket: this._websocket});
}
case "connected": {
document.title = mozL10n.get("standalone_title_with_status",
@ -774,6 +822,22 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
},
/**
* Asks the user for the media privileges, handling the result appropriately.
*/
getMediaPrivs: function() {
this.setState({callStatus: "gumPrompt"});
multiplexGum.getPermsAndCacheMedia({audio:true, video:true},
function(localStream) {
this.props.conversation.gotMediaPrivs();
}.bind(this),
function(errorCode) {
multiplexGum.reset();
this.setState({callStatus: "failure"});
}.bind(this)
);
},
/**
* Actually starts the call.
*/
@ -866,6 +930,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Handles ending a call by resetting the view to the start state.
*/
_endCall: function() {
multiplexGum.reset();
if (this.state.callStatus !== "failure") {
this.setState({callStatus: "end"});
}
@ -1050,6 +1116,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
return {
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
GumPromptConversationView: GumPromptConversationView,
WaitingConversationView: WaitingConversationView,
StartConversationView: StartConversationView,
FailedConversationView: FailedConversationView,
OutgoingConversationView: OutgoingConversationView,

View File

@ -270,7 +270,80 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
});
/**
* A view for when conversations are pending, displays any messages
* and an option cancel button.
*/
var PendingConversationView = React.createClass({
propTypes: {
callState: React.PropTypes.string.isRequired,
// If not supplied, the cancel button is not displayed.
cancelCallback: React.PropTypes.func
},
render: function() {
var cancelButtonClasses = React.addons.classSet({
btn: true,
"btn-large": true,
"btn-cancel": true,
hide: !this.props.cancelCallback
});
return (
<div className="container">
<div className="container-box">
<header className="pending-header header-box">
<ConversationBranding />
</header>
<div id="cameraPreview" />
<div id="messages" />
<p className="standalone-btn-label">
{this.props.callState}
</p>
<div className="btn-pending-cancel-group btn-group">
<div className="flex-padding-1" />
<button className={cancelButtonClasses}
onClick={this.props.cancelCallback} >
<span className="standalone-call-btn-text">
{mozL10n.get("initiate_call_cancel_button")}
</span>
</button>
<div className="flex-padding-1" />
</div>
</div>
<ConversationFooter />
</div>
);
}
});
/**
* View displayed whilst the get user media prompt is being displayed. Indicates
* to the user to accept the prompt.
*/
var GumPromptConversationView = React.createClass({
render: function() {
var callState = mozL10n.get("call_progress_getting_media_description", {
clientShortname: mozL10n.get("clientShortname2")
});
document.title = mozL10n.get("standalone_title_with_status", {
clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("call_progress_getting_media_title")
});
return <PendingConversationView callState={callState}/>;
}
});
/**
* View displayed waiting for a call to be connected. Updates the display
* once the websocket shows that the callee is being alerted.
*/
var WaitingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
@ -306,34 +379,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get(callStateStringEntityName)});
return (
<div className="container">
<div className="container-box">
<header className="pending-header header-box">
<ConversationBranding />
</header>
<div id="cameraPreview" />
<div id="messages" />
<p className="standalone-btn-label">
{callState}
</p>
<div className="btn-pending-cancel-group btn-group">
<div className="flex-padding-1" />
<button className="btn btn-large btn-cancel"
onClick={this._cancelOutgoingCall} >
<span className="standalone-call-btn-text">
{mozL10n.get("initiate_call_cancel_button")}
</span>
</button>
<div className="flex-padding-1" />
</div>
</div>
<ConversationFooter />
</div>
<PendingConversationView
callState={callState}
cancelCallback={this._cancelOutgoingCall}
/>
);
}
});
@ -458,15 +509,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/
startCall: function(callType) {
return function() {
multiplexGum.getPermsAndCacheMedia({audio:true, video:true},
function(localStream) {
this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true});
}.bind(this),
function(errorCode) {
multiplexGum.reset();
}.bind(this)
);
this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true});
}.bind(this);
},
@ -627,6 +671,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
componentDidMount: function() {
this.props.conversation.on("call:outgoing", this.startCall, this);
this.props.conversation.on("call:outgoing:get-media-privs", this.getMediaPrivs, this);
this.props.conversation.on("call:outgoing:setup", this.setupOutgoingCall, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
@ -674,8 +719,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
/>
);
}
case "gumPrompt": {
return <GumPromptConversationView />;
}
case "pending": {
return <PendingConversationView websocket={this._websocket} />;
return <WaitingConversationView websocket={this._websocket} />;
}
case "connected": {
document.title = mozL10n.get("standalone_title_with_status",
@ -774,6 +822,22 @@ loop.webapp = (function($, _, OT, mozL10n) {
}
},
/**
* Asks the user for the media privileges, handling the result appropriately.
*/
getMediaPrivs: function() {
this.setState({callStatus: "gumPrompt"});
multiplexGum.getPermsAndCacheMedia({audio:true, video:true},
function(localStream) {
this.props.conversation.gotMediaPrivs();
}.bind(this),
function(errorCode) {
multiplexGum.reset();
this.setState({callStatus: "failure"});
}.bind(this)
);
},
/**
* Actually starts the call.
*/
@ -866,6 +930,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Handles ending a call by resetting the view to the start state.
*/
_endCall: function() {
multiplexGum.reset();
if (this.state.callStatus !== "failure") {
this.setState({callStatus: "end"});
}
@ -1050,6 +1116,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
return {
CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView,
GumPromptConversationView: GumPromptConversationView,
WaitingConversationView: WaitingConversationView,
StartConversationView: StartConversationView,
FailedConversationView: FailedConversationView,
OutgoingConversationView: OutgoingConversationView,

View File

@ -58,6 +58,8 @@ vendor_alttext={{vendorShortname}} logo
## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
call_url_creation_date_label=(from {{call_url_creation_date}})
call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
call_progress_getting_media_title=Waiting for media…
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.

View File

@ -510,6 +510,22 @@ describe("loop.conversationViews", function () {
loop.shared.views.FeedbackView);
});
it("should play the terminated sound when the call state is 'finished'",
function() {
var fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
store.set({callState: CALL_STATES.FINISHED});
view = mountTestComponent();
sinon.assert.calledOnce(fakeAudio.play);
});
it("should update the rendered views when the state is changed.",
function() {
store.set({

View File

@ -199,7 +199,10 @@ describe("loop.roomViews", function () {
return TestUtils.renderIntoDocument(
new loop.roomViews.DesktopRoomConversationView({
dispatcher: dispatcher,
roomStore: roomStore
roomStore: roomStore,
feedbackStore: new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
})
}));
}
@ -316,6 +319,16 @@ describe("loop.roomViews", function () {
TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomConversationView);
});
it("should render the FeedbackView if roomState is `ENDED`",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.shared.views.FeedbackView);
});
});
});
});

View File

@ -574,10 +574,10 @@ describe("loop.store.ActiveRoomStore", function () {
"fakeToken", "1627384950");
});
it("should set the state to ready", function() {
it("should set the state to ENDED", function() {
store.windowUnload();
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
expect(store._storeState.roomState).eql(ROOM_STATES.ENDED);
});
});
@ -619,10 +619,10 @@ describe("loop.store.ActiveRoomStore", function () {
"fakeToken", "1627384950");
});
it("should set the state to ready", function() {
it("should set the state to ENDED", function() {
store.leaveRoom();
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
expect(store._storeState.roomState).eql(ROOM_STATES.ENDED);
});
});

View File

@ -16,28 +16,15 @@ var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
describe("loop.shared.views.FeedbackView", function() {
"use strict";
var sandbox, comp, dispatcher, feedbackStore, fakeAudioXHR, fakeFeedbackClient;
var sandbox, comp, dispatcher, fakeFeedbackClient, feedbackStore;
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
getResponseHeader: function(header) {
if (header === "Content-Type")
return "audio/ogg";
},
responseType: null,
response: new ArrayBuffer(10),
onload: null
};
dispatcher = new loop.Dispatcher();
fakeFeedbackClient = {send: sandbox.stub()};
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: fakeFeedbackClient
});
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({
feedbackStore: feedbackStore
}));

View File

@ -91,12 +91,22 @@ describe("loop.shared.models", function() {
expect(conversation.get("selectedCallType")).eql("audio-video");
});
it("should trigger a `call:outgoing:get-media-privs` event", function(done) {
conversation.once("call:outgoing:get-media-privs", function() {
done();
});
conversation.setupOutgoingCall();
});
});
describe("#gotMediaPrivs", function() {
it("should trigger a `call:outgoing:setup` event", function(done) {
conversation.once("call:outgoing:setup", function() {
done();
});
conversation.setupOutgoingCall();
conversation.gotMediaPrivs();
});
});

View File

@ -38,27 +38,42 @@ describe("loop.standaloneRoomViews", function() {
}));
}
function expectActionDispatched(view) {
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
return value.getLocalElementFunc() ===
view.getDOMNode().querySelector(".local");
}));
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
return value.getRemoteElementFunc() ===
view.getDOMNode().querySelector(".remote");
}));
}
describe("#componentWillUpdate", function() {
it("dispatch an `SetupStreamElements` action on room joined", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
var view = mountTestComponent();
it("should dispatch a `SetupStreamElements` action on room joined",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
var view = mountTestComponent();
sinon.assert.notCalled(dispatch);
sinon.assert.notCalled(dispatch);
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
return value.getLocalElementFunc() ===
view.getDOMNode().querySelector(".local");
}));
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
return value.getRemoteElementFunc() ===
view.getDOMNode().querySelector(".remote");
}));
});
expectActionDispatched(view);
});
it("should dispatch a `SetupStreamElements` action on room rejoined",
function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
var view = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
expectActionDispatched(view);
});
});
describe("#publishStream", function() {

View File

@ -369,6 +369,16 @@ describe("loop.webapp", function() {
});
describe("session:ended", function() {
it("should call multiplexGum.reset", function() {
var multiplexGum = new standaloneMedia._MultiplexGum();
standaloneMedia.setSingleton(multiplexGum);
sandbox.stub(standaloneMedia._MultiplexGum.prototype, "reset");
conversation.trigger("session:ended");
sinon.assert.calledOnce(multiplexGum.reset);
});
it("should display the StartConversationView", function() {
conversation.trigger("session:ended");
@ -474,14 +484,14 @@ describe("loop.webapp", function() {
});
it("should display the FailedConversationView", function() {
conversation.setupOutgoingCall();
ocView.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.FailedConversationView);
});
it("should display an error", function() {
conversation.setupOutgoingCall();
ocView.setupOutgoingCall();
sinon.assert.calledOnce(notifications.errorL10n);
});
@ -494,7 +504,8 @@ describe("loop.webapp", function() {
it("should call requestCallInfo on the client",
function() {
conversation.setupOutgoingCall("audio-video");
conversation.set("selectedCallType", "audio-video");
ocView.setupOutgoingCall();
sinon.assert.calledOnce(client.requestCallInfo);
sinon.assert.calledWith(client.requestCallInfo, "fakeToken",
@ -506,7 +517,7 @@ describe("loop.webapp", function() {
function() {
client.requestCallInfo.callsArgWith(2, {errno: 105});
conversation.setupOutgoingCall();
ocView.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.CallUrlExpiredView);
@ -516,7 +527,7 @@ describe("loop.webapp", function() {
function() {
client.requestCallInfo.callsArgWith(2, {errno: 104});
conversation.setupOutgoingCall();
ocView.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.FailedConversationView);
@ -525,7 +536,7 @@ describe("loop.webapp", function() {
it("should notify the user on any other error", function() {
client.requestCallInfo.callsArgWith(2, {errno: 104});
conversation.setupOutgoingCall();
ocView.setupOutgoingCall();
sinon.assert.calledOnce(notifications.errorL10n);
});
@ -534,7 +545,7 @@ describe("loop.webapp", function() {
"are successfully received", function() {
client.requestCallInfo.callsArgWith(2, null, fakeSessionData);
conversation.setupOutgoingCall();
ocView.setupOutgoingCall();
sinon.assert.calledOnce(conversation.outgoing);
sinon.assert.calledWithExactly(conversation.outgoing, fakeSessionData);
@ -542,6 +553,52 @@ describe("loop.webapp", function() {
});
});
});
describe("getMediaPrivs", function() {
var multiplexGum;
beforeEach(function() {
multiplexGum = new standaloneMedia._MultiplexGum();
standaloneMedia.setSingleton(multiplexGum);
sandbox.stub(standaloneMedia._MultiplexGum.prototype, "reset");
sandbox.stub(conversation, "gotMediaPrivs");
});
it("should call getPermsAndCacheMedia", function() {
conversation.trigger("call:outgoing:get-media-privs");
sinon.assert.calledOnce(stubGetPermsAndCacheMedia);
});
it("should call gotMediaPrevs on the model when successful", function() {
stubGetPermsAndCacheMedia.callsArgWith(1, {});
conversation.trigger("call:outgoing:get-media-privs");
sinon.assert.calledOnce(conversation.gotMediaPrivs);
});
it("should call multiplexGum.reset when getPermsAndCacheMedia fails",
function() {
stubGetPermsAndCacheMedia.callsArgWith(2, "FAKE_ERROR");
conversation.trigger("call:outgoing:get-media-privs");
sinon.assert.calledOnce(multiplexGum.reset);
});
it("should set state to `failure` when getPermsAndCacheMedia fails",
function() {
stubGetPermsAndCacheMedia.callsArgWith(2, "FAKE_ERROR");
conversation.trigger("call:outgoing:get-media-privs");
expect(ocView.state.callStatus).eql("failure");
});
});
});
describe("FailedConversationView", function() {
@ -693,7 +750,7 @@ describe("loop.webapp", function() {
});
});
describe("PendingConversationView", function() {
describe("WaitingConversationView", function() {
var view, websocket, fakeAudio;
beforeEach(function() {
@ -713,7 +770,7 @@ describe("loop.webapp", function() {
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.PendingConversationView({
loop.webapp.WaitingConversationView({
websocket: websocket
})
);
@ -802,27 +859,8 @@ describe("loop.webapp", function() {
client: standaloneClientStub
})
);
// default to succeeding with a null local media object
stubGetPermsAndCacheMedia.callsArgWith(1, {});
});
it("should fire multiplexGum.reset when getPermsAndCacheMedia calls" +
" back an error",
function() {
var setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
var multiplexGum = new standaloneMedia._MultiplexGum();
standaloneMedia.setSingleton(multiplexGum);
sandbox.stub(standaloneMedia._MultiplexGum.prototype, "reset");
stubGetPermsAndCacheMedia.callsArgWith(2, "FAKE_ERROR");
var button = view.getDOMNode().querySelector(".btn-accept");
React.addons.TestUtils.Simulate.click(button);
sinon.assert.calledOnce(multiplexGum.reset);
sinon.assert.calledWithExactly(multiplexGum.reset);
});
it("should start the audio-video conversation establishment process",
function() {
var setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
@ -1057,22 +1095,6 @@ describe("loop.webapp", function() {
it("should render a FeedbackView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
});
describe("#componentDidMount", function() {
it("should play a terminating sound, once", function() {
fakeAudioXHR.onload();
sinon.assert.called(fakeAudioXHR.open);
sinon.assert.calledWithExactly(
fakeAudioXHR.open, "GET", "shared/sounds/terminated.ogg", true);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.not.equal(true);
});
});
});
describe("PromoteFirefoxView", function() {

View File

@ -49,12 +49,14 @@ var fakeRooms = [
*/
navigator.mozLoop = {
ensureRegistered: function() {},
getAudioBlob: function(){},
getLoopPref: function(pref) {
// Ensure UI for rooms is displayed in the showcase.
if (pref === "rooms.enabled") {
return true;
}
},
setLoopPref: function(){},
releaseCallData: function() {},
copyString: function() {},
contacts: {

View File

@ -28,7 +28,8 @@
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var GumPromptConversationView = loop.webapp.GumPromptConversationView;
var WaitingConversationView = loop.webapp.WaitingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
@ -128,7 +129,7 @@
"audio", "audio-hover", "audio-active", "block",
"block-red", "block-hover", "block-active", "contacts", "contacts-hover",
"contacts-active", "copy", "checkmark", "google", "google-hover",
"google-active", "history", "history-hover", "history-active",
"google-active", "history", "history-hover", "history-active", "leave",
"precall", "precall-hover", "precall-active", "settings", "settings-hover",
"settings-active", "tag", "tag-hover", "tag-active", "trash", "unblock",
"unblock-hover", "unblock-active", "video", "video-hover", "video-active"
@ -326,16 +327,24 @@
)
),
Section({name: "PendingConversationView"},
Example({summary: "Pending conversation view (connecting)", dashed: "true"},
Section({name: "GumPromptConversationView"},
Example({summary: "Gum Prompt conversation view", dashed: "true"},
React.DOM.div({className: "standalone"},
PendingConversationView({websocket: mockWebSocket,
GumPromptConversationView(null)
)
)
),
Section({name: "WaitingConversationView"},
Example({summary: "Waiting conversation view (connecting)", dashed: "true"},
React.DOM.div({className: "standalone"},
WaitingConversationView({websocket: mockWebSocket,
dispatcher: dispatcher})
)
),
Example({summary: "Pending conversation view (ringing)", dashed: "true"},
Example({summary: "Waiting conversation view (ringing)", dashed: "true"},
React.DOM.div({className: "standalone"},
PendingConversationView({websocket: mockWebSocket,
WaitingConversationView({websocket: mockWebSocket,
dispatcher: dispatcher,
callState: "ringing"})
)

View File

@ -28,7 +28,8 @@
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var GumPromptConversationView = loop.webapp.GumPromptConversationView;
var WaitingConversationView = loop.webapp.WaitingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
@ -128,7 +129,7 @@
"audio", "audio-hover", "audio-active", "block",
"block-red", "block-hover", "block-active", "contacts", "contacts-hover",
"contacts-active", "copy", "checkmark", "google", "google-hover",
"google-active", "history", "history-hover", "history-active",
"google-active", "history", "history-hover", "history-active", "leave",
"precall", "precall-hover", "precall-active", "settings", "settings-hover",
"settings-active", "tag", "tag-hover", "tag-active", "trash", "unblock",
"unblock-hover", "unblock-active", "video", "video-hover", "video-active"
@ -326,16 +327,24 @@
</div>
</Section>
<Section name="PendingConversationView">
<Example summary="Pending conversation view (connecting)" dashed="true">
<Section name="GumPromptConversationView">
<Example summary="Gum Prompt conversation view" dashed="true">
<div className="standalone">
<PendingConversationView websocket={mockWebSocket}
<GumPromptConversationView />
</div>
</Example>
</Section>
<Section name="WaitingConversationView">
<Example summary="Waiting conversation view (connecting)" dashed="true">
<div className="standalone">
<WaitingConversationView websocket={mockWebSocket}
dispatcher={dispatcher} />
</div>
</Example>
<Example summary="Pending conversation view (ringing)" dashed="true">
<Example summary="Waiting conversation view (ringing)" dashed="true">
<div className="standalone">
<PendingConversationView websocket={mockWebSocket}
<WaitingConversationView websocket={mockWebSocket}
dispatcher={dispatcher}
callState="ringing"/>
</div>

View File

@ -1,4 +1,5 @@
[DEFAULT]
skip-if = e10s && os == 'linux' # bug 1104908
subsuite = devtools
support-files =
doc_content_stylesheet.html

View File

@ -6,10 +6,11 @@
package org.mozilla.gecko;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.os.Build;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A collection of constants that pertain to the build and runtime state of the
* application. Typically these are sourced from build-time definitions (see
@ -19,7 +20,9 @@ import android.os.Build;
* See also SysInfo.java, which includes some of the values available from
* nsSystemInfo inside Gecko.
*/
@RobocopTarget
// Normally, we'd annotate with @RobocopTarget. Since AppConstants is compiled
// before RobocopTarget, we instead add o.m.g.AppConstants directly to the
// Proguard configuration.
public class AppConstants {
public static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@";
public static final String MANGLED_ANDROID_PACKAGE_NAME = "@MANGLED_ANDROID_PACKAGE_NAME@";
@ -262,4 +265,11 @@ public class AppConstants {
//#else
false;
//#endif
public static final boolean MOZ_LINKER_EXTRACT =
//#ifdef MOZ_LINKER_EXTRACT
true;
//#else
false;
//#endif
}

View File

@ -88,18 +88,19 @@ public class GeckoThread extends Thread implements GeckoEventListener {
}
private String initGeckoEnvironment() {
// At some point while loading the gecko libs our default locale gets set
// so just save it to locale here and reset it as default after the join
Locale locale = Locale.getDefault();
final Locale locale = Locale.getDefault();
final Context context = GeckoAppShell.getContext();
final Resources res = context.getResources();
if (locale.toString().equalsIgnoreCase("zh_hk")) {
locale = Locale.TRADITIONAL_CHINESE;
Locale.setDefault(locale);
final Locale mappedLocale = Locale.TRADITIONAL_CHINESE;
Locale.setDefault(mappedLocale);
Configuration config = res.getConfiguration();
config.locale = mappedLocale;
res.updateConfiguration(config, null);
}
Context context = GeckoAppShell.getContext();
String resourcePath = "";
Resources res = null;
String[] pluginDirs = null;
try {
pluginDirs = GeckoAppShell.getPluginDirectories();
@ -108,7 +109,6 @@ public class GeckoThread extends Thread implements GeckoEventListener {
}
resourcePath = context.getPackageResourcePath();
res = context.getResources();
GeckoLoader.setupGeckoEnvironment(context, pluginDirs, context.getFilesDir().getPath());
GeckoLoader.loadSQLiteLibs(context, resourcePath);
@ -116,12 +116,6 @@ public class GeckoThread extends Thread implements GeckoEventListener {
GeckoLoader.loadGeckoLibs(context, resourcePath);
GeckoJavaSampler.setLibsLoaded();
Locale.setDefault(locale);
Configuration config = res.getConfiguration();
config.locale = locale;
res.updateConfiguration(config, null);
return resourcePath;
}

View File

@ -79,6 +79,7 @@ endif
JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
ALL_JARS = \
constants.jar \
gecko-R.jar \
gecko-browser.jar \
gecko-mozglue.jar \
@ -151,7 +152,7 @@ classycle_jar := $(topsrcdir)/mobile/android/build/classycle/classycle-1.4.1.jar
# outputs are fresher than the target, preventing a subsequent
# invocation from thinking Proguard's outputs are stale. This is safe
# because Make removes the target file if any recipe command fails.
.proguard.deps: .geckoview.deps $(ALL_JARS)
.proguard.deps: .geckoview.deps $(ALL_JARS) $(topsrcdir)/mobile/android/config/proguard.cfg
$(REPORT_BUILD)
@$(TOUCH) $@
java \
@ -191,14 +192,6 @@ GeneratedJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES)
GeneratedJNIWrappers.cpp: $(ALL_JARS)
$(JAVA) -classpath gecko-mozglue.jar:$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) org.mozilla.gecko.annotationProcessors.AnnotationProcessor $(ALL_JARS)
# Like generated/org/mozilla/fennec_$USERID.
android_package_dir = $(addprefix generated/,$(subst .,/,$(ANDROID_PACKAGE_NAME)))
# These _PP_JAVAFILES are specified in moz.build and defined in
# backend.mk, which is included by config.mk. Therefore this needs to
# be defined after config.mk is included.
PP_JAVAFILES := $(filter-out generated/org/mozilla/gecko/R.java,$(gecko-mozglue_PP_JAVAFILES) $(gecko-browser_PP_JAVAFILES))
manifest := \
AndroidManifest.xml.in \
WebappManifestFragment.xml.frag.in \
@ -210,28 +203,20 @@ PP_TARGETS += manifest
# generates these files into generated/org/mozilla/gecko for
# consumption by the build system and IDEs.
preprocessed := $(addsuffix .in,$(subst generated/org/mozilla/gecko/,,$(filter generated/org/mozilla/gecko/%,$(PP_JAVAFILES))))
# The list in moz.build looks like
# 'preprocessed/org/mozilla/gecko/AppConstants.java'. The list in
# constants_PP_JAVAFILES looks like
# 'generated/preprocessed/org/mozilla/gecko/AppConstants.java'. We
# need to write AppConstants.java.in to
# generated/preprocessed/org/mozilla/gecko.
preprocessed := $(addsuffix .in,$(subst generated/preprocessed/org/mozilla/gecko/,,$(filter generated/preprocessed/org/mozilla/gecko/%,$(constants_PP_JAVAFILES))))
preprocessed_PATH := generated/org/mozilla/gecko
preprocessed_PATH := generated/preprocessed/org/mozilla/gecko
preprocessed_KEEP_PATH := 1
preprocessed_FLAGS := --marker='//\\\#'
PP_TARGETS += preprocessed
# Certain source files have Java package name @ANDROID_PACKAGE_NAME@.
# We hate these files but they are necessary for backwards
# compatibility. These special rules generate these files into
# generated/org/mozilla/{firefox,firefox_beta,fennec,fennec_$USER} for
# consumption by the build system and IDEs.
preprocessed_package := $(addsuffix .in,$(subst $(android_package_dir)/,,$(filter $(android_package_dir)/%,$(PP_JAVAFILES))))
preprocessed_package_PATH := $(android_package_dir)
preprocessed_package_KEEP_PATH := 1
preprocessed_package_FLAGS := --marker='//\\\#'
PP_TARGETS += preprocessed_package
include $(topsrcdir)/config/rules.mk
not_android_res_files := \

View File

@ -6,13 +6,17 @@
package org.mozilla.gecko;
import org.mozilla.gecko.util.HardwareUtils;
import android.os.StrictMode;
import android.util.Log;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.regex.Pattern;
@ -21,27 +25,35 @@ import java.util.regex.Pattern;
* nsSystemInfo. See also the constants in AppConstants, which reflect
* much of nsIXULAppInfo.
*/
// Normally, we'd annotate with @RobocopTarget. Since SysInfo is compiled
// before RobocopTarget, we instead add o.m.g.SysInfo directly to the Proguard
// configuration.
public final class SysInfo {
private static final String LOG_TAG = "GeckoSysInfo";
// Number of bytes of /proc/meminfo to read in one go.
private static final int MEMINFO_BUFFER_SIZE_BYTES = 256;
// We don't mind an instant of possible duplicate work, we only wish to
// avoid inconsistency, so we don't bother with synchronization for
// these.
private static volatile int cpuCount = -1;
/**
* Get the number of cores on the device.
*
* We can't use a nice tidy API call, because they're all wrong:
*
* <http://stackoverflow.com/questions/7962155/how-can-you-detect-a-dual-core-
* cpu-on-an-android-device-from-code>
*
* This method is based on that code.
*
* @return the number of CPU cores, or 1 if the number could not be
* determined.
*/
private static volatile int totalRAM = -1;
/**
* Get the number of cores on the device.
*
* We can't use a nice tidy API call, because they're all wrong:
*
* <http://stackoverflow.com/questions/7962155/how-can-you-detect-a-dual-core-
* cpu-on-an-android-device-from-code>
*
* This method is based on that code.
*
* @return the number of CPU cores, or 1 if the number could not be
* determined.
*/
public static int getCPUCount() {
if (cpuCount > 0) {
return cpuCount;
@ -73,10 +85,93 @@ public final class SysInfo {
}
/**
* Wraps HardwareUtils so callers don't need to know about it.
* Helper functions used to extract key/value data from /proc/meminfo
* Pulled from:
* http://androidxref.com/4.2_r1/xref/frameworks/base/core/java/com/android/internal/util/MemInfoReader.java
*/
private static boolean matchMemText(byte[] buffer, int index, int bufferLength, byte[] text) {
final int N = text.length;
if ((index + N) >= bufferLength) {
return false;
}
for (int i = 0; i < N; i++) {
if (buffer[index + i] != text[i]) {
return false;
}
}
return true;
}
/**
* Parses a line like:
*
* MemTotal: 1605324 kB
*
* into 1605324.
*
* @return the first uninterrupted sequence of digits following the
* specified index, parsed as an integer value in KB.
*/
private static int extractMemValue(byte[] buffer, int offset, int length) {
if (offset >= length) {
return 0;
}
while (offset < length && buffer[offset] != '\n') {
if (buffer[offset] >= '0' && buffer[offset] <= '9') {
int start = offset++;
while (offset < length &&
buffer[offset] >= '0' &&
buffer[offset] <= '9') {
++offset;
}
return Integer.parseInt(new String(buffer, start, offset - start), 10);
}
++offset;
}
return 0;
}
/**
* Fetch the total memory of the device in MB by parsing /proc/meminfo.
*
* Of course, Android doesn't have a neat and tidy way to find total
* RAM, so we do it by parsing /proc/meminfo.
*
* @return 0 if a problem occurred, or memory size in MB.
*/
public static int getMemSize() {
return HardwareUtils.getMemSize();
if (totalRAM >= 0) {
return totalRAM;
}
// This is the string "MemTotal" that we're searching for in the buffer.
final byte[] MEMTOTAL = {'M', 'e', 'm', 'T', 'o', 't', 'a', 'l'};
try {
final byte[] buffer = new byte[MEMINFO_BUFFER_SIZE_BYTES];
final FileInputStream is = new FileInputStream("/proc/meminfo");
try {
final int length = is.read(buffer);
for (int i = 0; i < length; i++) {
if (matchMemText(buffer, i, length, MEMTOTAL)) {
i += 8;
totalRAM = extractMemValue(buffer, i, length) / 1024;
Log.d(LOG_TAG, "System memory: " + totalRAM + "MB.");
return totalRAM;
}
}
} finally {
is.close();
}
Log.w(LOG_TAG, "Did not find MemTotal line in /proc/meminfo.");
return totalRAM = 0;
} catch (FileNotFoundException f) {
return totalRAM = 0;
} catch (IOException e) {
return totalRAM = 0;
}
}
/**

View File

@ -1137,8 +1137,8 @@ sync_java_files = [
]
sync_generated_java_files = [
'org/mozilla/gecko/background/common/GlobalConstants.java',
'org/mozilla/gecko/background/healthreport/HealthReportConstants.java',
'org/mozilla/gecko/fxa/FxAccountConstants.java',
'org/mozilla/gecko/sync/SyncConstants.java',
'background/common/GlobalConstants.java',
'background/healthreport/HealthReportConstants.java',
'fxa/FxAccountConstants.java',
'sync/SyncConstants.java',
]

View File

@ -35,13 +35,13 @@ public class FxAccountAgeLockoutHelper {
// Otherwise, find out how long it's been since we last failed.
long millsecondsSinceLastFailedAgeCheck = elapsedRealtime - ELAPSED_REALTIME_OF_LAST_FAILED_AGE_CHECK;
boolean isLockedOut = millsecondsSinceLastFailedAgeCheck < FxAccountConstants.MINIMUM_TIME_TO_WAIT_AFTER_AGE_CHECK_FAILED_IN_MILLISECONDS;
FxAccountConstants.pii(LOG_TAG, "Checking if locked out: it's been " + millsecondsSinceLastFailedAgeCheck + "ms " +
FxAccountUtils.pii(LOG_TAG, "Checking if locked out: it's been " + millsecondsSinceLastFailedAgeCheck + "ms " +
"since last lockout, so " + (isLockedOut ? "yes." : "no."));
return isLockedOut;
}
public static synchronized void lockOut(long elapsedRealtime) {
FxAccountConstants.pii(LOG_TAG, "Locking out at time: " + elapsedRealtime);
FxAccountUtils.pii(LOG_TAG, "Locking out at time: " + elapsedRealtime);
ELAPSED_REALTIME_OF_LAST_FAILED_AGE_CHECK = Math.max(elapsedRealtime, ELAPSED_REALTIME_OF_LAST_FAILED_AGE_CHECK);
}
@ -60,8 +60,8 @@ public class FxAccountAgeLockoutHelper {
int thisYear = Calendar.getInstance().get(Calendar.YEAR);
int approximateAge = thisYear - yearOfBirth;
boolean oldEnough = approximateAge >= FxAccountConstants.MINIMUM_AGE_TO_CREATE_AN_ACCOUNT;
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
FxAccountConstants.pii(LOG_TAG, "Age check " + (oldEnough ? "passes" : "fails") +
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Age check " + (oldEnough ? "passes" : "fails") +
": age is " + approximateAge + " = " + thisYear + " - " + yearOfBirth);
}
return oldEnough;
@ -79,7 +79,7 @@ public class FxAccountAgeLockoutHelper {
}
if (!Arrays.asList(yearItems).contains(yearText)) {
// This should never happen, but let's be careful.
FxAccountConstants.pii(LOG_TAG, "Failed age check: year text was not found in item list.");
FxAccountUtils.pii(LOG_TAG, "Failed age check: year text was not found in item list.");
return false;
}
Integer yearOfBirth;
@ -88,7 +88,7 @@ public class FxAccountAgeLockoutHelper {
} catch (NumberFormatException e) {
// Any non-numbers in the list are ranges (and we say as much to
// translators in the resource file), so these people pass the age check.
FxAccountConstants.pii(LOG_TAG, "Passed age check: year text was found in item list but was not a number.");
FxAccountUtils.pii(LOG_TAG, "Passed age check: year text was found in item list but was not a number.");
return true;
}
return passesAgeCheck(yearOfBirth);

View File

@ -13,7 +13,6 @@ import java.util.concurrent.Executor;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.net.BaseResource;
@ -206,7 +205,7 @@ public class FxAccountClient20 extends FxAccountClient10 implements FxAccountCli
final RequestDelegate<LoginResponse> delegate) {
byte[] quickStretchedPW;
try {
FxAccountConstants.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" );
FxAccountUtils.pii(LOG_TAG, "Trying user provided email: '" + new String(emailUTF8, "UTF-8") + "'" );
quickStretchedPW = stretcher.getQuickStretchedPW(emailUTF8);
} catch (Exception e) {
delegate.handleError(e);
@ -233,7 +232,7 @@ public class FxAccountClient20 extends FxAccountClient10 implements FxAccountCli
};
Logger.info(LOG_TAG, "Server returned alternate email; retrying login with provided email.");
FxAccountConstants.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" );
FxAccountUtils.pii(LOG_TAG, "Trying server provided email: '" + alternateEmail + "'" );
try {
// Nota bene: this is not recursive, since we call the fixed password

View File

@ -35,6 +35,16 @@ public class FxAccountUtils {
public static final int NUMBER_OF_QUICK_STRETCH_ROUNDS = 1000;
// For extra debugging. Not final so it can be changed from Fennec, or from
// an add-on.
public static boolean LOG_PERSONAL_INFORMATION = false;
public static void pii(String tag, String message) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
Logger.info(tag, "$$FxA PII$$: " + message);
}
}
public static String bytes(String string) throws UnsupportedEncodingException {
return Utils.byte2Hex(string.getBytes("UTF-8"));
}

View File

@ -6,7 +6,6 @@
package org.mozilla.gecko.fxa;
import org.mozilla.gecko.background.common.GlobalConstants;
import org.mozilla.gecko.background.common.log.Logger;
public class FxAccountConstants {
public static final String GLOBAL_LOG_TAG = "FxAccounts";
@ -18,16 +17,6 @@ public class FxAccountConstants {
public static final String STAGE_AUTH_SERVER_ENDPOINT = "https://api-accounts.stage.mozaws.net/v1";
public static final String STAGE_TOKEN_SERVER_ENDPOINT = "https://token.stage.mozaws.net/1.0/sync/1.5";
// For extra debugging. Not final so it can be changed from Fennec, or from
// an add-on.
public static boolean LOG_PERSONAL_INFORMATION = false;
public static void pii(String tag, String message) {
if (LOG_PERSONAL_INFORMATION) {
Logger.info(tag, "$$FxA PII$$: " + message);
}
}
// You must be at least 14 years old to create a Firefox Account.
public static final int MINIMUM_AGE_TO_CREATE_AN_ACCOUNT = 14;

View File

@ -324,7 +324,7 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
}
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
@ -403,9 +403,9 @@ abstract public class FxAccountAbstractSetupActivity extends FxAccountAbstractAc
// This sets defaults as well as extracting from extras, so it's not conditional.
updateServersFromIntentExtras(getIntent());
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
FxAccountConstants.pii(LOG_TAG, "Using auth server: " + authServerEndpoint);
FxAccountConstants.pii(LOG_TAG, "Using sync server: " + syncServerEndpoint);
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Using auth server: " + authServerEndpoint);
FxAccountUtils.pii(LOG_TAG, "Using sync server: " + syncServerEndpoint);
}
updateCustomServerView();

View File

@ -19,8 +19,8 @@ import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.tasks.FxAccountCreateAccountTask;
import android.app.AlertDialog;
@ -250,10 +250,10 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
? selectedEngines
: null;
if (FxAccountAgeLockoutHelper.passesAgeCheck(yearEdit.getText().toString(), yearItems)) {
FxAccountConstants.pii(LOG_TAG, "Passed age check.");
FxAccountUtils.pii(LOG_TAG, "Passed age check.");
createAccount(email, password, engines);
} else {
FxAccountConstants.pii(LOG_TAG, "Failed age check!");
FxAccountUtils.pii(LOG_TAG, "Failed age check!");
FxAccountAgeLockoutHelper.lockOut(SystemClock.elapsedRealtime());
setResult(RESULT_CANCELED);
redirectToActivity(FxAccountCreateAccountNotAllowedActivity.class);
@ -304,7 +304,7 @@ public class FxAccountCreateAccountActivity extends FxAccountAbstractSetupActivi
selectedEngines.put("history", checkedItems[INDEX_HISTORY]);
selectedEngines.put("tabs", checkedItems[INDEX_TABS]);
selectedEngines.put("passwords", checkedItems[INDEX_PASSWORDS]);
FxAccountConstants.pii(LOG_TAG, "Updating selectedEngines: " + selectedEngines.toString());
FxAccountUtils.pii(LOG_TAG, "Updating selectedEngines: " + selectedEngines.toString());
}
};

View File

@ -9,6 +9,7 @@ import java.util.Locale;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountAgeLockoutHelper;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
@ -133,7 +134,7 @@ public class FxAccountGetStartedActivity extends AccountAuthenticatorActivity {
TextView oldFirefox = (TextView) findViewById(R.id.old_firefox);
String text = getResources().getString(R.string.fxaccount_getting_started_old_firefox);
final String url = FirefoxAccounts.getOldSyncUpgradeURL(getResources(), Locale.getDefault());
FxAccountConstants.pii(LOG_TAG, "Old Firefox url is: " + url); // Don't want to leak locale in particular.
FxAccountUtils.pii(LOG_TAG, "Old Firefox url is: " + url); // Don't want to leak locale in particular.
ActivityUtils.linkTextView(oldFirefox, text, url);
}
}

View File

@ -10,6 +10,7 @@ import java.util.Set;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.preferences.PreferenceFragment;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
@ -145,7 +146,7 @@ public class FxAccountStatusFragment
tabsPreference = (CheckBoxPreference) ensureFindPreference("tabs");
passwordsPreference = (CheckBoxPreference) ensureFindPreference("passwords");
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
removeDebugButtons();
} else {
connectDebugButtons();

View File

@ -17,7 +17,6 @@ import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClient
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
@ -156,7 +155,7 @@ public class FxAccountUpdateCredentialsActivity extends FxAccountAbstractSetupAc
fxAccount.requestSync(FirefoxAccounts.FORCE);
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}

View File

@ -516,14 +516,14 @@ public class AndroidFxAccount {
* <b>For debugging only!</b>
*/
public void dump() {
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return;
}
ExtendedJSONObject o = toJSONObject();
ArrayList<String> list = new ArrayList<String>(o.keySet());
Collections.sort(list);
for (String key : list) {
FxAccountConstants.pii(LOG_TAG, key + ": " + o.get(key));
FxAccountUtils.pii(LOG_TAG, key + ": " + o.get(key));
}
}

View File

@ -4,9 +4,9 @@
package org.mozilla.gecko.fxa.login;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
@ -24,19 +24,19 @@ public class Cohabiting extends TokensAndKeysState {
new BaseRequestDelegate<String>(this, delegate) {
@Override
public void handleSuccess(String certificate) {
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
try {
FxAccountConstants.pii(LOG_TAG, "Fetched certificate: " + certificate);
FxAccountUtils.pii(LOG_TAG, "Fetched certificate: " + certificate);
ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
if (c != null) {
FxAccountConstants.pii(LOG_TAG, "Header : " + c.getObject("header"));
FxAccountConstants.pii(LOG_TAG, "Payload : " + c.getObject("payload"));
FxAccountConstants.pii(LOG_TAG, "Signature: " + c.getString("signature"));
FxAccountUtils.pii(LOG_TAG, "Header : " + c.getObject("header"));
FxAccountUtils.pii(LOG_TAG, "Payload : " + c.getObject("payload"));
FxAccountUtils.pii(LOG_TAG, "Signature: " + c.getString("signature"));
} else {
FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
} catch (Exception e) {
FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
}
delegate.handleTransition(new LogMessage("sign succeeded"), new Married(email, uid, sessionToken, kA, kB, keyPair, certificate));

View File

@ -9,7 +9,6 @@ import java.security.NoSuchAlgorithmException;
import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.AccountVerified;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LocalError;
@ -61,10 +60,10 @@ public class Engaged extends State {
byte[] kB;
try {
kB = FxAccountUtils.unwrapkB(unwrapkB, result.wrapkB);
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
FxAccountConstants.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
FxAccountConstants.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
FxAccountConstants.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(kB));
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Fetched kA: " + Utils.byte2Hex(result.kA));
FxAccountUtils.pii(LOG_TAG, "And wrapkB: " + Utils.byte2Hex(result.wrapkB));
FxAccountUtils.pii(LOG_TAG, "Giving kB : " + Utils.byte2Hex(kB));
}
} catch (Exception e) {
delegate.handleTransition(new RemoteError(e), new Separated(email, uid, verified));

View File

@ -17,7 +17,6 @@ import org.json.simple.parser.ParseException;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.ExecuteDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.LogMessage;
import org.mozilla.gecko.sync.ExtendedJSONObject;
@ -61,23 +60,23 @@ public class Married extends TokensAndKeysState {
// invalid-timestamp errors from the token server.
final long expiresAt = JSONWebTokenUtils.DEFAULT_FUTURE_EXPIRES_AT_IN_MILLISECONDS;
String assertion = JSONWebTokenUtils.createAssertion(keyPair.getPrivate(), certificate, audience, issuer, null, expiresAt);
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return assertion;
}
try {
FxAccountConstants.pii(LOG_TAG, "Generated assertion: " + assertion);
FxAccountUtils.pii(LOG_TAG, "Generated assertion: " + assertion);
ExtendedJSONObject a = JSONWebTokenUtils.parseAssertion(assertion);
if (a != null) {
FxAccountConstants.pii(LOG_TAG, "aHeader : " + a.getObject("header"));
FxAccountConstants.pii(LOG_TAG, "aPayload : " + a.getObject("payload"));
FxAccountConstants.pii(LOG_TAG, "aSignature: " + a.getString("signature"));
FxAccountUtils.pii(LOG_TAG, "aHeader : " + a.getObject("header"));
FxAccountUtils.pii(LOG_TAG, "aPayload : " + a.getObject("payload"));
FxAccountUtils.pii(LOG_TAG, "aSignature: " + a.getString("signature"));
String certificate = a.getString("certificate");
if (certificate != null) {
ExtendedJSONObject c = JSONWebTokenUtils.parseCertificate(certificate);
FxAccountConstants.pii(LOG_TAG, "cHeader : " + c.getObject("header"));
FxAccountConstants.pii(LOG_TAG, "cPayload : " + c.getObject("payload"));
FxAccountConstants.pii(LOG_TAG, "cSignature: " + c.getString("signature"));
FxAccountUtils.pii(LOG_TAG, "cHeader : " + c.getObject("header"));
FxAccountUtils.pii(LOG_TAG, "cPayload : " + c.getObject("payload"));
FxAccountUtils.pii(LOG_TAG, "cSignature: " + c.getString("signature"));
// Print the relevant timestamps in sorted order with labels.
HashMap<Long, String> map = new HashMap<Long, String>();
map.put(a.getObject("payload").getLong("iat"), "aiat");
@ -87,16 +86,16 @@ public class Married extends TokensAndKeysState {
ArrayList<Long> values = new ArrayList<Long>(map.keySet());
Collections.sort(values);
for (Long value : values) {
FxAccountConstants.pii(LOG_TAG, map.get(value) + ": " + value);
FxAccountUtils.pii(LOG_TAG, map.get(value) + ": " + value);
}
} else {
FxAccountConstants.pii(LOG_TAG, "Could not parse certificate!");
FxAccountUtils.pii(LOG_TAG, "Could not parse certificate!");
}
} else {
FxAccountConstants.pii(LOG_TAG, "Could not parse assertion!");
FxAccountUtils.pii(LOG_TAG, "Could not parse assertion!");
}
} catch (Exception e) {
FxAccountConstants.pii(LOG_TAG, "Got exception dumping assertion debug info.");
FxAccountUtils.pii(LOG_TAG, "Got exception dumping assertion debug info.");
}
return assertion;
}
@ -107,8 +106,8 @@ public class Married extends TokensAndKeysState {
}
public String getClientState() {
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
FxAccountConstants.pii(LOG_TAG, "Client state: " + this.clientState);
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Client state: " + this.clientState);
}
return this.clientState;
}

View File

@ -8,10 +8,10 @@ import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.DSACryptoImplementation;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.NonObjectJSONException;
@ -135,15 +135,15 @@ public class StateFactory {
}
protected static void logMigration(State from, State to) {
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (!FxAccountUtils.LOG_PERSONAL_INFORMATION) {
return;
}
try {
FxAccountConstants.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString());
FxAccountUtils.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString());
} catch (Exception e) {
Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e);
}
FxAccountConstants.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString());
FxAccountUtils.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString());
}
protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException {

View File

@ -10,6 +10,7 @@ import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
@ -112,7 +113,7 @@ public class FxAccountUpgradeReceiver extends BroadcastReceiver {
try {
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
// For great debugging.
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}
State state = fxAccount.getState();

View File

@ -7,7 +7,7 @@ package org.mozilla.gecko.fxa.sync;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.activities.FxAccountStatusActivity;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
@ -70,7 +70,7 @@ public class FxAccountNotificationManager {
final String title = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_title);
final String text = context.getResources().getString(R.string.fxaccount_sync_sign_in_error_notification_text, state.email);
Logger.info(LOG_TAG, "State " + state.getStateLabel() + " needs action; offering notification with title: " + title);
FxAccountConstants.pii(LOG_TAG, "And text: " + text);
FxAccountUtils.pii(LOG_TAG, "And text: " + text);
final Intent notificationIntent = new Intent(context, FxAccountStatusActivity.class);
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, 0);

View File

@ -17,6 +17,7 @@ import java.util.concurrent.Executors;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.SkewHandler;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
@ -315,7 +316,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void handleSuccess(final TokenServerToken token) {
FxAccountConstants.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
FxAccountUtils.pii(LOG_TAG, "Got token! uid is " + token.uid + " and endpoint is " + token.endpoint + ".");
if (!didReceiveBackoff) {
// We must be OK to touch this token server.
@ -358,9 +359,9 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
FxAccountGlobalSession globalSession = null;
try {
final ClientsDataDelegate clientsDataDelegate = new SharedPreferencesClientsDataDelegate(sharedPrefs, getContext());
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
FxAccountConstants.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'.");
FxAccountConstants.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp());
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
FxAccountUtils.pii(LOG_TAG, "Client device name is: '" + clientsDataDelegate.getClientName() + "'.");
FxAccountUtils.pii(LOG_TAG, "Client device data last modified: " + clientsDataDelegate.getLastModifiedTimestamp());
}
// We compute skew over time using SkewHandler. This yields an unchanging
@ -444,7 +445,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
Logger.info(LOG_TAG, "Account last synced at: " + fxAccount.getLastSyncedTimestamp());
if (FxAccountConstants.LOG_PERSONAL_INFORMATION) {
if (FxAccountUtils.LOG_PERSONAL_INFORMATION) {
fxAccount.dump();
}

View File

@ -40,6 +40,7 @@ show allResults
org.mozilla.gecko.PrefsHelper \
org.mozilla.gecko.SmsManager \
org.mozilla.gecko.SurfaceBits \
org.mozilla.gecko.SysInfo \
org.mozilla.gecko.TouchEventInterceptor \
org.mozilla.gecko.ZoomConstraints

View File

@ -11,6 +11,14 @@ include('android-services.mozbuild')
thirdparty_source_dir = TOPSRCDIR + '/mobile/android/thirdparty/'
constants_jar = add_java_jar('constants')
constants_jar.sources = []
constants_jar.generated_sources = [
'preprocessed/org/mozilla/gecko/AppConstants.java',
'preprocessed/org/mozilla/gecko/SysInfo.java',
]
constants_jar.generated_sources += ['preprocessed/org/mozilla/gecko/' + f for f in sync_generated_java_files]
resjar = add_java_jar('gecko-R')
resjar.sources = []
resjar.generated_sources += [
@ -29,6 +37,7 @@ mgjar.sources += [
'mozglue/ByteBufferInputStream.java',
'mozglue/ContextUtils.java',
'mozglue/DirectBufferAllocator.java',
'mozglue/GeckoLoader.java',
'mozglue/generatorannotations/OptionalGeneratedParameter.java',
'mozglue/generatorannotations/WrapElementForJNI.java',
'mozglue/generatorannotations/WrapEntireClassForJNI.java',
@ -38,9 +47,9 @@ mgjar.sources += [
'mozglue/RobocopTarget.java',
'mozglue/WebRTCJNITarget.java',
]
mgjar.generated_sources += [
'org/mozilla/gecko/AppConstants.java',
'org/mozilla/gecko/mozglue/GeckoLoader.java',
mgjar.generated_sources = [] # Keep it this way.
mgjar.extra_jars += [
'constants.jar',
]
mgjar.javac_flags += ['-Xlint:all']
@ -77,7 +86,8 @@ gujar.sources += [
'util/WebActivityMapper.java',
]
gujar.extra_jars = [
'gecko-mozglue.jar'
'constants.jar',
'gecko-mozglue.jar',
]
gujar.javac_flags += ['-Xlint:all,-deprecation']
@ -480,22 +490,28 @@ gbjar.sources += [
'widget/TwoWayView.java',
'ZoomConstraints.java',
]
# The following sources are checked in to version control but
# generated by a script (widget/generate_themed_views.py). If you're
# editing this list, make sure to edit that script.
gbjar.sources += [
'widget/ThemedEditText.java',
'widget/ThemedImageButton.java',
'widget/ThemedImageView.java',
'widget/ThemedLinearLayout.java',
'widget/ThemedRelativeLayout.java',
'widget/ThemedTextSwitcher.java',
'widget/ThemedTextView.java',
'widget/ThemedView.java',
]
gbjar.sources += [ thirdparty_source_dir + f for f in [
'com/googlecode/eyesfree/braille/selfbraille/ISelfBrailleService.java',
'com/googlecode/eyesfree/braille/selfbraille/SelfBrailleClient.java',
'com/googlecode/eyesfree/braille/selfbraille/WriteData.java',
] ]
android_package_dir = CONFIG['ANDROID_PACKAGE_NAME'].replace('.', '/')
gbjar.generated_sources += [
'org/mozilla/gecko/SysInfo.java',
'org/mozilla/gecko/widget/ThemedEditText.java',
'org/mozilla/gecko/widget/ThemedImageButton.java',
'org/mozilla/gecko/widget/ThemedImageView.java',
'org/mozilla/gecko/widget/ThemedLinearLayout.java',
'org/mozilla/gecko/widget/ThemedRelativeLayout.java',
'org/mozilla/gecko/widget/ThemedTextSwitcher.java',
'org/mozilla/gecko/widget/ThemedTextView.java',
'org/mozilla/gecko/widget/ThemedView.java',
gbjar.generated_sources = [] # Keep it this way.
gbjar.extra_jars += [
'constants.jar'
]
if CONFIG['MOZ_CRASHREPORTER']:
gbjar.sources += [ 'CrashReporter.java' ]
@ -534,8 +550,7 @@ if CONFIG['MOZ_ANDROID_NEW_TABLET_UI'] and max_sdk_version >= 11:
ANDROID_RES_DIRS += [ SRCDIR + '/newtablet/res' ]
gbjar.sources += sync_java_files
gbjar.generated_sources += sync_generated_java_files
gbjar.extra_jars = [
gbjar.extra_jars += [
'gecko-R.jar',
'gecko-mozglue.jar',
'gecko-thirdparty.jar',
@ -681,6 +696,7 @@ if CONFIG['MOZ_ANDROID_SEARCH_ACTIVITY']:
search_activity.sources += [search_source_dir + '/' + f for f in search_activity_sources]
search_activity.javac_flags += ['-Xlint:all']
search_activity.extra_jars = [
'constants.jar',
'gecko-R.jar',
'gecko-browser.jar',
'gecko-mozglue.jar',
@ -778,11 +794,8 @@ else:
# Manifest.java for all packages) but also preprocessed files. In the past, the
# generated R.java files were used by the Eclipse build, but now Eclipse
# generates these files itself. Therefore, we exclude those generated sources.
main.add_classpathentry('generated', OBJDIR + '/generated',
dstdir='generated',
exclude_patterns=[
'**/R.java',
'**/Manifest.java'])
main.add_classpathentry('generated', OBJDIR + '/generated/preprocessed',
dstdir='generated')
main.add_classpathentry('thirdparty', TOPSRCDIR + '/mobile/android/thirdparty',
dstdir='thirdparty',
ignore_warnings=True)

View File

@ -1,4 +1,3 @@
//#filter substitution
/* -*- 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
@ -22,15 +21,12 @@ import android.os.Build;
import android.os.Environment;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
public final class GeckoLoader {
private static final String LOGTAG = "GeckoLoader";
// These match AppConstants, but we're built earlier.
private static final String ANDROID_PACKAGE_NAME = "@ANDROID_PACKAGE_NAME@";
private static final String MOZ_APP_ABI = "@MOZ_APP_ABI@";
private static volatile SafeIntent sIntent;
private static File sCacheFile;
private static File sGREDir;
@ -228,16 +224,16 @@ public final class GeckoLoader {
}
}
//#ifdef MOZ_LINKER_EXTRACT
putenv("MOZ_LINKER_EXTRACT=1");
// Ensure that the cache dir is world-writable
File cacheDir = new File(linkerCache);
if (cacheDir.isDirectory()) {
cacheDir.setWritable(true, false);
cacheDir.setExecutable(true, false);
cacheDir.setReadable(true, false);
if (AppConstants.MOZ_LINKER_EXTRACT) {
putenv("MOZ_LINKER_EXTRACT=1");
// Ensure that the cache dir is world-writable
File cacheDir = new File(linkerCache);
if (cacheDir.isDirectory()) {
cacheDir.setWritable(true, false);
cacheDir.setExecutable(true, false);
cacheDir.setReadable(true, false);
}
}
//#endif
}
@RobocopTarget
@ -357,15 +353,17 @@ public final class GeckoLoader {
}
private static String getLoadDiagnostics(final Context context, final String lib) {
final String androidPackageName = context.getPackageName();
final StringBuilder message = new StringBuilder("LOAD ");
message.append(lib);
// These might differ. If so, we know why the library won't load!
message.append(": ABI: " + MOZ_APP_ABI + ", " + getCPUABI());
message.append(": ABI: " + AppConstants.MOZ_APP_ABI + ", " + getCPUABI());
message.append(": Data: " + context.getApplicationInfo().dataDir);
try {
final boolean appLibExists = new File("/data/app-lib/" + ANDROID_PACKAGE_NAME + "/lib" + lib + ".so").exists();
final boolean dataDataExists = new File("/data/data/" + ANDROID_PACKAGE_NAME + "/lib/lib" + lib + ".so").exists();
final boolean appLibExists = new File("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so").exists();
final boolean dataDataExists = new File("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so").exists();
message.append(", ax=" + appLibExists);
message.append(", ddx=" + dataDataExists);
} catch (Throwable e) {
@ -373,8 +371,8 @@ public final class GeckoLoader {
}
try {
final String dashOne = "/data/data/" + ANDROID_PACKAGE_NAME + "-1";
final String dashTwo = "/data/data/" + ANDROID_PACKAGE_NAME + "-2";
final String dashOne = "/data/data/" + androidPackageName + "-1";
final String dashTwo = "/data/data/" + androidPackageName + "-2";
final boolean dashOneExists = new File(dashOne).exists();
final boolean dashTwoExists = new File(dashTwo).exists();
message.append(", -1x=" + dashOneExists);
@ -475,12 +473,13 @@ public final class GeckoLoader {
}
// Attempt 4: use /data/app-lib directly. This is a last-ditch effort.
if (attemptLoad("/data/app-lib/" + ANDROID_PACKAGE_NAME + "/lib" + lib + ".so")) {
final String androidPackageName = context.getPackageName();
if (attemptLoad("/data/app-lib/" + androidPackageName + "/lib" + lib + ".so")) {
return;
}
// Attempt 5: even more optimistic.
if (attemptLoad("/data/data/" + ANDROID_PACKAGE_NAME + "/lib/lib" + lib + ".so")) {
if (attemptLoad("/data/data/" + androidPackageName + "/lib/lib" + lib + ".so")) {
return;
}

View File

@ -5,9 +5,7 @@
package org.mozilla.gecko.util;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.mozilla.gecko.SysInfo;
import android.content.Context;
import android.content.pm.PackageManager;
@ -27,14 +25,10 @@ public final class HardwareUtils {
// reading list capabilities in HomePager.
private static final int LOW_MEMORY_THRESHOLD_MB = 384;
// Number of bytes of /proc/meminfo to read in one go.
private static final int MEMINFO_BUFFER_SIZE_BYTES = 256;
private static final boolean IS_AMAZON_DEVICE = Build.MANUFACTURER.equalsIgnoreCase("Amazon");
public static final boolean IS_KINDLE_DEVICE = IS_AMAZON_DEVICE &&
(Build.MODEL.equals("Kindle Fire") ||
Build.MODEL.startsWith("KF"));
private static volatile int sTotalRAM = -1;
private static volatile boolean sInited;
@ -99,95 +93,8 @@ public final class HardwareUtils {
return sHasMenuButton;
}
/**
* Helper functions used to extract key/value data from /proc/meminfo
* Pulled from:
* http://androidxref.com/4.2_r1/xref/frameworks/base/core/java/com/android/internal/util/MemInfoReader.java
*/
private static boolean matchMemText(byte[] buffer, int index, int bufferLength, byte[] text) {
final int N = text.length;
if ((index + N) >= bufferLength) {
return false;
}
for (int i = 0; i < N; i++) {
if (buffer[index + i] != text[i]) {
return false;
}
}
return true;
}
/**
* Parses a line like:
*
* MemTotal: 1605324 kB
*
* into 1605324.
*
* @return the first uninterrupted sequence of digits following the
* specified index, parsed as an integer value in KB.
*/
private static int extractMemValue(byte[] buffer, int offset, int length) {
if (offset >= length) {
return 0;
}
while (offset < length && buffer[offset] != '\n') {
if (buffer[offset] >= '0' && buffer[offset] <= '9') {
int start = offset++;
while (offset < length &&
buffer[offset] >= '0' &&
buffer[offset] <= '9') {
++offset;
}
return Integer.parseInt(new String(buffer, start, offset - start), 10);
}
++offset;
}
return 0;
}
/**
* Fetch the total memory of the device in MB by parsing /proc/meminfo.
*
* Of course, Android doesn't have a neat and tidy way to find total
* RAM, so we do it by parsing /proc/meminfo.
*
* @return 0 if a problem occurred, or memory size in MB.
*/
public static int getMemSize() {
if (sTotalRAM >= 0) {
return sTotalRAM;
}
// This is the string "MemTotal" that we're searching for in the buffer.
final byte[] MEMTOTAL = {'M', 'e', 'm', 'T', 'o', 't', 'a', 'l'};
try {
final byte[] buffer = new byte[MEMINFO_BUFFER_SIZE_BYTES];
final FileInputStream is = new FileInputStream("/proc/meminfo");
try {
final int length = is.read(buffer);
for (int i = 0; i < length; i++) {
if (matchMemText(buffer, i, length, MEMTOTAL)) {
i += 8;
sTotalRAM = extractMemValue(buffer, i, length) / 1024;
Log.d(LOGTAG, "System memory: " + sTotalRAM + "MB.");
return sTotalRAM;
}
}
} finally {
is.close();
}
Log.w(LOGTAG, "Did not find MemTotal line in /proc/meminfo.");
return sTotalRAM = 0;
} catch (FileNotFoundException f) {
return sTotalRAM = 0;
} catch (IOException e) {
return sTotalRAM = 0;
}
return SysInfo.getMemSize();
}
public static boolean isLowMemoryPlatform() {

View File

@ -0,0 +1,155 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedEditText extends android.widget.EditText
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedEditText(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ThemedEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,5 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX EditText
//#define BASE_TYPE android.widget.EditText
//#define STYLE_CONSTRUCTOR 1
//#include ThemedView.java.frag

View File

@ -0,0 +1,155 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedImageButton extends android.widget.ImageButton
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedImageButton(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ThemedImageButton(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,5 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX ImageButton
//#define BASE_TYPE android.widget.ImageButton
//#define STYLE_CONSTRUCTOR 1
//#include ThemedView.java.frag

View File

@ -0,0 +1,155 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedImageView extends android.widget.ImageView
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedImageView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ThemedImageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,5 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX ImageView
//#define BASE_TYPE android.widget.ImageView
//#define STYLE_CONSTRUCTOR 1
//#include ThemedView.java.frag

View File

@ -0,0 +1,150 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedLinearLayout extends android.widget.LinearLayout
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,4 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX LinearLayout
//#define BASE_TYPE android.widget.LinearLayout
//#include ThemedView.java.frag

View File

@ -0,0 +1,155 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedRelativeLayout extends android.widget.RelativeLayout
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedRelativeLayout(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ThemedRelativeLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,5 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX RelativeLayout
//#define BASE_TYPE android.widget.RelativeLayout
//#define STYLE_CONSTRUCTOR 1
//#include ThemedView.java.frag

View File

@ -0,0 +1,150 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedTextSwitcher extends android.widget.TextSwitcher
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedTextSwitcher(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,4 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX TextSwitcher
//#define BASE_TYPE android.widget.TextSwitcher
//#include ThemedView.java.frag

View File

@ -0,0 +1,155 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedTextView extends android.widget.TextView
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ThemedTextView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,5 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX TextView
//#define BASE_TYPE android.widget.TextView
//#define STYLE_CONSTRUCTOR 1
//#include ThemedView.java.frag

View File

@ -0,0 +1,155 @@
// This file is generated by generate_themed_views.py; do not edit.
/* 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.widget;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.LightweightTheme;
import org.mozilla.gecko.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.ColorDrawable;
import android.util.AttributeSet;
public class ThemedView extends android.view.View
implements LightweightTheme.OnChangeListener {
private LightweightTheme mTheme;
private static final int[] STATE_PRIVATE_MODE = { R.attr.state_private };
private static final int[] STATE_LIGHT = { R.attr.state_light };
private static final int[] STATE_DARK = { R.attr.state_dark };
protected static final int[] PRIVATE_PRESSED_STATE_SET = { R.attr.state_private, android.R.attr.state_pressed };
protected static final int[] PRIVATE_FOCUSED_STATE_SET = { R.attr.state_private, android.R.attr.state_focused };
protected static final int[] PRIVATE_STATE_SET = { R.attr.state_private };
private boolean mIsPrivate;
private boolean mIsLight;
private boolean mIsDark;
private boolean mAutoUpdateTheme; // always false if there's no theme.
public ThemedView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context, attrs);
}
public ThemedView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialize(context, attrs);
}
private void initialize(final Context context, final AttributeSet attrs) {
// The theme can be null, particularly for webapps: Bug 1089266.
mTheme = ((GeckoApplication) context.getApplicationContext()).getLightweightTheme();
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.LightweightTheme);
mAutoUpdateTheme = mTheme != null && a.getBoolean(R.styleable.LightweightTheme_autoUpdateTheme, true);
a.recycle();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
if (mAutoUpdateTheme)
mTheme.addListener(this);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAutoUpdateTheme)
mTheme.removeListener(this);
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
if (mIsPrivate)
mergeDrawableStates(drawableState, STATE_PRIVATE_MODE);
else if (mIsLight)
mergeDrawableStates(drawableState, STATE_LIGHT);
else if (mIsDark)
mergeDrawableStates(drawableState, STATE_DARK);
return drawableState;
}
@Override
public void onLightweightThemeChanged() {
if (mAutoUpdateTheme && mTheme.isEnabled())
setTheme(mTheme.isLightTheme());
}
@Override
public void onLightweightThemeReset() {
if (mAutoUpdateTheme)
resetTheme();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
onLightweightThemeChanged();
}
public boolean isPrivateMode() {
return mIsPrivate;
}
public void setPrivateMode(boolean isPrivate) {
if (mIsPrivate != isPrivate) {
mIsPrivate = isPrivate;
refreshDrawableState();
}
}
public void setTheme(boolean isLight) {
// Set the theme only if it is different from existing theme.
if ((isLight && mIsLight != isLight) ||
(!isLight && mIsDark == isLight)) {
if (isLight) {
mIsLight = true;
mIsDark = false;
} else {
mIsLight = false;
mIsDark = true;
}
refreshDrawableState();
}
}
public void resetTheme() {
if (mIsLight || mIsDark) {
mIsLight = false;
mIsDark = false;
refreshDrawableState();
}
}
public void setAutoUpdateTheme(boolean autoUpdateTheme) {
if (mTheme == null) {
return;
}
if (mAutoUpdateTheme != autoUpdateTheme) {
mAutoUpdateTheme = autoUpdateTheme;
if (mAutoUpdateTheme)
mTheme.addListener(this);
else
mTheme.removeListener(this);
}
}
public ColorDrawable getColorDrawable(int id) {
return new ColorDrawable(getResources().getColor(id));
}
}

View File

@ -1,3 +1,6 @@
//#filter substitution
// This file is generated by generate_themed_views.py; do not edit.
/* 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/. */

View File

@ -1,5 +0,0 @@
//#filter substitution
//#define VIEW_NAME_SUFFIX View
//#define BASE_TYPE android.view.View
//#define STYLE_CONSTRUCTOR 1
//#include ThemedView.java.frag

View File

@ -0,0 +1,66 @@
#!/bin/python
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# 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/.
'''
Script to generate Themed*.java source files for Fennec.
This script runs the preprocessor on a input template and writes
updated files into the source directory.
To update the themed views, update the input template
(ThemedView.java.frag) and run the script. Use version control to
examine the differences, and don't forget to commit the changes to the
template and the outputs.
'''
from __future__ import (
print_function,
unicode_literals,
)
import os
from mozbuild.preprocessor import Preprocessor
__DIR__ = os.path.dirname(os.path.abspath(__file__))
template = os.path.join(__DIR__, 'ThemedView.java.frag')
dest_format_string = 'Themed%(VIEW_NAME_SUFFIX)s.java'
views = [
dict(VIEW_NAME_SUFFIX='EditText',
BASE_TYPE='android.widget.EditText',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='ImageButton',
BASE_TYPE='android.widget.ImageButton',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='ImageView',
BASE_TYPE='android.widget.ImageView',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='LinearLayout',
BASE_TYPE='android.widget.LinearLayout'),
dict(VIEW_NAME_SUFFIX='RelativeLayout',
BASE_TYPE='android.widget.RelativeLayout',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='TextSwitcher',
BASE_TYPE='android.widget.TextSwitcher'),
dict(VIEW_NAME_SUFFIX='TextView',
BASE_TYPE='android.widget.TextView',
STYLE_CONSTRUCTOR=1),
dict(VIEW_NAME_SUFFIX='View',
BASE_TYPE='android.view.View',
STYLE_CONSTRUCTOR=1),
]
for view in views:
pp = Preprocessor(defines=view, marker='//#')
dest = os.path.join(__DIR__, dest_format_string % view)
with open(template, 'rU') as input:
with open(dest, 'wt') as output:
pp.processFile(input=input, output=output)
print('%s' % dest)

View File

@ -194,6 +194,14 @@
-keep class **.R$*
# Keep classes, and all their contents, compiled before mozglue.RobocopTarget.
-keep class org.mozilla.gecko.AppConstants {
*;
}
-keep class org.mozilla.gecko.SysInfo {
*;
}
# Disable obfuscation because it makes exception stack traces more difficult to read.
-dontobfuscate

View File

@ -94,7 +94,7 @@ let Bookmarks = Object.freeze({
TYPE_SEPARATOR: 3,
/**
* Default index used to append a bookmark-item at the end of a folder.
* Default index used to append a bookmark-item at the end of a folder.
* This should stay consistent with nsINavBookmarksService.idl
*/
DEFAULT_INDEX: -1,
@ -238,7 +238,7 @@ let Bookmarks = Object.freeze({
let updateInfo = validateBookmarkObject(info,
{ guid: { required: true }
, index: { requiredIf: b => b.hasOwnProperty("parentGuid")
, validIf: b => b.index >= 0 }
, validIf: b => b.index >= 0 || b.index == this.DEFAULT_INDEX }
, parentGuid: { requiredIf: b => b.hasOwnProperty("index") }
});
@ -307,9 +307,15 @@ let Bookmarks = Object.freeze({
// the same container. Thus we know it exists.
if (!parent)
parent = yield fetchBookmark({ guid: item.parentGuid });
// Set index in the appending case.
if (updateInfo.index > parent._childCount)
updateInfo.index = parent._childCount;
if (updateInfo.index >= parent._childCount ||
updateInfo.index == this.DEFAULT_INDEX) {
updateInfo.index = parent._childCount;
// Fix the index when moving within the same container.
if (parent.guid == item.parentGuid)
updateInfo.index--;
}
}
let updatedItem = yield updateBookmark(updateInfo, item, parent);
@ -364,14 +370,14 @@ let Bookmarks = Object.freeze({
updatedItem.guid,
updatedItem.parentGuid ]);
}
// If the item was move, notify onItemMoved.
// If the item was moved, notify onItemMoved.
if (item.parentGuid != updatedItem.parentGuid ||
item.index != updatedItem.index) {
notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
item.index, updatedItem._parentId,
updatedItem.index, updatedItem.type,
updatedItem.guid, item.parentGuid,
updatedItem.newParentGuid ]);
updatedItem.parentGuid ]);
}
// Remove non-enumerable properties.
@ -975,8 +981,10 @@ function* fetchBookmarksByURL(info) {
LEFT JOIN moz_keywords k ON k.id = b.keyword_id
LEFT JOIN moz_places h ON h.id = b.fk
WHERE h.url = :url
AND _grandParentId <> :tags_folder
ORDER BY b.lastModified DESC
`, { url: info.url.href });
`, { url: info.url.href,
tags_folder: PlacesUtils.tagsFolderId });
return rows.length ? rowsToItemsArray(rows) : null;
}
@ -1158,7 +1166,7 @@ function rowsToItemsArray(rows) {
}
return item;
});
});
}
/**
@ -1267,7 +1275,7 @@ function validateBookmarkObject(input, behavior={}) {
}
}
if (required.size > 0)
throw new Error(`The following properties were expected: ${[...required].join(", ")}`);
throw new Error(`The following properties were expected: ${[...required].join(", ")}`);
return normalizedInput;
}

View File

@ -67,7 +67,8 @@ this.EXPORTED_SYMBOLS = ["PlacesTransactions"];
* a live bookmark is associated.
* - tag - a string.
* - tags: an array of strings.
* - guid, parentGuid, newParentGuid: a valid places GUID string.
* - guid, parentGuid, newParentGuid: a valid Places GUID string.
* - guids: an array of valid Places GUID strings.
* - title: a string
* - index, newIndex: the position of an item in its containing folder,
* starting from 0.
@ -463,7 +464,6 @@ Enqueuer.prototype = {
get promise() this._promise
};
let TransactionsManager = {
// See the documentation at the top of this file. |transact| calls are not
// serialized with |batch| calls.
@ -869,6 +869,7 @@ DefineTransaction.defineInputProps(["index", "newIndex"],
PlacesUtils.bookmarks.DEFAULT_INDEX);
DefineTransaction.defineInputProps(["annotation"],
DefineTransaction.annotationObjectValidate);
DefineTransaction.defineArrayInputProp("guids", "guid");
DefineTransaction.defineArrayInputProp("urls", "url");
DefineTransaction.defineArrayInputProp("tags", "tag");
DefineTransaction.defineArrayInputProp("annotations", "annotation");
@ -1269,29 +1270,40 @@ PT.EditUrl.prototype = Object.seal({
*
* Required Input Properties: guid, annotationObject
*/
PT.Annotate = DefineTransaction(["guid", "annotations"]);
PT.Annotate = DefineTransaction(["guids", "annotations"]);
PT.Annotate.prototype = {
execute: function* (aGuid, aNewAnnos) {
let itemId = yield PlacesUtils.promiseItemId(aGuid);
let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
let undoAnnos = [];
for (let newAnno of aNewAnnos) {
let currentAnno = currentAnnos.find( a => a.name == newAnno.name );
if (!!currentAnno) {
undoAnnos.push(currentAnno);
}
else {
// An unset value removes the annotation.
undoAnnos.push({ name: newAnno.name });
*execute(aGuids, aNewAnnos) {
let undoAnnosForItem = new Map(); // itemId => undoAnnos;
for (let guid of aGuids) {
let itemId = yield PlacesUtils.promiseItemId(guid);
let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
let undoAnnos = [];
for (let newAnno of aNewAnnos) {
let currentAnno = currentAnnos.find(a => a.name == newAnno.name);
if (!!currentAnno) {
undoAnnos.push(currentAnno);
}
else {
// An unset value removes the annotation.
undoAnnos.push({ name: newAnno.name });
}
}
undoAnnosForItem.set(itemId, undoAnnos);
PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
}
PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
this.undo = () => {
PlacesUtils.setAnnotationsForItem(itemId, undoAnnos);
this.undo = function() {
for (let [itemId, undoAnnos] of undoAnnosForItem) {
PlacesUtils.setAnnotationsForItem(itemId, undoAnnos);
}
};
this.redo = () => {
PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
this.redo = function* () {
for (let guid of aGuids) {
let itemId = yield PlacesUtils.promiseItemId(guid);
PlacesUtils.setAnnotationsForItem(itemId, aNewAnnos);
}
};
}
};
@ -1384,28 +1396,61 @@ PT.SortByName.prototype = {
/**
* Transaction for removing an item (any type).
*
* Required Input Properties: guid.
* Required Input Properties: guids.
*/
PT.Remove = DefineTransaction(["guid"]);
PT.Remove = DefineTransaction(["guids"]);
PT.Remove.prototype = {
execute: function* (aGuid) {
const bms = PlacesUtils.bookmarks;
*execute(aGuids) {
function promiseBookmarksTree(guid) {
try {
return PlacesUtils.promiseBookmarksTree(guid);
}
catch(ex) {
throw new Error("Failed to get info for the specified item (guid: " +
guid + "). Ex: " + ex);
}
}
let toRestore = [for (guid of aGuids) yield promiseBookmarksTree(guid)];
let itemInfo = null;
try {
itemInfo = yield PlacesUtils.promiseBookmarksTree(aGuid);
}
catch(ex) {
throw new Error("Failed to get info for the specified item (guid: " +
aGuid + "). Ex: " + ex);
}
PlacesUtils.bookmarks.removeItem(yield PlacesUtils.promiseItemId(aGuid));
this.undo = createItemsFromBookmarksTree.bind(null, itemInfo, true);
let removeThem = Task.async(function* () {
for (let guid of aGuids) {
PlacesUtils.bookmarks.removeItem(yield PlacesUtils.promiseItemId(guid));
}
});
yield removeThem();
this.undo = Task.async(function* () {
for (let info of toRestore) {
yield createItemsFromBookmarksTree(info, true);
}
});
this.redo = removeThem;
}
};
/**
* Transaction for tagging a URI.
* Transactions for removing all bookmarks for one or more urls.
*
* Required Input Properties: urls.
*/
PT.RemoveBookmarksForUrls = DefineTransaction(["urls"]);
PT.RemoveBookmarksForUrls.prototype = {
*execute(aUrls) {
let guids = [];
for (let url of aUrls) {
yield PlacesUtils.bookmarks.fetch({ url }, info => {
guids.push(info.guid);
});
}
let removeTxn = TransactionsHistory.getRawTransaction(PT.Remove(guids));
yield removeTxn.execute();
this.undo = removeTxn.undo.bind(removeTxn);
this.redo = removeTxn.redo.bind(removeTxn);
}
};
/**
* Transaction for tagging urls.
*
* Required Input Properties: urls, tags.
*/

View File

@ -101,7 +101,7 @@ interface mozILivemarkCallback : nsISupports
in mozILivemark aLivemark);
};
[scriptable, uuid(6e40d5b1-ce48-4458-8b68-6bee17d30ef3)]
[scriptable, uuid(E52B2273-729D-4EBC-A039-E9CD9E18FF86)]
interface mozILivemarkInfo : nsISupports
{
/**
@ -129,6 +129,11 @@ interface mozILivemarkInfo : nsISupports
*/
readonly attribute long index;
/**
* Time this livemark was created.
*/
readonly attribute PRTime dateAdded;
/**
* Time this livemark's details were last modified. Doesn't track changes to
* the livemark contents.

View File

@ -113,8 +113,7 @@ interface nsINavBookmarkObserver : nsISupports
* For certain properties, this is set to the new value of the
* property (see the list below).
* @param aLastModified
* If lastModified changed, this parameter is the new value, otherwise
* it's set to 0.
* The updated last-modified value.
* @param aItemType
* The type of the item to be removed (see TYPE_* constants below).
* @param aParentId

View File

@ -80,7 +80,8 @@ LivemarkService.prototype = {
AND n.name = ${aAnnoParam}`;
}
return `SELECT b.id, b.title, b.parent, b.position, b.guid, b.lastModified,
return `SELECT b.id, b.title, b.parent, b.position, b.guid,
b.dateAdded, b.lastModified,
( ${getAnnoSQLFragment(":feedURI_anno")} ) AS feedURI,
( ${getAnnoSQLFragment(":siteURI_anno")} ) AS siteURI
FROM moz_bookmarks b
@ -106,6 +107,7 @@ LivemarkService.prototype = {
title: row.getResultByName("title"),
parentId: row.getResultByName("parent"),
index: row.getResultByName("position"),
dateAdded: row.getResultByName("dateAdded"),
lastModified: row.getResultByName("lastModified"),
feedURI: NetUtil.newURI(row.getResultByName("feedURI")),
siteURI: siteURL ? NetUtil.newURI(siteURL) : null });
@ -202,11 +204,15 @@ LivemarkService.prototype = {
, feedURI: aLivemarkInfo.feedURI
, siteURI: aLivemarkInfo.siteURI
, guid: aLivemarkInfo.guid
, dateAdded: aLivemarkInfo.dateAdded
, lastModified: aLivemarkInfo.lastModified
});
if (this._itemAdded && this._itemAdded.id == livemark.id) {
livemark.index = this._itemAdded.index;
livemark.guid = this._itemAdded.guid;
if (!aLivemarkInfo.dateAdded) {
livemark.dateAdded = this._itemAdded.dateAdded;
}
if (!aLivemarkInfo.lastModified) {
livemark.lastModified = this._itemAdded.lastModified;
}
@ -411,6 +417,7 @@ LivemarkService.prototype = {
this._itemAdded = { id: aItemId
, guid: aGUID
, index: aIndex
, dateAdded: aDateAdded
, lastModified: aDateAdded
};
}
@ -422,11 +429,15 @@ LivemarkService.prototype = {
if (aItemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
if (this._itemAdded && this._itemAdded.id == aItemId) {
this._itemAdded.lastModified = aLastModified;
}
}
if (aItemId in this._livemarks) {
if (aProperty == "title") {
this._livemarks[aItemId].title = aValue;
}
else if (aProperty == "dateAdded") {
this._livemark[aItemId].dateAdded = parseInt(aValue, 10);
}
this._livemarks[aItemId].lastModified = aLastModified;
}
}
@ -526,8 +537,8 @@ function Livemark(aLivemarkInfo)
this._nodes = new Map();
this._guid = "";
this._dateAdded = 0;
this._lastModified = 0;
this.loadGroup = null;
this.feedURI = null;
this.siteURI = null;
@ -539,6 +550,7 @@ function Livemark(aLivemarkInfo)
this.guid = aLivemarkInfo.guid;
this.feedURI = aLivemarkInfo.feedURI;
this.siteURI = aLivemarkInfo.siteURI;
this.dateAdded = aLivemarkInfo.dateAdded;
this.lastModified = aLivemarkInfo.lastModified;
}
else {
@ -551,6 +563,10 @@ function Livemark(aLivemarkInfo)
if (aLivemarkInfo.siteURI) {
this.writeSiteURI(aLivemarkInfo.siteURI);
}
if (aLivemarkInfo.dateAdded) {
this.dateAdded = aLivemarkInfo.dateAdded;
PlacesUtils.bookmarks.setItemDateAdded(this.id, this.dateAdded);
}
// Last modified time must be the last change.
if (aLivemarkInfo.lastModified) {
this.lastModified = aLivemarkInfo.lastModified;
@ -614,16 +630,13 @@ Livemark.prototype = {
this.siteURI = aSiteURI;
},
set guid(aGUID) {
this._guid = aGUID;
return aGUID;
},
set guid(aGUID) this._guid = aGUID,
get guid() this._guid,
set lastModified(aLastModified) {
this._lastModified = aLastModified;
return aLastModified;
},
set dateAdded(aDateAdded) this._dateAdded = aDateAdded,
get dateAdded() this._dateAdded,
set lastModified(aLastModified) this._lastModified = aLastModified,
get lastModified() this._lastModified,
/**

View File

@ -210,6 +210,11 @@ public:
nsresult GetDescendantFolders(int64_t aFolderId,
nsTArray<int64_t>& aDescendantFoldersArray);
static const int32_t kGetChildrenIndex_Guid;
static const int32_t kGetChildrenIndex_Position;
static const int32_t kGetChildrenIndex_Type;
static const int32_t kGetChildrenIndex_PlaceID;
private:
static nsNavBookmarks* gBookmarksService;
@ -356,11 +361,6 @@ private:
int64_t RecursiveFindRedirectedBookmark(int64_t aPlaceId);
static const int32_t kGetChildrenIndex_Position;
static const int32_t kGetChildrenIndex_Type;
static const int32_t kGetChildrenIndex_PlaceID;
static const int32_t kGetChildrenIndex_Guid;
class RemoveFolderTransaction MOZ_FINAL : public nsITransaction {
public:
explicit RemoveFolderTransaction(int64_t aID) : mID(aID) {}

View File

@ -1512,7 +1512,8 @@ PlacesSQLQueryBuilder::SelectAsURI()
"SELECT b2.fk, h.url, COALESCE(b2.title, h.title) AS page_title, "
"h.rev_host, h.visit_count, h.last_visit_date, f.url, b2.id, "
"b2.dateAdded, b2.lastModified, b2.parent, ") +
tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid "
tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
"b2.guid, b2.position, b2.type, b2.fk "
"FROM moz_bookmarks b2 "
"JOIN (SELECT b.fk "
"FROM moz_bookmarks b "
@ -1536,7 +1537,8 @@ PlacesSQLQueryBuilder::SelectAsURI()
"SELECT b.fk, h.url, COALESCE(b.title, h.title) AS page_title, "
"h.rev_host, h.visit_count, h.last_visit_date, f.url, b.id, "
"b.dateAdded, b.lastModified, b.parent, ") +
tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid "
tagsSqlFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid,"
"b.guid, b.position, b.type, b.fk "
"FROM moz_bookmarks b "
"JOIN moz_places h ON b.fk = h.id "
"LEFT OUTER JOIN moz_favicons f ON h.favicon_id = f.id "
@ -3902,36 +3904,40 @@ nsNavHistory::RowToResult(mozIStorageValueArray* aRow,
}
if (IsQueryURI(url)) {
// special case "place:" URIs: turn them into containers
// We should never expose the history title for query nodes if the
// bookmark-item's title is set to null (the history title may be the
// query string without the place: prefix). Thus we call getItemTitle
// explicitly. Doing this in the SQL query would be less performant since
// it should be done for all results rather than only for queries.
if (itemId != -1) {
nsNavBookmarks *bookmarks = nsNavBookmarks::GetBookmarksService();
NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
rv = bookmarks->GetItemTitle(itemId, title);
NS_ENSURE_SUCCESS(rv, rv);
}
// Special case "place:" URIs: turn them into containers.
nsRefPtr<nsNavHistoryResultNode> resultNode;
rv = QueryRowToResult(itemId, url, title, accessCount, time, favicon,
getter_AddRefs(resultNode));
NS_ENSURE_SUCCESS(rv,rv);
if (aOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_TAG_QUERY) {
if (itemId != -1) {
rv = aRow->GetUTF8String(nsNavBookmarks::kGetChildrenIndex_Guid,
resultNode->mBookmarkGuid);
NS_ENSURE_SUCCESS(rv, rv);
// We should never expose the history title for query nodes if the
// bookmark-item's title is set to null (the history title may be the
// query string without the place: prefix). Thus we call getItemTitle
// explicitly. Doing this in the SQL query would be less performant since
// it should be done for all results rather than only for queries.
nsNavBookmarks *bookmarks = nsNavBookmarks::GetBookmarksService();
NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
rv = bookmarks->GetItemTitle(itemId, resultNode->mTitle);
NS_ENSURE_SUCCESS(rv, rv);
}
if (itemId != -1 ||
aOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_TAG_QUERY) {
// RESULTS_AS_TAG_QUERY has date columns
resultNode->mDateAdded = aRow->AsInt64(kGetInfoIndex_ItemDateAdded);
resultNode->mLastModified = aRow->AsInt64(kGetInfoIndex_ItemLastModified);
}
else if (resultNode->IsFolder()) {
// If it's a simple folder node (i.e. a shortcut to another folder), apply
// our options for it. However, if the parent type was tag query, we do not
// apply them, because it would not yield any results.
resultNode->GetAsContainer()->mOptions = aOptions;
if (resultNode->IsFolder()) {
// If it's a simple folder node (i.e. a shortcut to another folder), apply
// our options for it. However, if the parent type was tag query, we do not
// apply them, because it would not yield any results.
resultNode->GetAsContainer()->mOptions = aOptions;
}
}
resultNode.forget(aResult);
@ -3946,6 +3952,10 @@ nsNavHistory::RowToResult(mozIStorageValueArray* aRow,
resultNode->mFolderId = parentId;
resultNode->mDateAdded = aRow->AsInt64(kGetInfoIndex_ItemDateAdded);
resultNode->mLastModified = aRow->AsInt64(kGetInfoIndex_ItemLastModified);
rv = aRow->GetUTF8String(nsNavBookmarks::kGetChildrenIndex_Guid,
resultNode->mBookmarkGuid);
NS_ENSURE_SUCCESS(rv, rv);
}
resultNode->mFrecency = aRow->AsInt32(kGetInfoIndex_Frecency);
@ -4135,7 +4145,8 @@ nsNavHistory::BookmarkIdToResultNode(int64_t aBookmarkId, nsNavHistoryQueryOptio
"SELECT b.fk, h.url, COALESCE(b.title, h.title), "
"h.rev_host, h.visit_count, h.last_visit_date, f.url, b.id, "
"b.dateAdded, b.lastModified, b.parent, "
) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid "
) + tagsFragment + NS_LITERAL_CSTRING(", h.frecency, h.hidden, h.guid, "
"b.guid, b.position, b.type, b.fk "
"FROM moz_bookmarks b "
"JOIN moz_places h ON b.fk = h.id "
"LEFT JOIN moz_favicons f ON h.favicon_id = f.id "

View File

@ -3584,6 +3584,9 @@ nsNavHistoryFolderResultNode::OnItemAdded(int64_t aItemId,
node = new nsNavHistorySeparatorResultNode();
NS_ENSURE_TRUE(node, NS_ERROR_OUT_OF_MEMORY);
node->mItemId = aItemId;
node->mBookmarkGuid = aGUID;
node->mDateAdded = aDateAdded;
node->mLastModified = aDateAdded;
}
node->mBookmarkIndex = aIndex;

View File

@ -255,6 +255,9 @@ add_task(function* fetch_byurl() {
title: "a bookmark" });
checkBookmarkObject(bm1);
// Also ensure that fecth-by-url excludes the tags folder.
PlacesUtils.tagging.tagURI(uri(bm1.url.href), ["Test Tag"]);
let bm2 = yield PlacesUtils.bookmarks.fetch({ url: bm1.url },
gAccumulator.callback);
checkBookmarkObject(bm2);
@ -293,6 +296,9 @@ add_task(function* fetch_byurl() {
Assert.equal(gAccumulator.results.length, 2);
gAccumulator.results.forEach(checkBookmarkObject);
Assert.deepEqual(gAccumulator.results[0], bm5);
// cleanup
PlacesUtils.tagging.untagURI(uri(bm1.url.href), ["Test Tag"]);
});
add_task(function* fetch_bykeyword_nonexisting() {

View File

@ -110,10 +110,6 @@ add_task(function* insert_bookmark_tag_notification() {
arguments: [ tagId, tagParentId, tag.index, tag.type,
tag.url, null, tag.dateAdded,
tag.guid, tag.parentGuid ] },
{ name: "onItemChanged",
arguments: [ tagId, "tags", false, "",
tag.lastModified, tag.type, tagParentId,
tag.guid, tag.parentGuid ] },
{ name: "onItemChanged",
arguments: [ itemId, "tags", false, "",
bm.lastModified, bm.type, parentId,
@ -189,6 +185,65 @@ add_task(function* update_bookmark_keyword() {
]);
});
add_task(function* update_move_same_folder() {
// Ensure there are at least two items in place (others test do so for us,
// but we don't have to depend on that).
let sep = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
parentGuid: PlacesUtils.bookmarks.unfiledGuid });
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: new URL("http://move.example.com/") });
let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
let bmParentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
let bmOldIndex = bm.index;
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
index: 0 });
Assert.equal(bm.index, 0);
observer.check([ { name: "onItemMoved",
arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index,
bm.type, bm.guid, bm.parentGuid, bm.parentGuid ] }
]);
// Test that we get the right index for DEFAULT_INDEX input.
bmOldIndex = 0;
observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
index: PlacesUtils.bookmarks.DEFAULT_INDEX });
Assert.ok(bm.index > 0);
observer.check([ { name: "onItemMoved",
arguments: [ bmItemId, bmParentId, bmOldIndex, bmParentId, bm.index,
bm.type, bm.guid, bm.parentGuid, bm.parentGuid ] }
]);
});
add_task(function* update_move_different_folder() {
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: new URL("http://move.example.com/") });
let folder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: PlacesUtils.bookmarks.unfiledGuid });
let bmItemId = yield PlacesUtils.promiseItemId(bm.guid);
let bmOldParentId = PlacesUtils.unfiledBookmarksFolderId;
let bmOldIndex = bm.index;
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
parentGuid: folder.guid,
index: PlacesUtils.bookmarks.DEFAULT_INDEX });
Assert.equal(bm.index, 0);
let bmNewParentId = yield PlacesUtils.promiseItemId(folder.guid);
observer.check([ { name: "onItemMoved",
arguments: [ bmItemId, bmOldParentId, bmOldIndex, bmNewParentId,
bm.index, bm.type, bm.guid,
PlacesUtils.bookmarks.unfiledGuid,
bm.parentGuid ] }
]);
});
add_task(function* remove_bookmark() {
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,

View File

@ -402,6 +402,71 @@ add_task(function* update_move() {
Assert.equal(descendant.index, 1);
});
add_task(function* update_move_append() {
let folder_a =
yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
type: PlacesUtils.bookmarks.TYPE_FOLDER });
checkBookmarkObject(folder_a);
let folder_b =
yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
type: PlacesUtils.bookmarks.TYPE_FOLDER });
checkBookmarkObject(folder_b);
/* folder_a: [sep_1, sep_2, sep_3], folder_b: [] */
let sep_1 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid,
type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
checkBookmarkObject(sep_1);
let sep_2 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid,
type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
checkBookmarkObject(sep_2);
let sep_3 = yield PlacesUtils.bookmarks.insert({ parentGuid: folder_a.guid,
type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
checkBookmarkObject(sep_3);
function ensurePosition(info, parentGuid, index) {
checkBookmarkObject(info);
Assert.equal(info.parentGuid, parentGuid);
Assert.equal(info.index, index);
}
// folder_a: [sep_2, sep_3, sep_1], folder_b: []
sep_1.index = PlacesUtils.bookmarks.DEFAULT_INDEX;
// Note sep_1 includes parentGuid even though we're not moving the item to
// another folder
sep_1 = yield PlacesUtils.bookmarks.update(sep_1);
ensurePosition(sep_1, folder_a.guid, 2);
sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid);
ensurePosition(sep_2, folder_a.guid, 0);
sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid);
ensurePosition(sep_3, folder_a.guid, 1);
sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid);
ensurePosition(sep_1, folder_a.guid, 2);
// folder_a: [sep_2, sep_1], folder_b: [sep_3]
sep_3.index = PlacesUtils.bookmarks.DEFAULT_INDEX;
sep_3.parentGuid = folder_b.guid;
sep_3 = yield PlacesUtils.bookmarks.update(sep_3);
ensurePosition(sep_3, folder_b.guid, 0);
sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid);
ensurePosition(sep_2, folder_a.guid, 0);
sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid);
ensurePosition(sep_1, folder_a.guid, 1);
sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid);
ensurePosition(sep_3, folder_b.guid, 0);
// folder_a: [sep_1], folder_b: [sep_3, sep_2]
sep_2.index = Number.MAX_SAFE_INTEGER;
sep_2.parentGuid = folder_b.guid;
sep_2 = yield PlacesUtils.bookmarks.update(sep_2);
ensurePosition(sep_2, folder_b.guid, 1);
sep_1 = yield PlacesUtils.bookmarks.fetch(sep_1.guid);
ensurePosition(sep_1, folder_a.guid, 0);
sep_3 = yield PlacesUtils.bookmarks.fetch(sep_3.guid);
ensurePosition(sep_3, folder_b.guid, 0);
sep_2 = yield PlacesUtils.bookmarks.fetch(sep_2.guid);
ensurePosition(sep_2, folder_b.guid, 1);
});
function run_test() {
run_next_test();
}

View File

@ -0,0 +1,42 @@
add_task(function* () {
// test that nodes inserted by incremental update for bookmarks of all types
// have the extra bookmark properties (bookmarkGuid, dateAdded, lastModified).
// getFolderContents opens the root node.
let root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
function* insertAndTest(bmInfo) {
bmInfo = yield PlacesUtils.bookmarks.insert(bmInfo);
let node = root.getChild(root.childCount - 1);
Assert.equal(node.bookmarkGuid, bmInfo.guid);
Assert.equal(node.dateAdded, bmInfo.dateAdded * 1000);
Assert.equal(node.lastModified, bmInfo.lastModified * 1000);
}
// Normal bookmark.
yield insertAndTest({ parentGuid: root.bookmarkGuid
, type: PlacesUtils.bookmarks.TYPE_BOOKMARK
, title: "Test Bookmark"
, url: "http://test.url.tld" });
// place: query
yield insertAndTest({ parentGuid: root.bookmarkGuid
, type: PlacesUtils.bookmarks.TYPE_BOOKMARK
, title: "Test Query"
, url: "place:folder=BOOKMARKS_MENU" });
// folder
yield insertAndTest({ parentGuid: root.bookmarkGuid
, type: PlacesUtils.bookmarks.TYPE_FOLDER
, title: "Test Folder" });
// separator
yield insertAndTest({ parentGuid: root.bookmarkGuid
, type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
root.containerOpen = false;
});
function run_test() {
run_next_test();
}

View File

@ -4,10 +4,11 @@
* 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/. */
const bmsvc = PlacesUtils.bookmarks;
const tagssvc = PlacesUtils.tagging;
const annosvc = PlacesUtils.annotations;
const PT = PlacesTransactions;
const bmsvc = PlacesUtils.bookmarks;
const tagssvc = PlacesUtils.tagging;
const annosvc = PlacesUtils.annotations;
const PT = PlacesTransactions;
const rootGuid = PlacesUtils.bookmarks.rootGuid;
Components.utils.importGlobalProperties(["URL"]);
@ -106,9 +107,6 @@ observer.reset();
// index at which items should begin
let bmStartIndex = 0;
// get bookmarks root id
let root = PlacesUtils.bookmarksMenuFolderId;
function run_test() {
bmsvc.addObserver(observer, false);
do_register_cleanup(function () {
@ -248,9 +246,8 @@ function ensureTagsForURI(aURI, aTags) {
do_check_true(aTags.every( t => tagsSet.indexOf(t) != -1 ));
}
function* createTestFolderInfo(aTitle = "Test Folder") {
return { parentGuid: yield PlacesUtils.promiseItemGuid(root)
, title: "Test Folder" };
function createTestFolderInfo(aTitle = "Test Folder") {
return { parentGuid: rootGuid, title: "Test Folder" };
}
function isLivemarkTree(aTree) {
@ -315,10 +312,12 @@ function* ensureEqualBookmarksTrees(aOriginal,
yield ensureLivemarkCreatedByAddLivemark(aNew.guid);
}
function* ensureBookmarksTreeRestoredCorrectly(aOriginalBookmarksTree) {
let restoredTree =
yield PlacesUtils.promiseBookmarksTree(aOriginalBookmarksTree.guid);
yield ensureEqualBookmarksTrees(aOriginalBookmarksTree, restoredTree);
function* ensureBookmarksTreeRestoredCorrectly(...aOriginalBookmarksTrees) {
for (let originalTree of aOriginalBookmarksTrees) {
let restoredTree =
yield PlacesUtils.promiseBookmarksTree(originalTree.guid);
yield ensureEqualBookmarksTrees(originalTree, restoredTree);
}
}
function* ensureNonExistent(...aGuids) {
@ -338,7 +337,7 @@ add_task(function* test_recycled_transactions() {
ensureUndoState(txns, undoPosition);
}
let txn_a = PT.NewFolder(yield createTestFolderInfo());
let txn_a = PT.NewFolder(createTestFolderInfo());
yield txn_a.transact();
ensureUndoState([[txn_a]], 0);
yield ensureTransactThrowsFor(txn_a);
@ -351,7 +350,7 @@ add_task(function* test_recycled_transactions() {
ensureUndoState();
ensureTransactThrowsFor(txn_a);
let txn_b = PT.NewFolder(yield createTestFolderInfo());
let txn_b = PT.NewFolder(createTestFolderInfo());
yield PT.batch(function* () {
try {
yield txn_a.transact();
@ -375,7 +374,7 @@ add_task(function* test_recycled_transactions() {
add_task(function* test_new_folder_with_annotation() {
const ANNO = { name: "TestAnno", value: "TestValue" };
let folder_info = yield createTestFolderInfo();
let folder_info = createTestFolderInfo();
folder_info.index = bmStartIndex;
folder_info.annotations = [ANNO];
ensureUndoState();
@ -410,7 +409,7 @@ add_task(function* test_new_folder_with_annotation() {
});
add_task(function* test_new_bookmark() {
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
let bm_info = { parentGuid: rootGuid
, url: NetUtil.newURI("http://test_create_item.com")
, index: bmStartIndex
, title: "Test creating an item" };
@ -447,7 +446,7 @@ add_task(function* test_new_bookmark() {
});
add_task(function* test_merge_create_folder_and_item() {
let folder_info = yield createTestFolderInfo();
let folder_info = createTestFolderInfo();
let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com")
, title: "Test Bookmark"
, index: bmStartIndex };
@ -485,7 +484,7 @@ add_task(function* test_merge_create_folder_and_item() {
});
add_task(function* test_move_items_to_folder() {
let folder_a_info = yield createTestFolderInfo("Folder A");
let folder_a_info = createTestFolderInfo("Folder A");
let bkm_a_info = { url: new URL("http://test_move_items.com")
, title: "Bookmark A" };
let bkm_b_info = { url: NetUtil.newURI("http://test_move_items.com")
@ -541,7 +540,7 @@ add_task(function* test_move_items_to_folder() {
ensureUndoState([[bkm_b_txn, bkm_a_txn, folder_a_txn]], 0);
// Test moving items between folders.
let folder_b_info = yield createTestFolderInfo("Folder B");
let folder_b_info = createTestFolderInfo("Folder B");
let folder_b_txn = PT.NewFolder(folder_b_info);
folder_b_info.guid = yield folder_b_txn.transact();
ensureUndoState([ [folder_b_txn]
@ -595,7 +594,7 @@ add_task(function* test_move_items_to_folder() {
});
add_task(function* test_remove_folder() {
let folder_level_1_info = yield createTestFolderInfo("Folder Level 1");
let folder_level_1_info = createTestFolderInfo("Folder Level 1");
let folder_level_2_info = { title: "Folder Level 2" };
let [folder_level_1_txn,
folder_level_2_txn] = yield PT.batch(function* () {
@ -688,7 +687,7 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
, POST_DATA = "post_data"
, ANNO = { name: "TestAnno", value: "TestAnnoValue" };
let folder_info = yield createTestFolderInfo();
let folder_info = createTestFolderInfo();
folder_info.guid = yield PT.NewFolder(folder_info).transact();
let ensureTags = ensureTagsForURI.bind(null, testURI);
@ -803,7 +802,7 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
});
add_task(function* test_creating_and_removing_a_separator() {
let folder_info = yield createTestFolderInfo();
let folder_info = createTestFolderInfo();
let separator_info = {};
let undoEntries = [];
@ -872,7 +871,7 @@ add_task(function* test_creating_and_removing_a_separator() {
add_task(function* test_add_and_remove_livemark() {
let createLivemarkTxn = PT.NewLivemark(
{ feedUrl: NetUtil.newURI("http://test.remove.livemark")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, parentGuid: rootGuid
, title: "Test Remove Livemark" });
let guid = yield createLivemarkTxn.transact();
let originalInfo = yield PlacesUtils.promiseBookmarksTree(guid);
@ -913,7 +912,7 @@ add_task(function* test_add_and_remove_livemark() {
});
add_task(function* test_edit_title() {
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
let bm_info = { parentGuid: rootGuid
, url: NetUtil.newURI("http://test_create_item.com")
, title: "Original Title" };
@ -951,10 +950,7 @@ add_task(function* test_edit_title() {
add_task(function* test_edit_url() {
let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
, url: oldURI
, tags: ["TestTag"]};
let bm_info = { parentGuid: rootGuid, url: oldURI, tags: ["TestTag"] };
function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) {
ensureItemsChanged({ guid: bm_info.guid
, property: "uri"
@ -1016,8 +1012,8 @@ add_task(function* test_edit_url() {
});
add_task(function* test_edit_keyword() {
let bm_info = { parentGuid: yield PlacesUtils.promiseItemGuid(root)
, url: NetUtil.newURI("http://test.edit.keyword") };
let bm_info = { parentGuid: rootGuid
, url: NetUtil.newURI("http://test.edit.keyword") };
const KEYWORD = "test_keyword";
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
function ensureKeywordChange(aCurrentKeyword = "") {
@ -1054,9 +1050,9 @@ add_task(function* test_edit_keyword() {
add_task(function* test_tag_uri() {
// This also tests passing uri specs.
let bm_info_a = { url: "http://bookmarked.uri"
, parentGuid: yield PlacesUtils.promiseItemGuid(root) };
, parentGuid: rootGuid };
let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root) };
, parentGuid: rootGuid };
let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri");
function* promiseIsBookmarked(aURI) {
@ -1128,10 +1124,10 @@ add_task(function* test_tag_uri() {
add_task(function* test_untag_uri() {
let bm_info_a = { url: NetUtil.newURI("http://bookmarked.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, parentGuid: rootGuid
, tags: ["A", "B"] };
let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, parentGuid: rootGuid
, tag: "B" };
yield PT.batch(function* () {
@ -1206,7 +1202,7 @@ add_task(function* test_untag_uri() {
add_task(function* test_annotate() {
let bm_info = { url: NetUtil.newURI("http://test.item.annotation")
, parentGuid: yield PlacesUtils.promiseItemGuid(root) };
, parentGuid: rootGuid };
let anno_info = { name: "TestAnno", value: "TestValue" };
function ensureAnnoState(aSet) {
ensureAnnotationsSet(bm_info.guid,
@ -1248,7 +1244,7 @@ add_task(function* test_annotate() {
});
add_task(function* test_annotate_multiple() {
let guid = yield PT.NewFolder(yield createTestFolderInfo()).transact();
let guid = yield PT.NewFolder(createTestFolderInfo()).transact();
let itemId = yield PlacesUtils.promiseItemId(guid);
function AnnoObj(aName, aValue) {
@ -1302,7 +1298,7 @@ add_task(function* test_annotate_multiple() {
});
add_task(function* test_sort_folder_by_name() {
let folder_info = yield createTestFolderInfo();
let folder_info = createTestFolderInfo();
let url = NetUtil.newURI("http://sort.by.name/");
let preSep = [{ title: i, url } for (i of ["3","2","1"])];
@ -1349,7 +1345,7 @@ add_task(function* test_sort_folder_by_name() {
add_task(function* test_livemark_txns() {
let livemark_info =
{ feedUrl: NetUtil.newURI("http://test.feed.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, parentGuid: rootGuid
, title: "Test Livemark" };
function ensureLivemarkAdded() {
ensureItemsAdded({ guid: livemark_info.guid
@ -1396,11 +1392,9 @@ add_task(function* test_livemark_txns() {
});
add_task(function* test_copy() {
let rootGuid = yield PlacesUtils.promiseItemGuid(root);
function* duplicate_and_test(aOriginalGuid) {
yield duplicateGuid =
yield PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid }).transact();
let txn = PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid });
yield duplicateGuid = yield txn.transact();
let originalInfo = yield PlacesUtils.promiseBookmarksTree(aOriginalGuid);
let duplicateInfo = yield PlacesUtils.promiseBookmarksTree(duplicateGuid);
yield ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false);
@ -1436,9 +1430,9 @@ add_task(function* test_copy() {
let sepTxn = PT.NewSeparator({ parentGuid: rootGuid, index: 1 });
let livemarkTxn = PT.NewLivemark(
{ feedUrl: new URL("http://test.feed.uri")
, parentGuid: yield PlacesUtils.promiseItemGuid(root)
, parentGuid: rootGuid
, title: "Test Livemark", index: 1 });
let emptyFolderTxn = PT.NewFolder(yield createTestFolderInfo());
let emptyFolderTxn = PT.NewFolder(createTestFolderInfo());
for (let txn of [livemarkTxn, sepTxn, emptyFolderTxn]) {
let guid = yield txn.transact();
yield duplicate_and_test(guid);
@ -1446,8 +1440,7 @@ add_task(function* test_copy() {
// Test duplicating a folder having some contents.
let filledFolderGuid = yield PT.batch(function *() {
let folderGuid =
yield PT.NewFolder(yield createTestFolderInfo()).transact();
let folderGuid = yield PT.NewFolder(createTestFolderInfo()).transact();
let nestedFolderGuid =
yield PT.NewFolder({ parentGuid: folderGuid
, title: "Nested Folder" }).transact();
@ -1469,9 +1462,7 @@ add_task(function* test_copy() {
});
add_task(function* test_array_input_for_batch() {
let rootGuid = yield PlacesUtils.promiseItemGuid(root);
let folderTxn = PT.NewFolder(yield createTestFolderInfo());
let folderTxn = PT.NewFolder(createTestFolderInfo());
let folderGuid = yield folderTxn.transact();
let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid });
@ -1503,9 +1494,7 @@ add_task(function* test_array_input_for_batch() {
});
add_task(function* test_copy_excluding_annotations() {
let rootGuid = yield PlacesUtils.promiseItemGuid(root);
let folderInfo = yield createTestFolderInfo();
let folderInfo = createTestFolderInfo();
let anno = n => { return { name: n, value: 1 } };
folderInfo.annotations = [anno("a"), anno("b"), anno("c")];
let folderGuid = yield PT.NewFolder(folderInfo).transact();
@ -1539,7 +1528,6 @@ add_task(function* test_copy_excluding_annotations() {
});
add_task(function* test_invalid_uri_spec_throws() {
let rootGuid = yield PlacesUtils.promiseItemGuid(root);
Assert.throws(() =>
PT.NewBookmark({ parentGuid: rootGuid
, url: "invalid uri spec"
@ -1551,3 +1539,120 @@ add_task(function* test_invalid_uri_spec_throws() {
PT.Tag({ tag: "TheTag"
, urls: ["about:blank", "invalid uri spec"] }));
});
add_task(function* test_annotate_multiple_items() {
let parentGuid = rootGuid;
let guids = [
yield PT.NewBookmark({ url: "about:blank", parentGuid }).transact(),
yield PT.NewFolder({ title: "Test Folder", parentGuid }).transact()];
let annotation = { name: "TestAnno", value: "TestValue" };
yield PT.Annotate({ guids, annotation }).transact();
function *ensureAnnoSet() {
for (let guid of guids) {
let itemId = yield PlacesUtils.promiseItemId(guid);
Assert.equal(annosvc.getItemAnnotation(itemId, annotation.name),
annotation.value);
}
}
function *ensureAnnoUnset() {
for (let guid of guids) {
let itemId = yield PlacesUtils.promiseItemId(guid);
Assert.ok(!annosvc.itemHasAnnotation(itemId, annotation.name));
}
}
yield ensureAnnoSet();
yield PT.undo();
yield ensureAnnoUnset();
yield PT.redo();
yield ensureAnnoSet();
yield PT.undo();
yield ensureAnnoUnset();
// Cleanup
yield PT.undo();
yield PT.undo();
yield ensureNonExistent(...guids);
yield PT.clearTransactionsHistory();
observer.reset();
});
add_task(function* test_remove_multiple() {
let guids = [];
yield PT.batch(function* () {
let folderGuid = yield PT.NewFolder({ title: "Test Folder"
, parentGuid: rootGuid }).transact();
let nestedFolderGuid =
yield PT.NewFolder({ title: "Nested Test Folder"
, parentGuid: folderGuid }).transact();
let nestedSepGuid = yield PT.NewSeparator(nestedFolderGuid).transact();
guids.push(folderGuid);
let bmGuid =
yield PT.NewBookmark({ url: new URL("http://test.bookmark.removed")
, parentGuid: rootGuid }).transact();
guids.push(bmGuid);
});
let originalInfos = [for (guid of guids)
yield PlacesUtils.promiseBookmarksTree(guid)];
yield PT.Remove(guids).transact();
yield ensureNonExistent(...guids);
yield PT.undo();
yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
yield PT.redo();
yield ensureNonExistent(...guids);
yield PT.undo();
yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
// Undo the New* transactions batch.
yield PT.undo();
yield ensureNonExistent(...guids);
// Redo it.
yield PT.redo();
yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
// Redo remove.
yield PT.redo();
yield ensureNonExistent(...guids);
// Cleanup
yield PT.clearTransactionsHistory();
observer.reset();
});
add_task(function* test_remove_bookmarks_for_urls() {
let urls = [new URL("http://test.url.1"), new URL("http://test.url.2")];
let guids = [];
yield PT.batch(function* () {
for (let url of urls) {
for (let title of ["test title a", "test title b"]) {
let txn = PT.NewBookmark({ url, title, parentGuid: rootGuid });
guids.push(yield txn.transact());
}
}
});
let originalInfos = [for (guid of guids)
yield PlacesUtils.promiseBookmarksTree(guid)];
yield PT.RemoveBookmarksForUrls(urls).transact();
yield ensureNonExistent(...guids);
yield PT.undo();
yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
yield PT.redo();
yield ensureNonExistent(...guids);
yield PT.undo();
yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
// Cleanup.
yield PT.redo();
yield ensureNonExistent(...guids);
yield PT.clearTransactionsHistory();
observer.reset();
});

View File

@ -192,6 +192,7 @@ add_task(function test_addLivemark_noSiteURI_succeeds()
do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
do_check_eq(livemark.index, PlacesUtils.bookmarks.getItemIndex(livemark.id));
do_check_eq(livemark.lastModified, PlacesUtils.bookmarks.getItemLastModified(livemark.id));
do_check_eq(livemark.dateAdded, PlacesUtils.bookmarks.getItemDateAdded(livemark.id));
do_check_true(livemark.feedURI.equals(FEED_URI));
do_check_eq(livemark.siteURI, null);
});
@ -211,6 +212,7 @@ add_task(function test_addLivemark_succeeds()
do_check_eq(livemark.title, "test");
do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
do_check_eq(livemark.index, PlacesUtils.bookmarks.getItemIndex(livemark.id));
do_check_eq(livemark.dateAdded, PlacesUtils.bookmarks.getItemDateAdded(livemark.id));
do_check_eq(livemark.lastModified, PlacesUtils.bookmarks.getItemLastModified(livemark.id));
do_check_true(livemark.feedURI.equals(FEED_URI));
do_check_true(livemark.siteURI.equals(SITE_URI));
@ -287,6 +289,18 @@ add_task(function test_addLivemark_forceGuid_succeeds()
checkLivemark(livemark);
});
add_task(function* test_addLivemark_dateAdded_succeeds() {
let dateAdded = new Date("2013-03-01T01:10:00") * 1000;
let livemark = yield PlacesUtils.livemarks.addLivemark(
{ title: "test"
, parentId: PlacesUtils.unfiledBookmarksFolderId
, index: PlacesUtils.bookmarks.DEFAULT_INDEX
, feedURI: FEED_URI
, dateAdded
});
do_check_eq(livemark.dateAdded, dateAdded);
});
add_task(function test_addLivemark_lastModified_succeeds()
{
let now = Date.now() * 1000;

View File

@ -55,6 +55,7 @@ skip-if = os == "android"
[test_485442_crash_bug_nsNavHistoryQuery_GetUri.js]
[test_486978_sort_by_date_queries.js]
[test_536081.js]
[test_1085291.js]
[test_adaptive.js]
# Bug 676989: test hangs consistently on Android
skip-if = os == "android"