Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,5 @@
# Activities
component {70a83123-7467-4389-a309-3e81c74ad002} components/ActivitiesGlue.js
contract @mozilla.org/dom/activities/ui-glue;1 {70a83123-7467-4389-a309-3e81c74ad002}
interfaces components/activity_glue.xpt

View file

@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict"
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
function ActivitiesDialog() {
this._id = 0;
this.activities = [];
}
ActivitiesDialog.prototype = {
run: function ap_run() {
let id = "activity-choice" + this._id++;
let activity = this.activities.shift();
let choices = [];
activity.list.forEach(function(item) {
choices.push({ manifest: item.manifest, icon: item.icon });
});
// Keep up the frond-end of an activity choice. The messages contains
// a list of {names, icons} for applications able to handle this particular
// activity. The front-end should display a UI to pick one.
let browser = Services.wm.getMostRecentWindow("navigator:browser");
let content = browser.gBrowser.selectedBrowser.contentWindow;
let detail = {
type: "activity-choice",
id: id,
name: activity.name,
choices: choices
};
// Listen the resulting choice from the front-end. If there is no choice,
// let's return -1, which means the user has cancelled the dialog.
content.addEventListener("mozContentEvent", function act_getChoice(evt) {
if (evt.detail.id != id)
return;
content.removeEventListener("mozContentEvent", act_getChoice);
activity.callback.handleEvent(evt.detail.value !== undefined
? evt.detail.value
: -1);
});
let event = content.document.createEvent('CustomEvent');
event.initCustomEvent('mozChromeEvent', true, true,
Cu.cloneInto(detail, content));
content.dispatchEvent(event);
},
chooseActivity: function ap_chooseActivity(aName, aActivities, aCallback) {
this.activities.push({
name: aName,
list: aActivities,
callback: aCallback
});
Services.tm.currentThread.dispatch(this, Ci.nsIEventTarget.DISPATCH_NORMAL);
},
classID: Components.ID("{70a83123-7467-4389-a309-3e81c74ad002}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIActivityUIGlue, Ci.nsIRunnable])
}
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ActivitiesDialog]);

View file

@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsISupports.idl"
[scriptable, function, uuid(7a16feb4-5a78-4589-9174-b728f26942e2)]
interface nsIActivityUIGlueCallback : nsISupports
{
void handleEvent(in long choice);
};
/**
* To be implemented by @mozilla.org/dom/activities/ui-glue;1
*/
[scriptable, uuid(8624ad73-937a-400f-9d93-39ab5449b867)]
interface nsIActivityUIGlue : nsISupports
{
/**
* @param name The name of the activity to handle (eg. "share", "pick").
* @param activities A json blob which is an array of { "title":"...", "icon":"..." }.
* @param onresult The callback to send the index of the choosen activity. Send -1 if no choice is made.
*/
void chooseActivity(in DOMString title, in jsval activities, in nsIActivityUIGlueCallback onresult);
};

View file

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>activities@gaiamobile.org</em:id>
<em:name>activities.js</em:name>
<em:version>0.2</em:version>
<em:iconURL>chrome://activities.js/skin/logo.png</em:iconURL>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- Firefox -->
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>25.*</em:maxVersion>
</Description>
</em:targetApplication>
<em:unpack>true</em:unpack>
<em:bootstrap>false</em:bootstrap>
<em:creator>Vivien Nicolas</em:creator>
<em:description>activities.js</em:description>
<em:homepageURL>https://github.com/mozilla-b2g/gaia/</em:homepageURL>
</Description>
</RDF>

View file

@ -0,0 +1 @@
activities

View file

@ -0,0 +1 @@
interfaces components/alarms.xpt

Binary file not shown.

View file

@ -0,0 +1,14 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "domstubs.idl"
[scriptable, uuid(157b1ee4-f696-486d-bd55-205e21e88fab)]
interface nsIAlarmHalService : nsISupports
{
bool setAlarm(in long aSeconds, in long aNanoseconds);
void setAlarmFiredCb(in jsval aAlarmFiredCb);
void setTimezoneChangedCb(in jsval aTimezoneChangedCb);
};

View file

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>alarms@gaiamobile.org</em:id>
<em:name>alarms.js</em:name>
<em:version>0.1</em:version>
<em:iconURL>chrome://alarms.js/skin/logo.png</em:iconURL>
<em:targetApplication>
<Description>
<em:id>toolkit@mozilla.org</em:id>
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>22.0.*</em:maxVersion>
</Description>
</em:targetApplication>
<em:unpack>true</em:unpack>
<em:bootstrap>false</em:bootstrap>
<em:creator>Vivien Nicolas</em:creator>
<em:description>alarms.js</em:description>
<em:homepageURL>https://github.com/mozilla-b2g/gaia/</em:homepageURL>
</Description>
</RDF>

View file

@ -0,0 +1 @@
alarms

View file

@ -0,0 +1,292 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import('resource://gre/modules/Services.jsm');
function debug(data) {
dump('browser-helper: ' + data + '\n');
}
function initResponsiveDesign(browserWindow) {
// Inject custom controls in responsive view
Cu.import('resource:///modules/devtools/responsivedesign.jsm');
ResponsiveUIManager.once('on', function(event, {tab:tab}) {
let responsive = tab.__responsiveUI;
let document = tab.ownerDocument;
browserWindow.shell = {
sendChromeEvent: sendChromeEvent
};
// Ensure tweaking only the first responsive mode opened
responsive.stack.classList.add('os-mode');
let sleepButton = document.createElement('button');
sleepButton.id = 'os-sleep-button';
sleepButton.setAttribute('top', 0);
sleepButton.setAttribute('right', 0);
sleepButton.addEventListener('mousedown', function() {
sendChromeEvent({type: 'sleep-button-press'});
});
sleepButton.addEventListener('mouseup', function() {
sendChromeEvent({type: 'sleep-button-release'});
});
responsive.stack.appendChild(sleepButton);
let volumeButtons = document.createElement('vbox');
volumeButtons.id = 'os-volume-buttons';
volumeButtons.setAttribute('top', 0);
volumeButtons.setAttribute('left', 0);
let volumeUp = document.createElement('button');
volumeUp.id = 'os-volume-up-button';
volumeUp.addEventListener('mousedown', function() {
sendChromeEvent({type: 'volume-up-button-press'});
});
volumeUp.addEventListener('mouseup', function() {
sendChromeEvent({type: 'volume-up-button-release'});
});
let volumeDown = document.createElement('button');
volumeDown.id = 'os-volume-down-button';
volumeDown.addEventListener('mousedown', function() {
sendChromeEvent({type: 'volume-down-button-press'});
});
volumeDown.addEventListener('mouseup', function() {
sendChromeEvent({type: 'volume-down-button-release'});
});
volumeButtons.appendChild(volumeUp);
volumeButtons.appendChild(volumeDown);
responsive.stack.appendChild(volumeButtons);
// <toolbar id="os-hardware-button">
// <toolbarbutton id="os-home-button" />
// </toolbar>
let bottomToolbar = document.createElement('toolbar');
bottomToolbar.id = 'os-hardware-buttons';
bottomToolbar.setAttribute('align', 'center');
bottomToolbar.setAttribute('pack', 'center');
let homeButton = document.createElement('toolbarbutton');
homeButton.id = 'os-home-button';
homeButton.setAttribute('class', 'devtools-toolbarbutton');
homeButton.addEventListener('mousedown', function() {
sendChromeEvent({type: 'home-button-press'});
});
homeButton.addEventListener('mouseup', function() {
sendChromeEvent({type: 'home-button-release'});
});
bottomToolbar.appendChild(homeButton);
responsive.container.appendChild(bottomToolbar);
// Simulate chrome event.
function sendChromeEvent(detail) {
let contentWindow = responsive.browser.contentWindow;
var contentDetail = Components.utils.createObjectIn(contentWindow);
for (var i in detail) {
contentDetail[i] = detail[i];
}
Components.utils.makeObjectPropsNormal(contentDetail);
var customEvt = contentWindow.document.createEvent('CustomEvent');
customEvt.initCustomEvent('mozChromeEvent', true, true, contentDetail);
contentWindow.dispatchEvent(customEvt);
}
});
// Cleanup responsive mode if it's disabled
ResponsiveUIManager.on('off', function(event, tab, responsive) {
if (responsive.stack.classList.contains('os-mode')) {
responsive.stack.classList.remove('os-mode');
let document = tab.ownerDocument;
let sleepButton = document.getElementById('os-sleep-button');
responsive.stack.removeChild(sleepButton);
let volumeButtons = document.getElementById('os-volume-buttons');
responsive.stack.removeChild(volumeButtons);
let bottomToolbar = document.getElementById('os-hardware-buttons');
responsive.container.removeChild(bottomToolbar);
}
});
// Automatically toggle responsive design mode
let mgr = browserWindow.ResponsiveUI.ResponsiveUIManager;
mgr.handleGcliCommand(browserWindow,
browserWindow.gBrowser.selectedTab,
'resize on');
// Enable touch events
browserWindow.gBrowser.selectedTab.__responsiveUI.enableTouch();
}
function startup(data, reason) {
try {
// Initialize various JSM instanciated by shell.js
// All of them are usefull even if we don't use them in this file.
Cu.import('resource://gre/modules/ContactService.jsm');
Cu.import('resource://gre/modules/SettingsRequestManager.jsm');
Cu.import('resource://gre/modules/ActivitiesService.jsm');
var mm = Cc['@mozilla.org/globalmessagemanager;1']
.getService(Ci.nsIMessageBroadcaster);
// Allow panning to work in the system app.
mm.loadFrameScript('chrome://global/content/BrowserElementPanning.js', true);
// Enable mozApps.getAll and app.launch to work.
// By default on firefox, apps are consider as not launchable
// and only launchable apps appear in getAll().
Cu.import('resource://gre/modules/Webapps.jsm');
DOMApplicationRegistry.allAppsLaunchable = true;
// Track loading of the system app in order to load our striped down copy
// of shell.xul
Services.obs.addObserver(function(document) {
// Some documents like XBL don't have location and should be ignored
if (!document.location)
return;
let domain = document.location.toString();
if (domain.indexOf('system.gaiamobile.org') != -1) {
let scope = {
content: document.defaultView
};
Services.scriptloader.loadSubScript(
'chrome://browser-helper.js/content/shell.js', scope);
}
}, 'document-element-inserted', false);
// Watch for app load start in firefox tabs.
// We have to set the app id on each docshell where we are trying to load
// an app. That's because we can't set mozapp attribute on firefox iframes
// as Firefox is still using xul:browser.
let nsIWebProgressListener = Ci.nsIWebProgressListener;
let appsService = Cc['@mozilla.org/AppsService;1'].getService(Ci.nsIAppsService);
let docShellListener = {
onStateChange: function onStateChange(webProgress, request, flags, status) {
if (!webProgress.chromeEventHandler ||
webProgress.chromeEventHandler.tagName != "xul:browser") {
// Only set app id for firefox tab <xul:browser> frames
return;
}
// Catch any load start request
if (flags & nsIWebProgressListener.STATE_START &&
flags & nsIWebProgressListener.STATE_IS_DOCUMENT &&
(request instanceof Ci.nsIChannel || 'URI' in request)) {
// Try to compute the manifest URL out of document URL
// We should end up with urls like:
// http://system.gaiamobile.org:8080/manifest.webapp
// app://system.gaiamobile.org/manifest.webapp
let manifestURL = 'app://'+ request.URI.host +'/manifest.webapp';
let manifest = appsService.getAppByManifestURL(manifestURL);
if (manifest) {
let app = manifest.QueryInterface(Ci.mozIApplication);
// test-agent expects its iframes to load apps without any permissions
// so prevent setting the correct app id to its iframes
if (!webProgress.DOMWindow || !webProgress.DOMWindow.top ||
!webProgress.DOMWindow.top.location.host.startsWith("test-agent.gaiamobile.org")) {
webProgress.QueryInterface(Ci.nsIDocShell).setIsApp(app.localId);
}
}
}
}
};
// Watch for new Firefox top level windows
Services.obs.addObserver(function(win) {
win.addEventListener('DOMContentLoaded', function loaded() {
win.removeEventListener('DOMContentLoaded', loaded);
if (win.gBrowser) {
// Listen for loads happening in all browser windows.
win.gBrowser.addProgressListener(docShellListener);
}
});
}, 'domwindowopened', false);
Services.obs.addObserver(function() {
let browserWindow = Services.wm.getMostRecentWindow('navigator:browser');
// Inject CSS in browser to customize responsive view
let doc = browserWindow.document;
let pi = doc.createProcessingInstruction('xml-stylesheet', 'href="chrome://browser-helper.js/content/browser.css" type="text/css"');
doc.insertBefore(pi, doc.firstChild);
// Start responsive design mode
initResponsiveDesign(browserWindow);
// And devtool panel while maximizing its size according to screen size
Services.prefs.setIntPref('devtools.toolbox.sidebar.width',
browserWindow.outerWidth - 550);
gDevToolsBrowser.selectToolCommand(browserWindow.gBrowser);
let KeyboardGlobal = Cu.import('resource://gre/modules/Keyboard.jsm');
// SystemAppProxy.jsm doesn't ship into Firefox.
// Keyboard.jsm is the only one user of it outside of b2g/ folder.
// So hack it, in order to allow keyboard to send events to the system app
Object.defineProperty(KeyboardGlobal, "SystemAppProxy", {
value: {
dispatchEvent: function (detail) {
let contentWindow = browserWindow.content;
var contentDetail = Components.utils.createObjectIn(contentWindow);
for (var i in detail) {
contentDetail[i] = detail[i];
}
Components.utils.makeObjectPropsNormal(contentDetail);
var customEvt = contentWindow.document.createEvent('CustomEvent');
customEvt.initCustomEvent('mozChromeEvent', true, true, contentDetail);
contentWindow.dispatchEvent(customEvt);
}
}
});
Keyboard.initFormsFrameScript(mm);
mm.loadFrameScript('chrome://global/content/forms.js', true);
}, 'sessionstore-windows-restored', false);
try {
// Register a new devtool panel with various OS controls
Cu.import('resource:///modules/devtools/gDevTools.jsm');
gDevTools.registerTool({
id: 'firefox-os-controls',
key: 'F',
modifiers: 'accel,shift',
icon: 'chrome://browser-helper.js/content/panel/icon.gif',
url: 'chrome://browser-helper.js/content/panel/index.html',
label: 'FFOS Control',
tooltip: 'Set of controls to tune FirefoxOS apps in Desktop browser',
isTargetSupported: function(target) {
return target.isLocalTab;
},
build: function(iframeWindow, toolbox) {
iframeWindow.wrappedJSObject.tab = toolbox.target.window;
return {
destroy: function () {}
};
}
});
} catch(e) {
debug('Can\'t load the devtools panel. Likely because this version of Gecko is too old');
}
} catch (e) {
debug('Something went wrong while trying to start browser-helper: ' + e);
}
}
function shutdown(data, reason) {
// prevent stdout warnings
}
function install(data, reason) {
}
function uninstall(data, reason) {
}

