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

116
gaia/tools/cache_sync.py Executable file
View file

@ -0,0 +1,116 @@
# Synchronizes the offline cache of an external app into a local directory
# structure. Run from the base directory of an app in gaia/external-apps.
# Builds a directory for each known origin in the offline cache.
import json
import os
import requests
def read_manifest():
file = open('manifest.webapp', 'r').read()
content = json.loads(file)
return content
def read_origin():
# Extract the origin from the origin file
return open('origin', 'r').readline().rstrip()
def parse_offline_manifest(s, origin):
lines = s.split('\n')
# Bail if this is an invalid appcache manifest.
if lines[0] != 'CACHE MANIFEST':
return None
sections = ['CACHE:', 'NETWORK:', 'FALLBACK:']
current_section = 'CACHE'
cache = []
for line in lines[1:]:
# ignore comments, empty lines, and wildcards
if line.startswith('#') or line.startswith('*') or not line.rstrip():
continue
if line in sections:
current_section = line[:len(line)-1]
continue
# Don't cache NETWORK entries.
if current_section == 'NETWORK':
continue
# if we're in FALLBACK, extract the fallback path.
if current_section == 'FALLBACK':
line = line.split(' ')[1]
# Ensure that all paths start with an origin.
if not line.startswith('http'):
if line.startswith('/'):
line = line[1:]
line = origin + line
cache.append(line)
return cache
def fetch_with_path(url):
# are we dealing with something URL-esque?
if not url.find('//') + 1:
return None
print 'fetching %s' % url
file = requests.get(url, verify=False)
if file.status_code != 200:
return None
# base our paths in the 'cache' directory.
path = 'cache/' + url.split('//')[1]
directory, fn = path.rsplit('/', 1)
# make the directory structure
if not os.path.exists(directory):
os.makedirs(directory)
# write our file
out_file = open(path, 'w')
out_file.write(file.content)
return file.content;
def main():
# Learn about the external app.
app_manifest = read_manifest()
origin = read_origin()
if 'appcache_path' not in app_manifest:
print 'no appcache found'
return
# Construct the appcache url and drop the first slash from appcache_path.
appcache_url = origin + app_manifest.get('appcache_path')[1:]
print 'fetching appcache at %s' % appcache_url
cache_manifest = fetch_with_path(appcache_url)
if not cache_manifest:
print 'failed to fetch appcache'
return
cache = parse_offline_manifest(cache_manifest, origin)
if not cache:
print 'bad offline cache'
return
# fetch each file, and store it in the right place
for entry in cache:
fetch_with_path(entry);
main()

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

View file

@ -0,0 +1,110 @@
/**
* @fileoverview A collection of utilities for performing static analysis on
* and transforming apps' html files.
*/
var Promise = require('es6-promise').Promise;
var jsdom = require('jsdom');
function load(filename) {
return new Promise(function(resolve, reject) {
jsdom.env(filename, function(errors, window) {
if (errors && errors.length) {
return reject(errors[0]);
}
return resolve(new HTMLTransformer(window));
});
});
};
function HTMLTransformer(window) {
this.window = window;
}
HTMLTransformer.prototype = {
getSharedDependencies: function() {
var doc = this.window.document;
var links = getSharedElements(doc, 'link');
var scripts = getSharedElements(doc, 'script');
var commented = flatmap(getComments(doc), getSharedElementsFromComment);
return links.concat(scripts).concat(commented);
}
};
function getSharedElements(doc, selector) {
return querySelectorAll(doc, selector).map(getShared).filter(function(x) {
return !!x;
});
}
function getSharedElementsFromComment(comment) {
var sharedLink = /<link\s.*href="(app:\/\/.+\.gaiamobile\.org)?\/?(shared\/.+)".*>/;
var sharedScript = /<script\s.*src="(app:\/\/.+\.gaiamobile\.org)?\/?(shared\/.+)".*>/;
return flatmap(comment.split(/\n/), function(commentLine) {
var links = commentLine.match(sharedLink) || [];
var scripts = commentLine.match(sharedScript) || [];
var results = links.concat(scripts);
return results.filter(function(result) {
return result && result.indexOf('shared') === 0;
});
});
};
// Neither document.createTreeWalker nor document.createNodeIterator
// are supported by jsdom so we're going to do this the old fashioned way...
function getComments(doc) {
var results = [];
var queue = [doc.body];
while (queue.length) {
var next = queue.shift();
if (next.nodeType === 8 /* COMMENT_NODE */) {
results.push(next.textContent);
}
var children = next.childNodes;
if (children && children.length) {
for (var i = 0; i < children.length; i++) {
queue.push(children[i]);
}
}
}
return results;
}
function querySelectorAll(doc, selector) {
return Array.prototype.slice.call(doc.querySelectorAll(selector));
}
function getShared(node) {
var sharedUrl = /^(app:\/\/.+\.gaiamobile\.org)?\/?(shared\/.+)/;
var match;
switch (node.nodeName) {
case 'LINK':
match = node.href.match(sharedUrl);
break;
case 'SCRIPT':
match = node.src.match(sharedUrl);
break;
default:
return false;
}
return (match && match.length) ? match.filter(function(str) {
return str && str.indexOf('shared') === 0;
})[0] : false;
}
function flatmap(array, fn) {
var result = [];
array.map(fn).forEach(function(subresult) {
if (Array.isArray(subresult)) {
result = result.concat(subresult);
}
});
return result;
}
module.exports = { HTMLTransformer: HTMLTransformer, load: load };

