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.
126 lines
5.7 KiB
JavaScript
126 lines
5.7 KiB
JavaScript
// 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});
|
|
})();
|