View file

@ -0,0 +1 @@
content browser-helper.js content/

View file

@ -0,0 +1,102 @@
/* Ignore devtools usual resizer */
stack.browserStack.os-mode box.devtools-responsiveui-resizehandle,
stack.browserStack.os-mode box.devtools-responsiveui-resizebar {
display: none;
}
/* Gives responsive mode a phone look'n feel */
stack.browserStack.os-mode {
padding: 60px 15px 0;
border-radius: 25px / 20px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border: 1px solid #FFFFFF;
border-bottom-width: 0;
background-color: #353535;
box-shadow: 0 3px 0.7px 1px #777777, 0 5px rgba(0, 0, 0, 0.4) inset !important;
background-image: -moz-linear-gradient(right, #111 11%, #333 56%);
min-width: 320px;
}
#os-hardware-buttons {
-moz-appearance: none;
padding: 20px;
border: 1px solid #FFFFFF;
border-bottom-left-radius: 25px;
border-bottom-right-radius: 25px;
border-top-width: 0;
box-shadow: 0 3px 0.7px 1px #777777, 0 -7px rgba(0, 0, 0, 0.4) inset;
background-image: -moz-linear-gradient(right, #111 11%, #333 56%);
}
#os-home-button {
width: 40px;
height: 30px;
list-style-image: url("chrome://browser-helper.js/content/icons/home.png");
-moz-image-region: rect(0, 12px, 12px, 0);
}
#os-home-button:hover:active {
-moz-image-region: rect(0, 24px, 12px, 12px);
}
#os-sleep-button {
-moz-appearance: none;
/* compensate browserStack top padding */
margin-top: -67px;
margin-right: 10px;
min-width: 10px;
width: 50px;
height: 5px;
border: 1px solid #444;
border-top-right-radius: 12px;
border-top-left-radius: 12px;
border-bottom-color: transparent;
background-image: -moz-linear-gradient(top, #111 11%, #333 56%);
}
#os-sleep-button:hover:active {
background-image: -moz-linear-gradient(top, #aaa 11%, #ddd 56%);
}
#os-volume-buttons {
margin-left: -29px;
}
#os-volume-up-button,
#os-volume-down-button {
-moz-appearance: none;
border: 1px solid red;
min-width: 8px;
height: 40px;
border: 1px solid #444;
border-right-color: transparent;
background-image: -moz-linear-gradient(right, #111 11%, #333 56%);
}
#os-volume-up-button:hover:active,
#os-volume-down-button:hover:active {
background-image: -moz-linear-gradient(right, #aaa 11%, #ddd 56%);
}
#os-volume-up-button {
border-top-left-radius: 12px;
}
#os-volume-down-button {
border-bottom-left-radius: 12px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

View file

@ -0,0 +1,78 @@
!function() {
function sendChromeEvent(detail) {
var contentDetail = Components.utils.createObjectIn(tab);
for (var i in detail) {
contentDetail[i] = detail[i];
}
Components.utils.makeObjectPropsNormal(contentDetail);
var customEvt = tab.document.createEvent('CustomEvent');
customEvt.initCustomEvent('mozChromeEvent', true, true, contentDetail);
tab.dispatchEvent(customEvt);
}
function sendEvent(type, details) {
var event = new tab.CustomEvent(type, details);
tab.dispatchEvent(event);
}
function takeScreenshot() {
var canvas = document.createElementNS('http://www.w3.org/1999/xhtml',
'canvas');
var width = tab.innerWidth;
var height = tab.innerHeight;
canvas.setAttribute('width', width);
canvas.setAttribute('height', height);
var context = canvas.getContext('2d');
var flags =
context.DRAWWINDOW_DRAW_CARET |
context.DRAWWINDOW_DRAW_VIEW |
context.DRAWWINDOW_USE_WIDGET_LAYERS;
context.drawWindow(tab, 0, 0, width, height,
'rgb(255,255,255)', flags);
sendChromeEvent({
type: 'take-screenshot-success',
file: canvas.mozGetAsFile('screenshot', 'image/png')
});
}
function Emulation() {
}
Emulation.prototype = {
notification: function() {
sendChromeEvent({
type: 'desktop-notification',
id: 123,
title: 'Some Notification',
text: 'I love notifications.',
manifestURL: 'http://sytem.gaiamobile.org:8080/manifest.webapp'
});
}
};
window.emulation = new Emulation();
function Workflow() {
}
Workflow.prototype = {
reload: function() {
var global = tab.wrappedJSObject;
var app = global.System.curentApp;
app.reload();
},
screenshot: function() {
takeScreenshot();
}
}
window.workflow = new Workflow();
}();

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

View file

@ -0,0 +1,17 @@
<link rel="stylesheet" href="stylesheet.css">
<script defer src="controls.js"></script>
<script defer src="workloads.js"></script>
<h3>Emulation</h3>
<section>
<button onclick="emulation.notification()">Notification</button>
</section>
<h3>Developer</h3>
<section>
<button onclick="workflow.reload()">Reload Application</button>
<button onclick="workflow.screenshot()">Take a screenshot</button>
<select id="reference-workloads">
<option value="-1">Import workload:</option>
</select>
</section>

View file

@ -0,0 +1,12 @@
* {
font-family: sans-serif;
}
section {
border-bottom: 1px solid #ccc;
padding-bottom: 1rem;
}
h3 {
margin-bottom: 1rem;
}

View file

@ -0,0 +1,78 @@
!function() {
var select = document.getElementById('reference-workloads');
var options = [
{
name: 'contacts',
db: 'contacts',
counts: [0, 200, 500, 1000, 2000]
},
{
name: 'messages',
db: 'sms',
counts: [0, 200, 500, 1000, 2000]
},
{
name: 'dialer recents',
db: 'dialer',
counts: [0, 50, 100, 200, 500]
}
];
var targetFiles = {
sms: '226660312ssm.sqlite',
contacts: '3406066227csotncta.sqlite',
dialer: '2584670174dsitanleecreR.sqlite'
};
for (var i = 0, option; option = options[i]; i++) {
for (var j = 0, cLen = option.counts.length; j < cLen; j++) {
var count = option.counts[j];
var optionEl = document.createElement('option');
optionEl.innerHTML = count + ' ' + option.name;
optionEl.value = option.db + '-' + count;
select.appendChild(optionEl);
}
}
select.addEventListener('change', function(e) {
var parts = this.value.split('-');
var database = parts[0];
var count = parts[1];
importRecords(database, count);
tab.alert('Imported: ' + count + ' ' + database + ' records.');
this.value = '-1';
});
function importRecords(database, count) {
var Ci = Components.interfaces;
Components.utils.import('resource://gre/modules/Services.jsm');
var currProfD = Services.dirsvc.get('ProfD', Ci.nsIFile);
var profileDir = currProfD.path;
var file = database + 'Db-' + count + '.sqlite';
var sourceFile = Services.dirsvc.get('ProfD', Ci.nsILocalFile);
sourceFile.append('..');
sourceFile.append('test_media');
sourceFile.append('reference-workload');
sourceFile.append(file);
var targetFolder = Services.dirsvc.get('ProfD', Ci.nsILocalFile);
targetFolder.append('storage');
targetFolder.append('persistent');
targetFolder.append('chrome');
targetFolder.append('idb');
try {
sourceFile.copyTo(targetFolder, targetFiles[database]);
} catch (e) {
tab.alert('Could not import file: ' + e);
}
}
}();

View file

@ -0,0 +1,187 @@
dump('======== browser-helper: content.js loaded ========\n')
function debug(str) {
//dump('browser-helper (frame-script): ' + str + '\n');
}
let CC = Components.Constructor;
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let Cr = Components.results;
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/Keyboard.jsm');
// Various helpers coming from /b2g/chrome/content/shell.js
function getContentWindow() {
return content;
}
function sendChromeEvent(details, type) {
type = type || 'mozChromeEvent';
let content = getContentWindow();
details = details || {};
let event = content.document.createEvent('CustomEvent');
event.initCustomEvent(type, true, true,
Cu.cloneInto(details, content));
content.dispatchEvent(event);
}
// Listen for app.launch calls and forward them to the the system app.
// Copy of /b2g/chrome/content/shell.js
Cu.import('resource://gre/modules/Webapps.jsm');
Cu.import('resource://gre/modules/AppsUtils.jsm');
Services.obs.addObserver(function onLaunch(subject, topic, data) {
let json = JSON.parse(data);
DOMApplicationRegistry.getManifestFor(json.manifestURL).then((aManifest) => {
if (!aManifest)
return;
let manifest = new ManifestHelper(aManifest, json.origin,
json.manifestURL);
let data = {
'timestamp': json.timestamp,
'url': manifest.fullLaunchPath(json.startPoint),
'manifestURL': json.manifestURL
};
sendChromeEvent(data, 'webapps-launch');
});
}, 'webapps-launch', false);
// Listen for system messages and relay them to Gaia.
// Copy of /b2g/chrome/content/shell.js
// XXX This code should be loaded in the activities/ extension
Services.obs.addObserver(function onSystemMessage(subject, topic, data) {
let msg = JSON.parse(data);
let origin = Services.io.newURI(msg.manifest, null, null).prePath;
sendChromeEvent({
url: msg.uri,
manifestURL: msg.manifest,
isActivity: (msg.type == 'activity'),
target: msg.target,
expectingSystemMessage: true
}, 'open-app');
}, 'system-messages-open-app', false);
Services.obs.addObserver(function(aSubject, aTopic, aData) {
let data = JSON.parse(aData);
sendChromeEvent({
type: 'activity-done',
success: data.success,
manifestURL: data.manifestURL,
pageURL: data.pageURL
});
}, 'activity-done', false);
// =================== Settings ====================
// Copy of /b2g/chrome/content/settings.js
var SettingsListener = {
_callbacks: {},
init: function sl_init() {
if ('mozSettings' in content.navigator && content.navigator.mozSettings) {
content.navigator.mozSettings.onsettingchange = this.onchange.bind(this);
}
},
onchange: function sl_onchange(evt) {
var callback = this._callbacks[evt.settingName];
if (callback) {
callback(evt.settingValue);
}
},
observe: function sl_observe(name, defaultValue, callback) {
var settings = content.navigator.mozSettings;
if (!settings) {
content.setTimeout(function() { callback(defaultValue); });
return;
}
if (!callback || typeof callback !== 'function') {
throw new Error('Callback is not a function');
}
var req = settings.createLock().get(name);
req.addEventListener('success', (function onsuccess() {
callback(typeof(req.result[name]) != 'undefined' ?
req.result[name] : defaultValue);
}));
this._callbacks[name] = callback;
}
};
SettingsListener.init();
// =================== Languages ====================
// Modified copy of /b2g/chrome/content/settings.js
SettingsListener.observe('language.current', 'en-US', function(value) {
Services.prefs.setCharPref('general.useragent.locale', value);
let prefName = 'intl.accept_languages';
if (Services.prefs.prefHasUserValue(prefName)) {
Services.prefs.clearUserPref(prefName);
}
let intl = '';
try {
intl = Services.prefs.getComplexValue(prefName,
Ci.nsIPrefLocalizedString).data;
} catch(e) {}
// Bug 830782 - Homescreen is in English instead of selected locale after
// the first run experience.
// In order to ensure the current intl value is reflected on the child
// process let's always write a user value, even if this one match the
// current localized pref value.
if (!((new RegExp('^' + value + '[^a-z-_] *[,;]?', 'i')).test(intl))) {
value = value + ', ' + intl;
} else {
value = intl;
}
Services.prefs.setCharPref(prefName, value);
//XXX: modification of settings.js here:
var window = getContentWindow();
if (!('hasStarted' in window) && window.hasStarted != true) {
window.hasStarted = true;
window.addEventListener('load', function onload() {
window.removeEventListener('load', onload);
window.setTimeout(function() {
sendChromeEvent({'type': 'webapps-registry-ready'});
});
});
}
});
/**
* This code comes from b2g/chrome/content/shell.js
* For now just the keyboard stuff, should copy everything over at some point
*/
getContentWindow().addEventListener('mozContentEvent', function(evt) {
let detail = evt.detail;
debug('XXX FIXME : Got a mozContentEvent: ' + detail.type + "\n");
switch(detail.type) {
case 'inputmethod-update-layouts':
KeyboardHelper.handleEvent(detail);
break;
}
});
let KeyboardHelper = {
handleEvent: function keyboard_handleEvent(aMessage) {
Keyboard.setLayouts(aMessage.layouts);
}
};

View file

@ -0,0 +1,23 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>browser-helper@gaiamobile.org</em:id>
<em:name>browser-helper.js</em:name>
<em:version>0.1</em:version>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:unpack>true</em:unpack>
<em:bootstrap>true</em:bootstrap>
<em:creator>Mozilla</em:creator>
<em:description>browser shims to make gaia work in desktop browser</em:description>
<em:homepageURL>https://github.com/mozilla-b2g/gaia/</em:homepageURL>
</Description>
</RDF>

View file

@ -0,0 +1,95 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
const CC = Components.Constructor;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import('resource://gre/modules/Services.jsm');
function debug(data) {
//dump('desktop-helper: ' + data + '\n');
}
const kChromeRootPath = 'chrome://desktop-helper.js/content/data/';
// XXX Scripts should be loaded based on the permissions of the apps not
// based on the domain.
const kScriptsPerDomain = {
'.gaiamobile.org': [
'ffos_runtime.js',
'lib/bluetooth.js',
'lib/cameras.js',
'lib/mobile_connection.js',
'lib/icc_manager.js',
'lib/telephony.js',
'lib/wifi.js'
]
};
function injectMocks() {
// Track loading of apps to inject mock APIs
Services.obs.addObserver(function(document) {
// Some documents like XBL don't have location and should be ignored
if (!document.location || !document.defaultView)
return;
let currentDomain = document.location.toString();
let window = document.defaultView;
// Do not include mocks for unit test sandboxes
if (window.wrappedJSObject.mocha &&
currentDomain.indexOf('_sandbox.html') !== -1) {
return;
}
debug('+++ loading scripts for app: ' + currentDomain + "\n");
// Inject mocks based on domain
for (let domain in kScriptsPerDomain) {
if (currentDomain.indexOf(domain) == -1)
continue;
let includes = kScriptsPerDomain[domain];
for (let i = 0; i < includes.length; i++) {
debug('loading ' + includes[i] + '...');
Services.scriptloader.loadSubScript(kChromeRootPath + includes[i],
window.wrappedJSObject);
}
}
}, 'document-element-inserted', false);
}
function hotfixAlarms() {
// Replace existing alarm service xpcom factory by a new working one,
// until a working fallback is implemented in platform code (bug 867868)
// Sigh. Seems like there is a registration issue between all the addons.
// This is dirty but delaying this seems to make it.
var timer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
timer.initWithCallback(function() {
Services.scriptloader.loadSubScript('chrome://desktop-helper.js/content/alarms.js', {});
}, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
}
function startup(data, reason) {
try {
hotfixAlarms();
injectMocks();
} catch (e) {
debug('Something went wrong while trying to start desktop-helper: ' + e);
}
}
function shutdown(data, reason) {
}
function install(data, reason) {
}
function uninstall(data, reason) {
}

View file

@ -0,0 +1 @@
content desktop-helper.js content/

View file

@ -0,0 +1,102 @@
/* 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/. */
let Cc = Components.classes;
let Ci = Components.interfaces;
let Cu = Components.utils;
let Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');
function debug(str) {
//dump('AlarmHalService: ' + str + '\n');
}
// -----------------------------------------------------------------------
// AlarmHalService
// -----------------------------------------------------------------------
function AlarmHalService() {
debug('Constructor');
}
AlarmHalService.prototype = {
_timer: Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer),
setAlarm: function setAlarm(seconds, nanoseconds) {
debug('setAlarm: ' + seconds);
this._currentAlarm = seconds;
this._timer.cancel();
this._timer.initWithCallback(this, 1000, Ci.nsITimer.TYPE_REPEATING_SLACK);
return true;
},
notify: function notify() {
debug('notify: ' + (Date.now() / 1000) + ' < ' + this._currentAlarm + '\n');
if ((Date.now() / 1000) < this._currentAlarm)
return;
debug('fired!!!');
// Let's stop the timer to make sure it does not fire again...
this._timer.cancel();
if (this._alarmFiredCb) {
try {
this._alarmFiredCb();
} catch (e) {}
}
},
_alarmFiredCb: null,
setAlarmFiredCb: function setAlarmFiredCb(callback) {
debug('setAlarmFiredCb!!!');
this._alarmFiredCb = callback;
},
_timezoneChangeDb: null,
setTimezoneChangedCb: function setTimezoneChangedCb(callback) {
debug('setTzFiredCb!!!');
this._timezoneChangedCb = callback;
},
classID: newClassID,
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIAlarmHalService, Ci.nsITimerCallback
])
};
// Unregister the embedded low level component for alarm and
// replace it with our own.
let contract = '@mozilla.org/alarmHalService;1';
var newClassID = Cc['@mozilla.org/uuid-generator;1']
.getService(Ci.nsIUUIDGenerator)
.generateUUID();
// Unregister the old factory.
let oldCid = Cm.contractIDToCID(contract);
let oldFactory = Cm.getClassObjectByContractID(contract, Ci.nsIFactory);
Cm.unregisterFactory(oldCid, oldFactory);
// Register a new one.
let instance = null;
let newFactory = {
createInstance: function(outer, iid) {
if (outer)
throw Components.results.NS_ERROR_NO_AGGREGATION;
if (instance === null)
instance = new AlarmHalService();
instance.QueryInterface(iid);
return instance.QueryInterface(iid);
},
lockFactory: function(aLock) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
};
Cm.registerFactory(newClassID, '', contract, newFactory);
// Then let's start the AlarmService!
Cu.import('resource://gre/modules/AlarmService.jsm');

View file

@ -0,0 +1,65 @@
FFOS_RUNTIME = {
/**
* Emulates a dom request
* Returns an object and calls it's onsuccess handler.
*/
domRequest: function(result, timeout) {
timeout = timeout || 1000;
return function() {
var request = { result: result };
setTimeout(function() {
if (request.onsuccess) {
request.onsuccess();
}
}, timeout);
return request;
}
},
/**
* This is so we have a single entry-point for the APP window
* This is needed as the interface to window may be changing soon
*/
getAppWindow: function(callback) {
callback(window);
},
/**
* Proxies console.log statements to the app window
*/
debug: function() {
var args = Array.slice(arguments);
this.getAppWindow(function(win) {
win.console.log.apply(win.console, args);
});
},
/**
* Creates a navigator shim on the window
* @param {String} property name.
* @param {Object} property definition.
* @param {Boolean} if true, makes a navigator setter for this object. Useful for testing.
*/
makeNavigatorShim: function(property, definition, makeSetter) {
try {
window.navigator.__defineGetter__(property, function() {
return definition;
});
if (makeSetter) {
window.navigator.__defineSetter__(property, function(prop) {
definition = prop;
});
}
} catch (e) {
alert('Error intializing shim (' + property + '): ' + e);
}
}
};
var debug = FFOS_RUNTIME.debug;

View file

@ -0,0 +1,34 @@
!function() {
function debug(str) {
//dump('Bluetooth : ' + str + '\n');
}
function MockBluetoothRequest() {
}
MockBluetoothRequest.prototype = {
};
FFOS_RUNTIME.makeNavigatorShim('mozBluetooth', {
enabled: true,
addEventListener: function(type, callback, bubble) {},
onenabled: function(event) {},
ondisabled: function(event) {},
onadapteradded: function() {
debug('onadapteradded');
},
getDefaultAdapter: FFOS_RUNTIME.domRequest({
ondevicefound: function() {},
setDiscoverable: function() {},
getPairedDevices: FFOS_RUNTIME.domRequest([]),
startDiscovery: FFOS_RUNTIME.domRequest(),
name: 'I like blue',
setName: FFOS_RUNTIME.domRequest(),
getConnectedDevices: FFOS_RUNTIME.domRequest()
}),
isConnected: function() {
debug('isConnected');
}
});
}();

View file

@ -0,0 +1,88 @@
!function() {
var CameraManager = {
// The camera stream
stream: false,
capabilities: {
focusModes: '',
pictureSizes: [{
width: 600,
height: 600
}],
previewSizes: [{
width: 600,
height: 600
}],
recorderProfiles: {
cif: {
video: {
width: 600,
height: 600
}
}
}
},
getListOfCameras: function() {
console.log('mozCamera.getListOfCameras');
return [this];
},
/**
* Gets a camera
*/
getCamera: function(options, callback) {
console.log('mozCamera.getCamera');
navigator.mozGetUserMedia({video: true},
function(stream) {
this.stream = stream;
callback(this);
this.onPreviewStateChange('started');
}.bind(this),
function() {
console.log('Could not initialize camera.');
}
);
},
getPreviewStream: function(config, callback) {
console.log('mozCamera.getPreviewStream');
callback(this.stream);
},
getPreviewStreamVideoMode: function(config, callback) {
console.log('mozCamera.getPreviewStreamVideoMode');
callback(this.stream);
},
resumePreview: function() {
console.log('mozCamera.resumePreview');
},
autoFocus: function() {
console.log('mozCamera.autoFocus');
},
takePicture: function(config, onSuccess, onError) {
console.log('mozCamera.takePicture');
var cnvs = document.createElement('canvas');
cnvs.width = config.pictureSize.width;
cnvs.height = config.pictureSize.height;
var ctx = cnvs.getContext('2d');
ctx.drawImage(window.document.querySelector('video'), 0, 0, cnvs.width, cnvs.height);
cnvs.toBlob(function(blob) {
onSuccess(blob);
}, 'image/jpeg');
},
release: function() {
console.log('mozCamera.release');
}
};
FFOS_RUNTIME.makeNavigatorShim('mozCameras', CameraManager);
}();

View file

@ -0,0 +1,103 @@
!function() {
function debug(str) {
//dump('mozIccManager: ' + str + '\n');
}
var iccs = {
111: {
_retryCount: 3,
cardState: 'ready',
iccInfo: {
iccid: true,
msisdn: '5555555555'
},
setCardLock: function() {
debug('setCardLock');
},
getCardLock: FFOS_RUNTIME.domRequest({ enabled: false }),
getCardLockRetryCount: function(type) {
var request = {};
setTimeout(function() {
request.result = {
retryCount: this._retryCount
};
request.onsuccess && request.onsuccess();
}.bind(this));
return request;
},
unlockCardLock: function() {
// simulate invalid input
if (this._retryCount > 1) {
var request = {};
setTimeout(function() {
request.error = {
retryCount: --this._retryCount,
lockType: 'pin'
};
request.onerror && request.onerror();
}.bind(this), 200);
return request;
}
this.cardState = 'ready';
return FFOS_RUNTIME.domRequest()();
},
addEventListener: function() {
debug('addEventListener');
},
removeEventListener: function() {
debug('removeEventListener');
},
},
222: {
_retryCount: 3,
cardState: 'ready',
iccInfo: {
iccid: true
},
setCardLock: function() {
debug('setCardLock');
},
getCardLock: FFOS_RUNTIME.domRequest({ enabled: false }),
getCardLockRetryCount: function(type) {
var request = {};
setTimeout(function() {
request.result = {
retryCount: this._retryCount
};
request.onsuccess && request.onsuccess();
}.bind(this));
return request;
},
unlockCardLock: function() {
this.cardState = 'ready';
return FFOS_RUNTIME.domRequest()();
},
addEventListener: function() {
debug('addEventListener');
},
removeEventListener: function() {
debug('removeEventListener');
},
}
};
var iccManager = {
addEventListener: function() {
debug('addEventListener');
},
removeEventListener: function() {
debug('removeEventListener');
},
getIccById: function(iccId) {
return iccs[iccId];
},
iccIds: [111]
};
if (window._shimDualSim) {
iccManager.iccIds.push(222);
}
FFOS_RUNTIME.makeNavigatorShim('mozIccManager', iccManager, true);
}();

View file

