Update shared Gaia workflows and Sargo development tooling
Fix camera switching and Contacts/SMS activity routing across shared hosts. Integrate Marketplace installation and native standalone recovery tools, Bluetooth runtime packages and trusted certificates. Update English/French READMEs and the shared changelog with tested scope and remaining radio/audio/Bluetooth limitations. Preview17 remains the existing APK; no new native image is released. Validation: shared JavaScript and native Python tests; Android packaging and payload parity from an isolated publication checkout.
This commit is contained in:
parent
d04824ec89
commit
0041cd9660
101 changed files with 3883 additions and 335 deletions
|
|
@ -19,7 +19,7 @@ const VCARD_MIME_TYPES = [
|
|||
var ActivityHandler = {
|
||||
_currentActivity: null,
|
||||
|
||||
_launchedAsInlineActivity: (window.location.search == '?pick'),
|
||||
_launchedAsInlineActivity: new URLSearchParams(window.location.search).has('pick'),
|
||||
|
||||
mozContactParam: null,
|
||||
|
||||
|
|
@ -124,6 +124,7 @@ var ActivityHandler = {
|
|||
this.launch_activity(activity, 'add-parameters');
|
||||
break;
|
||||
case 'pick':
|
||||
if (window.VulpesCompat) this._launchedAsInlineActivity = true;
|
||||
if (!this._launchedAsInlineActivity) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -356,15 +357,18 @@ var ActivityHandler = {
|
|||
postNewSuccess: function ah_postNewSuccess(contact) {
|
||||
this._currentActivity.postResult({ contact: contact });
|
||||
this._currentActivity = null;
|
||||
if (window.VulpesCompat) this._launchedAsInlineActivity = false;
|
||||
},
|
||||
|
||||
postPickSuccess: function ah_postPickSuccess(result) {
|
||||
this._currentActivity.postResult(result);
|
||||
this._currentActivity = null;
|
||||
if (window.VulpesCompat) this._launchedAsInlineActivity = false;
|
||||
},
|
||||
|
||||
postCancel: function ah_postCancel() {
|
||||
this._currentActivity.postError('canceled');
|
||||
this._currentActivity = null;
|
||||
if (window.VulpesCompat) this._launchedAsInlineActivity = false;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -655,7 +655,9 @@ var Contacts = (function() {
|
|||
utils.PerformanceHelper.contentInteractive();
|
||||
utils.PerformanceHelper.chromeInteractive();
|
||||
window.setTimeout(Contacts && Contacts.onLocalized);
|
||||
if (window.navigator.mozSetMessageHandler && window.self == window.top) {
|
||||
// Vulpes embeds each app in its own permission-checked frame.
|
||||
if (window.navigator.mozSetMessageHandler &&
|
||||
(window.self == window.top || window.VulpesCompat)) {
|
||||
LazyLoader.load(['/shared/js/contacts/import/utilities/misc.js',
|
||||
'/shared/js/contacts/import/utilities/vcard_reader.js',
|
||||
'/shared/js/contacts/import/utilities/vcard_parser.js'],
|
||||
|
|
|
|||
|
|
@ -1038,6 +1038,8 @@ contacts.Form = (function() {
|
|||
// (Bug 1041455).
|
||||
if (editingContact) {
|
||||
Contacts.setCurrent(contact);
|
||||
// The change event may arrive while the edit form is still visible.
|
||||
Contacts.updateContactDetail(contact.id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -408,6 +408,10 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (HIDDEN_ROLES.includes(manifest.role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not add blacklisted apps
|
||||
if (BLACKLIST.includes(app.origin)) {
|
||||
return;
|
||||
|
|
|
|||
BIN
gaia/apps/marketplace/icon.png
Normal file
BIN
gaia/apps/marketplace/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
5
gaia/apps/marketplace/index.html
Normal file
5
gaia/apps/marketplace/index.html
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Marketplace</title><link rel="stylesheet" href="/shared/style/confirm.css"><link rel="stylesheet" href="marketplace.css"><script defer src="marketplace.js"></script></head><body>
|
||||
<iframe id="catalogue" mozbrowser title="Marketplace"></iframe>
|
||||
<section id="message" hidden><img src="icon.png" alt="" width="96" height="96"><p id="status" role="status"></p><button id="browse" type="button"></button><button id="open" type="button" hidden></button></section>
|
||||
<form id="confirm" role="dialog" data-type="confirm" hidden><section><h1 id="title"></h1><p id="question"></p><p id="permissions"></p></section><menu><button id="cancel" type="button"></button><button id="install" type="submit" class="recommend"></button></menu></form>
|
||||
</body></html>
|
||||
19
gaia/apps/marketplace/manifest.webapp
Normal file
19
gaia/apps/marketplace/manifest.webapp
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "Marketplace",
|
||||
"description": "Vulpes OS application catalogue",
|
||||
"launch_path": "/index.html",
|
||||
"type": "certified",
|
||||
"icons": {
|
||||
"512": "/icon.png"
|
||||
},
|
||||
"permissions": {
|
||||
"browser": {},
|
||||
"webapps-manage": {}
|
||||
},
|
||||
"locales": {
|
||||
"fr": {
|
||||
"name": "Marketplace",
|
||||
"description": "Catalogue des applications Vulpes OS"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
gaia/apps/marketplace/marketplace.css
Normal file
1
gaia/apps/marketplace/marketplace.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
html,body{width:100%;height:100%;margin:0;background:#f4f4f4;font-family:Fira Sans,sans-serif}#catalogue{position:absolute;inset:0;width:100%;height:100%;border:0}#message{padding:3rem 2rem;text-align:center;font-size:1.8rem}#message button{display:block;width:100%;padding:1rem;margin:1rem 0;font:inherit}#open[hidden],[hidden]{display:none!important}#confirm{z-index:10}#confirm section{overflow:auto}#permissions{font-size:1.5rem}
|
||||
32
gaia/apps/marketplace/marketplace.js
Normal file
32
gaia/apps/marketplace/marketplace.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
(async()=>{
|
||||
const frame=document.getElementById('catalogue'),message=document.getElementById('message'),dialog=document.getElementById('confirm');
|
||||
let attempt=0,busy=false,prepared=null,installed=null,language=await VulpesCompat.call('locale.current');
|
||||
const fr=()=>/^fr(?:-|$)/i.test(language);
|
||||
const text=(f,e)=>fr()?f:e;
|
||||
function labels(){document.getElementById('browse').textContent=text('Ouvrir le catalogue','Open catalogue');document.getElementById('open').textContent=text('Ouvrir','Open');document.getElementById('install').textContent=text('Installer','Install');document.getElementById('cancel').textContent=text('Annuler','Cancel');}
|
||||
function loadFailed(){status(text('Catalogue inaccessible. Vérifiez la connexion Internet, puis réessayez.','Catalogue unavailable. Check your Internet connection, then try again.'));document.getElementById('open').hidden=true;}
|
||||
frame.addEventListener('mozbrowsererror',loadFailed);
|
||||
function browse(){labels();message.hidden=true;dialog.hidden=true;frame.style.visibility='visible';frame.src='https://vulpes-os.org/marketplace/?mode=app&lang='+(fr()?'fr':'en');}
|
||||
function status(value){frame.style.visibility='hidden';message.hidden=false;document.getElementById('status').textContent=value;}
|
||||
async function prepare(url){
|
||||
if(busy)return;const current=++attempt;busy=true;prepared=null;installed=null;document.getElementById('open').hidden=true;
|
||||
status(text('Vérification de l’application…','Checking application…'));
|
||||
try {const value=await VulpesCompat.call('marketplace.prepare',{url});if(current!==attempt)return;prepared=value;
|
||||
document.getElementById('title').textContent=prepared.name;
|
||||
document.getElementById('question').textContent=text('Installer cette application archivée ?','Install this archived application?')+' ('+(prepared.bytes/1048576).toFixed(1)+' Mio)';
|
||||
document.getElementById('permissions').textContent=text('Elle sera exécutée comme une application web, sans accès aux fonctions système. Certains anciens services peuvent ne plus fonctionner.','It will run as a web application without system access. Some old services may no longer work.');
|
||||
message.hidden=true;dialog.hidden=false;
|
||||
}catch(error){if(current!==attempt)return;status(text('Installation impossible : ','Cannot install: ')+error.message);busy=false;}
|
||||
}
|
||||
dialog.addEventListener('submit',async event=>{event.preventDefault();if(!prepared)return;const token=prepared.token;prepared=null;dialog.hidden=true;status(text('Installation…','Installing…'));
|
||||
try {installed=await VulpesCompat.call('marketplace.install',{token});status(text('Application ajoutée à l’accueil.','Application added to Home.'));document.getElementById('open').hidden=false;}
|
||||
catch(error){status(text('Installation impossible : ','Cannot install: ')+error.message);}finally{busy=false;}
|
||||
});
|
||||
const cancel=()=>{attempt++;prepared=null;busy=false;VulpesCompat.call('marketplace.cancel').catch(console.error);browse();};
|
||||
document.getElementById('cancel').onclick=cancel;
|
||||
document.getElementById('browse').onclick=cancel;
|
||||
document.getElementById('open').onclick=()=>installed && VulpesCompat.call('apps.launch',{manifestURL:installed.manifestURL});
|
||||
addEventListener('languagechange',()=>{language=navigator.language;if(!busy)browse();});
|
||||
browse();
|
||||
setInterval(async()=>{if(busy)return;try{const url=await VulpesCompat.call('marketplace.poll');if(url)await prepare(url);}catch(error){console.error(error);}},750);
|
||||
})();
|
||||
|
|
@ -143,7 +143,7 @@ TonePlayer.prototype = {
|
|||
_setExclusiveMode: function(exclusive) {
|
||||
if (exclusive) {
|
||||
if (!this._source) {
|
||||
this._context = new AudioContext('ringer');
|
||||
this._context = new AudioContext();
|
||||
this._source = this._context.createMediaElementSource(this._player);
|
||||
this._source.connect(this._context.destination);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,39 @@ define(function() {
|
|||
return;
|
||||
}
|
||||
|
||||
// A hidden or newly inserted panel may not animate. Complete navigation
|
||||
// in that case too, otherwise its onShow hook never runs.
|
||||
var completed = false;
|
||||
var fallback;
|
||||
function paintWait(event) {
|
||||
if (event && event.target !== newPanel) {
|
||||
return;
|
||||
}
|
||||
if (completed) {
|
||||
return;
|
||||
}
|
||||
completed = true;
|
||||
clearTimeout(fallback);
|
||||
newPanel.removeEventListener('transitionend', paintWait);
|
||||
newPanel.removeEventListener('transitioncancel', paintWait);
|
||||
_sendPanelReady(oldPanel && '#' + oldPanel.id, '#' + newPanel.id);
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
newPanel.addEventListener('transitionend', paintWait);
|
||||
newPanel.addEventListener('transitioncancel', paintWait);
|
||||
newPanel.className = 'current';
|
||||
var style = window.getComputedStyle(newPanel);
|
||||
var seconds = function(value) {
|
||||
return parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000) || 0;
|
||||
};
|
||||
var durations = style.transitionDuration.split(',').map(seconds);
|
||||
var delays = style.transitionDelay.split(',').map(seconds);
|
||||
var duration = Math.max.apply(Math, durations.map(function(value, index) {
|
||||
return value + delays[index % delays.length];
|
||||
}));
|
||||
fallback = setTimeout(paintWait, Math.max(0, duration) + 100);
|
||||
|
||||
/**
|
||||
* Most browsers now scroll content into view taking CSS transforms into
|
||||
|
|
@ -57,22 +89,6 @@ define(function() {
|
|||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
newPanel.addEventListener('transitionend', function paintWait() {
|
||||
newPanel.removeEventListener('transitionend', paintWait);
|
||||
if (oldPanel) {
|
||||
_sendPanelReady('#' + oldPanel.id, '#' + newPanel.id);
|
||||
|
||||
if (oldPanel.className === 'current') {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_sendPanelReady(null, '#' + newPanel.id);
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@
|
|||
* application installation, crash reporter, and low storage notices.
|
||||
* @class SystemBanner
|
||||
*/
|
||||
function SystemBanner() {}
|
||||
function SystemBanner() {
|
||||
this._generation = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Amount of time before banner is automatically dismissed.
|
||||
|
|
@ -77,10 +79,18 @@
|
|||
* { label: l10nAttrs, callback: ..., dismiss: ... }
|
||||
*/
|
||||
show: function(messages, buttonParams) {
|
||||
var generation = ++this._generation;
|
||||
clearTimeout(this._dismissTimer);
|
||||
if (this._cancelRemoval) {
|
||||
this._cancelRemoval();
|
||||
}
|
||||
this._clicked = false;
|
||||
this._showing = true;
|
||||
|
||||
return this.getBanner().then(banner => {
|
||||
if (generation !== this._generation || !this._showing) {
|
||||
return;
|
||||
}
|
||||
// Clear the button and message containers.
|
||||
var messageContainer = banner.querySelector('.messages');
|
||||
var buttonContainer = banner.querySelector('.buttons');
|
||||
|
|
@ -110,13 +120,16 @@
|
|||
|
||||
// Hide banner and invoke button dismiss() on click or timeout.
|
||||
var hideBanner = () => {
|
||||
if (generation !== this._generation || !this._showing) {
|
||||
return;
|
||||
}
|
||||
this.hide();
|
||||
if (!this._clicked) {
|
||||
buttonParams && buttonParams.dismiss && buttonParams.dismiss();
|
||||
}
|
||||
};
|
||||
banner.onclick = hideBanner;
|
||||
setTimeout(hideBanner, SystemBanner.TIMEOUT);
|
||||
this._dismissTimer = setTimeout(hideBanner, SystemBanner.TIMEOUT);
|
||||
|
||||
document.getElementById('screen').appendChild(banner);
|
||||
banner.show();
|
||||
|
|
@ -131,15 +144,37 @@
|
|||
return;
|
||||
}
|
||||
this._showing = false;
|
||||
clearTimeout(this._dismissTimer);
|
||||
var generation = this._generation;
|
||||
|
||||
return this.getBanner().then(banner => {
|
||||
banner.addEventListener('animationend', function removeBanner() {
|
||||
banner.removeEventListener('animationend', removeBanner);
|
||||
// Only remove banner if we haven't called show() in the meantime.
|
||||
if (!this._showing) {
|
||||
if (generation !== this._generation || this._showing) {
|
||||
return;
|
||||
}
|
||||
// Animation events inside a modern shadow root do not reach the host.
|
||||
var animation = banner.shadowRoot &&
|
||||
banner.shadowRoot.querySelector('.bread') || banner;
|
||||
var cleanup = () => {
|
||||
clearTimeout(this._removeTimer);
|
||||
animation.removeEventListener('animationend', removeBanner);
|
||||
animation.removeEventListener('animationcancel', removeBanner);
|
||||
this._cancelRemoval = null;
|
||||
};
|
||||
var removeBanner = event => {
|
||||
if (event && event.target !== animation) {
|
||||
return;
|
||||
}
|
||||
cleanup();
|
||||
if (!this._showing && generation === this._generation) {
|
||||
banner.parentNode && banner.parentNode.removeChild(banner);
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
this._cancelRemoval = cleanup;
|
||||
animation.addEventListener('animationend', removeBanner);
|
||||
animation.addEventListener('animationcancel', removeBanner);
|
||||
// Gaia's exit animation lasts 600 ms. Also dismiss if it is disabled
|
||||
// or no animation event is produced.
|
||||
this._removeTimer = setTimeout(removeBanner, 700);
|
||||
banner.hide();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue