Add permission-based reading of Android contacts, SMS and call history. Include shared Gaia fixes and document device validation limits. Pixel image qualification remains pending.
549 lines
22 KiB
JavaScript
549 lines
22 KiB
JavaScript
import {AndroidPersonalData, authorizePersonal} from './personal-data.mjs';
|
|
import {Input} from './services/input.mjs';
|
|
import { Packages, packageURL } from './services/packages.mjs';
|
|
import { Activities, isGaiaActivity } from './services/activities.mjs';
|
|
import { authorizePlatform } from './services/platform.mjs';
|
|
import { SettingsService } from './services/settings.mjs';
|
|
import { DataStoreService } from './services/datastores.mjs';
|
|
import { DataStoresIndexedDB } from './adapters/datastores-indexeddb.mjs';
|
|
import { Contacts } from './Contacts.mjs';
|
|
import { Messages } from './Messages.mjs';
|
|
import { Connections } from './Connections.mjs';
|
|
import { Alarms } from './Alarms.mjs';
|
|
const peers = new Set();
|
|
let activities, packages;
|
|
const input = new Input({identity:peer=>peer.app,broadcast});
|
|
let editSequence=0;
|
|
const nativePackages=(operation,data={})=>browser.runtime.sendNativeMessage('vulpes',{type:'packages.'+operation,...data}).then(JSON.parse);
|
|
async function refreshInstalled(){const installed=await nativePackages('list');apps.splice(0,apps.length,...apps.filter(a=>!a.installed),...installed);}
|
|
let apps, values, settings, dataStores, contacts, messages, connections, alarms, storage, personal;
|
|
const identity = (peer) => peer.app;
|
|
function notify(peer, type, data) {
|
|
try {
|
|
peer.port.postMessage({ event: { type, data } });
|
|
} catch (_) {
|
|
peers.delete(peer);
|
|
}
|
|
}
|
|
function grants(app) {
|
|
const access = app.manifest.permissions?.settings?.access || '';
|
|
return [
|
|
access.includes('read') && 'settings.read',
|
|
access.includes('write') && 'settings.write',
|
|
].filter(Boolean);
|
|
}
|
|
function broadcast(type, data, target) {
|
|
for (const peer of peers) {
|
|
if (target && peer.app.id !== target) continue;
|
|
if (type === 'settingchange' && !['language.current', 'vulpes.theme'].includes(data.key) && !grants(peer.app).includes('settings.read')) continue;
|
|
if (
|
|
type === 'contactchange' &&
|
|
!peer.app.manifest.permissions?.contacts?.access?.includes('read')
|
|
)
|
|
continue;
|
|
if (
|
|
type === 'datastorechange' &&
|
|
!dataStores.list(peer.app, data.name).some((s) => s.owner === data.owner)
|
|
)
|
|
continue;
|
|
if (
|
|
type === 'mediachange' &&
|
|
!peer.app.manifest.permissions?.['device-storage:' + data.type]?.access?.includes('read')
|
|
)
|
|
continue;
|
|
notify(peer, type, data);
|
|
}
|
|
}
|
|
function launch(target, entryPoint, path, reload = false) {
|
|
path ||=
|
|
target.manifest.entry_points?.[entryPoint]?.launch_path ||
|
|
target.manifest.launch_path ||
|
|
'/index.html';
|
|
const url = new URL(path, target.origin);
|
|
if (url.origin !== target.origin) throw Error('INVALID_URL');
|
|
broadcast(
|
|
reload ? 'activity-launch' : 'launch',
|
|
{
|
|
manifestURL: target.manifestURL,
|
|
origin: target.origin,
|
|
url: url.href,
|
|
entryPoint,
|
|
timestamp: Date.now(),
|
|
},
|
|
'system',
|
|
);
|
|
}
|
|
const ready = (async () => {
|
|
apps = await (await fetch('registry.json')).json();
|
|
await refreshInstalled();
|
|
packages=new Packages({save:item=>nativePackages('save',{item}),changed:refreshInstalled});
|
|
const defaults = await (await fetch('defaults.json')).json();
|
|
const saved = await browser.storage.local.get('settings');
|
|
values = { ...defaults, ...saved.settings };
|
|
if (!values['vulpes.gaia-restoration.v1']) {
|
|
values['lockscreen.enabled'] = true;
|
|
values['vulpes.gaia-restoration.v1'] = true;
|
|
await browser.storage.local.set({settings:values});
|
|
}
|
|
// Preview 1 serialized wallpaper Blobs as {}. Repair that one invalid value
|
|
// on upgrade, retaining all other user settings and valid custom wallpapers.
|
|
const wallpaper = values['wallpaper.image'];
|
|
if (wallpaper && typeof wallpaper === 'object' && !Object.keys(wallpaper).length) {
|
|
values['wallpaper.image'] = defaults['wallpaper.image'];
|
|
values['wallpaper.image.valid'] = false;
|
|
await browser.storage.local.set({ settings: values });
|
|
}
|
|
activities = new Activities({identity,apps:()=>apps,notify,uuid:()=>crypto.randomUUID(),
|
|
choose:data=>broadcast('activity-choice',data,'system'),
|
|
launch:(target,path,{reload=false,entryPoint}={})=>launch(target,entryPoint,path,reload)});
|
|
let writes = Promise.resolve();
|
|
settings = new SettingsService(
|
|
{
|
|
get: async (key) => values[key],
|
|
setMany(entries) {
|
|
const write = writes.then(async () => {
|
|
const next = { ...values, ...Object.fromEntries(entries) };
|
|
await browser.storage.local.set({ settings: next });
|
|
values = next;
|
|
for (const [key, value] of entries) {
|
|
broadcast('settingchange', { key, value });
|
|
if(key==='keyboard.gaia.enabled') {
|
|
input.hide();broadcast('input-mode',{enabled:value===true});
|
|
}
|
|
}
|
|
});
|
|
writes = write.catch(() => {});
|
|
return write;
|
|
},
|
|
},
|
|
{ maxBytes: 32 * 1024 * 1024 },
|
|
); // Allows the bounded base64 image envelope on high-DPI Android screens.
|
|
storage = new DataStoresIndexedDB(indexedDB, IDBKeyRange, () => crypto.randomUUID());
|
|
contacts = new Contacts(storage, (data) => broadcast('contactchange', data));
|
|
await contacts.init();
|
|
messages = new Messages(storage);
|
|
personal = new AndroidPersonalData(params=>android("personal",params), contacts, messages, storage);
|
|
dataStores = new DataStoreService(apps, storage, (data) => broadcast('datastorechange', data));
|
|
connections = new Connections(identity, notify);
|
|
alarms = new Alarms(
|
|
identity,
|
|
(id) => {
|
|
const app = apps.find((a) => a.id === id);
|
|
if (app) launch(app, undefined, app.manifest.messages?.find((m) => m.alarm)?.alarm);
|
|
},
|
|
{
|
|
load: async () => (await browser.storage.local.get('alarms')).alarms,
|
|
save: (state) => browser.storage.local.set({ alarms: state }),
|
|
},
|
|
(callback, delay) => setInterval(callback, delay),
|
|
);
|
|
await alarms.init();
|
|
console.info('VULPES Android services ready');
|
|
})();
|
|
// Java owns this port. Web pages have no access to native messaging.
|
|
let native;
|
|
let activeWebView;
|
|
let systemReady = false;
|
|
let pendingScreenLock = false;
|
|
function deliverScreenLock() {
|
|
if (!systemReady || !pendingScreenLock) return;
|
|
pendingScreenLock = false;
|
|
broadcast('screen-off', {}, 'system');
|
|
}
|
|
let pendingNotification;
|
|
function deliverNotification() {
|
|
if (!systemReady || !pendingNotification) return;
|
|
const data = pendingNotification;
|
|
pendingNotification = null;
|
|
const target = apps.find((a) => a.id === data.owner);
|
|
if (target) {
|
|
launch(target);
|
|
broadcast('notification-click', data, target.id);
|
|
}
|
|
}
|
|
function connectNative() {
|
|
native = browser.runtime.connectNative('vulpes');
|
|
native.onDisconnect.addListener(() => setTimeout(connectNative, 1000));
|
|
native.onMessage.addListener(async (message) => {
|
|
await ready;
|
|
if (message.type === 'browser-closed' && activeWebView) {
|
|
const {peer, contextId} = activeWebView;
|
|
activeWebView = null;
|
|
notify(peer, 'view', {contextId, type:'close', detail:{}});
|
|
}
|
|
if (['home', 'holdhome'].includes(message.type)) broadcast(message.type, {}, 'system');
|
|
if (message.type === 'android-screen-off') {
|
|
pendingScreenLock = true;
|
|
deliverScreenLock();
|
|
}
|
|
if (message.type === 'android-data-changed') {
|
|
broadcast('android-data-changed', {kind:message.kind}, message.kind === 'sms' ? 'sms' : 'communications');
|
|
}
|
|
if (message.type === 'android-resume') {
|
|
deliverScreenLock();
|
|
broadcast('screen-on', {}, 'system');
|
|
await refreshAndroid().catch(console.warn);
|
|
broadcast('android-data-changed', {kind:'contacts'}, 'communications');
|
|
broadcast('android-data-changed', {kind:'calls'}, 'communications');
|
|
broadcast('android-data-changed', {kind:'sms'}, 'sms');
|
|
}
|
|
if (message.type === 'notification-click') {
|
|
pendingNotification = message.data;
|
|
deliverNotification();
|
|
}
|
|
});
|
|
}
|
|
connectNative();
|
|
async function android(type, params = {}) {
|
|
const reply = await browser.runtime.sendNativeMessage('vulpes', {
|
|
type: 'android.' + type,
|
|
params,
|
|
});
|
|
const result = typeof reply === 'string' ? JSON.parse(reply).result : reply;
|
|
if (result?.error) throw Error(result.error);
|
|
return result;
|
|
}
|
|
async function refreshAndroid() {
|
|
const snapshot = await android('snapshot');
|
|
for (const [key, value] of Object.entries(snapshot)) {
|
|
if (values[key] === value) continue;
|
|
values[key] = value;
|
|
broadcast('settingchange', { key, value });
|
|
}
|
|
return snapshot;
|
|
}
|
|
ready.then(refreshAndroid).catch(console.warn);
|
|
async function handle(peer, request) {
|
|
const app = peer.app;
|
|
if (
|
|
!request ||
|
|
request.version !== 1 ||
|
|
!Number.isSafeInteger(request.id) ||
|
|
request.id < 0 ||
|
|
typeof request.method !== 'string'
|
|
)
|
|
throw Error('INVALID_REQUEST');
|
|
const { params = {} } = request;
|
|
let method = request.method;
|
|
if (method.startsWith('platform.')) {
|
|
authorizePlatform(app, method.slice(9), params);
|
|
method = 'android.' + method.slice(9);
|
|
}
|
|
if (method === 'android.personal') {
|
|
authorizePersonal(app, params.kind, params.operation);
|
|
if (params.operation === 'page') return personal.rows(params.kind);
|
|
if (!['status','permission','disconnect','open'].includes(params.operation)) throw Error('METHOD_NOT_SUPPORTED');
|
|
return android('personal', params);
|
|
}
|
|
if (method === 'android.capabilities') {
|
|
const release = await (await fetch('release.json')).json();
|
|
const device = app.id === 'settings' ? await android('snapshot') : {};
|
|
return {
|
|
platform: 'android',
|
|
platformName: device['deviceinfo.firmware_revision'] || 'Android',
|
|
model: device['deviceinfo.product_model'],
|
|
adapter: { version: release.adapters?.android, device: 'android' },
|
|
version: release.version,
|
|
gaia: release.gaia,
|
|
gecko: release.androidEngine,
|
|
camera: true,
|
|
media: true,
|
|
contacts: true,
|
|
notifications: true,
|
|
dial: true,
|
|
sms: true,
|
|
alarms: true,
|
|
};
|
|
}
|
|
if (method === 'android.systemReady') {
|
|
if (app.id !== 'system') throw Error('PERMISSION_DENIED');
|
|
systemReady = true;
|
|
deliverScreenLock();
|
|
deliverNotification();
|
|
return null;
|
|
}
|
|
if (method === 'android.notification') {
|
|
if (!app.manifest.permissions?.['desktop-notification']) throw Error('PERMISSION_DENIED');
|
|
if (JSON.stringify(params).length > 65536) throw Error('QuotaExceededError');
|
|
return android('notification', { ...params, owner: app.id });
|
|
}
|
|
if (method === 'android.settings' || method === 'android.snapshot') {
|
|
if (!['settings', 'system', 'costcontrol'].includes(app.id)) throw Error('PERMISSION_DENIED');
|
|
return method === 'android.snapshot' ? refreshAndroid() : android('settings', params);
|
|
}
|
|
if (method === 'android.capture') {
|
|
if (app.id !== 'camera') throw Error('PERMISSION_DENIED');
|
|
const names = await android('capture');
|
|
for (const path of names)
|
|
broadcast('mediachange', { type: 'pictures', path, reason: 'modified' });
|
|
return names;
|
|
}
|
|
if (method === 'android.pick') {
|
|
const access = app.manifest.permissions?.['device-storage:' + params.type]?.access || '';
|
|
if (!access.includes('write')) throw Error('PERMISSION_DENIED');
|
|
const names = await android('pick', params);
|
|
for (const path of names)
|
|
broadcast('mediachange', { type: params.type, path, reason: 'modified' });
|
|
return names;
|
|
}
|
|
if (method === 'android.alarms') {
|
|
if (app.id !== 'clock') throw Error('PERMISSION_DENIED');
|
|
return android('alarms');
|
|
}
|
|
if (method === 'android.contact') {
|
|
if (!app.manifest.permissions?.contacts?.access?.includes('write'))
|
|
throw Error('PERMISSION_DENIED');
|
|
const contact = await android('contact');
|
|
return contacts.request(app, { operation: 'save', contact });
|
|
}
|
|
if (method === 'android.sms') {
|
|
if (app.id !== 'sms') throw Error('PERMISSION_DENIED');
|
|
return android('sms', params);
|
|
}
|
|
if (method === 'android.dial') {
|
|
if (app.id !== 'communications') throw Error('PERMISSION_DENIED');
|
|
return android('dial', params);
|
|
}
|
|
if (method === 'settings.set' && params.values && typeof params.values === 'object') {
|
|
const validation = await new SettingsService(
|
|
{ setMany: async () => {} },
|
|
{ maxBytes: 32 * 1024 * 1024 },
|
|
).bindSession(
|
|
app.id,
|
|
grants(app),
|
|
)(request);
|
|
if (validation.error) throw Error(validation.error.code);
|
|
for (const [key, value] of Object.entries(params.values)) {
|
|
if (app.id === 'settings' && ['screen.brightness', 'audio.volume.content'].includes(key))
|
|
await android('set', { key, value });
|
|
const panel = {
|
|
'wifi.enabled': 'wifi',
|
|
'bluetooth.enabled': 'bluetooth',
|
|
'airplaneMode.enabled': 'airplane-mode',
|
|
'geolocation.enabled': 'geolocation',
|
|
'ril.data.enabled': 'data',
|
|
}[key];
|
|
if (panel) {
|
|
// UI clicks open the native panel. Gaia startup writes must not open Android activities.
|
|
// Android controls these values; never persist a fictitious toggle state.
|
|
delete params.values[key];
|
|
}
|
|
}
|
|
if (!Object.keys(params.values).length) return null;
|
|
}
|
|
if (method === 'input.config') return values['keyboard.gaia.enabled'] === true;
|
|
if (method === 'input.request') return input.request(peer,params);
|
|
if (method === 'appearance.current') return values['vulpes.theme'] || 'light';
|
|
if (method === 'locale.current') return values['language.current'] || 'en-US';
|
|
if (method === 'settings.snapshot') {
|
|
if (!grants(app).includes('settings.read')) throw Error('PERMISSION_DENIED');
|
|
return { ...values };
|
|
}
|
|
if (method.startsWith('settings.')) {
|
|
const reply = await settings.bindSession(app.id, grants(app))(request);
|
|
if (reply.error) throw Error(reply.error.code);
|
|
return reply.result;
|
|
}
|
|
if(method.startsWith('marketplace.')) {
|
|
if(app.id!=='marketplace')throw Error('PERMISSION_DENIED');
|
|
if(method==='marketplace.poll'){const url=marketplaceDownload;marketplaceDownload=null;return url;}
|
|
if(method==='marketplace.cancel'){packages.cancel();return true;}
|
|
if(method==='marketplace.prepare')return packages.prepare(params.url);
|
|
if(method==='marketplace.install'){const id=await packages.install(params.token);const installed=apps.find(a=>a.id===id);broadcast('apps-installed',installed);return installed;}
|
|
throw Error('METHOD_NOT_SUPPORTED');
|
|
}
|
|
if (method === 'apps.self') {
|
|
console.info('VULPES app ready', app.id);
|
|
return app;
|
|
}
|
|
const manage =
|
|
app.manifest.permissions?.['webapps-manage'] ||
|
|
app.manifest.permissions?.['homescreen-webapps-manage'];
|
|
if (method === 'apps.list') {
|
|
if (!manage) throw Error('PERMISSION_DENIED');
|
|
return apps;
|
|
}
|
|
if(method==='apps.uninstall') {
|
|
const installed=apps.find(a=>a.id===params.id && a.installed);if(!manage || !installed)throw Error('PERMISSION_DENIED');
|
|
await nativePackages('remove',{id:params.id});await refreshInstalled();broadcast('apps-uninstalled',installed);return true;
|
|
}
|
|
if (method === 'apps.launch') {
|
|
const target = apps.find((a) => a.manifestURL === params.manifestURL);
|
|
if (!target || (!manage && app.id !== target.id)) throw Error('PERMISSION_DENIED');
|
|
launch(target, params.entryPoint);
|
|
return null;
|
|
}
|
|
if (method === 'datastores.list') return dataStores.list(app, params.name);
|
|
if (method === 'datastores.run') return dataStores.run(app, params);
|
|
if (method === 'contacts.request') return personal.contactRequest(app, params);
|
|
if (method === 'messages.request') return personal.messageRequest(app, params);
|
|
if (method === 'alarms.request') return alarms.request(peer, app, params);
|
|
if (method === 'iac.subscribe') {
|
|
connections.subscribe(peer);
|
|
return null;
|
|
}
|
|
if (method === 'iac.connect') return connections.connect(peer, params.keyword, apps);
|
|
if (method === 'iac.send') return connections.send(peer, params);
|
|
if (method === 'iac.close') {
|
|
connections.close(peer, params.id);
|
|
return null;
|
|
}
|
|
if (method === 'activities.cancel') return activities.cancel(peer);
|
|
if (method === 'activities.select') return activities.select(peer,params);
|
|
if (method === 'activities.subscribe') return activities.subscribe(peer);
|
|
if (method === 'activities.finish') return activities.finish(peer,params);
|
|
if (method === 'activities.start' && isGaiaActivity(params)) return activities.start(peer,params);
|
|
if (method === 'activities.start') {
|
|
if (params.name === 'record' && params.data?.type === 'photos') {
|
|
if (['gallery', 'system'].includes(app.id)) {
|
|
const camera = apps.find((a) => a.id === 'camera');
|
|
if (!camera) throw Error('ACTIVITY_NOT_SUPPORTED');
|
|
launch(camera);
|
|
return null;
|
|
}
|
|
if (!app.manifest.permissions?.['device-storage:pictures']?.access?.includes('write'))
|
|
throw Error('PERMISSION_DENIED');
|
|
const names = await android('capture');
|
|
for (const path of names)
|
|
broadcast('mediachange', { type: 'pictures', path, reason: 'modified' });
|
|
return { filenames: names };
|
|
}
|
|
if (params.name !== 'view' || params.data?.type !== 'url')
|
|
throw Error('ACTIVITY_NOT_SUPPORTED');
|
|
return openWeb(params.data.url);
|
|
}
|
|
if (method === 'views.request') {
|
|
if (!app.manifest.permissions?.browser && !app.manifest.permissions?.['embed-apps'])
|
|
throw Error('PERMISSION_DENIED');
|
|
if (params.operation === 'open') {
|
|
await openWeb(params.url);
|
|
activeWebView = {peer, contextId:params.contextId};
|
|
notify(peer, 'view', {
|
|
contextId: params.contextId,
|
|
type: 'loadend',
|
|
detail: { backgroundColor: 'white' },
|
|
});
|
|
return null;
|
|
}
|
|
if (['geometry', 'cancelTouch', 'close'].includes(params.operation)) return null;
|
|
if (['canGoBack', 'canGoForward'].includes(params.operation)) return false;
|
|
throw Error('NotSupportedError');
|
|
}
|
|
if (method === 'desktop.idle') {
|
|
if (!app.manifest.permissions?.idle) throw Error('PERMISSION_DENIED');
|
|
return null;
|
|
}
|
|
if (method === 'desktop.homeScroll') {
|
|
if (app.id !== 'homescreen' || typeof params?.bottom !== 'boolean') throw Error('PERMISSION_DENIED');
|
|
return browser.runtime.sendNativeMessage('vulpes', { type: 'homeScroll', bottom: params.bottom });
|
|
}
|
|
if (method === 'desktop.navigation') {
|
|
if (app.id !== 'system' || typeof params?.home !== 'boolean') throw Error('PERMISSION_DENIED');
|
|
return browser.runtime.sendNativeMessage('vulpes', { type: 'navigation', home: params.home });
|
|
}
|
|
if (method === 'desktop.screen') {
|
|
if (!app.manifest.permissions?.power) throw Error('PERMISSION_DENIED');
|
|
return null;
|
|
}
|
|
if (method === 'desktop.wakelock') return null; // Android Activity owns the visible screen.
|
|
if (method === 'storage.stats') {if(app.id!=='settings')throw Error('PERMISSION_DENIED');return android('storage',{});}
|
|
if (method === 'media.request') return media(app, params);
|
|
throw Error('METHOD_NOT_SUPPORTED');
|
|
}
|
|
async function openWeb(url) {
|
|
const parsed = new URL(url);
|
|
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.hostname.endsWith('.localhost'))
|
|
throw Error('URL_SCHEME_DENIED');
|
|
return browser.runtime.sendNativeMessage('vulpes', { type: 'open', url: parsed.href });
|
|
}
|
|
async function media(app, p) {
|
|
const access = app.manifest.permissions?.['device-storage:' + p.type]?.access || '';
|
|
if (
|
|
!['pictures', 'videos', 'music'].includes(p.type) ||
|
|
!access.includes(['add', 'delete'].includes(p.operation) ? 'write' : 'read')
|
|
)
|
|
throw Error('PERMISSION_DENIED');
|
|
const result = await android('media', p);
|
|
if (['add', 'delete'].includes(p.operation))
|
|
broadcast('mediachange', {
|
|
type: p.type,
|
|
path: result,
|
|
reason: p.operation === 'add' ? 'modified' : 'deleted',
|
|
});
|
|
return result;
|
|
}
|
|
browser.runtime.onConnect.addListener((port) => {
|
|
if (port.name !== 'gaia') return;
|
|
let peer;
|
|
const attached = ready.then(() => {
|
|
const url = new URL(port.sender.url);
|
|
const app = apps.find((a) => a.origin === url.origin);
|
|
if (!app) throw Error('PERMISSION_DENIED');
|
|
peer = {
|
|
port,
|
|
app,
|
|
manager: { documentURI: { spec: url.href } },
|
|
sendAsyncMessage: (_, event) => notify(peer, event.type, event.data),
|
|
edits: new Map(),
|
|
sendQuery(_, params) {
|
|
const id=++editSequence;
|
|
return new Promise((resolve,reject)=>{
|
|
const timer=setTimeout(()=>{peer.edits.delete(id);reject(Error('INPUT_CONTEXT_EXPIRED'));},5000);
|
|
peer.edits.set(id,{resolve,reject,timer});
|
|
port.postMessage({edit:{id,params}});
|
|
});
|
|
},
|
|
};
|
|
peers.add(peer);
|
|
});
|
|
port.onMessage.addListener(async (message) => {
|
|
let reply;
|
|
try {
|
|
await attached;
|
|
if(Object.hasOwn(message,'inputFocus')) {
|
|
if(values['keyboard.gaia.enabled']===true) input.changed(peer,message.inputFocus);
|
|
return;
|
|
}
|
|
if(Object.hasOwn(message,'editReply')) {
|
|
const edit=peer.edits.get(message.editReply);
|
|
if(edit) {clearTimeout(edit.timer);peer.edits.delete(message.editReply);
|
|
if(message.error)edit.reject(Error(message.error));else edit.resolve(message.result);}
|
|
return;
|
|
}
|
|
reply = { result: await handle(peer, message.request) };
|
|
} catch (e) {
|
|
reply = { error: { code: e.message || 'HOST_ERROR' } };
|
|
console.warn('VULPES RPC', message.request?.method, e.message);
|
|
}
|
|
try {
|
|
port.postMessage({ id: message.id, reply });
|
|
} catch (_) {}
|
|
});
|
|
port.onDisconnect.addListener(() => {
|
|
attached
|
|
.then(() => {
|
|
peers.delete(peer);
|
|
if (activeWebView?.peer === peer) activeWebView = null;
|
|
input.cleanup(peer);
|
|
for(const edit of peer.edits.values()) {clearTimeout(edit.timer);edit.reject(Error('INPUT_CONTEXT_EXPIRED'));}
|
|
peer.edits.clear();
|
|
if (peer.app.id === 'system' && ![...peers].some((p) => p.app.id === 'system'))
|
|
systemReady = false;
|
|
activities.cleanup(peer);
|
|
connections.cleanup(peer);
|
|
alarms.cleanup(peer);
|
|
})
|
|
.catch(() => {});
|
|
});
|
|
});
|
|
|
|
let marketplaceDownload=null;
|
|
browser.runtime.onMessage.addListener(async (message,sender)=>{
|
|
if(!message.marketplaceDownload)return;
|
|
const origin=new URL(sender.url);
|
|
if(origin.origin!=='https://vulpes-os.org' || !origin.pathname.startsWith('/marketplace/'))throw Error('PERMISSION_DENIED');
|
|
await ready;marketplaceDownload=packageURL(message.marketplaceDownload);
|
|
const app=apps.find(a=>a.id==='marketplace');launch(app);
|
|
await browser.runtime.sendNativeMessage('vulpes',{type:'marketplace.return'});
|
|
return true;
|
|
});
|