@ -0,0 +1,96 @@
!function() {
function debug(str) {
//dump('mozMobileConnection: ' + str + '\n');
}
var initialized = true;
var fakeNetwork = { shortName: 'Fake Orange F', mcc: '208', mnc: '1' };
var fakeVoice = {
network: fakeNetwork,
state: 'notSearching',
roaming: true,
connected: true,
emergencyCallsOnly: false
};
var fakeNetwork2 = { shortName: 'Verizon Fake', mcc: '208', mnc: '1' };
var fakeVoice2 = {
network: fakeNetwork2,
state: 'notSearching',
roaming: true,
connected: true,
emergencyCallsOnly: false
};
function fakeEventListener(type, callback, bubble) {
if (initialized) {
if (typeof callback === 'function') {
setTimeout(callback.bind(this, { iccId: this.iccId }));
}
return;
}
// simulates a connection to a data network;
setTimeout(function fakeCallback() {
initialized = true;
if (typeof callback === 'function') {
callback({ iccId: this.iccId });
}
}.bind(this), 5000);
}
var mobileConnections = [{
iccId: 111,
addEventListener: fakeEventListener,
removeEventListener: function() {},
getCallWaitingOption: FFOS_RUNTIME.domRequest(true),
getCallForwardingOption: FFOS_RUNTIME.domRequest(1),
setCallWaitingOption: FFOS_RUNTIME.domRequest(),
getCallingLineIdRestriction: FFOS_RUNTIME.domRequest(),
setVoicePrivacyMode: FFOS_RUNTIME.domRequest(),
getVoicePrivacyMode: FFOS_RUNTIME.domRequest(),
setRadioEnabled: FFOS_RUNTIME.domRequest(),
getRadioEnabled: FFOS_RUNTIME.domRequest(),
getRoamingPreference: FFOS_RUNTIME.domRequest(),
setRoamingPreference: FFOS_RUNTIME.domRequest(),
getPreferredNetworkType: FFOS_RUNTIME.domRequest(),
setPreferredNetworkType: FFOS_RUNTIME.domRequest(),
get data() {
return initialized ? { network: fakeNetwork } : null;
},
get voice() {
return initialized ? fakeVoice : null;
}
}];
if (window._shimDualSim) {
mobileConnections.push({
iccId: 222,
addEventListener: fakeEventListener,
removeEventListener: function() {},
getCallWaitingOption: FFOS_RUNTIME.domRequest(true),
getCallForwardingOption: FFOS_RUNTIME.domRequest(1),
setCallWaitingOption: FFOS_RUNTIME.domRequest(),
getCallingLineIdRestriction: FFOS_RUNTIME.domRequest(),
setVoicePrivacyMode: FFOS_RUNTIME.domRequest(),
getVoicePrivacyMode: FFOS_RUNTIME.domRequest(),
setRadioEnabled: FFOS_RUNTIME.domRequest(),
getRadioEnabled: FFOS_RUNTIME.domRequest(),
getRoamingPreference: FFOS_RUNTIME.domRequest(),
setRoamingPreference: FFOS_RUNTIME.domRequest(),
getPreferredNetworkType: FFOS_RUNTIME.domRequest(),
setPreferredNetworkType: FFOS_RUNTIME.domRequest(),
get data() {
return initialized ? { network: fakeNetwork2 } : null;
},
get voice() {
return initialized ? fakeVoice2 : null;
}
});
}
FFOS_RUNTIME.makeNavigatorShim('mozMobileConnections',
mobileConnections, true);
}();

View file

@ -0,0 +1,16 @@
!function() {
function debug(str) {
//dump('mozTelephony: ' + str + '\n');
}
FFOS_RUNTIME.makeNavigatorShim('mozTelephony', {
calls: [],
addEventListener: function() {
debug('addEventListener');
},
removeEventListener: function() {
debug('removeEventListener');
}
}, true);
}();

View file

@ -0,0 +1,132 @@
!function() {
function debug(str) {
//dump('wifi: ' + str + '\n');
}
/**
* fake network list, where each network object looks like:
* {
* ssid : SSID string (human-readable name)
* bssid : network identifier string
* security : array of strings (supported authentication methods)
* relSignalStrength : 0-100 signal level (integer)
* connected : boolean state
* }
*/
var fakeNetworks = [
{
ssid: 'Mozilla-G',
bssid: 'xx:xx:xx:xx:xx:xx',
security: ['WPA-EAP'],
relSignalStrength: 67,
connected: false
},
{
ssid: 'Livebox 6752',
bssid: 'xx:xx:xx:xx:xx:xx',
security: ['WEP'],
relSignalStrength: 32,
connected: false
},
{
ssid: 'Mozilla Guest',
bssid: 'xx:xx:xx:xx:xx:xx',
security: [],
relSignalStrength: 98,
connected: false
},
{
ssid: 'Freebox 8953',
bssid: 'xx:xx:xx:xx:xx:xx',
security: ['WPA2-PSK'],
relSignalStrength: 89,
connected: false
}
];
FFOS_RUNTIME.makeNavigatorShim('mozWifiManager', {
enabled: true,
macAddress: 'xx:xx:xx:xx:xx:xx',
// enables/disables the wifi
setEnabled: function fakeSetEnabled(bool) {
var self = this;
var request = { result: bool };
setTimeout(function() {
if (request.onsuccess) {
request.onsuccess();
}
if (bool) {
self.onenabled();
} else {
self.ondisabled();
}
});
self.enabled = bool;
return request;
},
// returns a list of visible/known networks
getNetworks: FFOS_RUNTIME.domRequest(fakeNetworks),
getKnownNetworks: FFOS_RUNTIME.domRequest(fakeNetworks),
// selects a network
associate: function fakeAssociate(network) {
var self = this;
var connection = { result: network };
var networkEvent = { network: network };
setTimeout(function fakeConnecting() {
self.connection.network = network;
self.connection.status = 'connecting';
self.onstatuschange(networkEvent);
}, 0);
setTimeout(function fakeAssociated() {
self.connection.network = network;
self.connection.status = 'associated';
self.onstatuschange(networkEvent);
}, 1000);
setTimeout(function fakeConnected() {
network.connected = true;
self.connected = network;
self.connection.network = network;
self.connection.status = 'connected';
self.onstatuschange(networkEvent);
}, 2000);
return connection;
},
// forgets a network (disconnect)
forget: function fakeForget(network) {
var self = this;
var networkEvent = { network: network };
setTimeout(function() {
network.connected = false;
self.connected = null;
self.connection.network = null;
self.connection.status = 'disconnected';
self.onstatuschange(networkEvent);
}, 0);
},
// event listeners
onenabled: function(event) {},
ondisabled: function(event) {},
onstatuschange: function(event) {},
// returns a network object for the currently connected network (if any)
connected: null,
connection: {
status: 'disconnected',
network: null
}
}, true);
}();

View file

@ -0,0 +1,30 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>desktop-helper@gaiamobile.org</em:id>
<em:name>desktop-helper.js</em:name>
<em:version>0.1</em:version>
<em:targetApplication>
<Description>
<em:id>toolkit@mozilla.org</em:id>
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> <!-- Firefox -->
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:unpack>true</em:unpack>
<em:bootstrap>true</em:bootstrap>
<em:creator>Kevin Grandon</em:creator>
<em:description>Shims to make gaia work on top of Xulrunner runtime</em:description>
<em:homepageURL>https://github.com/mozilla-b2g/gaia/</em:homepageURL>
</Description>
</RDF>

View file

@ -0,0 +1 @@
desktop-helper

View file

@ -0,0 +1,8 @@
xpi:
zip gaia-build@gaiamobile.org.xpi -r bootstrap.js install.rdf chrome.manifest content/
run:
mkdir -p profile/extensions/
echo "../../" > profile/extensions/gaia-build@gaiamobile.org
echo "user_pref(\"extensions.autoDisableScopes\", 14);" > profile/user.js
$(FIREFOX) -profile $(shell pwd)/profile -no-remote about:gaia

View file

@ -0,0 +1,68 @@
'use strict';
const CC = Components.Constructor;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');
// Register about:gaia page
let classID = Components.ID('{58d667ee-f9e8-4545-841c-927d86906620}');
function AboutGaia() {}
AboutGaia.prototype = {
classID: classID,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
getURIFlags: function(aURI) {
return Ci.nsIAboutModule.ALLOW_SCRIPT;
},
newChannel: function(aURI, aLoadInfo) {
let spec = 'chrome://gaia-build/content/aboutGaia.html';
let uri = Services.io.newURI(spec, null, null);
let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
channel.originalURI = aURI;
return channel;
}
};
function registerAbout() {
let contract = '@mozilla.org/network/protocol/about;1?what=gaia';
let instance = null;
let newFactory = {
createInstance: function(outer, iid) {
if (outer)
throw Components.results.NS_ERROR_NO_AGGREGATION;
if (instance === null)
instance = new AboutGaia();
return instance;
},
lockFactory: function(aLock) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
};
Cm.registerFactory(classID, '', contract, newFactory);
}
function startup(data, reason) {
try {
registerAbout();
} catch (e) {
Cu.reportError(e + '\n' + e.stack);
}
}
function shutdown(data, reason) {
}
function install(data, reason) {
}
function uninstall(data, reason) {
}

View file

@ -0,0 +1 @@
content gaia-build content/

View file

@ -0,0 +1,410 @@
body {
font-size: 16px;
}
select, button {
font-size: 1em;
}
#tools > label {
display: block;
}
#available-apps > li {
display: inline-block;
margin: 0.2rem;
}
.page > ul {
padding-left: 0.2rem;
font-size: 0.7em;
}
.page > ul > li {
display: inline-block;
margin: 0.2rem;
}
.page > .container > div {
font-size: 0.7em;
display: inline-block;
margin: 0.2rem;
width: 3rem;
height: 3rem;
overflow: hidden;
background-color: white;
border: 1px solid black;
border-radius: 0.4rem;
word-wrap:break-word;
}
#available-apps div.app {
display: inline-block;
margin: 0.2rem;
padding: 0.2rem;
background-color: white;
border: 1px solid black;
border-radius: 0.4rem;
}
#available-apps div.app[data-source="external"] {
background-color: #fcaf3e;
}
#available-apps div.app:hover {
background-color: #204a87;
color: white;
}
#available-apps {
padding: 0;
margin: 0;
margin-bottom: 2rem;
}
#main {
flex: 1;
padding: 2rem;
}
#tools {
width: 18rem;
border-right: 1px solid black;
padding: 2rem;
}
#content {
display: flex;
flex-flow: row;
}
#notification {
text-align: center;
}
.page {
width: 15rem;
height: 25rem;
background-color: rgba(0,0,0,0.2);
margin: 1rem;
}
.close-button {
display: block;
float: right;
width: 1.2rem;
height: 1.2rem;
line-height: 1.2rem;
font-size: 0.8em;
margin: 0.5rem;
text-align: center;
border-radius: 50%;
border: 1px solid black;
background-color: white;
}
#add-page {
font-size: 5em;
text-align: center;
line-height: 25rem;
}
.page > input[type="radio"], #dock > input[type="radio"] {
position: absolute;
display: none;
}
.page > input[type="radio"] + label, #dock > input[type="radio"] + label {
opacity: 0.1;
display: block;
height: 0;
margin-left: -1.5rem;
margin-top:1.2rem;
transform: rotate(-45deg);
width: 4rem;
line-height: 1.5rem;
border-bottom: 1.5rem solid red;
border-left: 1.5rem solid transparent;
border-right: 1.5rem solid transparent;
text-align: center;
font-size: 0.9em;
color: white;
transition: opacity 0.5s;
}
#dock > input[type="radio"] + label {
padding-top: 1.3rem;
margin-left:-2rem;
}
.page > input[type="radio"]:checked + label,
#dock > input[type="radio"]:checked + label {
opacity: 1;
}
.page > input[type="radio"]:checked + label:after {
content: "selected";
}
#dock {
margin: 1rem;
width: 24rem;
height: 5rem;
background-color: grey;
}
#dock > .container {
margin-top: -2rem;
margin-left: 3rem;
}
.page > .container {
margin-top: 2rem;
}
#grid {
overflow-x: scroll;
display: -moz-box;
-moz-box-orient: horizontal;
-moz-box-pack: center;
-moz-box-align: center;
box-orient: horizontal;
box-pack: center;
box-align: center;
}
div.lightbox {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
div.lightbox:target {
display: table;
}
div.lightbox .lightbox-background {
display: table-cell;
padding: 0;
width: 100%;
height: 100%;
vertical-align: middle;
}
div.lightbox .lightbox-background .lightbox-container {
display: block;
margin: auto;
padding: 2rem;
background-color: white;
height: 25rem;
position: relative;
overflow: auto;
border: 1px #000000 solid;
border-radius: 0.5rem;
text-align: justify;
font-size: 1.2em;
}
div.lightbox .lightbox-background .closemsg {
display: block;
margin: auto;
height: 0;
overflow: visible;
text-align: right;
z-index: 5001;
cursor: default;
}
div.lightbox .lightbox-background .closemsg, div.lightbox .lightbox-background .lightbox-container {
width: 50rem;
}
.closemsg::after {
content: "\00D7";
display: inline-block;
position: relative;
right: -2rem;
top: -1rem;
z-index: 5002;
color: #ffffff;
border: 1px #ffffff solid;
border-radius: 1rem;
width: 2rem;
height: 2rem;
line-height: 1.8rem;
text-align: center;
margin: 0;
background-color: #000000;
font-weight: bold;
cursor: pointer;
}
.closemsg.hidden:after {
content: none;
display: none;
}
.closemsg::before {
content: "";
display: block;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #000000;
opacity: 0.85;
}
.lightbox-buttons {
position: absolute;
right: 0;
bottom: 0;
}
.lightbox-buttons > a {
display: inline-block;
margin: 0.5rem;
border-radius: 0.5rem;
border: 1px solid black;
padding: 0.5rem;
text-decoration: none;
color: black;
}
#gaia-path-lightbox .lightbox-container {
text-align: center;
line-height: 25rem;
}
#marketplace-search-result .lightbox-container {
background-color: #BEBEBE;
}
#reset-gaia {
font-size: 0.8em;
color: blue;
text-decoration: underline;
cursor: pointer;
}
.tool-section {
margin-bottom: 1rem;
}
.marketplace-app-container {
background-color: white;
margin: 1rem;
border-radius: 0.5rem;
position: relative;
height: 8rem;
display: block;
}
.marketplace-app-container:hover {
box-shadow: 0 0 0.5rem black;
}
.marketplace-app-container > * {
position: absolute;
left: 1rem;
top: 1rem;
}
.marketplace-app-icon {
left: 1rem;
top: 1rem;
}
.marketplace-app-ratings {
top: 5rem;
color: #3388C1;
font-size: 1.1em;
}
.marketplace-app-review {
left: 6rem;
top: 5.5rem;
font-size: 0.8em;
color: #3388C1;
}
.marketplace-app-name {
left: 6rem;
top: 1rem;
color: #333333;
font-family: "Fira Sans","Open Sans","Helvetica Neue",Arial,sans-serif;
font-size: 1.4em;
}
.marketplace-app-author {
left: 6rem;
top: 3.5rem;
font-size: 0.8em;
color: #A7A7A7;
}
.noted {
font-size: 0.8em;
margin: 0.5rem;
}
.marketplace-app-type {
position: relative;
float: right;
margin-right: 2rem;
width: 10rem;
height: 1.5rem;
line-height: 1.5rem;
border-radius: 0.5rem;
font-size: 0.8em;
text-align: center;
color: #FFF;
}
.marketplace-app-type[data-is-packaged="true"] {
background-color: #204a78;
}
.marketplace-app-type[data-is-packaged="true"]:before {
content: "Packaged app";
background-color: #204a78;
}
.lightbox-title {
font-size: 2em;
border-bottom: 1px solid black;
text-transform: capitalize;
}
.lightbox-content {
margin: 2rem;
}
.lightbox-content a {
color: blue;
text-decoration: underline;
cursor: pointer;
}
.operator input {
margin: 0.2rem;
}
.hidden {
display: none;
}
.description {
margin: 0.6rem;
font-size: 0.8em;
}
#certain-mcc-mnc:not(:checked) ~ #operators-options {
display: none
}

