/* global dump,
ModalDialog,
MozActivity,
Notification,
Service
*/
(function(exports) {
'use strict';
const DEBUG = false;
/**
* This developer system module captures a snapshot of the current device
* logs as displayed by logcat using DeviceStorage to persist the file for
* future access. It communicates with gecko code running in
* b2g/chrome/content/shell.js using a SystemAppProxy custom event based API.
* It requires the preference 'devtools.logshake' to be enabled
*
* @class LogShake
*/
function LogShake() {
}
function debug(str) {
if (DEBUG) {
dump('LogShake: ' + str + '\n');
}
}
LogShake.prototype = {
/**
* Start existing, observing for capture-logs events caused by Gecko
* LogShake
*/
start: function() {
Service.request('handleSystemMessageNotification', 'logshake', this);
window.addEventListener('volumeup+volumedown', this);
this.startCaptureLogsListener();
},
/**
* Stop the component, removing all listeners if necessary
*/
stop: function() {
Service.request('unhandleSystemMessageNotification', 'logshake', this);
this.stopCaptureLogsListener();
},
startCaptureLogsListener: function() {
debug('starting captureLogs listener');
window.addEventListener('capture-logs-start', this);
window.addEventListener('capture-logs-success', this);
window.addEventListener('capture-logs-error', this);
},
stopCaptureLogsListener: function() {
debug('stopping captureLogs listener');
window.removeEventListener('capture-logs-start', this);
window.removeEventListener('capture-logs-success', this);
window.removeEventListener('capture-logs-error', this);
},
/**
* Handle a capture-logs-start, capture-logs-success or capture-logs-error
* event, dispatching to the appropriate handler
*/
handleEvent: function(event) {
debug('handling event ' + event.type);
switch(event.type) {
case 'volumeup+volumedown':
this.requestSystemLogs();
break;
case 'capture-logs-start':
this.handleCaptureLogsStart(event);
break;
case 'capture-logs-success':
this.handleCaptureLogsSuccess(event);
break;
case 'capture-logs-error':
this.handleCaptureLogsError(event);
break;
}
},
_shakeId: null,
handleCaptureLogsStart: function(event) {
debug('handling capture-logs-start');
this._shakeId = Date.now();
this._notify('logsSaving', '');
},
requestSystemLogs: function() {
window.dispatchEvent(new CustomEvent('requestSystemLogs'));
},
/**
* Handle an event of type capture-logs-success. event.detail.locations is
* an array of absolute paths to the saved log files, and
* event.detail.logFolder is the folder name where the logs are located.
*/
handleCaptureLogsSuccess: function(event) {
debug('handling capture-logs-success');
navigator.vibrate(100);
this._notify('logsSaved', 'logsSavedBody',
this.triggerShareLogs.bind(this, event.detail.logFilenames),
event.detail);
this._shakeId = null;
},
handleCaptureLogsError: function(event) {
debug('handling capture logs error');
var error = event ? event.detail.error : '';
var errorMsg = this.formatError(error);
this._notify('logsSaveError', errorMsg,
this.showErrorMessage.bind(this, error),
event.detail);
this._shakeId = null;
},
getDeviceStorage: function() {
var storageName = 'sdcard';
var storages = navigator.getDeviceStorages(storageName);
for (var i = 0; i < storages.length; i++) {
if (storages[i].storageName === storageName) {
return storages[i];
}
}
return navigator.getDeviceStorage('sdcard');
},
triggerShareLogs: function(logFilenames, notif) {
var logFiles = [];
var storage = this.getDeviceStorage();
var requestsRemaining = logFilenames.length;
var self = this;
if (notif) {
notif.close();
}
function onSuccess() {
/* jshint validthis: true */
logFiles.push(this.result);
requestsRemaining -= 1;
if (requestsRemaining === 0) {
var logNames = logFiles.map(function(file) {
// For some reason file.name contains the full path despite
// File's documentation explicitly stating the opposite.
var pathComponents = file.name.split('/');
return pathComponents[pathComponents.length - 1];
});
/* jshint nonew: false */
new MozActivity({
name: 'share',
data: {
type: 'application/vnd.moz-systemlog',
blobs: logFiles,
filenames: logNames
}
});
}
}
function onError() {
/* jshint validthis: true */
self.handleCaptureLogsError({detail: {error: this.error}});
}
for (var logFilename of logFilenames) {
var req = storage.get(logFilename);
req.onsuccess = onSuccess;
req.onerror = onError;
}
},
ERRNO_TO_MSG: {
0: 'logsGenericError',
30: 'logsSDCardMaybeShared' // errno: filesystem ro-mounted
},
formatError: function(error) {
if (typeof error === 'string') {
return error;
}
if (typeof error === 'object') {
if ('operation' in error) {
return navigator.mozL10n.get('logsOperationFailed',
{ operation: error.operation });
}
}
return '';
},
showErrorMessage: function(error, notif) {
if (notif) {
notif.close();
}
// Do nothing for error string
if (typeof error === 'string') {
return;
}
if (typeof error !== 'object') {
console.warn('Unexpected error type: ' + typeof error);
return;
}
// Small heuristic for some frequent unix error cases
if ('unixErrno' in error) {
var errno = error.unixErrno;
debug('errno: ' + errno);
// Gracefully fallback to a generic error messages if we don't know
// this errno code.
if (!this.ERRNO_TO_MSG[errno]) {
errno = 0;
}
ModalDialog.alert('logsSaveError',
this.ERRNO_TO_MSG[errno], { title: 'ok' });
}
},
_notify: function(titleId, body, onclick, dataPayload) {
var title = navigator.mozL10n.get(titleId) || titleId;
var payload = {
body: navigator.mozL10n.get(body) || body,
tag: 'logshake:' + this._shakeId,
data: {
systemMessageTarget: 'logshake',
logshakePayload: dataPayload || undefined
}
};
var notification = new Notification(title, payload);
if (onclick) {
notification.onclick = onclick.bind(this, notification);
}
},
handleSystemMessageNotification: function(message) {
debug('Received system message: ' + JSON.stringify(message));
this.closeSystemMessageNotification(message);
if (!('logshakePayload' in message.data)) {
console.warn('Received logshake system message notification without ' +
'payload, silently discarding.');
return;
}
debug('Message payload: ' + message.data.logshakePayload);
var payload = message.data.logshakePayload;
if ('error' in payload) {
this.showErrorMessage(payload.error);
} else if ('logFilenames' in payload) {
this.triggerShareLogs(payload.logFilenames);
} else {
console.warn('Unidentified payload: ' + JSON.stringify(payload));
}
},
closeSystemMessageNotification: function(msg) {
Notification.get({ tag: msg.tag }).then(notifs => {
notifs.forEach(notif => {
if (notif.tag) {
// Close notification with the matching tag
if (notif.tag === msg.tag) {
notif.close && notif.close();
}
}
});
});
}
};
exports.LogShake = LogShake;
// XXX: See issue described in screenshot.js
exports.logshake = new LogShake();
exports.logshake.start();
})(window);