Release Preview 20 shared desktop, Android and Sargo updates

Restore Gaia settings and media workflows, extend native network adapters, and align the APK payload. Generate French/English changelogs and website entries from the simplified JSON source.

Validation: shared JS and Tundra unit tests; desktop workflows and persistence; signed APK payload parity; Sargo RAM and installed startup with boot readback. Android runtime qualification is recorded separately.
This commit is contained in:
OverSu 2026-10-13 00:19:45 +02:00
commit 1311bed5e9
170 changed files with 19698 additions and 2495 deletions

126
host/gaia-bluetooth.js Normal file
View file

@ -0,0 +1,126 @@
// Gaia Bluetooth v2, backed by the host's permission-checked BlueZ broker.
(() => {
const app = location.hostname.split('.')[0];
if (!(window.VulpesPlatform === 'tundra' || window.VulpesTestBluetooth) ||
!['system', 'settings', 'bluetooth'].includes(app)) return;
const {call} = VulpesCompat;
const manager = Object.assign(new EventTarget(), {defaultAdapter:null});
const adapter = Object.assign(new EventTarget(), {
state:'disabled', address:'', name:'', discoverable:false, discovering:false,
pairingReqs:new EventTarget(),
});
const devices = new Map();
let discovery = null, seen = new Set(), pending = false, timer, closed = false;
let currentPairing = null, initialized = false;
const emit = (target, type, values={}) => {
const event = Object.assign(new Event(type), values);
target.dispatchEvent(event); target['on'+type]?.(event);
};
function change(target, fields) {
const attrs = Object.keys(fields).filter(k=>JSON.stringify(target[k])!==JSON.stringify(fields[k]));
Object.assign(target, fields);
if (attrs.length) emit(target, 'attributechanged', {attrs});
}
function apply(state) {
if (!state?.available) return;
for (const row of state.devices) {
let device = devices.get(row.address);
const oldPaired = device?.paired;
if (!device) {
device = new EventTarget(); devices.set(row.address, device);
}
change(device, {...row, connectionStatus:row.connected?'connected':'disconnected'});
if (oldPaired !== undefined && oldPaired !== row.paired)
emit(adapter, row.paired?'devicepaired':'deviceunpaired', {device,address:row.address});
if (discovery && state.discovering && !seen.has(row.address)) {
seen.add(row.address); emit(discovery, 'devicefound', {device});
}
}
for (const [address, device] of devices) {
if (!state.devices.some(d=>d.address===address)) {
if (device.paired) emit(adapter,'deviceunpaired',{address,device});
devices.delete(address);
}
}
change(adapter, {state:state.enabled?'enabled':'disabled',address:state.address,
name:state.name,discoverable:state.discoverable,discovering:state.discovering});
if (!manager.defaultAdapter) {
manager.defaultAdapter=adapter; emit(manager,'attributechanged',{attrs:['defaultAdapter']});
}
currentPairing = state.pairing?.[0] || null;
if (app==='bluetooth' && location.pathname==='/onpair.html') showPairing();
}
async function command(operation, params={}) {
const value=await call('platform.bluetooth',{operation,...params});
if (value?.available) apply(value);
return value;
}
async function refresh() {
if (pending || closed) return;
pending=true;
try {apply(await command('status'));}
catch(error) {console.warn('Bluetooth:',error.message);}
finally {pending=false;}
}
adapter.enable=()=>command('power',{enabled:true});
adapter.disable=()=>command('power',{enabled:false});
adapter.setName=name=>command('rename',{name});
adapter.setDiscoverable=enabled=>command('discoverable',{enabled});
adapter.startDiscovery=async()=>{
await command('scan');
discovery=new EventTarget(); seen=new Set();
const handle=discovery;
// Gaia installs its discovery listeners after this promise resolves.
setTimeout(()=>refresh(),0);
return handle;
};
adapter.stopDiscovery=async()=>{await command('stopScan');discovery=null;};
adapter.getPairedDevices=()=>[...devices.values()].filter(d=>d.paired);
adapter.getConnectedDevices=async profile=>{
await refresh();
const uuid=Number(profile).toString(16).padStart(8,'0')+'-0000-1000-8000-00805f9b34fb';
return [...devices.values()].filter(d=>d.connected && d.uuids.includes(uuid));
};
for (const op of ['pair','unpair','connect','disconnect'])
adapter[op]=async value=>{
await command(op,{address:typeof value==='string'?value:value.address});
await refresh();
};
const unsupported=()=>Promise.reject(new DOMException('Bluetooth profile is unavailable','NotSupportedError'));
for (const name of ['sendFile','stopSendingFile','confirmReceivingFile','connectSco','disconnectSco',
'sendMediaMetaData','sendMediaPlayStatus']) adapter[name]=unsupported;
adapter.isScoConnected=()=>Promise.resolve(false);
manager.getAdapters=()=>Promise.resolve(manager.defaultAdapter?[adapter]:[]);
Object.defineProperty(navigator,'mozBluetooth',{value:manager});
let shown = null;
function showPairing() {
if (!initialized || !window.Pairview) return;
const item=currentPairing;
if (!item) {if(shown) window.close();return;}
if (shown===item.id) return;
shown=item.id;
const reply=(accept,code)=>command('reply',{id:item.id,accept,code});
Pairview.init(item.method==='enterpasskey'?'enterpincode':item.method,{
deviceName:item.deviceName,handle:{passkey:item.passkey,
accept:()=>reply(true),reject:()=>reply(false),setPinCode:code=>reply(true,code)},
});
}
addEventListener('DOMContentLoaded',async()=>{
initialized=true;
await refresh(); showPairing();
if(app==='system') {
navigator.mozSettings.addObserver('bluetooth.enabled',event=>{
const enabled=event.settingValue===true;
if((adapter.state==='enabled')!==enabled)
command('power',{enabled}).catch(()=>call('settings.set',{values:{'bluetooth.enabled':adapter.state==='enabled'}}));
});
await call('settings.set',{values:{'bluetooth.enabled':adapter.state==='enabled'}});
}
timer=setInterval(()=>{if(app==='system'||!document.hidden||discovery)refresh();},1500);
},{once:true});
addEventListener('pagehide',()=>{
closed=true;clearInterval(timer);
if(discovery)command('stopScan').catch(()=>{});
},{once:true});
})();