View file

@ -0,0 +1,87 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Homescreen customization</title>
<link rel="stylesheet" href="chrome://gaia-build/content/aboutGaia.css">
<script type="application/javascript;version=1.8" src="chrome://gaia-build/content/gaia.js"></script>
<script type="application/javascript;version=1.8" src="chrome://gaia-build/content/aboutGaia.js"></script>
</head>
<body>
<div id="content">
<div id="tools">
<section class="tool-section">
<div id="builds-label">Build config: </div>
<select name="" id="gaia-builds"></select>
</section>
<section class="tool-section">
<div id="available-apps-label">Available apps</div>
<div id="available-apps"></div>
</section>
<section class="tool-section">
<div id="marketplace-label">Search from Marketplace</div>
<form>
<input id="marketplace-keyword" type="text" placeholder="keyword"></input>
<button type="submit" id="marketplace-search">Search</button>
</form>
</section>
<button id="launch">Preview</button><button id="save">Save</button>
<a id="reset-gaia">Reset gaia path</a>
</div>
<div id="main">
<div id="notification">Your app will be added into selected page.</div>
<div id="grid">
<div class="page" id="add-page">+</div>
</div>
<div id="dock" class="page">
<input type="radio" name="page-selection" id="page0" checked="checked">
<label for="page0"></label>
<div class="container"></div>
</div>
</div>
</div>
<div class="lightbox" id="options">
<div class="lightbox-background">
<a href="#" class="closemsg"></a>
<div class="lightbox-container">
<div class="lightbox-title" id="options-title"></div>
<div id="operators-disabled-desc" class="description hidden">
Built-in apps can't be disabled for certain operators
</div>
<div class="lightbox-content hidden" id="options-content">
<input id="certain-mcc-mnc" type="checkbox">
<label for="certain-mcc-mnc">Available only for certain operators</label>
<hr>
<div id="operators-options">
<div id="operators"></div>
<div class="description">Use commas to split different mcc/mnc pairs, e.g., "310-410, 310-260" (without double quotes)</div>
<a id="add-operator">Add operator</a>
</div>
</div>
<div class="lightbox-buttons">
<a href="#">Close</a>
<a href="#" id="options-remove">Remove</a>
</div>
</div>
</div>
</div>
<div class="lightbox" id="gaia-path-lightbox">
<div class="lightbox-background">
<a class="closemsg hidden"></a>
<div class="lightbox-container">
Gaia path is necessary.
<button id="select-gaia">Select</button>
</div>
</div>
</div>
<div class="lightbox" id="marketplace-search-result">
<div class="lightbox-background">
<a href="#" class="closemsg"></a>
<div class="lightbox-container">
<div class="noted">Noted: Packaged app can't be executed in Browser, you need to verify it in b2g-desktop or device.</div>
<div id="marketplace-search-container"></div>
</div>
</div>
</div>
</body>
</html>

View file