View file

@ -0,0 +1,20 @@
{
"name": "html-transformer",
"version": "0.0.1",
"description": "Figures out app shared deps, localizes your strings, other mischief",
"main": "html_transformer.js",
"dependencies": {
"es6-promise": "^2.3.0",
"jsdom": "^3"
},
"devDependencies": {
"chai": "^3.0.0",
"mocha": "^2.2.5"
},
"scripts": {
"test": "./node_modules/.bin/mocha"
}
}

View file

@ -0,0 +1,18 @@
<html>
<head>
<title>Basic html page</title>
</head>
<body>
<!-- hello -->
<p>A really great thing to say</p>
<link rel="stylesheet" type="text/css" href="/shared/style/icons.css">
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/style.css">
<script defer src="shared/js/component_utils.js"></script>
<script defer src="shared/elements/gaia_subheader/script.js"></script>
<!--
<link rel="stylesheet" type="text/css" href="shared/style/tabs.css">
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/other.css">
<script defer src="/shared/js/gesture_detector.js"></script>
-->
</body>
</html>

View file

@ -0,0 +1,38 @@
var expect = require('chai').expect;
var htmlTransformer = require('../html_transformer');
suite('htmlTransformer', function() {
var subject;
setup(function() {
return htmlTransformer.load(__dirname + '/fixtures/basic.html')
.then(function(result) {
subject = result;
});
});
test('should be an HTMLTransformer', function() {
expect(subject).to.be.an.instanceOf(htmlTransformer.HTMLTransformer);
});
test('should load html', function() {
var doc = subject.window.document;
var p = doc.querySelector('p');
expect(p.textContent).to.equal('A really great thing to say');
});
test('#getSharedDependencies', function() {
expect(subject.getSharedDependencies())
.to
.deep
.equal([
'shared/style/icons.css',
'shared/elements/gaia-theme/style.css',
'shared/js/component_utils.js',
'shared/elements/gaia_subheader/script.js',
'shared/style/tabs.css',
'shared/elements/gaia-theme/other.css',
'shared/js/gesture_detector.js'
]);
});
});

View file

@ -0,0 +1 @@
--ui tdd

323
gaia/tools/png_recompress.sh Executable file
View file

@ -0,0 +1,323 @@
#!/bin/sh
###############################################################################
# Option variables
opt_verbose=no
opt_recursive=no
opt_backup=no
opt_keepgoing=no
opt_optipng_args="-o7 -strip all -clobber -quiet"
opt_optipng_args_nostrip="-o7 -clobber -quiet"
opt_advpng_args="-z -4 -q"
opt_help=no
opt_paths=""
###############################################################################
# Other globals
# Temporary directory and log file used through the script
tmpdir=`mktemp -d -t tmp.XXXXXXXXXX`
if [ $? -ne 0 ]; then
printf "error: Cannot create a temporary directory"
exit 1
fi
tmplog=`mktemp -t tmp.XXXXXXXXXX`
if [ $? -ne 0 ]; then
printf "error: Cannot create a temporary log file"
exit 1
fi
trap 'rm -rf "$tmpdir $tmplog"' EXIT INT TERM HUP
size_cmd="printf 1" # Command used to obtain the size of a file
total_src_size="0" # Total size of the source files
total_comp_size="0" # Total size of the compressed files
processed_files="0" # Number of files having been processed
###############################################################################
# Print the help message
print_help() {
printf "Usage: `basename $0` [OPTION...] [PATH...]
Recompresses PNG files using the 'optipng' and 'advpng' tools.
Application options:
-v, --verbose Print information on the files being processed
-r, --recursive Recurse into subdirectories and process all files
-b, --backup Keep a backup of the modified files
-k, --keepgoing Do not stop in case of errors
Compression options:
--optipng-args Arguments passed to optipng (default: $opt_optipng_args)
--advpng-args Arguments passed to advpng (default: $opt_advpng_args)
Help options:
-h, --help Show this help message
"
}
suggest_help() {
printf "Try \``basename $0` --help' for more information\n"
}
###############################################################################
# Parse the program arguments
parse_args() {
# Set the options variables depending on the passed arguments
while [ $# -gt 0 ]; do
if [ `expr "$1" : "^-"` -eq 1 ]; then
if [ $1 = "-v" ] || [ $1 = "--verbose" ]; then
shift
opt_verbose=yes
elif [ $1 = "-r" ] || [ $1 = "--recursive" ]; then
shift
opt_recursive=yes
elif [ $1 = "-b" ] || [ $1 = "--backup" ]; then
shift
opt_backup=yes
elif [ $1 = "-k" ] || [ $1 = "--keepgoing" ]; then
shift
opt_keepgoing=yes
elif [ $1 = "--optipng-args" ]; then
if [ $# -le 1 ]; then
printf "error: No argument specified for --optipng-args\n"
suggest_help
exit 1
else
opt_optipng_args="$2"
opt_optipng_args_nostrip=`echo $2 | sed -e 's/-strip all//'`
shift 2
fi
elif [ $1 = "--advpng-args" ]; then
if [ $# -le 1 ]; then
printf "error: No argument specified for --advpng-args\n"
suggest_help
exit 1
else
opt_advpng_args="$2"
shift 2
fi
elif [ $1 = "-h" ] || [ $1 = "--help" ]; then
opt_help=yes
shift 1
else
printf "error: Unknown option $1\n"
exit 1
fi
else
# No more arguments, the following parameters will be paths
break
fi
done
if [ $# -eq 0 ] && [ $opt_help != yes ]; then
printf "error: no files specified\n"
suggest_help
exit 1
fi
# Gather all the paths
while [ $# -gt 0 ]; do
opt_paths="$opt_paths$1\n"
shift
done
# Adjust the arguments
if [ $opt_backup = yes ]; then
opt_optipng_args="$opt_optipng_args -backup"
fi
}
###############################################################################
# Check for programs
check_optipng_version() {
version=$(optipng --version | head -n 1)
version="${version%%: Advanced PNG optimizer.}"
version="${version##OptiPNG version }"
version="${version##OptiPNG }"
if [ ${#version} -le 3 ]; then
version="$version.0"
fi
if [ "$version" \< 0.7.0 ]; then
printf "error: the version of optipng you have installed ($version) is"\
" too old, you need at least version 0.7.0 for this script to work correctly\n"
exit 1
fi
}
check_programs() {
which optipng 1>/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
printf "error: optipng not found\n"
exit 1;
fi
check_optipng_version
which advpng 1>/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
printf "error: advpng not found\n"
exit 1;
fi
if stat --printf %s . 1>/dev/null 2>/dev/null; then
size_cmd="stat --printf %s"
elif stat -f %z . 1>/dev/null 2>/dev/null; then
size_cmd="stat -f %z"
fi
}
###############################################################################
# Recompress all files in a directory, if recursive behavior was requested will
# recurse in all subdirectories
recompress_dir() {
local files
if [ $opt_recursive = yes ]; then
files=`find "$1" -iname "*.png" -type f`
else
files=`find "$1" -maxdepth 1 -iname "*.png" -type f`
fi
mkfifo "$tmpdir/files_pipe"
printf -- "$files\n" > "$tmpdir/files_pipe" &
while read -r fd; do
recompress_file "$fd"
done < "$tmpdir/files_pipe"
}
###############################################################################
# Recompress a single file using the specified options
recompress_file() {
local rv=0
local apng=0
local src_size=`$size_cmd "$1"`
if [ $opt_backup = yes ]; then
if [ -e "$1.bak" ] ; then
printf "error: backup file $1.bak already exists\n"
if [ $opt_keepgoing = no ]; then
exit 1
fi
else
cp -p "$1" "$1.bak"
fi
fi
optipng -o0 -simulate "$1" 1>"$tmplog" 2>"$tmplog"
grep -q APNG "$tmplog"
apng=$?
if [ $apng -eq 0 ]; then
# Do not strip animated PNG files otherwise the frames will be gone
optipng $opt_optipng_args_nostrip "$1"
rv=$?
else
optipng $opt_optipng_args "$1"
rv=$?
fi
if [ $rv -ne 0 ]; then
printf "error: optipng $opt_optipng_args \"$1\" returned $rv\n"
if [ $opt_keepgoing = no ]; then
exit 1
fi
fi
if [ $apng -ne 0 ]; then
# Do not invoke advpng on animated PNG files as it will strip the frames
advpng $opt_advpng_args "$1"
rv=$?
if [ $rv -ne 0 ]; then
printf "error: advpng $opt_optipng_args \"$1\" returned $rv\n"
if [ $opt_keepgoing = no ]; then
exit 1
fi
fi
fi
local comp_size=`$size_cmd "$1"`
local ratio=`compression_ratio $src_size $comp_size`
if [ $opt_verbose = yes ]; then
printf "$1 $src_size $comp_size $ratio\n"
fi
total_src_size=$((total_src_size + src_size))
total_comp_size=$((total_comp_size + comp_size))
processed_files=$((processed_files + 1))
}
###############################################################################
# Prints out the compression ratio, $1 is the source file size, $2 is the
# compressed file size
compression_ratio() {
echo "scale=3;
result = $2.0 / $1.0;
if (result >= 0 && result < 1) {
print \"0\";
}
if (result == 0) {
print \".\";
}
print result;" | bc
}
###############################################################################
# Main script
parse_args "$@"
check_programs
if [ $opt_help = yes ] || [ $# -eq 0 ]; then
print_help
exit 0
fi
# Process all the specified paths
mkfifo "$tmpdir/paths_pipe"
printf -- "$opt_paths" > "$tmpdir/paths_pipe" &
while read -r fd; do
if [ -f "$fd" ]; then
recompress_file "$fd"
elif [ -d "$fd" ]; then
recompress_dir "$fd"
elif ! [ -e "$fd" ]; then
printf "error: $fd no such file or directory\n"
if [ $opt_keepgoing = no ]; then
exit 1
fi
else
printf "warning: skipping $fd, as it is neither a file nor a directory\n"
fi
done < "$tmpdir/paths_pipe"
if [ $opt_verbose = yes ]; then
printf "
Number of files processed: $processed_files
Total size of the files prior to recompression: $total_src_size
Total size of the files after recompression: $total_comp_size
Compression ratio: `compression_ratio $total_src_size $total_comp_size`
"
fi

211
gaia/tools/pre-commit Executable file
View file

@ -0,0 +1,211 @@
#!/bin/bash
export LC_ALL=C
############################### JS Lint #####################################
LINTED_DIRECTORIES="apps shared build/test/unit tv_apps"
JSHINT_EXECUTABLE="node_modules/.bin/jshint"
JSHINT_ARGS="--reporter=build/jshint/reporter $JSHINT_ARGS"
ESLINT_EXECUTABLE="node_modules/.bin/eslint"
ESLINT_XFAIL_LIST="build/eslint/xfail.list"
ESLINT_ARGS="--ignore-path ${ESLINT_XFAIL_LIST}.tmp -f compact -c .eslintrc"
excluded_list=".jshintignore"
L10N_XFAIL_LIST="build/l10n/xfail.list"
jshint_information() {
echo "Please read https://github.com/mozilla-b2g/gaia/tree/master/build/jshint/README.md for more information.\n"
}
mozL10n_information() {
echo "Please read https://github.com/mozilla-b2g/gaia/tree/master/build/l10n/README.md for more information.\n"
}
eslint_information() {
echo "Please read https://github.com/mozilla-b2g/gaia/tree/master/build/eslint/README.md for more information.\n"
}
if [ ! -r $excluded_list ];
then
exit 0
fi
hash node > /dev/null 2>&1
if [ $? -eq 1 ];
then
echo >&2 "You should install node to lint your patch"
echo >&2 "http://nodejs.org/download/"
exit 0
fi
if [ ! -x "$JSHINT_EXECUTABLE" ];
then
echo >&2 'You should run `npm install` to lint your patch'
exit 0
fi
# Skips if this repo is on an older commit where eslint is not yet available
if [ ! -x "$ESLINT_EXECUTABLE" -a -r ".eslintrc" ];
then
echo >&2 'You should run `npm install` to lint your patch'
exit 0
fi
js_changed_files=`git diff --staged --name-only --diff-filter=ACMRT -- $LINTED_DIRECTORIES | grep '\.js$'`
if [ -n "$js_changed_files" ]
then
echo "jshint check:"
# using the real jshint instead of the make target to prevent running npm
# install at commit time
$JSHINT_EXECUTABLE $JSHINT_ARGS $js_changed_files
jshint_result=$?
eslint_result=0
# Skip if this repo is on an older commit where eslint is not yet available
if [ -r ".eslintrc" ];
then
echo "eslint check:"
# filter comments from xfail.list:
sed 's/\s*#.*$//' "$ESLINT_XFAIL_LIST" | sort -u > "$ESLINT_XFAIL_LIST".tmp
# check if a changed file is in the eslint xfail list
xfailed_eslint=`echo "$js_changed_files" | sort | comm -12 "$ESLINT_XFAIL_LIST".tmp - | tr '[:space:]' ' '`
if [ -n "$xfailed_eslint" ] ; then
# warn about files being xfailed
for file in $js_changed_files ; do
one_was_ignored=""
if grep -q $file "$ESLINT_XFAIL_LIST" ; then
one_was_ignored="1"
echo "$file: eslint errors are ignored. Please consider fixing these errors and removing the filename from $ESLINT_XFAIL_LIST."
fi
done
test -n "$one_was_ignored" && eslint_information
fi
$ESLINT_EXECUTABLE $ESLINT_ARGS $js_changed_files
eslint_result=$?
rm "$ESLINT_XFAIL_LIST".tmp
fi
if [ $jshint_result -ne 0 -o $eslint_result -ne 0 ] ; then
echo "There were errors while linting the files, please see above."
test $jshint_result -ne 0 && jshint_information
test $eslint_result -ne 0 && eslint_information
exit 1
fi
# XXX migrate this to an eslint plugin: https://bugzil.la/1159667
if [ -r "$L10N_XFAIL_LIST" ]; then
echo
echo "mozL10n check:"
# warn about files being xfailed
for file in $js_changed_files ; do
one_was_ignored=""
if grep -q $file "$L10N_XFAIL_LIST" ; then
one_was_ignored="1"
echo "$file: mozL10n errors are ignored. Please consider fixing errors and removing it from $L10N_XFAIL_LIST."
fi
done
test -n "$one_was_ignored" && mozL10n_information
to_lint=`echo "$js_changed_files" | sort | comm -13 "$L10N_XFAIL_LIST" - | tr '[:space:]' ' '`
# check for deprecated mozL10n API uses
for file in $to_lint ; do
one_failed=""
if grep -q "mozL10n.get\b" "$file" ; then
one_failed="1"
echo "$file uses mozL10n.get which is deprecated."
fi
done
test -n "$one_failed" && mozL10n_information && exit 1
fi
fi
############################### CSS Lint #####################################
css_changed_files=`git diff --staged --name-only --diff-filter=ACMRT -- apps shared/ | grep '\.css$' | tr '\n' ' '`
if [ -n "$css_changed_files" ]
then
XULRUNNER_SDK=$(make xulrunner_sdk)
XPCSHELL_SDK=$(make xpcshell_sdk)
# $XULRUNNER_SDK is an environment variable assignment on some platforms, for
# example: LD_LIBRARY_PATH=/xxx/xxx/xxx
# so we need to use "env" to properly execute this command expanding this
# variable assignment as intended.
env GAIA_DIR=$(pwd) $XULRUNNER_SDK $XPCSHELL_SDK \
-f build/xpcshell-commonjs.js \
-e "quit(require('csslint').lint('$(pwd)', '$css_changed_files'));"
if [ $? -ne 0 ]
then
echo "There were errors while linting the files, please see above."
exit 1
fi
fi
echo "No errors - committing"
############################# locale check #####################################
locales_changed_files=`git diff --staged --name-only --diff-filter=ACMRT | grep '\.properties$'`
if [ -n "$locales_changed_files" ]
then
updatedL10nIds=()
needToHintDevelopers=0
for file in $locales_changed_files ; do
grepRuleForBoth="^[+-].*=.*"
grepRuleForAddition="^[+](.*)="
grepRuleForDeletion="^[-](.*)="
addition_locale_ids=()
deletion_locale_ids=()
count=0
while read -r eachLine
do
if [[ "$eachLine" =~ $grepRuleForAddition ]]
then
addition_locale_ids+=(${BASH_REMATCH[1]})
fi
if [[ "$eachLine" =~ $grepRuleForDeletion ]]
then
deletion_locale_ids+=(${BASH_REMATCH[1]})
fi
# check http://mywiki.wooledge.org/BashFAQ/024
done < <(git diff HEAD~ --staged $file | grep $grepRuleForBoth)
# final compare
for eachAdditionId in "${addition_locale_ids[@]}"
do
for eachDeletionId in "${deletion_locale_ids[@]}"
do
if [[ "$eachAdditionId" =~ "$eachDeletionId" ]]
then
needToHintDevelopers=1
updatedL10nIds+=($eachAdditionId)
fi
done
done
done
if [ $needToHintDevelopers -ne 0 ]
then
echo "[NOTE] You are updating the same translation under these locale ids :"
for eachL10nId in "${updatedL10nIds[@]}"
do
echo "Updated id : $eachL10nId"
done
echo "Please double check whether you should introduce a new locale id or not."
fi
fi

367
gaia/tools/preload.py Executable file
View file

@ -0,0 +1,367 @@
# preload.py comes from https://github.com/mozilla-b2g/preload-app-toolkit/
# DO NOT EDIT DIRECTLY, make PR's to above repo and copy across file
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import urllib
import base64
import mimetypes
import os
import json
import codecs
from urlparse import urlparse
from zipfile import ZipFile
from datetime import datetime
import time
import shutil
import re
import logging
import argparse
APPCACHE_LOCAL_DEFAULT_PATH = 'cache/'
APPCACHE_LOCAL_DEFAULT_NAME = 'manifest.appcache'
logger = logging.getLogger('GaiaPreloadApp')
logger.setLevel(logging.INFO)
sh = logging.StreamHandler()
logger.addHandler(sh)
def set_logger_level(lv):
logger.setLevel(lv)
def has_scheme(url):
return bool(url.scheme)
def retrieve_from_url(url, target):
"""
save file from url
"""
if (os.path.isdir(target)):
target += 'index.html'
return urllib.urlretrieve(url, target)
def open_from_url(url):
"""
return file object from url
"""
return urllib.urlopen(url)
def get_absolute_url(domain, path, icon):
icon_path = None
origin = urlparse(''.join([domain, path]))
if has_scheme(icon):
return icon.geturl()
if icon.path[0] == '/':
icon_path = icon.path
else:
icon_path = '%s/%s' % (os.path.dirname(origin.path), icon.path)
if(path.startswith('http')):
return icon_path
else:
return '%s://%s%s' % (origin.scheme, origin.netloc, icon_path)
def get_directory_name(appname):
return re.sub(r'[\W\s]', '', appname).lower()
def split_url(manifest_url):
path = None
url = urlparse(manifest_url)
domain = '%s://%s' % (url.scheme, url.netloc)
if url.path.count('/') > 1:
path = ''.join([os.path.dirname(url.path), '/'])
else:
path = '/'
return (domain, path)
def convert_icon(image, mimetype):
return 'data:%s;base64,%s' % (mimetype, base64.b64encode(image))
def fetch_icon_from_url(url):
image = open_from_url(url).read()
return convert_icon(image, mimetypes.guess_type(url)[0])
def fetch_icon(key, icons, domain, path, apppath):
iconurl = get_absolute_url(domain, path,
urlparse(icons[key]))
icon_base64 = ''
if iconurl[0] == '/':
logger.info('locally...')
icon_base64 = iconurl
# fetch icon from url
elif (iconurl.startswith('http') and
(iconurl.endswith(".png") or iconurl.endswith(".jpg"))):
logger.info(' ' + key + ' from internet...',)
subfix = "/icon.png" if iconurl.endswith(".png") else "/icon.jpg"
retrieve_from_url(iconurl, apppath + subfix)
with open(apppath + subfix) as fd:
image = fd.read()
icon_base64 = convert_icon(image,
mimetypes.guess_type(iconurl)[0])
os.remove(apppath + subfix)
# fetch icon from local
else:
icon_base64 = fetch_icon_from_url(iconurl)
return icon_base64
def get_resource_path_and_url(domain, workingdir, resource_path):
if has_scheme(urlparse(resource_path)):
resource_url = resource_path
elif resource_path[0] == '/':
resource_url = ''.join([domain, resource_path])
resource_path = resource_path.lstrip('/')
else:
resource_url = ''.join([domain, workingdir, resource_path])
resource_path = ''.join([workingdir, resource_path]).lstrip('/')
return (resource_url, resource_path)
def fetch_resource(domain, workingdir, local_dir, resource_path):
"""
fetch resource described in appcache manifest
"""
try:
url, path = get_resource_path_and_url(domain, workingdir, resource_path)
# create directories if not exist
local_resource_path = ''.join([local_dir, path])
local_resource_dir = os.path.dirname(local_resource_path)
if not os.path.exists(local_resource_dir):
os.makedirs(local_resource_dir)
(_filename, _headers) = retrieve_from_url(url, local_resource_path)
# I need to save the header at some place so they can be written (and processed at install time)
return (path.lstrip('/'), _headers)
except IOError as e:
logger.info('IO failed ', e)
except urllib.URLError as e:
logger.info('fetch failed ', e)
def get_appcache_manifest_dir(manifest_dir, appcache_path):
logger.info('Manifest dir: ' + manifest_dir + '. appcache_path: ' + appcache_path)
if (appcache_path[0] != '/'):
result = os.path.join(manifest_dir, os.path.dirname(appcache_path))
else:
result = os.path.dirname(appcache_path)
return result if result[-1] == '/' else result + '/'
def get_appcache_manifest(domain, manifest_dir, app_dir, appcache_path):
manifest = {
'remote_dir': get_appcache_manifest_dir(manifest_dir, appcache_path),
'filename': os.path.basename(appcache_path),
'local_dir': os.path.join(app_dir, APPCACHE_LOCAL_DEFAULT_PATH)
}
manifest['local_path'] = os.path.join(manifest[
'local_dir'], APPCACHE_LOCAL_DEFAULT_NAME)
origin = urlparse(domain)
base_path = '%s://%s' % (origin.scheme, origin.netloc)
manifest['url'] = ''.join([base_path,
manifest['remote_dir'],
manifest['filename']])
return manifest
def format_resource_metadata(file, header):
"""
documentation here
"""
lastModified = header.getheader('last-modified', 'Epoch')
lastFetched = header.getheader('date', 'Now')
return '"' + file + '": { "lastModified": "' + lastModified + '", "lastFetched": "' + lastFetched + '"}'
def fetch_appcache(domain, remote_dir, local_dir, lines):
"""
fetch appcache file described in manifest.webapp
output:
[appname]/cache/[name].appcache
[appname]/cache/[resources] (if defined)
"""
newlines = []
headers = []
try:
# retrieve resources from appcache
curr = None
SECTIONS = ['CACHE MANIFEST', 'NETWORK:', 'CACHE:', 'FALLBACK:']
for line in lines:
line = line.strip()
if line in SECTIONS:
curr = line
newlines.append(line)
continue
if len(line) == 0 or line[0] == '#':
newlines.append(line)
continue
if curr == SECTIONS[0] or curr == SECTIONS[2]:
logger.info(' get resource ' + line + '...')
(line, header) = fetch_resource(domain, remote_dir, local_dir, line)
elif curr == SECTIONS[3]:
logger.info(' get resource ' + line + '...')
(_line, header) = fetch_resource(domain, remote_dir, local_dir,
line.split(' ')[1])
newlines.append(line)
headers.append(format_resource_metadata(line, header))
except Exception as e:
logger.info(' fetch failed ', e)
return (newlines, headers)
def fetch_webapp(app_url, directory=None):
"""
get webapp file and parse for preinstalled webapp
output:
[appname]/manifest.webapp
[appname]/metadata.json
[appname]/update.webapp (if package_path is defined)
[appname]/cache/ (if appcache_path is defined)
"""
domain, path = split_url(app_url)
url = urlparse(app_url)
metadata = {'origin': domain}
manifest_filename = 'manifest.webapp'
if url.scheme:
logger.info('manifest: ' + app_url)
logger.info('fetching manifest...')
manifest_url = open_from_url(app_url)
manifest = json.loads(manifest_url.read().decode('utf-8-sig'))
metadata['installOrigin'] = 'https://marketplace.firefox.com'
if 'etag' in manifest_url.headers:
metadata['etag'] = manifest_url.headers['etag']
else:
logger.info('extract manifest from zip...')
appzip = ZipFile(app_url, 'r').read('manifest.webapp')
manifest = json.loads(appzip.decode('utf-8-sig'))
appname = get_directory_name(manifest['name'])
app_dir = appname
if directory is not None:
app_dir = os.path.join(directory, appname)
if not os.path.exists(app_dir):
os.mkdir(app_dir)
if 'package_path' in manifest or not url.scheme:
manifest_filename = 'update.webapp'
filename = 'application.zip'
metadata.pop('origin', None)
if url.scheme:
logger.info('downloading app...')
path = manifest['package_path']
retrieve_from_url(
manifest['package_path'],
os.path.join(app_dir, filename))
metadata['manifestURL'] = url.geturl()
package_url = open_from_url(path)
if 'etag' in package_url.headers:
metadata['packageEtag'] = package_url.headers['etag']
else:
logger.info('copying app...')
shutil.copyfile(app_url, '%s%s%s' % (appname, os.sep, filename))
metadata['manifestURL'] = ''.join(
[domain, path, 'manifest.webapp'])
manifest['package_path'] = ''.join(['/', filename])
logger.info('fetching icons...')
for key in manifest['icons']:
manifest['icons'][key] = fetch_icon(
key, manifest['icons'], domain, path, app_dir)
if 'appcache_path' in manifest:
metadata_info = [];
logger.info('fetching appcache...',)
appcache_manifest = get_appcache_manifest(
domain, path, app_dir, manifest['appcache_path'])
if not os.path.exists(appcache_manifest['local_dir']):
os.makedirs(appcache_manifest['local_dir'])
logger.info(' from ' + appcache_manifest['url'])
logger.info(' save to ' + appcache_manifest['local_path'],)
( _filename, headerAC ) = retrieve_from_url(appcache_manifest['url'],
appcache_manifest['local_path'])
lines = []
with open(appcache_manifest['local_path']) as fd:
lines = fd.readlines()
(lines, headers) = fetch_appcache(domain, appcache_manifest['remote_dir'],
appcache_manifest['local_dir'], lines)
with open(appcache_manifest['local_path'], 'w') as fd:
logger.info('overwrite new appcache')
lines.append('')
fd.write('\n'.join(lines))
with open(appcache_manifest['local_dir'] + '../resources_metadata.json', 'w') as resources:
resources.write('{\n');
headers.append(format_resource_metadata(appcache_manifest['local_path'], headerAC))
resources.write(',\n'.join(headers))
resources.write('\n}\n');
# add manifestURL for update
metadata['manifestURL'] = app_url
metadata['external'] = True
f = file(os.path.join(app_dir, 'metadata.json'), 'w')
f.write(json.dumps(metadata))
f.close()
f = codecs.open(os.path.join(app_dir, manifest_filename), 'w', 'utf-8')
f.write(json.dumps(manifest, ensure_ascii=False))
return manifest
def main():
parser = argparse.ArgumentParser(description = 'Preload webapps.')
parser.add_argument('--root', default=os.getcwd(),
help = 'Root directory to run from')
parser.add_argument('--icon',
help = 'Fetch webapp icon')
parser.add_argument('webapp', nargs = '?')
args = parser.parse_args()
os.chdir(args.root)
print args.root
# icon convertion script
if (args.icon):
result = fetch_icon_from_url(args.icon)
logger.info(result.replace('/', '\/'))
# fetch single webapp
elif (args.webapp):
fetch_webapp(args.webapp)
else:
# automatically read and compose customized webapp from list
# support csv like list format with ',' separator, ex:
#
# Youtube,http://m.youtube.com/mozilla_youtube_webapp
with open('list') as fd:
while True:
line = fd.readline()
if (len(line.split(',')) > 1):
fetch_webapp(line.split(',')[1].rstrip('\n'))
else:
break
if __name__ == '__main__':
main()

95
gaia/tools/svg_recompress.sh Executable file
View file

@ -0,0 +1,95 @@
#!/bin/sh
###############################################################################
# Compress SVG files using svgo (https://github.com/svg/svgo)
#
# The following svgo plugins are disabled:
# * convertPathData
# Though the resulting files are OK in any browsers, they're rendered in a
# weird way on Ubuntu files explorer.
# * mergePaths
# Can result on wrong odd/even filling if some paths are overlapping.
###############################################################################
# Option variables
opt_help=no
opt_paths=""
###############################################################################
# Print the help message
print_help() {
printf "Usage: `basename $0` [OPTION...] [PATH...]
Recompresses SVG files using 'svgo' tools.
Help options:
-h, --help Show this help message
"
}
suggest_help() {
printf "Try \``basename $0` --help' for more information\n"
}
###############################################################################
# Parse the program arguments
parse_args() {
# Set the options variables depending on the passed arguments
while [ $# -gt 0 ]; do
if [ `expr "$1" : "^-"` -eq 1 ]; then
if [ $1 = "-h" ] || [ $1 = "--help" ]; then
opt_help=yes
shift 1
else
printf "error: Unknown option $1\n"
exit 1
fi
else
# No more arguments, the following parameters will be paths
break
fi
done
if [ $# -eq 0 ] && [ $opt_help != yes ]; then
printf "error: no files specified\n"
suggest_help
exit 1
fi
# Gather all the paths
while [ $# -gt 0 ]; do
opt_paths="$opt_paths$1\n"
shift
done
}
###############################################################################
# Check for program
check_program() {
which svgo 1>/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
printf "error: svgo not found\n"
exit 1;
fi
}
###############################################################################
# Main script
parse_args "$@"
check_program
if [ $opt_help = yes ] || [ $# -eq 0 ]; then
print_help
exit 0
fi
# Process all the specified paths
while [ $# -gt 0 ]; do
svgo --disable=convertPathData --disable=mergePaths --multipass --folder="$1"
shift
done