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:
parent
be2cdce823
commit
1311bed5e9
170 changed files with 19698 additions and 2495 deletions
126
host/gaia-bluetooth.js
Normal file
126
host/gaia-bluetooth.js
Normal 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});
|
||||
})();
|
||||
Loading…
Reference in a new issue