@ -0,0 +1,862 @@
'use strict';
const CC = Components.Constructor;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const RE_MCCMNC = /^\d{3}-\d{3}$/;
const PAGE_MAX_APP = 16;
const DOCK_MAX_APP = 5;
const MARKETPLACE_INSTALL_ORIGIN = 'https://marketplace.firefox.com';
const EXTERNAL_APP_DIR = 'external-apps';
const MARKETPLACE_SEARCH_HASH = '#marketplace-search-result';
const MARKETPLACE_SEARCH_BASEURL = 'https://marketplace.firefox.com/api/v1/' +
'apps/search/?device=firefoxos&q=';
Cu.import('resource://gre/modules/Services.jsm');
Cu.import('resource://gre/modules/FileUtils.jsm');
const GAIA_DIR_PREF_NAME = 'extensions.gaia.dir';
const DEFAULT_PAGE = 2;
var utils, variant;
var Homescreen = {
operators: {},
init: function hs_init() {
var self = this;
this.addPageButton = document.getElementById('add-page');
this.grid = document.getElementById('grid');
this.dock = document.getElementById('dock');
this.apps = [];
this.searchResultContainer =
document.getElementById('marketplace-search-container');
this.addPageButton.addEventListener('click', this.addPage.bind(this));
this.operatorsElement = document.getElementById('operators');
if (this.grid.children.length <= 1) {
for (var i = 0; i < DEFAULT_PAGE; i++) {
this.addPage();
}
}
var gaiaBuilds = document.getElementById('gaia-builds');
gaiaBuilds.addEventListener('change', function(evt) {
var selected = gaiaBuilds.options[gaiaBuilds.options.selectedIndex];
self.selectBuild(selected.value);
});
document.getElementById('save').addEventListener('click',
this.save.bind(this));
document.getElementById('select-gaia').addEventListener('click',
this.selectGaiaFolder.bind(this));
document.getElementById('options-remove').addEventListener('click',
this.removeApp.bind(this));
document.getElementById('reset-gaia').addEventListener('click',
resetGaiaPath);
document.getElementById('marketplace-keyword').addEventListener('click',
function(evt) {
this.select();
});
document.getElementById('marketplace-search').addEventListener('click',
function(evt) {
var keyword = document.getElementById('marketplace-keyword').value;
if (keyword) {
self.search(keyword);
} else {
alert('Please specify a keyword.');
}
});
function createOperator() {
var div = document.createElement('div');
div.classList.add('operator');
self.createOperatorEditor(div);
self.operatorsElement.appendChild(div);
}
document.getElementById('add-operator').addEventListener('click',
createOperator);
document.getElementById('certain-mcc-mnc').addEventListener('change',
function(evt) {
var app = self.apps[self.editingAppElement.dataset.name];
if (evt.target.checked) {
app.forOperators = true;
} else {
app.forOperators = false;
}
});
window.addEventListener('hashchange', function(evt) {
var oldURL = new URL(evt.oldURL);
if (oldURL.hash === MARKETPLACE_SEARCH_HASH) {
delete self.searchResult;
}
});
createOperator();
},
search: function hs_search(keyword) {
var {downloadJSON} = utils;
this.searchResultContainer.innerHTML = 'Loading...';
window.location.hash = MARKETPLACE_SEARCH_HASH;
var self = this;
var url = MARKETPLACE_SEARCH_BASEURL + encodeURI(keyword);
downloadJSON(url, function(json) {
if (json) {
self.searchResult = json;
self.showSearchResult(self.searchResult);
} else {
var msg = 'Marketplace search request failed';
alert(msg);
throw new Error(msg);
}
});
},
showSearchResult: function hs_showSearchResult(json) {
var result = this.searchResultContainer;
while(result.firstChild) {
result.removeChild(result.firstChild);
}
json.objects.forEach(function(obj, index) {
result.appendChild(this.createSearchEntry(obj, index));
}.bind(this));
},
createSearchEntry: function hs_createSearchEntry(obj, index) {
const FULL_STARS = 5;
var container = document.createElement('a');
container.classList.add('marketplace-app-container');
var elements = {
'icon': 'img',
'name': 'div',
'author': 'div',
'ratings': 'div',
'review': 'div',
'type': 'div'
};
for (var key in elements) {
var el = document.createElement(elements[key]);
el.classList.add('marketplace-app-' + key);
container.appendChild(el);
elements[key] = el;
}
elements.name.textContent = obj.name;
elements.author.textContent = obj.author;
elements.icon.src = obj.icons['64'];
var ratings = Math.round(obj.ratings.average);
elements.ratings.innerHTML = new Array(ratings + 1).join('&#9733;');
var len = elements.ratings.innerHTML.length;
elements.ratings.innerHTML += new Array(FULL_STARS - len + 1).join('&#9734;');
elements.ratings.dataset.average = obj.ratings.average;
elements.review.textContent = obj.ratings.count + ' reviews';
elements.type.dataset.isPackaged = obj.is_packaged;
container.dataset.index = index;
container.addEventListener('click', this.addAppFromMarketplace.bind(this));
return container;
},
selectBuild: function hs_selectBuild(buildname) {
if (!this.gaiaConfig) {
throw new Error('Homescreen.gaiaConfig doesn\'t exist');
}
var c = this.gaiaConfig;
var {getAppsByList} = utils;
var apps = getAppsByList(this.buildConfig[buildname].content,
c.GAIA_DIR, c.GAIA_DISTRIBUTION_DIR);
this.setApps(apps);
},
selectGaiaFolder: function hs_selectGaiaFolder() {
this.reset();
let path = this.promptFolder();
if (!path) {
return;
}
Services.prefs.setCharPref(GAIA_DIR_PREF_NAME, path);
},
promptFolder: function hs_promptFolder() {
let fp = Cc['@mozilla.org/filepicker;1'].createInstance(Ci.nsIFilePicker);
fp.init(window, 'Select Gaia directory', Ci.nsIFilePicker.modeGetFolder);
let res = fp.show();
if (res != Ci.nsIFilePicker.returnCancel) {
return fp.file.path;
}
return null;
},
reset: function hs_reset() {
var availableApps = document.getElementById('available-apps');
while (availableApps.firstChild) {
availableApps.removeChild(availableApps.firstChild);
}
var pages = document.getElementsByClassName('container');
Array.prototype.forEach.call(pages, function(page, index) {
while (page.firstChild) {
page.removeChild(page.firstChild);
}
});
},
resetBuilds: function hs_resetBuilds() {
var builds = document.getElementById('gaia-builds');
Array.prototype.forEach.call(builds.children, function(option) {
option.remove();
});
},
setApps: function hs_setApps(apps) {
this.installedApps = {};
var availableApps = document.getElementById('available-apps');
this.apps = apps;
this.reset();
for (var app in apps) {
// app with role in manifest means it has specific purpose and those apps
// usually doesn't show on homescreen. e.g., role of bluetooth, pdf.js &
// ringtones is "system" which means they are executed by another apps but
// doesn't show on homescreen, but those apps are essential for gaia .
if (apps[app].manifest.role) {
this.installedApps[app] = apps[app];
continue;
} else {
this.createAppElements(apps[app]).forEach(function(el) {
availableApps.appendChild(el);
});
}
}
},
showOptions: function hs_showOptions(evt) {
var self = this;
this.editingAppElement = evt.target;
var operatorsCheckbox = document.getElementById('certain-mcc-mnc');
var operatorsOptions = document.getElementById('options-content');
var operatorsDisabledMsg = document.getElementById('operators-disabled-desc');
var name = this.editingAppElement.dataset.name;
document.getElementById('options-title').innerHTML = evt.target.innerHTML;
window.location.hash = '#options';
function updateOperators(name) {
var inputs =
document.querySelectorAll('.operator input[type="checkbox"]');
Array.prototype.forEach.call(inputs, function(input) {
var operatorId = input.dataset.operatorId;
input.checked = self.operators[operatorId].apps[name] ? true : false;
});
}
if (!this.apps[name]) {
throw new Error('App doesn\'t exist: ' + name);
} else if (this.apps[name].metadata &&
this.apps[name].metadata.source === 'external') {
updateOperators(name);
operatorsCheckbox.checked = this.apps[name].forOperators || false;
operatorsOptions.classList.remove('hidden');
operatorsDisabledMsg.classList.add('hidden');
operatorsCheckbox.disabled = false;
} else {
operatorsDisabledMsg.classList.remove('hidden');
operatorsOptions.classList.add('hidden');
operatorsCheckbox.disabled = true;
}
},
updateOperators: function hs_updateOperators(operator, appName) {
var prevOperatorId;
if (this.editingOperator) {
prevOperatorId = this.editingOperator.id;
} else {
prevOperatorId = operator.id;
}
if (this.operators[prevOperatorId] && prevOperatorId !== operator.id) {
operator.apps = this.operators[prevOperatorId].apps;
delete this.operators[prevOperatorId];
}
this.operators[operator.id] = operator;
if (appName) {
operator.apps[appName] = true;
}
},
getOperator: function hs_getOperator(name) {
var operator;
this.operators.every(function(opr) {
if (opr.id === name) {
operator = opr;
return false;
}
return true;
});
return operator;
},
convertToOperator: function hs_convertToOperator(el) {
var operatorId = el.querySelector('[data-field="operatorId"]').value;
var mccmnc = el.querySelector('[data-field="mccmnc"]').value;
var formatError = false;
var mccmncArray = mccmnc.split(',').map(function(item) {
var pair = item.trim();
formatError = !RE_MCCMNC.test(pair) || formatError;
return pair;
});
if (formatError) {
throw new Error('MCC/MNC format incorrect, both of MCC and MNC need ' +
'3 digits like "310-410" (without double quotes)');
}
return {
'id': operatorId,
'mcc-mnc': mccmncArray,
'apps': {}
};
},
switchOperatorMode: function hs_switchOperatorMode(operatorDiv, toEdit) {
function removeAllChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
if (toEdit) {
var operatorId =
operatorDiv.querySelector('input[type="checkbox"]').dataset.operatorId;
var operator = this.operators[operatorId];
this.editingOperator = operator;
removeAllChildren(operatorDiv);
this.createOperatorEditor(operatorDiv, operator);
} else {
var operator = this.convertToOperator(operatorDiv);
removeAllChildren(operatorDiv);
this.createOperatorView(operatorDiv, operator.id);
delete this.editingOperator;
}
},
createOperatorView: function hs_createOperatorView(div, operatorId) {
var {normalizeString} = utils;
var self = this;
var operator = this.operators[operatorId];
var checkbox = document.createElement('input');
checkbox.dataset.operatorId = operatorId;
checkbox.dataset.normalizedName = normalizeString(operatorId);
checkbox.type = 'checkbox';
checkbox.id = 'checkbox-' + checkbox.dataset.normalizedName;
var label = document.createElement('label');
label.setAttribute('for', checkbox.id);
label.innerHTML = operatorId;
var span = document.createElement('span');
span.innerHTML = ' - ';
var edit = document.createElement('a');
edit.innerHTML = 'edit';
checkbox.checked = (operator &&
operator.apps[this.editingAppElement.dataset.name]);
checkbox.addEventListener('change', function(evt) {
var operator = self.operators[evt.target.dataset.operatorId];
if (evt.target.checked) {
operator.apps[self.editingAppElement.dataset.name] = true;
} else {
delete operator.apps[self.editingAppElement.dataset.name];
}
});
edit.addEventListener('click', function(evt) {
self.switchOperatorMode(div, true);
});
span.appendChild(edit);
[checkbox, label, span].forEach(function(child) {
div.appendChild(child);
});
},
createOperatorEditor: function hs_createOperatorEditor(div, operator) {
var self = this;
function createInput(field, placeholder) {
var input = document.createElement('input');
input.type = 'text';
input.placeholder = placeholder;
input.dataset.field = field;
return input;
}
var idInput = createInput('operatorId', 'Operator ID');
var mccmncInput = createInput('mccmnc', 'mcc/mnc');
var save = document.createElement('a');
save.innerHTML = 'Save';
save.addEventListener('click', function(evt) {
var operatorDiv = evt.target.parentElement;
try {
var operator = self.convertToOperator(operatorDiv);
self.updateOperators(operator, self.editingAppElement.dataset.name);
self.switchOperatorMode(operatorDiv, false);
} catch (e) {
alert(e.message);
}
});
if (operator) {
idInput.value = operator.id;
mccmncInput.value = operator['mcc-mnc'].join(', ');
}
[idInput, mccmncInput, save].forEach(function(el) {
div.appendChild(el);
});
},
removeApp: function hs_removeApp() {
if (!this.editingAppElement) {
throw new Error('You didn\'t select an app to edit');
}
var availableApps = document.getElementById('available-apps');
availableApps.appendChild(this.createApp(this.editingAppElement.dataset));
this.editingAppElement.remove();
delete this.editingAppElement;
for (var key in this.operators) {
delete this.operators[key].apps[this.editingAppElement.dataset.name];
}
window.location.hash = '';
},
getMetadata: function hs_getMetadata(entry, manifest, callback) {
var {normalizeString} = utils;
var name = normalizeString(entry.name)
var metadata = {
'name': name,
'installOrigin': MARKETPLACE_INSTALL_ORIGIN,
'manifestURL': entry.manifest_url,
'source': 'external'
};
if (entry.is_packaged) {
metadata.origin = 'app://' + name;
var xhr = new XMLHttpRequest();
xhr.open('HEAD', manifest.package_path, false);
xhr.addEventListener('load', function() {
if (xhr.status !== 200 && xhr.status !== 304) {
console.warn('Get header of packaged app failed, Status Code: ' +
xhr.status);
callback(metadata);
return;
}
var etag = xhr.getResponseHeader('etag');
if (etag) {
metadata.packageEtag = etag;
}
callback(metadata);
});
xhr.send();
} else {
var url = new URL(entry.manifest_url);
metadata.origin = url.origin;
callback(metadata);
}
},
filterProperties: function hs_filterProperties(obj, names) {
var newObj = {};
for (var prop in obj) {
if (names.indexOf(prop) >= 0) {
newObj[prop] = obj[prop];
}
}
return newObj;
},
processAppDownload: function hs_processAppDownload(metadata, json) {
var {getFile, ensureFolderExists} = utils;
var {downloadApp} = variant;
var filteredMetadata = this.filterProperties(metadata, ['origin',
'manifestURL', 'installOrigin', 'etag', 'packageEtag']);
var appPath = getFile(this.gaiaConfig.GAIA_DISTRIBUTION_DIR,
EXTERNAL_APP_DIR, metadata.name);
ensureFolderExists(appPath);
downloadApp(json, filteredMetadata, appPath.path, function() {
var {getApp} = utils;
var app = getApp(EXTERNAL_APP_DIR, metadata.name,
this.gaiaConfig.GAIA_DIR,
this.gaiaConfig.GAIA_DISTRIBUTION_DIR);
app.metadata = metadata;
this.apps[metadata.name] = app;
window.location.hash = '';
}.bind(this));
},
addAppFromMarketplace: function hs_addAppFromMarketplace(evt) {
var self = this;
var {normalizeString} = utils;
var entry = this.searchResult.objects[evt.currentTarget.dataset.index];
var name = normalizeString(entry.name);
if (this.apps[name]) {
alert(name + ' app already exists.');
return;
}
this.searchResultContainer.innerHTML = 'Downloading...';
var xhr = new XMLHttpRequest();
xhr.open('GET', entry.manifest_url, false);
xhr.addEventListener('load', function() {
if (xhr.status !== 200 && xhr.status !== 304) {
alert('Download Error, Status Code: ' + xhr.status);
window.location.hash = '';
return;
}
var manifest = JSON.parse(xhr.responseText);
self.getMetadata(entry, manifest, function(metadata) {
var app = self.createApp({
'name': metadata.name,
'source': 'external'
});
document.getElementById('available-apps').appendChild(app);
var etag = xhr.getResponseHeader('etag');
if (etag) {
metadata.etag = etag;
}
self.processAppDownload(metadata, manifest);
});
});
xhr.send();
},
addToPage: function hs_addToPage(evt) {
var props = {};
Object.keys(evt.target.dataset).forEach(function(key) {
props[key] = evt.target.dataset[key];
});
var selectedPage =
document.querySelector('input[type=radio]:checked ~ .container');
var app = this.createApp(props, true);
var max = selectedPage.parentElement.id === 'dock' ?
DOCK_MAX_APP : PAGE_MAX_APP;
if (selectedPage.children.length < max) {
selectedPage.appendChild(app);
evt.target.remove();
} else {
alert('can\'t add anymore');
}
},
createAppElements: function hs_createAppElements(app) {
function logMissingIcon(app, entryPoint) {
var name = app + (entryPoint ? ':' + entryPoint : '');
console.warn('icons for ' + name + ' doesn\'t exist');
}
var elements = [];
if (!app.manifest.icons && !app.manifest.entry_points) {
logMissingIcon(app.name);
} else if (!app.manifest.entry_points && app.manifest.icons) {
elements.push(this.createApp({'name': app.name}));
} else {
for (var entryPoint in app.manifest.entry_points) {
if (!app.manifest.entry_points[entryPoint].icons) {
logMissingIcon(app.name, entryPoint);
continue;
}
elements.push(this.createApp({
'name': app.name,
'entryPoint': entryPoint
}));
}
}
return elements;
},
createApp: function hs_createApp(props, inPage) {
var div = document.createElement('div');
div.classList.add('app');
div.innerHTML = props.name;
if (props.entryPoint) {
div.innerHTML += ':' + props.entryPoint;
}
Object.keys(props).forEach(function(key) {
div.dataset[key] = props[key];
});
if (!div.dataset.source) {
div.dataset.source = 'builtin';
}
if (!inPage) {
div.addEventListener('click', this.addToPage.bind(this));
} else {
div.addEventListener('click', this.showOptions.bind(this));
}
return div;
},
setBuildConfig: function hs_setBuildConfig(config) {
this.reset();
this.buildConfig = config;
var buildsElement = document.getElementById('gaia-builds');
for (var build in config) {
var option = document.createElement('option');
option.setAttribute('value', build);
option.innerHTML = build;
if (build === 'production') {
option.setAttribute('selected', 'selected');
this.selectBuild(build);
}
buildsElement.appendChild(option);
}
},
addPage: function hs_addPage() {
var self = this;
var lastInput =
document.querySelector('#grid > div:nth-last-child(2) > input');
var lastId = lastInput ? parseInt(lastInput.id.substr('page'.length)) : 0;
var page = document.createElement('div');
page.classList.add('page');
var input = document.createElement('input');
input.setAttribute('type', 'radio');
input.setAttribute('name', 'page-selection');
input.id = 'page' + (lastId + 1);
var label = document.createElement('label');
label.setAttribute('for', input.id);
var closeBtn = document.createElement('div');
closeBtn.classList.add('close-button');
closeBtn.innerHTML = '×';
closeBtn.addEventListener('click', function(evt) {
var targetPage = evt.target.parentNode;
var targetInput = targetPage.getElementsByTagName('input')[0];
if (targetInput.checked) {
document.getElementById('page0').checked = true;
}
var apps = targetPage.querySelectorAll('.app');
var availableApps = document.getElementById('available-apps');
Array.prototype.forEach.call(apps, function(app) {
var {name, entryPoint} = app.dataset;
var restoredApp = self.createApp({
'name': name, 'entryPoint': entryPoint});
availableApps.appendChild(restoredApp);
});
targetPage.remove();
});
var container = document.createElement('div');
container.classList.add('container');
page.appendChild(closeBtn);
page.appendChild(input);
page.appendChild(label);
page.appendChild(container);
this.grid.insertBefore(page, this.addPageButton);
},
save: function hs_save(evt) {
var self = this;
if (!utils) {
throw new Error('utils module doesn\'t exist');
}
var variant = {
'apps': {},
'operators': []
};
var operators = JSON.parse(JSON.stringify(this.operators));
var distDir = utils.getFile(this.gaiaConfig.GAIA_DISTRIBUTION_DIR);
utils.ensureFolderExists(distDir);
var pages = document.querySelectorAll('div.page > div.container');
var grid = {
homescreens: [],
search_page: { enabled: true }
};
Array.prototype.forEach.call(pages, function(page, index) {
var pageArray = [];
Array.prototype.forEach.call(page.children, function(el, position) {
var parent;
var {name, entryPoint} = el.dataset;
if (!self.apps[name]) {
throw new Error('App doesn\'t exist: ' + name);
}
if (self.apps[name].forOperators) {
for (var key in self.operators) {
if (self.operators[key].apps[name]) {
operators[key].apps[name] = {
'id': name,
'screen': index,
'position': position
};
variant.apps[name] =
self.filterProperties(self.apps[name].metadata,
['origin', 'manifestURL', 'installOrigin', 'etag']
);
}
}
return;
}
var f = utils.getFile(self.apps[name].path);
parent = f.parent.leafName;
var appArray = [parent, name];
if (entryPoint) {
appArray.push(entryPoint);
}
pageArray.push(appArray);
self.installedApps[name] = self.apps[name];
});
if (index !== pages.length - 1) {
grid.homescreens.push(pageArray);
} else {
// insert apps in dock to first one element of pageArray
grid.homescreens.splice(0, 0, pageArray);
}
});
var appslistFile = distDir.clone();
appslistFile.append('apps.list');
var applistArray = [];
for (var key in self.installedApps) {
applistArray.push(self.installedApps[key].parent + '/' + key);
}
utils.writeContent(appslistFile, applistArray.join('\n'));
var homescreenFile = distDir.clone();
homescreenFile.append('homescreens.json');
utils.writeContent(homescreenFile, JSON.stringify(grid, undefined, 2));
for (var key in operators) {
var operator = operators[key];
var apps = [];
for (var appname in operator.apps) {
apps.push(operator.apps[appname]);
}
operator.apps = apps;
variant.operators.push(operators[key]);
}
var variantFile = distDir.clone();
variantFile.append('variant.json');
utils.writeContent(variantFile, JSON.stringify(variant, undefined, 2));
// open directory in file manager if click save button
if (evt && evt.target) {
distDir.launch();
}
}
};
function resetGaiaPath() {
Homescreen.resetBuilds();
Services.prefs.setCharPref(GAIA_DIR_PREF_NAME, '');
window.location.hash = '#gaia-path-lightbox';
}
window.addEventListener('load', function() {
Homescreen.init();
Gaia.addEventListener('setupFinished', function() {
utils = Gaia.require('utils');
variant = Gaia.require('variant');
Homescreen.gaiaConfig = Gaia.config;
var {getFile, getBuildConfig} = utils;
var configs = getBuildConfig(getFile(Gaia.config.GAIA_DIR, 'build',
'config').path);
Homescreen.setBuildConfig(configs);
});
var observer = {
observe: function(subject, topic, data) {
if (data !== GAIA_DIR_PREF_NAME) {
return;
}
var gaiaPath = Services.prefs.getCharPref(GAIA_DIR_PREF_NAME);
if (!gaiaPath) {
return;
}
try {
Gaia.setup(gaiaPath);
window.location.hash = '';
} catch (e) {
alert(e.message);
resetGaiaPath();
}
}
};
Services.prefs.addObserver(GAIA_DIR_PREF_NAME, observer, false);
document.getElementById('launch').addEventListener('click', function(evt) {
Homescreen.save();
var installedAppPaths = [];
for (var key in Homescreen.installedApps) {
installedAppPaths.push(Homescreen.installedApps[key].path);
}
var original = evt.target.textContent;
evt.target.textContent = 'Building gaia... please wait.';
setTimeout(function() {
if (!Services.prefs.prefHasUserValue(GAIA_DIR_PREF_NAME)) {
Homescreen.selectGaiaFolder();
}
Gaia.profile.remove(true);
utils.ensureFolderExists(Gaia.profile);
Gaia.config.GAIA_APPDIRS = installedAppPaths.join(' ');
Gaia.install();
Gaia.launch();
evt.target.textContent = original;
}, 100);
});
if (Services.prefs.prefHasUserValue(GAIA_DIR_PREF_NAME)) {
var gaiaPath = Services.prefs.getCharPref(GAIA_DIR_PREF_NAME);
if (gaiaPath) {
try {
Gaia.setup(gaiaPath);
} catch (e) {
alert(e.message);
resetGaiaPath();
}
} else {
resetGaiaPath();
}
} else {
resetGaiaPath();
}
});
// FIXME: this is a workaround because we can't use <a href="#something"> to
// apply hash tag to url if using about:gaia.
if (window.location.href === 'about:gaia') {
window.location = 'chrome://gaia-build/content/aboutGaia.html';
}

View file

@ -0,0 +1,171 @@
var Gaia = {
_listeners: {},
buildModules: {},
require: null,
loadXpcshellScript: function gaia_loadXpcshellScript(config, name) {
let module =
Cu.Sandbox(Services.scriptSecurityManager.getSystemPrincipal());
for (var i in config) {
module[i] = config[i];
}
Services.scriptloader.loadSubScript(
'file:///' + config.GAIA_DIR.replace(/\\/g, '/') + '/build/' + name +
'.js', module
);
return module;
},
getConfig: function gaia_getConfig(profilePath, gaiaPath) {
let os = Cc['@mozilla.org/xre/app-info;1'].getService(Ci.nsIXULRuntime).OS;
const SEP = os === 'WINNT' ? '\\' : '/';
let config = {};
config.GAIA_DIR = gaiaPath;
config.PROFILE_DIR = profilePath;
config.PROFILE_FOLDER = 'profile-debug';
config.GAIA_SCHEME = 'http://';
config.GAIA_DOMAIN = 'gaiamobile.org';
config.DEBUG = 1;
config.DEVICE_DEBUG = 0;
config.LOCAL_DOMAINS = 1;
config.DESKTOP = 1;
config.HOMESCREEN = config.GAIA_SCHEME + 'system.' + config.GAIA_DOMAIN;
config.GAIA_PORT = ':8080';
config.GAIA_LOCALES_PATH = 'locales';
config.LOCALES_FILE = 'shared/resources/languages.json';
config.LOCALE_BASEDIR = '';
config.BUILD_APP_NAME = '*';
config.PRODUCTION = '0';
config.GAIA_OPTIMIZE = '0';
config.GAIA_DEV_PIXELS_PER_PX = '1';
config.DOGFOOD = '0';
config.OFFICIAL = '';
config.GAIA_DEFAULT_LOCALE = 'en-US';
config.GAIA_PRETRANSLATE = '1';
config.GAIA_CONCAT_LOCALES = '1';
config.GAIA_ENGINE = 'xpcshell';
config.TARGET_BUILD_VARIANT = '';
config.NOFTU = '1';
config.REMOTE_DEBUGGER = '0';
config.SETTINGS_PATH = ['build', 'config',
'custom-settings.json'].join(SEP);
config.GAIA_DISTRIBUTION_DIR = [gaiaPath, 'distribution'].join(SEP);
config.GAIA_BUILD_DIR = 'file://' + gaiaPath + '/build/';
config.GAIA_KEYBOARD_LAYOUTS = 'en,pt-BR,es,de,fr,pl';
return config;
},
getTmpFolder: function gaia_getTmpFolder(name) {
let file = Cc['@mozilla.org/file/directory_service;1']
.getService(Ci.nsIProperties).get('TmpD', Ci.nsIFile);
file.append(name);
file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, parseInt('0777', 8));
return file;
},
installExtension: function gaia_installExtension(extensionsDir, gaiaPath,
extName, domain) {
let utils = this.buildModules['utils'];
let ext = extensionsDir.clone();
let src = utils.getFile(gaiaPath, 'tools', 'extensions', extName);
let destname = extName.contains('@') ? extName : extName + '@' + domain;
ext.append(destname);
utils.writeContent(ext, src.path);
},
setup: function gaia_setup(gaiaPath) {
var self = this;
if (!gaiaPath) {
throw new Error('gaiaPath is empty');
}
this.profile = this.getTmpFolder('gaia');
// Generate global variables required by build scripts
try {
this.config = this.getConfig(this.profile.path, gaiaPath);
} catch (e) {
var msg = 'Can\'t load Gaia configuration, You may choose wrong gaia' +
' directory';
throw new Error(msg);
}
try {
this.require = this.loadXpcshellScript(this.config,
'xpcshell-commonjs').require;
} catch (e) {
throw new Error('xpcshell-commonjs doesn\'t exist, you may choose wrong' +
' gaia directory');
}
var modules = ['utils', 'preferences', 'applications-data', 'variant',
'webapp-manifests', 'webapp-optimize', 'webapp-zip', 'settings'];
modules.forEach(function(m) { self.buildModules[m] = self.require(m); });
var evt = new CustomEvent('setupFinished');
this.dispatchEvent(evt);
},
install: function gaia_install() {
var self = this;
if (!this.config || !this.profile) {
throw new Error('config or profile doesn\'t exist');
}
var config = this.config;
// Execute all parts of the build system
this.buildModules['preferences'].execute(config);
this.buildModules['applications-data'].execute(config);
this.buildModules['webapp-manifests'].execute(config);
this.buildModules['settings'].execute(config);
// Install all helper addons
let extensions = this.profile.clone();
extensions.append('extensions');
this.buildModules['utils'].ensureFolderExists(extensions);
let exts = ['desktop-helper', 'browser-helper@gaiamobile.org', 'httpd'];
exts.forEach(function(ext) {
self.installExtension(extensions, config.GAIA_DIR, ext,
config.GAIA_DOMAIN);
});
},
launch: function gaia_launch() {
// Launch firefox with the custom profile
let firefox = Cc['@mozilla.org/file/directory_service;1']
.getService(Ci.nsIProperties).get('XREExeF', Ci.nsIFile);
let process = Cc['@mozilla.org/process/util;1']
.createInstance(Ci.nsIProcess);
process.init(firefox);
let args = ['-no-remote', '-profile', this.config.PROFILE_DIR];
process.run(false, args, args.length);
},
addEventListener: function gaia_addEventListener(type, listener) {
if (!this._listeners[type]) {
this._listeners[type] = [];
}
this._listeners[type].push(listener);
},
removeEventListener: function gaia_removeEventListener(type, listener) {
if (this._listeners[type]) {
this._listeners[type].every(function(li, index) {
if (li === listener) {
this._listeners.splice(index, 1);
return false;
}
return true;
});
}
},
dispatchEvent: function gaia_dispatchEvent(event) {
if (this._listeners[event.type]) {
this._listeners[event.type].forEach(function(listener) {
listener(event);
});
}
}
};

View file

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>gaia-build@gaiamobile.org</em:id>
<em:name>gaia-build</em:name>
<em:version>0.1</em:version>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>32.0.*</em:maxVersion>
</Description>
</em:targetApplication>
<em:unpack>true</em:unpack>
<em:bootstrap>true</em:bootstrap>
<em:description>Build and setup gaia into a firefox instance</em:description>
<em:homepageURL>https://github.com/mozilla-b2g/gaia/</em:homepageURL>
</Description>
</RDF>

525
gaia/tools/extensions/httpd/bootstrap.js vendored Executable file
View file

@ -0,0 +1,525 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
function startup(data, reason) {
const CC = Components.Constructor;
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cm = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
Cu.import('resource://gre/modules/Services.jsm');
try {
// Gecko < 21
Cu.import('resource:///modules/devtools/dbg-client.jsm');
} catch (e) {
// Gecko >= 21
Cu.import('resource://gre/modules/devtools/dbg-client.jsm');
}
// Make sure all applications are considered launchable
Cu.import('resource://gre/modules/Webapps.jsm');
const env = Cc['@mozilla.org/process/environment;1'].
getService(Ci.nsIEnvironment);
DOMApplicationRegistry.allAppsLaunchable = true;
const LANG_EN = 'en-US';
const GAIA_DOMAIN = Services.prefs.getCharPref("extensions.gaia.domain");
const GAIA_APPDIRS = Services.prefs.getCharPref("extensions.gaia.appdirs");
const GAIA_ALLAPPDIRS = Services.prefs.getCharPref("extensions.gaia.allappdirs");
const GAIA_DIR = Services.prefs.getCharPref("extensions.gaia.dir");
const GAIA_PORT = Services.prefs.getIntPref("extensions.gaia.port");
const GAIA_OFFICIAL = Services.prefs.getBoolPref("extensions.gaia.official");
const LOCALES_FILE =
Services.prefs.getCharPref("extensions.gaia.locales_file");
const LOCALE_BASEDIR =
Services.prefs.getCharPref("extensions.gaia.locale_basedir");
const LocalFile = CC('@mozilla.org/file/local;1',
'nsILocalFile',
'initWithPath');
const ScriptableInputStream = CC('@mozilla.org/scriptableinputstream;1',
'nsIScriptableInputStream',
'init');
let debugEnv = env.get('DEBUG');
let DEBUG = debugEnv === '*' || debugEnv.includes('bootstrap');
let l10nManager, utils;
function debug(data) {
if (!DEBUG)
return;
dump(data + '\n');
}
function isl10nFile(fname) {
var matched;
return fname.contains('.ini') || fname.contains('manifest.webapp');
}
function startupHttpd(baseDir, port) {
const httpdURL = 'chrome://httpd.js/content/httpd.js';
let httpd = {};
env.set('GAIA_DIR', GAIA_DIR);
Services.scriptloader.loadSubScript(httpdURL, httpd);
let server = new httpd.nsHttpServer();
server.registerDirectory('/', new LocalFile(baseDir));
server.registerContentType('appcache', 'text/cache-manifest');
server.start(port);
// Add {appname}.domain.tld:port to the list of domains handle by httpd
let identity = server.identity;
let scheme = 'http';
let host = GAIA_DOMAIN;
identity.add(scheme, host, port);
let directories = getDirectories(GAIA_ALLAPPDIRS.split(' '));
directories.forEach(function appendDir(name) {
// Some app names can cause a raise here, preventing other apps
// from being added.
try {
identity.add(scheme, name + '.' + host, port);
} catch (e) {
dump(e);
}
});
identity.add(scheme, 'theme.' + host, port);
let commonjs = {
GAIA_BUILD_DIR: 'file://' + GAIA_DIR.replace(/\\/g, '/') + '/build/'
};
// Loading |require| then we can use build scripts of gaia.
Services.scriptloader.loadSubScript(commonjs.GAIA_BUILD_DIR +
'/xpcshell-commonjs.js', commonjs);
utils = commonjs.require('./utils');
if (LOCALE_BASEDIR) {
let appDirs = GAIA_APPDIRS.split(' ');
let multilocale = commonjs.require('./multilocale');
let sharedDir = utils.getFile(GAIA_DIR, 'shared');
l10nManager = new multilocale.L10nManager(GAIA_DIR,
LOCALES_FILE, LOCALE_BASEDIR);
let filesInShared = utils.ls(sharedDir, true);
let localesFile = utils.resolve(LOCALES_FILE, GAIA_DIR);
// Finding out which file we need to handle.
appDirs.forEach(function(appDir) {
let appDirFile = utils.getFile(appDir);
let filesInApp = utils.ls(appDirFile, true);
[...filesInApp, ...filesInShared].forEach(function(file) {
let fname = file.leafName;
let relativePath = file.path.substr(GAIA_DIR.length);
// localized ini and manifest files will be generated in runtime
// so we need handle path if client request those files.
// and we use "LocalizationHandler" to handle them.
if (isl10nFile(fname)) {
server.registerPathHandler(relativePath, LocalizationHandler);
}
});
});
// languages.json
server.registerFile('/shared/resources/languages.json', localesFile);
server.registerDirectory('/locales/',
new LocalFile(l10nManager.localeBasedir));
}
let testAgentBoilerplateDir = utils.getFile(GAIA_DIR, 'dev_apps',
'test-agent', 'common', 'test', 'boilerplate');
let proxyFile = testAgentBoilerplateDir.clone();
let sandboxFile = testAgentBoilerplateDir.clone();
proxyFile.append('_proxy.html');
sandboxFile.append('_sandbox.html');
server.registerFile('/test/unit/_proxy.html', proxyFile);
server.registerFile('/test/unit/_sandbox.html', sandboxFile);
server.registerPathHandler('/marionette', MarionetteHandler);
server.registerDirectory(
'/common/', new LocalFile(baseDir + '/dev_apps/test-agent/common')
);
server.registerDirectory('/shared/', new LocalFile(baseDir + '/shared'));
let brandingLocalFile = new LocalFile(
baseDir + '/shared/resources/branding/' + (GAIA_OFFICIAL ? 'official' : 'unofficial')
);
server.registerDirectory('/shared/resources/branding/', brandingLocalFile);
}
function getDirectories(appDirs) {
return appDirs.map(function (path) {
let appsDir = Cc['@mozilla.org/file/local;1']
.createInstance(Ci.nsILocalFile);
appsDir.initWithPath(path);
return appsDir.leafName;
});
}
function LongPoll(connid, transport) {
this.transport = transport;
this.connid = connid;
this.buffer = [];
this.outstanding = {
push: null,
noop: null,
timeout: null
};
}
LongPoll.prototype = {
cast: function cast(msg) {
let outstanding = this.outstanding;
clearTimeout(outstanding.noop);
this.buffer.push(msg);
if (outstanding.push === null)
return;
debug('cast ' + JSON.stringify(msg) + ' ' + outstanding.push);
this.drain_queue(outstanding.push);
outstanding.push.finish();
outstanding.push = null;
outstanding.timeout = setTimeout(function close(uid) {
Connections.close(uid);
}, 5000, this.connid);
},
drain_queue: function drain_queue(response) {
let queue = this.buffer;
this.buffer = [];
let json = { 'messages': [] };
queue.forEach(function(chunk) {
if (chunk.noop) {
//we need to send something to keep the client polling
json.messages.push({});
return;
}
json.messages.push({ 'id': this.connid, 'response': chunk });
}.bind(this));
response.write(JSON.stringify(json));
},
send: function send(obj) {
this.transport.send(obj);
}
};
let transports = {};
function getUID() {
return Cc['@mozilla.org/uuid-generator;1'].getService(Ci.nsIUUIDGenerator)
.generateUUID()
.toString();
}
var Connections = {
_connections: {},
close: function connections_close(uid) {
if (uid in this._connections) {
this._connections[uid].transport.close();
delete this._connections[uid];
}
},
add: function connections_add(uid, connection) {
this._connections[uid] = connection;
},
get: function connections_get(uid) {
return this._connections[uid] || null;
}
};
var LocalizationHandler = {
// we only handle GET method because we always use GET method to get those
// files.
handle: function lh_handle(request, response) {
if (request.method === 'GET' && l10nManager) {
let locales = JSON.parse(JSON.stringify(l10nManager.locales));
// no need to handle en-US.
var enIndex = locales.indexOf('en-US');
if (enIndex !== -1) {
locales.splice(enIndex, 1);
}
let {getFile, getFileContent} = utils;
let {modifyLocaleIni, serializeIni} = l10nManager;
let file = getFile(GAIA_DIR, request.path);
if (file.leafName.contains('.ini')) {
response.setHeader('Content-Type', 'text/plain');
let iniOriginal = getFileContent(file);
let modifiedIni = serializeIni(modifyLocaleIni(iniOriginal, locales));
response.write(modifiedIni);
} else if (file.leafName.contains('manifest.webapp')) {
let buildDirectoryFile = getFile(GAIA_DIR, 'build_stage',
file.parent.leafName);
response.setHeader('Content-Type', 'application/json');
let webapp = {
manifestFile: file,
sourceDirectoryFile: file.parent,
sourceDirectoryName: file.parent.leafName,
buildDirectoryFile: buildDirectoryFile
};
let manifest = l10nManager.localizeManifest(webapp);
response.write(JSON.stringify(manifest));
}
}
}
}
var MarionetteHandler = {
/**
* Adding only test-agent for now.
* In the future individual apps may need
* access as well.
*/
crosDomains: [
'http://test-agent.gaiamobile.org',
'http://test-agent.gaiamobile.org:8080',
'http://test-agent.trunk.gaiamobile.org',
'http://test-agent.trunk.gaiamobile.org:8080'
],
// nsIHttpRequestHandler
handle: function mh_handle(request, response) {
try {
this.handleCrosHeaders(request, response);
switch (request.method) {
case 'GET':
this.onGet(request, response);
break;
case 'POST':
this.onPost(request, response);
break;
case 'OPTIONS':
this.onOptions(request, response);
break;
case 'PUT':
this.onPut(request, response);
break;
case 'DELETE':
this.onDelete(request, response);
break;
default:
this.onDefault(request, response);
break;
}
} catch (e) {
response.setStatusLine('1.1', 500, 'Internal Server Error');
response.write(e.toString() + e.stack == undefined ? '' : ' ' + e.stack);
}
},
onOptions: function mh_options(request, response) {
response.setStatusLine('1.1', 200, 'OK');
},
handleCrosHeaders: function mh_options(request, response) {
if(!request.hasHeader('Origin'))
return;
let methods = 'GET, POST, PUT, DELETE, OPTIONS';
let origin = request.getHeader('Origin');
if (!origin || this.crosDomains.indexOf(origin) === -1) {
return;
}
response.setHeader(
'Access-Control-Allow-Origin',
origin
);
response.setHeader(
'Access-Control-Allow-Credentials',
'true'
);
response.setHeader(
'Access-Control-Max-Age',
'2500'
);
response.setHeader(
'Access-Control-Allow-Methods',
methods
);
response.setHeader('Access-Control-Allow-Headers', 'Content-Type, *');
},
onGet: function mh_get(request, response) {
debug('/marionette?' + request.queryString);
if (request.queryString.length === 0) {
response.setStatusLine('1.1', 404, 'Not Found');
response.write(JSON.stringify({ 'error': 'connection not found' }));
return;
}
response.setStatusLine('1.1', 200, 'OK');
response.setHeader('Content-Type', 'application/json');
let uid = request.queryString.split('=')[0];
let connection = Connections.get(uid);
if (!connection) {
debug('Could not get connection for ' + request.queryString);
return;
}
clearTimeout(connection.outstanding.timeout);
if (connection.buffer.length !== 0) {
connection.drain_queue(response);
return;
}
response.processAsync();
connection.outstanding.push = response;
connection.outstanding.noop = setTimeout(function() {
debug('sending noop');
connection.cast({ 'noop': true });
}, 25000);
},
onPost: function mh_post(request, response) {
response.setStatusLine('1.1', 200, 'OK');
response.setHeader('Content-Type', 'application/json');
let uid = getUID();
response.write(JSON.stringify({ 'id': uid }));
request.bodyInputStream.asyncWait(function(stream) {
let scriptable = new ScriptableInputStream(stream);
let json = scriptable.read(scriptable.available());
let { server: server, port: port } = JSON.parse(json);
let hostport = server + ':' + port;
if (hostport in transports) {
Connections.close(transports[hostport]);
}
debug('Opening a transport socket to the debugger...');
let transport = debuggerSocketConnect(server, parseInt(port));
let connection = new LongPoll(uid, transport);
Connections.add(uid, connection);
transports[hostport] = uid;
transport.hooks = {
onPacket: function onPacket(pack) {
debug('onpacket ' + JSON.stringify(pack));
connection.cast(pack);
},
onClosed: function onClosed(status) {
debug('onclosed ' + JSON.stringify(status));
}
};
transport.ready();
}, 0, 0, Services.tm.currentThread);
},
onPut: function mh_put(request, response) {
response.setStatusLine('1.1', 201, 'Accepted');
response.setHeader('Content-Type', 'text/plain', false);
response.write('');
let uid = request.queryString.split('=')[0];
let connection = Connections.get(uid);
if (!connection) {
debug('Could not get connection for ' + request.queryString);
return;
}
request.bodyInputStream.asyncWait(function(stream) {
let scriptable = new ScriptableInputStream(stream);
let json = scriptable.read(scriptable.available());
connection.send(JSON.parse(json));
}, 0, 0, Services.tm.currentThread);
},
onDelete: function mh_delete(request, response) {
Connections.close(request.queryString.split('=')[0]);
},
onDefault: function mh_default(request, response) {
response.setStatusLine('1.1', 405, 'Method Not Allowed');
response.write('');
}
};
function registerAppProtocol() {
let classID = Components.ID('{e3bace70-f074-11e3-ac10-0800200c9a66}');
let contract = '@mozilla.org/network/protocol;1?name=app';
let instance = null;
function AppProtocol() {}
AppProtocol.prototype = {
scheme: 'app',
classID: classID,
QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolHandler]),
newURI: function(aSpec, aOriginCharset, aBaseURI) {
let uri = Cc["@mozilla.org/network/standard-url;1"]
.createInstance(Ci.nsIStandardURL);
uri.init(Ci.nsIStandardURL.URLTYPE_STANDARD, -1, aSpec, aOriginCharset,
aBaseURI);
return uri.QueryInterface(Ci.nsIURI);
},
newChannel2: function(aURI, aLoadInfo) {
let url = aURI.QueryInterface(Ci.nsIURL);
let appId = aURI.host;
let fileSpec = url.filePath;
let spec = 'http://' + appId + ':' + GAIA_PORT + fileSpec;
let uri = Services.io.newURI(spec, null, null);
let channel = Services.io.newChannelFromURIWithLoadInfo(uri, aLoadInfo);
channel.QueryInterface(Ci.nsIChannel).originalURI = aURI;
return channel;
},
newChannel: function(aURI) {
return newChannel2(aURI, null);
}
};
let newFactory = {
createInstance: function(outer, iid) {
if (outer) {
throw Components.results.NS_ERROR_NO_AGGREGATION;
}
if (instance === null) {
instance = new AppProtocol();
}
return instance;
},
lockFactory: function(aLock) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
},
QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory])
};
Cm.registerFactory(classID, '', contract, newFactory);
}
registerAppProtocol();
startupHttpd(GAIA_DIR, GAIA_PORT);
}
function shutdown(data, reason) {
}
function install(data, reason) {
}
function uninstall(data, reason) {
}

View file

@ -0,0 +1 @@
content httpd.js content/

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,29 @@
<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:id>httpd@gaiamobile.org</em:id>
<em:name>httpd.js</em:name>
<em:version>0.1</em:version>
<em:iconURL>chrome://httpd.js/skin/logo.png</em:iconURL>
<em:targetApplication>
<Description>
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
<Description>
<em:id>{3c2e2abc-06d4-11e1-ac3b-374f68613e61}</em:id>
<em:minVersion>6.0</em:minVersion>
<em:maxVersion>*</em:maxVersion>
</Description>
</em:targetApplication>
<em:unpack>true</em:unpack>
<em:bootstrap>true</em:bootstrap>
<em:creator>Vivien Nicolas</em:creator>
<em:description>httpd.js</em:description>
<em:homepageURL>https://github.com/andreasgal/gaia/</em:homepageURL>
</Description>
</RDF>

View file

@ -0,0 +1 @@
httpd