Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200d1fb235 | ||
|
|
1c20a0b6d5 | ||
|
|
7c86dbcd33 | ||
|
|
1311bed5e9 | ||
| be2cdce823 | |||
| 631de97a09 | |||
| e14e2b00e1 | |||
| 7839ff24c5 | |||
| be551ac49f | |||
| aeea5ab595 | |||
| 402e5daec8 | |||
|
|
0041cd9660 | ||
|
|
d04824ec89 | ||
|
|
978542bc87 | ||
|
|
d5cfc8a7d7 | ||
|
|
bdbb2e13f3 | ||
|
|
9b606ed5f3 | ||
|
|
a15c2915dc | ||
|
|
18338e44c7 |
283 changed files with 28769 additions and 1505 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -19,3 +19,6 @@ __pycache__/
|
|||
|
||||
# Old B2G registry is unused by the modern host and contains historical paths.
|
||||
/assets/webapps/webapps.json
|
||||
|
||||
# Local issue tracking and roadmap; not included in publication.
|
||||
/tickets/
|
||||
|
|
|
|||
904
CHANGELOG.en.md
Normal file
904
CHANGELOG.en.md
Normal file
|
|
@ -0,0 +1,904 @@
|
|||
# Changelog — Vulpes OS
|
||||
|
||||
[Français](CHANGELOG.md)
|
||||
|
||||
Updates by date and platform. Within each group: Add, Change, Fix, Note, Test. Entries describe the state at their date; a fix in development requires an updated installed version.
|
||||
|
||||
<a id="2.7-preview.21"></a>
|
||||
|
||||
## 14/10/2026 : 2.7-preview.21
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Unavailable-feature messages are easier to read in light and dark themes.
|
||||
-   : Contact names now appear when the call history first loads.
|
||||
-   : Additional dictionaries available offline; improved Gaia keyboard activation and recovery. Verification on affected devices is still needed.
|
||||
-   : Storage panels restored. A loading error no longer locks the other menus.
|
||||
|
||||
### 
|
||||
|
||||
-   : Read Android contacts, SMS and call history in Gaia after granting access. Existing local Vulpes data is preserved.
|
||||
-   : Return to Vulpes without a persistent white overlay; a closed catalogue can be reopened.
|
||||
-   : The reported blank keyboard still needs reproduction on the affected devices.
|
||||
-   : First, read-only integration, excluding MMS. Sending and editing Android records use Android apps; new Gaia contacts stay local. SMS and call log access depend on ROM and installer permissions. Tested on Android 9; other versions need verification.
|
||||
|
||||
### 
|
||||
|
||||
-   : Shared Gaia fixes are included in full installation images and boot updates.
|
||||
-   : New installation image built; Pixel boot and flashing must be tested before publication.
|
||||
|
||||
<a id="2.7-preview.20"></a>
|
||||
|
||||
## 13/10/2026 : 2.7-preview.20
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Video mode, with thumbnails and playback in Gallery and Video.
|
||||
-   : Improvements to settings, appearance, pickers, keyboard, contacts and media.
|
||||
-   : More reliable camera switching, shutdown and resume.
|
||||
-   : Mozilla Account, Sync, remote location, extensions and USB transfer remain incomplete.
|
||||
|
||||
### 
|
||||
|
||||
-   : Preview 20 APK available.
|
||||
-   : Calls and SMS use Android. This Preview has not yet been validated in use on Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Preview runs at around 16 frames per second.
|
||||
-   : Updates to Wi-Fi, Bluetooth, calls, SMS/MMS and network settings.
|
||||
-   : Video sound and two-way call audio still need separate testing.
|
||||
-   : Preview 20 installed on Pixel 3a with data preserved. Wi-Fi connected and 4G registration observed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Simpler French and English history, with badges and platform filters. Shared content for the website and releases.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20)
|
||||
|
||||
<a id="development-2026-10-12-video-clock"></a>
|
||||
|
||||
## 12/10/2026 : Video and time
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Video recording with sound, thumbnails and playback in Gallery and Video.
|
||||
-   : “Improve Vulpes” opens the project’s issues.
|
||||
-   : Date and time pickers restored; Keyboard back navigation and list text fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Recording checked with a simulated camera.
|
||||
|
||||
### 
|
||||
|
||||
-   : Changes prepared for a future APK; video not yet tested on Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Improved preview, at around 16 frames per second.
|
||||
-   : Automatic time depends on network information; time zone remains manual.
|
||||
-   : Video recording and camera switching checked. Video sound still needs listening tests.
|
||||
|
||||
<a id="development-2026-10-12-settings-calls-hotspot"></a>
|
||||
|
||||
## 12/10/2026 : Calls and hotspot
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Dialog closing and retry after an error fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Settings for call waiting, caller ID and call forwarding.
|
||||
-   : Hotspot with or without a password.
|
||||
-   : Slow settings no longer block calls and SMS.
|
||||
-   : Hotspot activation checked; Internet access from another device still needs testing.
|
||||
|
||||
<a id="development-2026-10-12-settings-network"></a>
|
||||
|
||||
## 12/10/2026 : Network settings
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : IP address and connection speed displayed correctly.
|
||||
|
||||
### 
|
||||
|
||||
-   : Network type selection and mobile access point editing.
|
||||
-   : More reliable network settings when the modem responds slowly.
|
||||
-   : Adding, deleting and resetting access points remain incomplete.
|
||||
|
||||
<a id="development-2026-10-12-settings-audit"></a>
|
||||
|
||||
## 12/10/2026 : Settings review
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Navigation, accessibility and help fixed. Incomplete panels remain visible.
|
||||
-   : Menus reviewed; some actions still depend on hardware or the carrier.
|
||||
|
||||
### 
|
||||
|
||||
-   : Storage information and downloads connected.
|
||||
|
||||
### 
|
||||
|
||||
-   : Display of storage used by Vulpes.
|
||||
|
||||
### 
|
||||
|
||||
-   : Mobile data, roaming, time, hidden Wi-Fi networks and Sound panel fixed.
|
||||
|
||||
<a id="development-2026-10-12-settings-navigation"></a>
|
||||
|
||||
## 12/10/2026 : Settings navigation
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Light/Dark selection and picker confirmation or cancellation restored.
|
||||
-   : Manager adapted to single-SIM phones.
|
||||
|
||||
### 
|
||||
|
||||
-   : SIM information, message centre and delivery reports.
|
||||
-   : Faster loading of saved networks.
|
||||
-   : Wi-Fi certificates, SIM security and USB storage remain incomplete.
|
||||
|
||||
<a id="development-2026-10-12-sargo-appearance"></a>
|
||||
|
||||
## 12/10/2026 : Appearance on Pixel 3a
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Shared light and dark modes available on Pixel 3a.
|
||||
-   : Settings fixes and Firefox OS fonts included.
|
||||
-   : No new call, SMS or Bluetooth test at this stage.
|
||||
-   : Themes and display checked on the phone.
|
||||
|
||||
<a id="development-2026-10-11-appearance-input"></a>
|
||||
|
||||
## 11/10/2026 : Appearance and keyboard
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Light, Dark and System modes, preserved after restart.
|
||||
-   : Text selection and deletion fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Themes and SMS typing checked.
|
||||
|
||||
### 
|
||||
|
||||
-   : Experimental Gaia keyboard; Android keyboard remains the default.
|
||||
-   : Retry available when the camera is busy.
|
||||
-   : Preview 19 built; themes, keyboard and front camera still need testing on Android.
|
||||
|
||||
<a id="development-2026-10-11-settings"></a>
|
||||
|
||||
## 11/10/2026 : Settings and home screen
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Second home screen, wallpaper previews and installation banner fixed.
|
||||
-   : Error message dismissal restored.
|
||||
-   : Ringtones, lock code, permissions and French/English labels fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Open the notification tray by tapping the status bar.
|
||||
-   : Preview 18 built locally.
|
||||
|
||||
### 
|
||||
|
||||
-   : Returned to the previous version after an unstable interface restart.
|
||||
-   : Operation confirmed by the user.
|
||||
|
||||
<a id="development-2026-10-09-mms-bluetooth"></a>
|
||||
|
||||
## 09/10/2026 : MMS and Bluetooth
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Gaia panels connected to each platform’s capabilities.
|
||||
|
||||
### 
|
||||
|
||||
-   : Discovery, pairing, connection, forgetting devices and audio output.
|
||||
-   : Sending preserved and resumed after interruption.
|
||||
-   : MMS accepted by the carrier, delivery not confirmed. Headset call audio still needs testing.
|
||||
-   : Bluetooth audio playback to a PC checked.
|
||||
|
||||
<a id="development-2026-10-08-camera-mms"></a>
|
||||
|
||||
## 08/10/2026 : Camera and MMS
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Camera switching, shutdown and display after rotation fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Attachments preserved and sending shown as pending until confirmation.
|
||||
|
||||
### 
|
||||
|
||||
-   : Preview measured at around 16 fps on the rear camera and 25–27 on the front.
|
||||
-   : Real sending still blocked by the mobile data connection.
|
||||
-   : Focus, zoom and photo capture checked.
|
||||
|
||||
<a id="development-2026-10-08-call-log-dialogs"></a>
|
||||
|
||||
## 08/10/2026 : Call history and dialogs
|
||||
|
||||
### 
|
||||
|
||||
-   : Readable error messages with usable buttons.
|
||||
|
||||
### 
|
||||
|
||||
-   : Clear message when sending is unavailable, without losing the draft.
|
||||
-   : Calling back from call history restored.
|
||||
|
||||
### 
|
||||
|
||||
-   : Flow checked with simulated calls; real audio not tested.
|
||||
|
||||
<a id="development-2026-10-08-sms-audio"></a>
|
||||
|
||||
## 08/10/2026 : SMS and audio
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Volume display, lock screen and notification swiping fixed.
|
||||
-   : Send with one tap without first closing the keyboard. Attachment menu restored.
|
||||
|
||||
### 
|
||||
|
||||
-   : Call audio routing fixes.
|
||||
-   : Network issues prevented testing a real conversation; two-way audio not confirmed.
|
||||
|
||||
<a id="development-2026-10-08-radio-reliability"></a>
|
||||
|
||||
## 08/10/2026 : Network reliability
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Messages linked to the correct conversation.
|
||||
|
||||
### 
|
||||
|
||||
-   : Improved modem recovery; retries avoid duplicate calls and SMS.
|
||||
-   : Network stability and two-way audio still need confirmation.
|
||||
-   : Return to the 4G network after modem restart checked.
|
||||
|
||||
<a id="2026-10-08-source-publication"></a>
|
||||
|
||||
## 08/10/2026 : Public source code
|
||||
|
||||
### 
|
||||
|
||||
-   : Camera, contacts, SMS and Marketplace fixes published, with updated French/English project overviews.
|
||||
|
||||
### 
|
||||
|
||||
-   : Building from a fresh repository copy fixed.
|
||||
-   : The public APK remains Preview 17; these fixes are in the source code.
|
||||
|
||||
### 
|
||||
|
||||
-   : Network search still unstable at this stage; no new image published.
|
||||
|
||||
<a id="development-2026-10-08-camera-lifecycle"></a>
|
||||
|
||||
## 08/10/2026 : Camera and contacts
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : More reliable opening and camera switching.
|
||||
-   : Photos, dates, edits and number selection from Messages or Phone fixed.
|
||||
-   : Media selection fixed.
|
||||
-   : Fixes in source code; public APK unchanged at this stage.
|
||||
|
||||
### 
|
||||
|
||||
-   : Number and text passed correctly to the Android app.
|
||||
|
||||
### 
|
||||
|
||||
-   : Discovery available; pairing and audio use still need testing.
|
||||
-   : Photo capture, focus, zoom and camera switching checked.
|
||||
|
||||
<a id="development-2026-10-06-marketplace-theme"></a>
|
||||
|
||||
## 06/10/2026 : Marketplace theme
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Integration planned for a future APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Embedded theme: Fira font, compact navigation, touch buttons and scrollable screenshots.
|
||||
-   : Embedded display preserved during search, filtering and language changes.
|
||||
-   : Theme prepared locally, not yet deployed to the website.
|
||||
-   : French and English pages checked on small screens.
|
||||
|
||||
<a id="development-2026-10-06-system-banner"></a>
|
||||
|
||||
## 06/10/2026 : Notification banners
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : The “Application installed” banner disappears automatically and is replaced correctly.
|
||||
|
||||
### 
|
||||
|
||||
-   : Banner display and dismissal checked.
|
||||
|
||||
### 
|
||||
|
||||
-   : Fix prepared for the next APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Banner disappearance after five seconds checked.
|
||||
|
||||
<a id="development-2026-10-06-wifi"></a>
|
||||
|
||||
## 06/10/2026 : Wi-Fi and settings
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Panels open even when an animation does not finish normally.
|
||||
|
||||
### 
|
||||
|
||||
-   : Fixes not yet included in the public APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Detected networks and disconnections displayed correctly.
|
||||
-   : Search, connection and Internet access checked.
|
||||
|
||||
<a id="development-2026-10-06-marketplace-views"></a>
|
||||
|
||||
## 06/10/2026 : Marketplace navigation
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Local message with a Retry button when the catalogue is unavailable.
|
||||
-   : The catalogue stays below the status bar and hides when returning home or closing it.
|
||||
-   : An earlier error no longer hides a new attempt.
|
||||
|
||||
### 
|
||||
|
||||
-   : Navigation, errors, returning home and closing checked.
|
||||
|
||||
### 
|
||||
|
||||
-   : Preview 17 rebuilt locally, without a new Android test.
|
||||
|
||||
### 
|
||||
|
||||
-   : Interface tested with a local page; remote catalogue not checked without Internet.
|
||||
|
||||
<a id="2.7-preview.17"></a>
|
||||
|
||||
## 05/10/2026 : 2.7-preview.17
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Install, launch and uninstall packaged web apps.
|
||||
-   : Vulpes catalogue on the home screen, in French and English, with its original icon.
|
||||
-   : Installed apps kept separate from system apps.
|
||||
-   : Some old Firefox OS apps and APIs are not yet supported.
|
||||
|
||||
### 
|
||||
|
||||
-   : Installation and use of 2048 checked.
|
||||
|
||||
### 
|
||||
|
||||
-   : 2048 installed, preserved after restart and uninstalled on an Android emulator.
|
||||
|
||||
### 
|
||||
|
||||
-   : Home screen checked; app installation still needs validation on Pixel.
|
||||
|
||||
<a id="2.7-preview.16"></a>
|
||||
|
||||
## 05/10/2026 : 2.7-preview.16
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Vulpes startup logo.
|
||||
-   : Unwanted horizontal scrolling removed; test apps hidden.
|
||||
-   : Wallpaper and sound selection restored. Home screen language applied and preserved after restart.
|
||||
-   : APK built, not published; Pixel image tried without permanent installation.
|
||||
|
||||
### 
|
||||
|
||||
-   : Android bars hidden and accessible by swiping; launcher mode checked.
|
||||
-   : Return from sleep and the lock screen camera fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Old files no longer overwrite updates at startup.
|
||||
|
||||
<a id="2026-10-04-sargo-standalone"></a>
|
||||
|
||||
## 04/10/2026 : Standalone installation
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Installer with device and file checks before writing.
|
||||
-   : Standalone Vulpes installation without going through Droidian.
|
||||
-   : Package not published; tested on one Pixel only. Installation replaces phone data.
|
||||
-   : Backups checked; full restoration still needs testing.
|
||||
-   : Installed boots, saved settings, camera, keyboard and notifications checked.
|
||||
|
||||
<a id="2026-10-03-sargo-per-device-installation"></a>
|
||||
|
||||
## 03/10/2026 : Per-device installation
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Preparation adapted to each Pixel, with device-specific backups.
|
||||
-   : Focus sometimes times out; issue still open.
|
||||
-   : Direct installation from stock Android not yet available.
|
||||
-   : Temporary boots, saved settings and return to the previous installation checked.
|
||||
|
||||
<a id="2026-10-03-sargo-installation-tools"></a>
|
||||
|
||||
## 03/10/2026 : Installation and recovery
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Package preparation and recovery kit with a boot backup.
|
||||
-   : Restoration possible without booting the diagnostic tool.
|
||||
-   : Preparation checked locally; installation on another Pixel still needs completion.
|
||||
|
||||
### 
|
||||
|
||||
-   : Shared history included in upcoming source releases.
|
||||
|
||||
<a id="2026-10-03-sargo-modem-persistence"></a>
|
||||
|
||||
## 03/10/2026 : Modem and first startup
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Recover the modem without restarting the whole phone.
|
||||
-   : Fonts and modem initialization fixed on a fresh installation.
|
||||
-   : Intermittent modem restarts still need fixing; public package incomplete.
|
||||
-   : Cameras, focus, zoom, keyboard, notifications and Wi-Fi search checked.
|
||||
|
||||
<a id="2026-10-01-sargo-distribution-preparation"></a>
|
||||
|
||||
## 01/10/2026 : Preparing distribution
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Preparation of an image with clean user data.
|
||||
-   : Font display on first boot fixed.
|
||||
-   : Image not published. Calls, SMS and mobile data still need end-to-end testing.
|
||||
-   : Intermittent modem restart identified.
|
||||
-   : SIM, 4G, Wi-Fi search, cameras, keyboard and notifications checked in a temporary boot.
|
||||
|
||||
<a id="2.7-preview.15"></a>
|
||||
|
||||
## 30/09/2026 : 2.7-preview.15
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : First publication of Desktop, Android and Tundra source code, with French/English project overviews.
|
||||
-   : More reliable camera stream shutdown and replacement.
|
||||
-   : Dates and sending statuses fixed in conversations.
|
||||
|
||||
### 
|
||||
|
||||
-   : Navigation, settings, gestures and saved data checked.
|
||||
|
||||
### 
|
||||
|
||||
-   : Update, Camera, Gallery and returns from the background checked on an Android emulator.
|
||||
|
||||
### 
|
||||
|
||||
-   : Source code included; universal installation still unavailable at this stage.
|
||||
-   : Flash, video, rotation, sleep and call audio still incomplete or awaiting tests.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)
|
||||
|
||||
<a id="development-2026-09-30-sms-camera-followup"></a>
|
||||
|
||||
## 30/09/2026 : SMS and camera follow-up
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Camera release and switching fixed.
|
||||
-   : Empty conversations fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Shared sources updated, without a new APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Vibration now stops correctly.
|
||||
-   : Dialog after sending SMS and camera smoothness still need fixing.
|
||||
-   : Focus and zoom confirmed by the user.
|
||||
-   : Sending and receiving confirmed by the user; conversations displayed.
|
||||
|
||||
<a id="development-2026-09-30-sms-camera"></a>
|
||||
|
||||
## 30/09/2026 : SMS and photos
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Failed messages preserved and sending statuses updated.
|
||||
|
||||
### 
|
||||
|
||||
-   : Preview 14 APK unchanged at this stage.
|
||||
|
||||
### 
|
||||
|
||||
-   : Tap to focus and digital zoom. Photos up to 12 MP at the rear and 8 MP at the front.
|
||||
-   : Preview return after taking a photo fixed.
|
||||
-   : Delivery to the recipient not yet confirmed.
|
||||
|
||||
<a id="2.7-preview.14"></a>
|
||||
|
||||
## 30/09/2026 : 2.7-preview.14
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Fonts, version information, lock screen and notifications aligned.
|
||||
|
||||
### 
|
||||
|
||||
-   : Larger keyboard, improved text editing and recipient number entry.
|
||||
|
||||
### 
|
||||
|
||||
-   : Startup and fonts checked on an Android emulator.
|
||||
|
||||
### 
|
||||
|
||||
-   : Vibration stopping fixed.
|
||||
-   : Notifications preserved, conversation opening and duplicate messages fixed.
|
||||
-   : Saved networks, reconnection and forgetting a network fixed.
|
||||
-   : Audio controls tested outside calls; conversation audio and notification sounds still need testing.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)
|
||||
|
||||
<a id="2.7-preview.13"></a>
|
||||
|
||||
## 27/09/2026 : 2.7-preview.13
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Event reminders fixed, including after deletion.
|
||||
-   : Initial display and closing the editor fixed.
|
||||
-   : Conversation filters fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Photo, video, calendar and conversations checked.
|
||||
|
||||
### 
|
||||
|
||||
-   : Camera, Gallery, Calendar and background resume checked.
|
||||
|
||||
### 
|
||||
|
||||
-   : Camera, Wi-Fi and brightness checked on the installed version.
|
||||
|
||||
<a id="2.7-preview.12"></a>
|
||||
|
||||
## 27/09/2026 : 2.7-preview.12
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Local start page, in French or English according to language.
|
||||
-   : SMS conversations and Calendar translations fixed.
|
||||
-   : Camera switching and interface zoom fixed; flash disabled when unavailable.
|
||||
-   : Sharper icons, long press on Home and gestures fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Smoother preview, at around 23 frames per second in testing.
|
||||
-   : More responsive brightness and Wi-Fi connection fixed.
|
||||
-   : Video, flash, Bluetooth and advanced call settings remain incomplete.
|
||||
|
||||
<a id="development-2026-09-27-4"></a>
|
||||
|
||||
## 27/09/2026 : Interface and hardware
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : First local start page.
|
||||
-   : Original shutdown animation retained, with blue circles.
|
||||
-   : List and Video text fixed; transparent Home bar adapted to landscape.
|
||||
-   : Call history headers and icons fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Brightness adjustment connected.
|
||||
-   : Call audio, video, full-resolution photos and rotation still need testing.
|
||||
|
||||
<a id="development-2026-09-27-3"></a>
|
||||
|
||||
## 27/09/2026 : Wi-Fi stability
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Preview 9 built with shared fixes; Wi-Fi handled by Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Network discovery stabilized to avoid phone freezes.
|
||||
-   : Camera and calls remain incomplete.
|
||||
-   : Lock screen, fonts, date, battery and Wi-Fi checked.
|
||||
|
||||
<a id="development-2026-09-27-2"></a>
|
||||
|
||||
## 27/09/2026 : Restoring Gaia components
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Credits, startup screen and browser name updated.
|
||||
-   : Original Gaia panels and styles restored in Settings, Data Usage, FM Radio and imports.
|
||||
-   : Home button no longer overlaps controls; date, battery and lock screen restored.
|
||||
|
||||
### 
|
||||
|
||||
-   : Launcher mode, access to Android settings and background resume.
|
||||
|
||||
### 
|
||||
|
||||
-   : Initial integration unstable; returned to the previous version.
|
||||
|
||||
<a id="development-2026-09-27-1"></a>
|
||||
|
||||
## 27/09/2026 : Calls and hardware buttons
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Gaia incoming and outgoing call interfaces, with call history.
|
||||
-   : Fira fonts, contacts and battery display fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : Support for Power and Volume keys, shutdown menu and waking the screen.
|
||||
-   : Native camera still unavailable.
|
||||
-   : Ringing and hanging up checked; conversation audio still needs testing.
|
||||
|
||||
<a id="development-2026-09-26-1"></a>
|
||||
|
||||
## 26/09/2026 : Startup and fonts
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Fira fonts and battery information fixed; test buttons removed from settings.
|
||||
|
||||
### 
|
||||
|
||||
-   : Data preserved in the installed boot.
|
||||
-   : Stable fullscreen, without automatic gestures or app launches.
|
||||
-   : Sound and microphone tested separately; call audio, camera and sleep not validated.
|
||||
|
||||
<a id="development-2026-09-25-1"></a>
|
||||
|
||||
## 25/09/2026 : Home screen and first native trials
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Round button, transparent background on Home and blue background in apps.
|
||||
-   : Pressed button state, gradient, fonts and system information fixed.
|
||||
|
||||
### 
|
||||
|
||||
-   : App resume changed in source code, not yet tested on Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : First native image trials with accelerated display and battery support.
|
||||
-   : Temporary trials; many hardware features still need connecting.
|
||||
|
||||
<a id="2.7-preview.7"></a>
|
||||
|
||||
## 2.7-preview.7
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Vulpes, Gaia and Gecko versions shown separately.
|
||||
|
||||
### 
|
||||
|
||||
-   : Improved permission handling and clearer error messages.
|
||||
|
||||
### 
|
||||
|
||||
-   : Resume after the first permission request fixed.
|
||||
|
||||
<a id="2.7-preview.6"></a>
|
||||
|
||||
## 2.7-preview.6
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Gaia Camera app connected to the webcam.
|
||||
-   : File import and photos available in Gallery.
|
||||
-   : Camera switching depends on hardware; flash and video unavailable.
|
||||
|
||||
### 
|
||||
|
||||
-   : Gaia Camera app connected to Android cameras.
|
||||
-   : File import and photos available in Gallery.
|
||||
-   : Camera switching depends on hardware; flash and video unavailable.
|
||||
|
||||
<a id="2.7-preview.5"></a>
|
||||
|
||||
## 2.7-preview.5
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : System settings, media and contact imports, notifications and webcam test.
|
||||
-   : Interface and version numbering aligned with Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Interface and version numbering aligned with Desktop.
|
||||
-   : Improved background resume and display recovery.
|
||||
|
||||
<a id="2.7-156.0.2"></a>
|
||||
|
||||
## 2.7-156.0.2
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Android settings, sound, brightness, imports and notifications.
|
||||
-   : Calls, SMS, clock and camera handled by Android apps.
|
||||
-   : White screens and background resume fixed.
|
||||
-   : Historical numbering: this number does not represent a Gecko update.
|
||||
|
||||
<a id="2.7-156.0.1"></a>
|
||||
|
||||
## 2.7-156.0.1
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : “Vulpes OS – Preview” name and project icon.
|
||||
-   : Wallpaper flickering fixed.
|
||||
-   : Historical numbering retained to identify this release.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)
|
||||
|
||||
<a id="2.7-preview.2"></a>
|
||||
|
||||
## 2.7-preview.2
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Wallpaper flickering fixed.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)
|
||||
|
||||
<a id="2.7-preview.1"></a>
|
||||
|
||||
## 2.7-preview.1
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : First Android Preview to try Vulpes without flashing the phone.
|
||||
-   : Known wallpaper flickering in this first version.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)
|
||||
|
||||
<a id="history"></a>
|
||||
|
||||
## Early milestones
|
||||
|
||||
### 
|
||||
|
||||
-   : Adaptation of contacts, messages, settings, alarms and storage.
|
||||
-   : Creation of Vulpes: visual identity, translations and system information.
|
||||
-   : Move from old Firefox OS engines to a modern Gecko.
|
||||
|
||||
### 
|
||||
|
||||
-   : First native boots and hardware trials on Pixel 3a.
|
||||
904
CHANGELOG.md
Normal file
904
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,904 @@
|
|||
# Journal des modifications — Vulpes OS
|
||||
|
||||
[English](CHANGELOG.en.md)
|
||||
|
||||
Les nouveautés, par date et par plateforme. Dans chaque groupe : Add, Change, Fix, Note, Test. Les entrées décrivent la situation à leur date ; une correction en développement nécessite une nouvelle version installée.
|
||||
|
||||
<a id="2.7-preview.21"></a>
|
||||
|
||||
## 14/10/2026 : 2.7-preview.21
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Messages des fonctions indisponibles plus lisibles en thèmes clair et sombre.
|
||||
-   : Dictionnaires supplémentaires disponibles hors ligne ; activation et reprise du clavier Gaia fiabilisées. La vérification sur les appareils concernés reste nécessaire.
|
||||
-   : Affichage du nom des contacts rétabli au premier chargement du journal.
|
||||
-   : Panneaux Stockage rétablis. Une erreur de chargement ne bloque plus les autres menus.
|
||||
|
||||
### 
|
||||
|
||||
-   : Lecture des contacts, SMS et journaux Android dans Gaia, après autorisation. Les données locales de Vulpes sont conservées.
|
||||
-   : Retour vers Vulpes sans écran blanc persistant ; un catalogue fermé peut être rouvert.
|
||||
-   : Le rectangle blanc signalé reste à reproduire sur les appareils concernés.
|
||||
-   : Première étape en lecture seule, hors MMS. Envoi et édition des données Android dans les applications Android ; les nouveaux contacts Gaia restent locaux. Accès SMS et journal selon les autorisations de la ROM et de l’installateur. Essais sur Android 9 ; autres versions à confirmer.
|
||||
|
||||
### 
|
||||
|
||||
-   : Les corrections communes de Gaia sont incluses dans les images complètes et les mises à jour de démarrage.
|
||||
-   : Nouvelle image d’installation construite ; démarrage et flash sur Pixel à vérifier avant publication.
|
||||
|
||||
<a id="2.7-preview.20"></a>
|
||||
|
||||
## 13/10/2026 : 2.7-preview.20
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Mode vidéo, avec miniature et lecture dans Galerie et Vidéo.
|
||||
-   : Améliorations des paramètres, de l’apparence, des sélecteurs, du clavier, des contacts et des médias.
|
||||
-   : Changement de capteur, arrêt et reprise fiabilisés.
|
||||
-   : Compte Mozilla, Sync, localisation distante, extensions et transfert USB restent incomplets.
|
||||
|
||||
### 
|
||||
|
||||
-   : APK Preview 20 disponible.
|
||||
-   : Appels et SMS utilisent Android. Cette Preview n’a pas encore été validée en utilisation sur Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Aperçu autour de 16 images par seconde.
|
||||
-   : Mise à jour du Wi-Fi, du Bluetooth, des appels, des SMS/MMS et des réglages réseau.
|
||||
-   : Son des vidéos et conversation téléphonique à vérifier séparément.
|
||||
-   : Preview 20 installée sur le Pixel 3a, données conservées. Wi-Fi connecté et réseau 4G détecté.
|
||||
|
||||
### 
|
||||
|
||||
-   : Historique FR/EN simplifié, badges et filtres par plateforme. Même contenu pour le site et les releases.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20)
|
||||
|
||||
<a id="development-2026-10-12-video-clock"></a>
|
||||
|
||||
## 12/10/2026 : Vidéo et heure
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Enregistrement vidéo avec son, miniature et lecture dans Galerie et Vidéo.
|
||||
-   : « Améliorer Vulpes » mène aux tickets du projet.
|
||||
-   : Sélecteurs de date et d’heure rétablis ; retour du menu Clavier et texte des listes corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Enregistrement vérifié avec une caméra simulée.
|
||||
|
||||
### 
|
||||
|
||||
-   : Changements préparés pour un prochain APK ; vidéo non encore testée sur Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Aperçu amélioré, autour de 16 images par seconde.
|
||||
-   : Heure automatique selon les informations du réseau ; fuseau horaire encore manuel.
|
||||
-   : Vidéo et changement de capteur vérifiés. Son des vidéos encore à écouter.
|
||||
|
||||
<a id="development-2026-10-12-settings-calls-hotspot"></a>
|
||||
|
||||
## 12/10/2026 : Appels et partage Wi-Fi
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Fermeture des dialogues et nouvelle tentative après une erreur corrigées.
|
||||
|
||||
### 
|
||||
|
||||
-   : Réglages du double appel, de l’affichage du numéro et du renvoi d’appel.
|
||||
-   : Partage de connexion avec ou sans mot de passe.
|
||||
-   : Les réglages lents ne bloquent plus les appels et les SMS.
|
||||
-   : Activation du point d’accès vérifiée ; accès Internet d’un autre appareil encore à tester.
|
||||
|
||||
<a id="development-2026-10-12-settings-network"></a>
|
||||
|
||||
## 12/10/2026 : Paramètres réseau
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Adresse IP et vitesse de connexion affichées correctement.
|
||||
|
||||
### 
|
||||
|
||||
-   : Choix du type de réseau et modification des points d’accès mobiles.
|
||||
-   : Réglages réseau plus fiables lorsque le modem répond lentement.
|
||||
-   : Ajout, suppression et réinitialisation des points d’accès encore incomplets.
|
||||
|
||||
<a id="development-2026-10-12-settings-audit"></a>
|
||||
|
||||
## 12/10/2026 : Revue des paramètres
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Navigation, accessibilité et aide corrigées. Les panneaux incomplets restent visibles.
|
||||
-   : Menus parcourus ; certaines actions dépendent encore du matériel ou de l’opérateur.
|
||||
|
||||
### 
|
||||
|
||||
-   : Informations de stockage et téléchargements raccordés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Affichage de l’espace utilisé par Vulpes.
|
||||
|
||||
### 
|
||||
|
||||
-   : Données mobiles, itinérance, heure, réseaux Wi-Fi masqués et panneau Son corrigés.
|
||||
|
||||
<a id="development-2026-10-12-settings-navigation"></a>
|
||||
|
||||
## 12/10/2026 : Navigation des paramètres
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Choix Clair/Sombre et validation ou annulation des sélecteurs rétablis.
|
||||
-   : Gestionnaire adapté aux téléphones à une seule SIM.
|
||||
|
||||
### 
|
||||
|
||||
-   : Informations SIM, centre de messagerie et accusés de réception.
|
||||
-   : Chargement plus rapide des réseaux enregistrés.
|
||||
-   : Certificats Wi-Fi, sécurité SIM et stockage USB encore incomplets.
|
||||
|
||||
<a id="development-2026-10-12-sargo-appearance"></a>
|
||||
|
||||
## 12/10/2026 : Apparence sur Pixel 3a
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Modes clair et sombre communs disponibles sur le Pixel 3a.
|
||||
-   : Correctifs des paramètres et polices Firefox OS intégrés.
|
||||
-   : Pas de nouvel essai des appels, SMS ou Bluetooth à cette étape.
|
||||
-   : Thèmes et affichage vérifiés sur le téléphone.
|
||||
|
||||
<a id="development-2026-10-11-appearance-input"></a>
|
||||
|
||||
## 11/10/2026 : Apparence et clavier
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Modes Clair, Sombre et Système, conservés au redémarrage.
|
||||
-   : Sélection et suppression de texte corrigées.
|
||||
|
||||
### 
|
||||
|
||||
-   : Thèmes et saisie de SMS vérifiés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Clavier Gaia expérimental ; le clavier Android reste proposé par défaut.
|
||||
-   : Nouvelle tentative possible quand la caméra est occupée.
|
||||
-   : Preview 19 construite ; thèmes, clavier et caméra avant encore à vérifier sur Android.
|
||||
|
||||
<a id="development-2026-10-11-settings"></a>
|
||||
|
||||
## 11/10/2026 : Paramètres et accueil
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Second écran d’accueil, aperçus de fonds d’écran et bandeau d’installation corrigés.
|
||||
-   : Fermeture du message d’erreur rétablie.
|
||||
-   : Sonneries, code de verrouillage, permissions et libellés FR/EN corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Ouverture du volet en touchant la barre de statut.
|
||||
-   : Preview 18 construite localement.
|
||||
|
||||
### 
|
||||
|
||||
-   : Retour à la version précédente après un redémarrage instable de l’interface.
|
||||
-   : Fonctionnement confirmé par l’utilisateur.
|
||||
|
||||
<a id="development-2026-10-09-mms-bluetooth"></a>
|
||||
|
||||
## 09/10/2026 : MMS et Bluetooth
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Panneaux Gaia reliés aux capacités de chaque plateforme.
|
||||
|
||||
### 
|
||||
|
||||
-   : Recherche, association, connexion, oubli d’appareils et sortie audio.
|
||||
-   : Envoi conservé et repris après une interruption.
|
||||
-   : MMS accepté par l’opérateur, réception non confirmée. Audio d’appel sur casque à tester.
|
||||
-   : Lecture audio Bluetooth vers un PC vérifiée.
|
||||
|
||||
<a id="development-2026-10-08-camera-mms"></a>
|
||||
|
||||
## 08/10/2026 : Caméra et MMS
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Changement de capteur, arrêt et affichage après rotation corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Pièces jointes conservées et envoi affiché en attente jusqu’à confirmation.
|
||||
|
||||
### 
|
||||
|
||||
-   : Aperçu mesuré autour de 16 images/s à l’arrière et 25 à 27 à l’avant.
|
||||
-   : Envoi réel encore bloqué par la connexion aux données mobiles.
|
||||
-   : Mise au point, zoom et prise de photo vérifiés.
|
||||
|
||||
<a id="development-2026-10-08-call-log-dialogs"></a>
|
||||
|
||||
## 08/10/2026 : Journal d’appel et dialogues
|
||||
|
||||
### 
|
||||
|
||||
-   : Messages d’erreur lisibles, avec des boutons utilisables.
|
||||
|
||||
### 
|
||||
|
||||
-   : Message clair si l’envoi est indisponible, sans perdre le brouillon.
|
||||
-   : Rappel depuis le journal d’appel rétabli.
|
||||
|
||||
### 
|
||||
|
||||
-   : Parcours vérifié avec des appels simulés ; audio réel non testé.
|
||||
|
||||
<a id="development-2026-10-08-sms-audio"></a>
|
||||
|
||||
## 08/10/2026 : SMS et audio
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Affichage du volume, verrouillage et balayage des notifications corrigés.
|
||||
-   : Envoi en un appui, sans fermer d’abord le clavier. Menu des pièces jointes rétabli.
|
||||
|
||||
### 
|
||||
|
||||
-   : Corrections du routage du son des appels.
|
||||
-   : Le réseau a empêché de vérifier une conversation réelle ; son dans les deux sens non confirmé.
|
||||
|
||||
<a id="development-2026-10-08-radio-reliability"></a>
|
||||
|
||||
## 08/10/2026 : Fiabilité du réseau
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Messages associés à la bonne conversation.
|
||||
|
||||
### 
|
||||
|
||||
-   : Récupération du modem améliorée ; les nouvelles tentatives évitent de doubler appels et SMS.
|
||||
-   : Stabilité du réseau et audio dans les deux sens encore à confirmer.
|
||||
-   : Retour sur le réseau 4G après redémarrage du modem vérifié.
|
||||
|
||||
<a id="2026-10-08-source-publication"></a>
|
||||
|
||||
## 08/10/2026 : Sources publiques
|
||||
|
||||
### 
|
||||
|
||||
-   : Publication des corrections caméra, contacts, SMS et Marketplace, avec présentations FR/EN à jour.
|
||||
|
||||
### 
|
||||
|
||||
-   : Construction depuis une copie neuve du dépôt corrigée.
|
||||
-   : L’APK public reste en Preview 17 ; ces corrections sont dans les sources.
|
||||
|
||||
### 
|
||||
|
||||
-   : Recherche de réseau encore instable à cette étape ; pas de nouvelle image publiée.
|
||||
|
||||
<a id="development-2026-10-08-camera-lifecycle"></a>
|
||||
|
||||
## 08/10/2026 : Caméra et contacts
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Ouverture et changement de capteur fiabilisés.
|
||||
-   : Photos, dates, modifications et choix d’un numéro depuis SMS ou Téléphone corrigés.
|
||||
-   : Sélection des médias corrigée.
|
||||
-   : Correctifs dans les sources ; APK public inchangé à cette étape.
|
||||
|
||||
### 
|
||||
|
||||
-   : Numéro et texte transmis correctement à l’application Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Recherche disponible ; association et usages audio encore à vérifier.
|
||||
-   : Photo, mise au point, zoom et changements de capteur vérifiés.
|
||||
|
||||
<a id="development-2026-10-06-marketplace-theme"></a>
|
||||
|
||||
## 06/10/2026 : Thème Marketplace
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Intégration prévue dans un prochain APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Thème intégré : police Fira, navigation compacte, boutons tactiles et captures défilantes.
|
||||
-   : Affichage intégré conservé pendant la recherche, les filtres et les changements de langue.
|
||||
-   : Thème préparé localement, pas encore déployé sur le site.
|
||||
-   : Pages françaises et anglaises vérifiées sur petits écrans.
|
||||
|
||||
<a id="development-2026-10-06-system-banner"></a>
|
||||
|
||||
## 06/10/2026 : Bandeaux de notification
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Le bandeau « Application installée » disparaît automatiquement et se remplace correctement.
|
||||
|
||||
### 
|
||||
|
||||
-   : Affichage et fermeture des bandeaux vérifiés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Correctif préparé pour le prochain APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Disparition du bandeau après cinq secondes vérifiée.
|
||||
|
||||
<a id="development-2026-10-06-wifi"></a>
|
||||
|
||||
## 06/10/2026 : Wi-Fi et paramètres
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Ouverture des panneaux rétablie quand une animation ne se termine pas normalement.
|
||||
|
||||
### 
|
||||
|
||||
-   : Correctifs encore absents de l’APK public.
|
||||
|
||||
### 
|
||||
|
||||
-   : Réseaux détectés et déconnexions affichés correctement.
|
||||
-   : Recherche, connexion et accès Internet vérifiés.
|
||||
|
||||
<a id="development-2026-10-06-marketplace-views"></a>
|
||||
|
||||
## 06/10/2026 : Navigation Marketplace
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Message local avec bouton Réessayer si le catalogue est inaccessible.
|
||||
-   : Le catalogue reste sous la barre de statut et se masque en revenant à l’accueil ou en le fermant.
|
||||
-   : Une ancienne erreur ne masque plus une nouvelle tentative.
|
||||
|
||||
### 
|
||||
|
||||
-   : Navigation, erreurs, retour à l’accueil et fermeture vérifiés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Preview 17 reconstruite localement, sans nouveau test Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Interface testée avec une page locale ; catalogue distant non vérifié sans Internet.
|
||||
|
||||
<a id="2.7-preview.17"></a>
|
||||
|
||||
## 05/10/2026 : 2.7-preview.17
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Installation, lancement et désinstallation d’applications web empaquetées.
|
||||
-   : Catalogue Vulpes sur l’accueil, en français et en anglais, avec son icône originale.
|
||||
-   : Applications installées séparées des applications système.
|
||||
-   : Certaines anciennes applications et API Firefox OS ne sont pas encore prises en charge.
|
||||
|
||||
### 
|
||||
|
||||
-   : Installation et utilisation de 2048 vérifiées.
|
||||
|
||||
### 
|
||||
|
||||
-   : 2048 installé, conservé après redémarrage et désinstallé sur un émulateur Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Accueil vérifié ; installation d’applications encore à valider sur le Pixel.
|
||||
|
||||
<a id="2.7-preview.16"></a>
|
||||
|
||||
## 05/10/2026 : 2.7-preview.16
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Logo Vulpes au démarrage.
|
||||
-   : Défilement horizontal involontaire supprimé ; applications de test masquées.
|
||||
-   : Choix des fonds d’écran et des sons rétabli. Langue appliquée à l’accueil et conservée au redémarrage.
|
||||
-   : APK construit, non publié ; image Pixel essayée sans installation permanente.
|
||||
|
||||
### 
|
||||
|
||||
-   : Barres Android masquées, accessibles par balayage ; mode lanceur vérifié.
|
||||
-   : Retour après veille et depuis la caméra du verrouillage corrigé.
|
||||
|
||||
### 
|
||||
|
||||
-   : Les anciens fichiers ne remplacent plus les mises à jour au démarrage.
|
||||
|
||||
<a id="2026-10-04-sargo-standalone"></a>
|
||||
|
||||
## 04/10/2026 : Installation autonome
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Installateur avec vérification du modèle et des fichiers avant écriture.
|
||||
-   : Installation autonome de Vulpes, sans passer par Droidian.
|
||||
-   : Paquet non publié ; un seul Pixel testé. L’installation remplace les données du téléphone.
|
||||
-   : Sauvegardes vérifiées ; restauration complète encore à essayer.
|
||||
-   : Démarrages installés, conservation des réglages, caméra, clavier et notifications vérifiés.
|
||||
|
||||
<a id="2026-10-03-sargo-per-device-installation"></a>
|
||||
|
||||
## 03/10/2026 : Installation par appareil
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Préparation adaptée à chaque Pixel, avec sauvegardes propres à l’appareil.
|
||||
-   : Mise au point parfois trop lente ; problème encore ouvert.
|
||||
-   : Installation directe depuis Android d’origine pas encore disponible.
|
||||
-   : Démarrages temporaires, conservation des réglages et retour à l’installation précédente vérifiés.
|
||||
|
||||
<a id="2026-10-03-sargo-installation-tools"></a>
|
||||
|
||||
## 03/10/2026 : Installation et secours
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Préparation des paquets et kit de secours avec sauvegarde du démarrage.
|
||||
-   : Restauration possible sans démarrer l’outil de diagnostic.
|
||||
-   : Préparation vérifiée localement ; installation sur un autre Pixel encore à finaliser.
|
||||
|
||||
### 
|
||||
|
||||
-   : Historique commun inclus dans les prochaines publications de sources.
|
||||
|
||||
<a id="2026-10-03-sargo-modem-persistence"></a>
|
||||
|
||||
## 03/10/2026 : Modem et premier démarrage
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Récupération du modem sans redémarrer tout le téléphone.
|
||||
-   : Polices et initialisation du modem corrigées sur une installation neuve.
|
||||
-   : Redémarrages intermittents du modem encore à résoudre ; paquet public incomplet.
|
||||
-   : Caméras, focus, zoom, clavier, notifications et recherche Wi-Fi vérifiés.
|
||||
|
||||
<a id="2026-10-01-sargo-distribution-preparation"></a>
|
||||
|
||||
## 01/10/2026 : Préparation de la distribution
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Préparation d’une image avec des données vierges.
|
||||
-   : Affichage des polices au premier démarrage corrigé.
|
||||
-   : Image non publiée. Appels, SMS et données mobiles encore à vérifier de bout en bout.
|
||||
-   : Redémarrage intermittent du modem identifié.
|
||||
-   : SIM, 4G, recherche Wi-Fi, caméras, clavier et notifications vérifiés en démarrage temporaire.
|
||||
|
||||
<a id="2.7-preview.15"></a>
|
||||
|
||||
## 30/09/2026 : 2.7-preview.15
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Première publication des sources PC, Android et Tundra, avec présentation FR/EN.
|
||||
-   : Arrêt et remplacement du flux caméra fiabilisés.
|
||||
-   : Dates et états d’envoi corrigés dans les conversations.
|
||||
|
||||
### 
|
||||
|
||||
-   : Navigation, réglages, gestes et conservation des données vérifiés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Mise à jour, Photo, Galerie et retours d’arrière-plan vérifiés sur un émulateur Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Sources incluses ; installation universelle encore indisponible à cette étape.
|
||||
-   : Flash, vidéo, rotation, veille et audio d’appel encore incomplets ou à vérifier.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)
|
||||
|
||||
<a id="development-2026-09-30-sms-camera-followup"></a>
|
||||
|
||||
## 30/09/2026 : Retours SMS et caméra
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Libération du capteur et changement de caméra corrigés.
|
||||
-   : Conversations vides corrigées.
|
||||
|
||||
### 
|
||||
|
||||
-   : Sources communes mises à jour, sans nouvel APK.
|
||||
|
||||
### 
|
||||
|
||||
-   : Arrêt du vibreur rétabli.
|
||||
-   : Dialogue après envoi SMS et fluidité caméra encore à corriger.
|
||||
-   : Focus et zoom confirmés par l’utilisateur.
|
||||
-   : Envoi et réception confirmés par l’utilisateur ; conversations affichées.
|
||||
|
||||
<a id="development-2026-09-30-sms-camera"></a>
|
||||
|
||||
## 30/09/2026 : SMS et photo
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Messages en échec conservés et états d’envoi mis à jour.
|
||||
|
||||
### 
|
||||
|
||||
-   : APK Preview 14 inchangé à cette étape.
|
||||
|
||||
### 
|
||||
|
||||
-   : Mise au point au toucher et zoom numérique. Photos jusqu’à 12 Mpx à l’arrière et 8 Mpx à l’avant.
|
||||
-   : Retour de l’aperçu après une photo corrigé.
|
||||
-   : Réception par le destinataire non encore confirmée.
|
||||
|
||||
<a id="2.7-preview.14"></a>
|
||||
|
||||
## 30/09/2026 : 2.7-preview.14
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Polices, informations de version, verrouillage et notifications harmonisés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Clavier agrandi, édition de texte et saisie du numéro de destinataire améliorées.
|
||||
|
||||
### 
|
||||
|
||||
-   : Démarrage et polices vérifiés sur un émulateur Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Arrêt de la vibration corrigé.
|
||||
-   : Notifications conservées, ouverture de la conversation et doublons corrigés.
|
||||
-   : Réseaux enregistrés, reconnexion et oubli d’un réseau corrigés.
|
||||
-   : Contrôles audio testés hors appel ; conversation et son des notifications encore à vérifier.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)
|
||||
|
||||
<a id="2.7-preview.13"></a>
|
||||
|
||||
## 27/09/2026 : 2.7-preview.13
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Rappels des événements corrigés, y compris après suppression.
|
||||
-   : Premier affichage et fermeture de l’éditeur corrigés.
|
||||
-   : Filtres des conversations corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Photo, vidéo, calendrier et conversations vérifiés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Caméra, Galerie, Calendrier et reprise après arrière-plan vérifiés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Caméra, Wi-Fi et luminosité vérifiés sur la version installée.
|
||||
|
||||
<a id="2.7-preview.12"></a>
|
||||
|
||||
## 27/09/2026 : 2.7-preview.12
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Page d’accueil locale, en français ou en anglais selon la langue.
|
||||
-   : Conversations SMS et traductions du Calendrier corrigées.
|
||||
-   : Changement de capteur et zoom de l’interface corrigés ; flash grisé s’il est indisponible.
|
||||
-   : Icônes plus nettes, appui long sur Accueil et gestes corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Aperçu plus fluide, autour de 23 images par seconde lors des essais.
|
||||
-   : Luminosité plus réactive et connexion Wi-Fi corrigée.
|
||||
-   : Vidéo, flash, Bluetooth et réglages d’appel avancés encore incomplets.
|
||||
|
||||
<a id="development-2026-09-27-4"></a>
|
||||
|
||||
## 27/09/2026 : Interface et matériel
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Première page d’accueil locale.
|
||||
-   : Animation d’extinction conservée, avec des ronds bleus.
|
||||
-   : Textes des listes et de Vidéo corrigés ; barre Accueil transparente et adaptée au paysage.
|
||||
-   : En-têtes et icônes du journal d’appel corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Réglage de la luminosité raccordé.
|
||||
-   : Appels audio, vidéo, pleine résolution photo et rotation encore à vérifier.
|
||||
|
||||
<a id="development-2026-09-27-3"></a>
|
||||
|
||||
## 27/09/2026 : Stabilité Wi-Fi
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Preview 9 construite avec les corrections communes ; Wi-Fi géré par Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Recherche des réseaux stabilisée pour éviter les blocages du téléphone.
|
||||
-   : Caméra et appels encore incomplets.
|
||||
-   : Verrouillage, polices, date, batterie et Wi-Fi vérifiés.
|
||||
|
||||
<a id="development-2026-09-27-2"></a>
|
||||
|
||||
## 27/09/2026 : Retour aux composants Gaia
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Remerciements, écran de démarrage et nom du navigateur mis à jour.
|
||||
-   : Panneaux et styles Gaia d’origine rétablis dans Paramètres, Suivi conso, Radio FM et les imports.
|
||||
-   : Bouton Accueil dégagé des commandes ; date, batterie et écran de verrouillage rétablis.
|
||||
|
||||
### 
|
||||
|
||||
-   : Mode lanceur, accès aux paramètres Android et reprise après arrière-plan.
|
||||
|
||||
### 
|
||||
|
||||
-   : Première intégration instable ; retour à la version précédente.
|
||||
|
||||
<a id="development-2026-09-27-1"></a>
|
||||
|
||||
## 27/09/2026 : Appels et boutons physiques
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Interfaces Gaia d’appel entrant et sortant, avec journal d’appel.
|
||||
-   : Polices Fira, contacts et affichage de la batterie corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Prise en charge des touches Marche/Arrêt et Volume, du menu d’extinction et du réveil de l’écran.
|
||||
-   : Caméra native encore indisponible.
|
||||
-   : Sonnerie et raccrochage vérifiés ; conversation audio encore à tester.
|
||||
|
||||
<a id="development-2026-09-26-1"></a>
|
||||
|
||||
## 26/09/2026 : Démarrage et polices
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Polices Fira et informations de batterie corrigées ; boutons de test retirés des paramètres.
|
||||
|
||||
### 
|
||||
|
||||
-   : Conservation des données lors du démarrage installé.
|
||||
-   : Plein écran stable, sans gestes ni ouverture automatique d’applications.
|
||||
-   : Son et micro testés séparément ; audio d’appel, caméra et veille non validés.
|
||||
|
||||
<a id="development-2026-09-25-1"></a>
|
||||
|
||||
## 25/09/2026 : Accueil et premiers essais natifs
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Bouton rond, fond transparent sur l’accueil et bleu dans les applications.
|
||||
-   : État appuyé du bouton, dégradé, polices et informations système corrigés.
|
||||
|
||||
### 
|
||||
|
||||
-   : Reprise de l’application modifiée dans les sources, pas encore testée sur Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Premiers essais d’une image native avec affichage accéléré et batterie.
|
||||
-   : Essais temporaires ; de nombreuses fonctions matérielles restent à raccorder.
|
||||
|
||||
<a id="2.7-preview.7"></a>
|
||||
|
||||
## 2.7-preview.7
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Versions de Vulpes, Gaia et Gecko distinguées.
|
||||
|
||||
### 
|
||||
|
||||
-   : Gestion des autorisations et messages d’erreur plus précis.
|
||||
|
||||
### 
|
||||
|
||||
-   : Reprise après la première demande d’autorisation corrigée.
|
||||
|
||||
<a id="2.7-preview.6"></a>
|
||||
|
||||
## 2.7-preview.6
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Application Photo Gaia reliée à la webcam.
|
||||
-   : Import de fichiers et photos disponibles dans Galerie.
|
||||
-   : Changement de capteur selon le matériel ; flash et vidéo indisponibles.
|
||||
|
||||
### 
|
||||
|
||||
-   : Application Photo Gaia reliée aux caméras Android.
|
||||
-   : Import de fichiers et photos disponibles dans Galerie.
|
||||
-   : Changement de capteur selon le matériel ; flash et vidéo indisponibles.
|
||||
|
||||
<a id="2.7-preview.5"></a>
|
||||
|
||||
## 2.7-preview.5
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Réglages système, imports de médias et de contacts, notifications et essai webcam.
|
||||
-   : Interface et numérotation des versions alignées avec Android.
|
||||
|
||||
### 
|
||||
|
||||
-   : Interface et numérotation des versions alignées avec le PC.
|
||||
-   : Reprise après arrière-plan et récupération de l’affichage améliorées.
|
||||
|
||||
<a id="2.7-156.0.2"></a>
|
||||
|
||||
## 2.7-156.0.2
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Réglages Android, son, luminosité, imports et notifications.
|
||||
-   : Appels, SMS, horloge et caméra confiés aux applications Android.
|
||||
-   : Écrans blancs et reprise après arrière-plan corrigés.
|
||||
-   : Ancienne numérotation : ce numéro ne correspond pas à une mise à jour de Gecko.
|
||||
|
||||
<a id="2.7-156.0.1"></a>
|
||||
|
||||
## 2.7-156.0.1
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Nom « Vulpes OS – Preview » et icône du projet.
|
||||
-   : Clignotement du fond d’écran corrigé.
|
||||
-   : Ancienne numérotation, conservée pour retrouver cette publication.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)
|
||||
|
||||
<a id="2.7-preview.2"></a>
|
||||
|
||||
## 2.7-preview.2
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Clignotement du fond d’écran corrigé.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)
|
||||
|
||||
<a id="2.7-preview.1"></a>
|
||||
|
||||
## 2.7-preview.1
|
||||
|
||||

|
||||
|
||||
### 
|
||||
|
||||
-   : Première Preview Android pour découvrir Vulpes sans flasher le téléphone.
|
||||
-   : Fond d’écran clignotant connu dans cette première version.
|
||||
|
||||
[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)
|
||||
|
||||
<a id="history"></a>
|
||||
|
||||
## Premières étapes
|
||||
|
||||
### 
|
||||
|
||||
-   : Adaptation des contacts, messages, paramètres, alarmes et du stockage.
|
||||
-   : Création de Vulpes : identité visuelle, traductions et informations système.
|
||||
-   : Passage des anciens moteurs Firefox OS à un Gecko moderne.
|
||||
|
||||
### 
|
||||
|
||||
-   : Premiers démarrages natifs et essais matériels sur le Pixel 3a.
|
||||
107
README.fr.md
107
README.fr.md
|
|
@ -1,45 +1,52 @@
|
|||
<p align="center"><a href="https://nnsprod.com/git/oversu/VulpesOS">🇬🇧 English</a> · <strong>🇫🇷 Français</strong></p>
|
||||
<p align="center"><img src="docs/media/vulpes-logo.png" width="160" alt="Logo renard de Vulpes OS"></p>
|
||||
<p align="center"><a href="https://nnsprod.com/git/oversu/VulpesOS/src/branch/main/README.md">English</a> · <strong>Français</strong></p>
|
||||
<p align="center"><img src="docs/media/vulpes-logo.png" width="120" alt="Vulpes OS"></p>
|
||||
<h1 align="center">Vulpes OS</h1>
|
||||
<p align="center"><strong>L'esprit de Firefox OS. Un Gecko moderne. Une base Gaia commune.</strong><br>Un projet communautaire pour retrouver une expérience mobile fondée sur le Web, sur PC, Android et téléphone natif.</p>
|
||||
<p align="center">Firefox OS fork · Open source</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Vulpes-2.7_preview.15-0755be?style=flat-square" alt="Vulpes Preview 15">
|
||||
<img src="https://img.shields.io/badge/Gecko_bureau-156.0.1-0099cc?style=flat-square" alt="Gecko bureau 156.0.1">
|
||||
<img src="https://img.shields.io/badge/Statut-Expérimental-f3a712?style=flat-square" alt="Expérimental">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Vulpes-2.7%20Preview%2020-0755be?style=flat-square" alt="Vulpes: 2.7 Preview 20"></a>
|
||||
<img src="https://img.shields.io/badge/Gaia-2.7-0755be?style=flat-square" alt="Gaia: 2.7">
|
||||
<img src="https://img.shields.io/badge/Gecko-156.0.1-0099cc?style=flat-square" alt="Gecko: 156.0.1">
|
||||
<img src="https://img.shields.io/badge/Tundra-0.1.0-0099cc?style=flat-square" alt="Tundra: 0.1.0">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Android%20APK-Available-26764a?style=flat-square" alt="Android APK: Available"></a>
|
||||
<a href="#linux"><img src="https://img.shields.io/badge/Desktop-Available-26764a?style=flat-square" alt="Desktop: Available"></a>
|
||||
<a href="https://vulpes-os.org/devices/pixel-3a.php?lang=fr"><img src="https://img.shields.io/badge/Native%20image-In%20development-996516?style=flat-square" alt="Native image: In development"></a>
|
||||
<a href="https://vulpes-os.org/devices/pixel-3a.php?lang=fr"><img src="https://img.shields.io/badge/Pixel%203a-Bootable%20and%20usable-26764a?style=flat-square" alt="Pixel 3a: Bootable and usable"></a>
|
||||
</p>
|
||||
<p align="center"><a href="https://vulpes-os.org">🌐 Le projet</a> · <a href="https://nnsprod.com/git/oversu/VulpesOS/releases">📦 Téléchargements</a> · <a href="https://vulpes-os.org/changelog.php">📝 Changelog</a> · <a href="https://nnsprod.com/git/oversu/VulpesOS/issues">🐞 Signaler un bug</a></p>
|
||||
|
||||
## 🦊 Le Web comme interface
|
||||
[Site](https://vulpes-os.org/?lang=fr) · [Téléchargements](https://nnsprod.com/git/oversu/VulpesOS/releases) · [Changelog](CHANGELOG.md) · [Signaler un bug](https://nnsprod.com/git/oversu/VulpesOS/issues)
|
||||
|
||||
Accueil, paramètres, appareil photo, galerie, messages : **Gaia reste l'interface**, composée de HTML, CSS et JavaScript. Vulpes adapte les API attendues par ces applications Firefox OS à un moteur Gecko moderne et à des services propres à chaque plateforme.
|
||||
Vulpes OS reprend Firefox OS pour faire fonctionner **Gaia**, son interface et ses applications HTML/CSS/JavaScript, avec un moteur **Gecko** récent.
|
||||
|
||||
Ce dépôt contient désormais l'interface commune, l'hôte PC, l'application Android, le runtime Tundra et les outils de développement. C'est une **première préversion pour développeurs**, pas un système mobile terminé.
|
||||
Je développe Vulpes principalement seul, sous le nom d’OverSu, avec l’aide des personnes qui testent et contribuent au projet. **Vulpes est libre et gratuit, et le restera.** Aucun abonnement ni fonctionnalité payante n’est prévu.
|
||||
|
||||
<p align="center"><img src="docs/media/vulpes-desktop.png" width="290" alt="Capture réelle du bureau Gaia en anglais"> <img src="docs/media/vulpes-gecko.png" width="290" alt="Capture réelle des informations système et du moteur Gecko"></p>
|
||||
<p align="center"><em>Captures de l'environnement de développement PC. Les fonctionnalités dépendent de la plateforme et du matériel disponible.</em></p>
|
||||
<p align="center">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-desktop.png"><img src="docs/media/vulpes-desktop.png" width="260" alt="L’accueil de Gaia"></a>
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-gecko.png"><img src="docs/media/vulpes-gecko.png" width="260" alt="Les paramètres de Gaia, écran Informations système"></a>
|
||||
</p>
|
||||
<p align="center"><em>Accueil et paramètres de Gaia · captures de la version pour ordinateur.</em></p>
|
||||
|
||||
## 📥 Choisissez votre plateforme
|
||||
## Essayer Vulpes
|
||||
|
||||
| Plateforme | Disponible aujourd'hui | Moteur |
|
||||
| --- | --- | --- |
|
||||
| **PC Linux** | Sources, fenêtre Gaia au format téléphone, développement et tests d'applications | Gecko **156.0.1** |
|
||||
| **APK Android** | Preview installable ; Android et ses permissions restent en dessous | GeckoView **156.0** |
|
||||
| **Pixel 3a · Sargo** | Sources Tundra ; build natif de développement testé sur notre appareil | Gecko **156.0.1** ARM64 |
|
||||
| Plateforme | Comment l’utiliser |
|
||||
| --- | --- |
|
||||
| **Linux** | Lancer Gaia depuis les sources pour découvrir l’interface ou développer les applications. |
|
||||
| **Android** | Installer l’[APK disponible dans les releases](https://nnsprod.com/git/oversu/VulpesOS/releases). Il fonctionne dans Android et ne remplace pas le système du téléphone. |
|
||||
| **Google Pixel 3a · Sargo** | Faire fonctionner Vulpes comme système natif avec Tundra. L’image à télécharger est encore en préparation ; les fonctions testées et les limites sont décrites dans la [fiche de l’appareil](https://vulpes-os.org/devices/pixel-3a.php?lang=fr). |
|
||||
|
||||
### Android — Preview 15
|
||||
Sur Pixel 3a, **Tundra démarre directement Gecko et Gaia** et assure leur accès au matériel. Droidian a servi d’environnement de test au début du projet : il n’est pas inclus dans Vulpes OS et Tundra ne repose pas dessus.
|
||||
|
||||
Téléchargez l'APK et `SHA256SUMS.txt` depuis [la release Preview 15](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15). Une installation par-dessus une ancienne Preview Vulpes signée conserve ses données.
|
||||
### Preview 20 · 13 octobre 2026
|
||||
|
||||
- Minimum déclaré : **Android 8 / API 26**. Cible : **API 37**.
|
||||
- APK universel : **ARM64, ARMv7 et x86_64** ; environ **600 Mio**, moteur GeckoView inclus.
|
||||
- Photo et Galerie utilisent Gaia, avec les permissions et l'import de fichiers Android.
|
||||
- Les appels et SMS sont transmis aux applications Android ; Vulpes n'est pas l'application Téléphone/SMS par défaut.
|
||||
- Des parcours sur émulateur Android 16 sont testés. Le minimum SDK déclaré ne signifie pas que tous les appareils Android ont été validés.
|
||||
- Il s'agit d'un **build de développement signé**, avec débogage Gecko accessible via ADB autorisé, pas d'une application durcie pour la production.
|
||||
[Télécharger l’APK](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20). Les corrections communes concernent les paramètres Gaia, l’enregistrement vidéo, les contacts et les médias. Les adaptations réseau natives sont incluses dans les sources. Le [changelog](CHANGELOG.md) précise les vérifications et les limites restantes.
|
||||
|
||||
### Linux — lancer depuis les sources
|
||||
PC/Sargo utilisent Gecko **156.0.1** ; Android utilise GeckoView **156.0**. Android 8 / API 26 minimum déclaré, sans validation de tous les appareils. Appels et SMS ouvrent les applications Android. L’APK est un build de développement signé, avec débogage via ADB autorisé.
|
||||
|
||||
Prérequis : Linux x86_64, Python 3, Git et les bibliothèques système nécessaires à Firefox. Sur Debian/Ubuntu : `git python3 libgtk-3-0 libdbus-glib-1-2 libasound2` (les noms de paquets peuvent varier selon la distribution).
|
||||
### Linux
|
||||
|
||||
Prérequis : Linux x86_64, Git, Python 3 et les bibliothèques système nécessaires à Firefox.
|
||||
|
||||
```sh
|
||||
git clone https://nnsprod.com/git/oversu/VulpesOS.git
|
||||
|
|
@ -48,45 +55,29 @@ cd VulpesOS
|
|||
./run.sh
|
||||
```
|
||||
|
||||
`setup.sh` télécharge le moteur Mozilla verrouillé et vérifie son empreinte. Le lancement normal ouvre Gaia sans débogueur. Les profils et moteurs téléchargés restent hors Git.
|
||||
`setup.sh` télécharge le moteur Gecko prévu pour cette version et vérifie son empreinte.
|
||||
|
||||
### Pixel 3a — développement natif Tundra
|
||||
### Compiler l’APK
|
||||
|
||||
**Le build natif démarre directement dans Vulpes**, avec des services matériels Android sélectionnés, un espace utilisateur Linux, Gecko et Gaia. Phosh et l'environnement applicatif Android ne sont pas démarrés.
|
||||
|
||||
Notre image de développement Sargo installée a passé les contrôles de démarrage et de persistance. Des appels réels et des SMS dans les deux sens ont été observés ; les derniers essais couvrent les conversations, les bascules de capteur photo, la capture JPEG pleine résolution, le focus tactile, le zoom numérique et l'arrêt du vibreur.
|
||||
|
||||
**Cette release ne contient pas d'image générique à flasher.** Le boot privé dépend encore de la disposition du stockage du téléphone inventorié, d'images système/données déjà déployées et d'accès de maintenance. Publier ce fichier ne fournirait pas une installation fonctionnelle à un autre Pixel. Les recettes Tundra sont incluses pour les contributeurs ; elles nécessitent un inventaire, des sauvegardes vérifiées et des composants matériels fournis localement. Ne réutilisez pas la sauvegarde d'un autre appareil et ne considérez pas un boot isolé comme une ROM complète.
|
||||
|
||||
## ✨ Ce que nous partageons
|
||||
|
||||
- **Une base Gaia :** interface et services communs pour PC, Android et Tundra.
|
||||
- **Les applications d'origine :** Photo, Galerie, Paramètres et Messages conservent leur interface Gaia, reliée aux adaptateurs de chaque plateforme.
|
||||
- **Du code Web éditable :** les ressources servies sont dans `assets/webapps/`, la compatibilité commune dans `host/`, les remplacements dans `overrides/`. Rechargez ou redémarrez pour tester, sans recompiler Gecko. Certaines sources historiques de `gaia/` nécessitent encore la mise à jour de leur version regroupée.
|
||||
- **Un moteur remplaçable :** téléchargements verrouillés, contrôle d'empreinte, tests isolés et outils de retour arrière. Une mise à jour du moteur se teste et se sélectionne explicitement ; la compatibilité n'est pas garantie automatiquement.
|
||||
- **Des versions distinctes :** Vulpes, Gaia, Gecko et adaptation matérielle sont identifiés séparément. GeckoView 156.0 n'est pas présenté comme Gecko bureau 156.0.1.
|
||||
|
||||
## 🧪 Limites actuelles
|
||||
|
||||
Cette Preview contient des adaptateurs matériels incomplets et des comportements hérités à corriger. Sur Sargo, l'aperçu photo tourne autour de 16 images/s ; flash/vidéo, rotation automatique par capteur et suspension complète restent incomplets. L'enregistrement réseau peut être instable et l'audio d'une conversation dans les deux sens reste à qualifier. Un grand dialogue signalé après envoi de SMS est encore en cours d'analyse.
|
||||
|
||||
Sur Android, les intégrations suivent les permissions et les restrictions de cycle de vie du système. Les alarmes Gaia ne sont pas garanties après l'arrêt de Vulpes par Android. Certaines commandes non prises en charge restent visibles mais désactivées. Des applications de développement/test sont incluses.
|
||||
|
||||
Conservez une sauvegarde avant vos essais. Pour signaler un problème, indiquez la plateforme, les versions Vulpes/Gecko et les étapes de reproduction ; retirez des logs les numéros, contacts et autres informations personnelles.
|
||||
|
||||
## 🛠️ Construire l'APK Android
|
||||
Depuis la racine du dépôt :
|
||||
|
||||
```sh
|
||||
cd android
|
||||
./build-apk.sh
|
||||
```
|
||||
|
||||
Le script installe les outils verrouillés, prépare les ressources Gaia communes et construit un APK de développement signé dans `android/dist/`. L'acceptation des licences SDK Android peut être nécessaire. Un clone neuf génère sa propre clé de signature : il ne pourra pas mettre à jour un APK signé avec la clé privée du projet.
|
||||
L’APK de développement est créé dans `android/dist/`. Une compilation personnelle utilise sa propre clé de signature : elle ne peut pas mettre à jour un APK signé avec la clé du projet.
|
||||
|
||||
## 📜 Remerciements et licences
|
||||
## Participer
|
||||
|
||||
Vulpes OS est un projet communautaire d'**oversu / NNS Production**, inspiré de Firefox OS. Merci à **Mozilla, aux contributeurs Gaia, à Capyloon, Droidian, UBports, libhybris, Debian, AOSP** et aux communautés qui font vivre les systèmes mobiles ouverts.
|
||||
Vous pouvez tester Vulpes, [signaler un problème](https://nnsprod.com/git/oversu/VulpesOS/issues), améliorer les traductions ou proposer du code. Pour un bug, indiquez la plateforme, la version utilisée et les étapes pour le reproduire.
|
||||
|
||||
Les nouveaux fichiers Vulpes utilisent **MPL 2.0**. Gaia conserve **Apache 2.0** ; les composants hérités conservent leurs licences et mentions. Voir [LICENSE](LICENSE), [NOTICE](NOTICE) et [la licence Gaia](gaia/LICENSE). Les forks doivent conserver les mentions d'attribution et de licence applicables.
|
||||
Les avancées et les comptes rendus d’essais sont dans le [changelog](CHANGELOG.md). Si vous souhaitez contribuer aux frais du projet, la page [Me soutenir](https://vulpes-os.org/support.php?lang=fr) explique à quoi sert ce soutien.
|
||||
|
||||
Vulpes OS est indépendant de Mozilla et n'est pas une version officielle de Firefox OS.
|
||||
## Crédits et licences
|
||||
|
||||
Merci à Mozilla, aux contributeurs de Gaia, à Capyloon, Droidian, UBports, libhybris, Debian et AOSP pour le code, les outils et les travaux qui ont aidé le projet.
|
||||
|
||||
Les nouveaux fichiers Vulpes sont sous **MPL 2.0**. Gaia conserve sa licence **Apache 2.0** ; les autres composants conservent leurs licences et mentions. Voir [LICENSE](LICENSE), [NOTICE](NOTICE) et [la licence de Gaia](gaia/LICENSE).
|
||||
|
||||
Vulpes OS est un projet indépendant d’**OverSu / NNS Production**, sans affiliation avec Mozilla.
|
||||
|
|
|
|||
107
README.md
107
README.md
|
|
@ -1,45 +1,52 @@
|
|||
<p align="center"><strong>🇬🇧 English</strong> · <a href="https://nnsprod.com/git/oversu/VulpesOS/src/branch/main/README.fr.md">🇫🇷 Français</a></p>
|
||||
<p align="center"><img src="docs/media/vulpes-logo.png" width="160" alt="Vulpes OS fox logo"></p>
|
||||
<p align="center"><strong>English</strong> · <a href="https://nnsprod.com/git/oversu/VulpesOS/src/branch/main/README.fr.md">Français</a></p>
|
||||
<p align="center"><img src="docs/media/vulpes-logo.png" width="120" alt="Vulpes OS"></p>
|
||||
<h1 align="center">Vulpes OS</h1>
|
||||
<p align="center"><strong>The spirit of Firefox OS. A modern Gecko. One shared Gaia.</strong><br>A community project bringing a web-based phone experience to desktop, Android and native hardware.</p>
|
||||
<p align="center">Firefox OS fork · Open source</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="https://img.shields.io/badge/Vulpes-2.7_preview.15-0755be?style=flat-square" alt="Vulpes Preview 15">
|
||||
<img src="https://img.shields.io/badge/Desktop_Gecko-156.0.1-0099cc?style=flat-square" alt="Desktop Gecko 156.0.1">
|
||||
<img src="https://img.shields.io/badge/Status-Experimental-f3a712?style=flat-square" alt="Experimental">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Vulpes-2.7%20Preview%2020-0755be?style=flat-square" alt="Vulpes: 2.7 Preview 20"></a>
|
||||
<img src="https://img.shields.io/badge/Gaia-2.7-0755be?style=flat-square" alt="Gaia: 2.7">
|
||||
<img src="https://img.shields.io/badge/Gecko-156.0.1-0099cc?style=flat-square" alt="Gecko: 156.0.1">
|
||||
<img src="https://img.shields.io/badge/Tundra-0.1.0-0099cc?style=flat-square" alt="Tundra: 0.1.0">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/releases"><img src="https://img.shields.io/badge/Android%20APK-Available-26764a?style=flat-square" alt="Android APK: Available"></a>
|
||||
<a href="#linux"><img src="https://img.shields.io/badge/Desktop-Available-26764a?style=flat-square" alt="Desktop: Available"></a>
|
||||
<a href="https://vulpes-os.org/devices/pixel-3a.php?lang=en"><img src="https://img.shields.io/badge/Native%20image-In%20development-996516?style=flat-square" alt="Native image: In development"></a>
|
||||
<a href="https://vulpes-os.org/devices/pixel-3a.php?lang=en"><img src="https://img.shields.io/badge/Pixel%203a-Bootable%20and%20usable-26764a?style=flat-square" alt="Pixel 3a: Bootable and usable"></a>
|
||||
</p>
|
||||
<p align="center"><a href="https://vulpes-os.org">🌐 Website</a> · <a href="https://nnsprod.com/git/oversu/VulpesOS/releases">📦 Downloads</a> · <a href="https://vulpes-os.org/changelog.php">📝 Changelog</a> · <a href="https://nnsprod.com/git/oversu/VulpesOS/issues">🐞 Report a bug</a></p>
|
||||
|
||||
## 🦊 The Web as your interface
|
||||
[Website](https://vulpes-os.org/?lang=en) · [Downloads](https://nnsprod.com/git/oversu/VulpesOS/releases) · [Changelog](CHANGELOG.en.md) · [Report a bug](https://nnsprod.com/git/oversu/VulpesOS/issues)
|
||||
|
||||
Home screen, settings, camera, gallery, messages: **Gaia remains the interface**, built from HTML, CSS and JavaScript. Vulpes adapts the APIs that these Firefox OS applications expect to a modern Gecko engine and platform-specific services.
|
||||
Vulpes OS picks up where Firefox OS left off, adapting **Gaia**, its interface and HTML/CSS/JavaScript applications, to run on a recent **Gecko** engine.
|
||||
|
||||
This repository now contains the shared interface, desktop host, Android application, Tundra runtime and development tools. This is an **early developer preview**, not a finished phone operating system.
|
||||
I’m OverSu, and I develop Vulpes mostly on my own, with help from people who test and contribute to the project. **Vulpes is free and open source, and will stay that way.** There are no plans for subscriptions or paid features.
|
||||
|
||||
<p align="center"><img src="docs/media/vulpes-desktop.png" width="290" alt="Actual desktop screenshot of the Gaia home screen in English"> <img src="docs/media/vulpes-gecko.png" width="290" alt="Actual desktop screenshot of device information showing the Gecko engine"></p>
|
||||
<p align="center"><em>Desktop development screenshots. Capabilities depend on the platform and available hardware.</em></p>
|
||||
<p align="center">
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-desktop.png"><img src="docs/media/vulpes-desktop.png" width="260" alt="The Gaia home screen"></a>
|
||||
<a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-gecko.png"><img src="docs/media/vulpes-gecko.png" width="260" alt="Gaia Settings, system information screen"></a>
|
||||
</p>
|
||||
<p align="center"><em>Gaia home screen and Settings · screenshots from the desktop version.</em></p>
|
||||
|
||||
## 📥 Choose your platform
|
||||
## Try Vulpes
|
||||
|
||||
| Platform | Available today | Engine |
|
||||
| --- | --- | --- |
|
||||
| **Linux desktop** | Source checkout, phone-sized Gaia window, development and app tests | Gecko **156.0.1** |
|
||||
| **Android APK** | Installable Preview; keeps Android and its permissions underneath | GeckoView **156.0** |
|
||||
| **Pixel 3a · Sargo** | Tundra sources; native development build tested on our device | Gecko **156.0.1** ARM64 |
|
||||
| Platform | How to use it |
|
||||
| --- | --- |
|
||||
| **Linux** | Run Gaia from source to explore the interface or develop applications. |
|
||||
| **Android** | Install the [APK from the releases page](https://nnsprod.com/git/oversu/VulpesOS/releases). It runs as an Android application and does not replace your phone’s operating system. |
|
||||
| **Google Pixel 3a · Sargo** | Run Vulpes as a native operating system with Tundra. A downloadable image is still being prepared; see the [device page](https://vulpes-os.org/devices/pixel-3a.php?lang=en) for tested features and known limitations. |
|
||||
|
||||
### Android — Preview 15
|
||||
On the Pixel 3a, **Tundra boots Gecko and Gaia directly** and provides access to the hardware. Droidian was used as a testing environment early in development. It is not included in Vulpes OS, and Tundra is not based on it.
|
||||
|
||||
Download the APK and `SHA256SUMS.txt` from [the Preview 15 release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15). Installing over an earlier signed Vulpes Preview preserves its data.
|
||||
### Preview 20 · 13 October 2026
|
||||
|
||||
- Minimum declared: **Android 8 / API 26**. Target: **API 37**.
|
||||
- Universal APK: **ARM64, ARMv7 and x86_64**; approximately **600 MiB** including GeckoView.
|
||||
- Camera and Gallery use Gaia, with Android permissions and file import.
|
||||
- Calls and SMS hand off to Android's applications; Vulpes is not the default SMS or phone app.
|
||||
- Android 16 emulator workflows are tested. Minimum SDK support is not a claim that every Android device is validated.
|
||||
- This is a signed **development build** with Gecko debugging available through authorized ADB, not a production-hardened app.
|
||||
[Download the APK](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20). Shared updates cover Gaia settings, camera recording, contacts and media. Native network changes are included in the sources. See the [changelog](CHANGELOG.en.md) for checks and remaining limitations.
|
||||
|
||||
### Linux — run from source
|
||||
Desktop/Sargo use Gecko **156.0.1**; Android uses GeckoView **156.0**. Android 8 / API 26 minimum is declared; not every device is tested. Calls and SMS open Android applications. The APK is a signed development build, with debugging through authorized ADB.
|
||||
|
||||
Linux x86_64, Python 3, Git and the system libraries needed by Firefox are required. On Debian/Ubuntu, install `git python3 libgtk-3-0 libdbus-glib-1-2 libasound2` (package names may differ by distribution).
|
||||
### Linux
|
||||
|
||||
You’ll need Linux x86_64, Git, Python 3 and the system libraries required by Firefox.
|
||||
|
||||
```sh
|
||||
git clone https://nnsprod.com/git/oversu/VulpesOS.git
|
||||
|
|
@ -48,45 +55,29 @@ cd VulpesOS
|
|||
./run.sh
|
||||
```
|
||||
|
||||
`setup.sh` downloads the pinned Mozilla engine and verifies its checksum. Normal startup opens the Gaia interface without a debugger. Profiles and downloaded engines stay outside Git.
|
||||
`setup.sh` downloads the Gecko engine selected for this version and verifies its checksum.
|
||||
|
||||
### Pixel 3a — native Tundra development
|
||||
### Build the APK
|
||||
|
||||
**The native build boots directly into Vulpes**, with selected Android hardware services, Linux userspace, Gecko and Gaia; Phosh and the Android application framework are not started.
|
||||
|
||||
Our installed Sargo development image has passed startup and persistence checks. Real calls and two-way SMS have been observed; the latest checks cover conversation rendering, camera sensor switching, full-resolution JPEG capture, touch focus, digital zoom and bounded vibration.
|
||||
|
||||
**A generic flashable image is not included in this release.** The current private boot depends on the inventoried phone's storage layout, staged system/data images and maintenance credentials. Publishing that file would not give another Pixel a working installation. Tundra's source recipes are included for contributors; they require device inventory, verified backups and locally supplied hardware components. Do not use another device's backup or treat a boot image as a complete ROM.
|
||||
|
||||
## ✨ What is shared
|
||||
|
||||
- **One Gaia base:** interface and common services serve desktop, Android and Tundra.
|
||||
- **Original applications:** Camera, Gallery, Settings and Messages keep their Gaia UI, backed by platform adapters.
|
||||
- **Editable web code:** change served assets in `assets/webapps/`, shared compatibility code in `host/`, and replacements in `overrides/`. Restart/reload to test; no Gecko rebuild is needed for these changes. Some historical `gaia/` sources still need their bundled counterparts updated.
|
||||
- **A replaceable engine:** pinned downloads, checksum verification, isolated compatibility checks and rollback tools. An engine update is tested and explicitly selected; compatibility is not guaranteed automatically.
|
||||
- **Separate version numbers:** Vulpes release, Gaia, Gecko and hardware adaptation are reported independently. GeckoView 156.0 is not presented as desktop Gecko 156.0.1.
|
||||
|
||||
## 🧪 Current limits
|
||||
|
||||
This preview contains incomplete hardware adapters and legacy application behavior. On Sargo, camera preview is around 16 fps; flash/video, automatic sensor rotation and full suspend remain incomplete. Network registration can be unstable and two-way call audio still needs end-to-end qualification. A reported large dialog after SMS sending remains under investigation.
|
||||
|
||||
On Android, platform integration follows Android permissions and lifecycle restrictions. Gaia alarms are not guaranteed after Android stops Vulpes. Unsupported controls may remain visible but disabled. Development/test applications are included.
|
||||
|
||||
Keep a backup before testing. Include the platform, Vulpes/Gecko versions and reproduction steps when reporting a problem; remove phone numbers, contacts and other personal information from logs.
|
||||
|
||||
## 🛠️ Build the Android APK
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
cd android
|
||||
./build-apk.sh
|
||||
```
|
||||
|
||||
The script installs the pinned toolchain, packages the shared Gaia resources and builds a signed development APK in `android/dist/`. Android SDK licenses may need acceptance. A fresh checkout generates its own signing key; it cannot update an APK signed by the project's private release key.
|
||||
The development APK is created in `android/dist/`. A personal build uses its own signing key, so it cannot update an APK signed with the project’s key.
|
||||
|
||||
## 📜 Credits and licenses
|
||||
## Contribute
|
||||
|
||||
Vulpes OS is a community project by **oversu / NNS Production**, inspired by Firefox OS. Thanks to **Mozilla, Gaia contributors, Capyloon, Droidian, UBports, libhybris, Debian, AOSP** and the communities keeping open mobile systems alive.
|
||||
You can test Vulpes, [report an issue](https://nnsprod.com/git/oversu/VulpesOS/issues), improve translations or contribute code. When reporting a bug, include your platform, the version you’re using and the steps to reproduce it.
|
||||
|
||||
New Vulpes code uses **MPL 2.0**. Gaia retains **Apache 2.0** and inherited components retain their own licenses and notices. See [LICENSE](LICENSE), [NOTICE](NOTICE) and [Gaia's license](gaia/LICENSE). Preserve the applicable attribution and license notices in forks.
|
||||
Development updates and test results are in the [changelog](CHANGELOG.en.md). If you’d like to help cover project costs, the [Support me](https://vulpes-os.org/support.php?lang=en) page explains what your contribution supports.
|
||||
|
||||
Vulpes OS is independent of Mozilla and is not an official Firefox OS release.
|
||||
## Credits and licenses
|
||||
|
||||
Thanks to Mozilla, Gaia contributors, Capyloon, Droidian, UBports, libhybris, Debian and AOSP for the code, tools and work that have helped this project.
|
||||
|
||||
New Vulpes code is licensed under **MPL 2.0**. Gaia retains its **Apache 2.0** license; other components retain their own licenses and notices. See [LICENSE](LICENSE), [NOTICE](NOTICE) and [Gaia’s license](gaia/LICENSE).
|
||||
|
||||
Vulpes OS is an independent project by **OverSu / NNS Production**, with no affiliation with Mozilla.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
||||
<uses-feature android:name="android.hardware.telephony" android:required="false" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-feature android:name="android.hardware.camera.any" android:required="false" />
|
||||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
package org.vulpes_os.preview;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import android.net.Uri;
|
||||
import android.provider.CallLog;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.Telephony;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/** Read-through access. Android remains the owner of contacts and telephony records. */
|
||||
final class AndroidPersonalData {
|
||||
private final Context context;
|
||||
private final Handler main = new Handler(Looper.getMainLooper());
|
||||
private final Map<String, ContentObserver> observers = new HashMap<>();
|
||||
private final Map<String, Runnable> pending = new HashMap<>();
|
||||
void changed(String kind) {
|
||||
Runnable previous = pending.remove(kind);
|
||||
if (previous != null) main.removeCallbacks(previous);
|
||||
Runnable next = () -> {
|
||||
pending.remove(kind);
|
||||
VulpesApplication app = (VulpesApplication) context;
|
||||
if (app.port == null) return;
|
||||
try { app.port.postMessage(new JSONObject().put("type", "android-data-changed").put("kind", kind)); }
|
||||
catch (Exception ignored) { /* The resume event refreshes detached sessions. */ }
|
||||
};
|
||||
pending.put(kind, next);
|
||||
main.postDelayed(next, 250);
|
||||
}
|
||||
void observe() {
|
||||
for (String kind : new String[]{"contacts", "sms", "calls"}) {
|
||||
if (granted(kind) && !observers.containsKey(kind)) {
|
||||
ContentObserver observer = new ContentObserver(main) {
|
||||
@Override public void onChange(boolean selfChange) { changed(kind); }
|
||||
};
|
||||
Uri uri = kind.equals("contacts") ? ContactsContract.Contacts.CONTENT_URI :
|
||||
kind.equals("sms") ? Telephony.Sms.CONTENT_URI : CallLog.Calls.CONTENT_URI;
|
||||
try {
|
||||
context.getContentResolver().registerContentObserver(uri, true, observer);
|
||||
observers.put(kind, observer);
|
||||
} catch (SecurityException ignored) { /* Permission can be revoked during registration. */ }
|
||||
} else if (!granted(kind) && observers.containsKey(kind)) {
|
||||
context.getContentResolver().unregisterContentObserver(observers.remove(kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AndroidPersonalData(Context context) { this.context = context; }
|
||||
|
||||
static String permission(String kind) {
|
||||
switch (kind) {
|
||||
case "contacts": return Manifest.permission.READ_CONTACTS;
|
||||
case "sms": return Manifest.permission.READ_SMS;
|
||||
case "calls": return Manifest.permission.READ_CALL_LOG;
|
||||
default: throw new IllegalArgumentException("INVALID_DATA_KIND");
|
||||
}
|
||||
}
|
||||
|
||||
boolean granted(String kind) {
|
||||
return context.getSharedPreferences("personal-data", 0).getBoolean(kind, false) &&
|
||||
context.checkSelfPermission(permission(kind)) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
void enable(String kind, boolean value) {
|
||||
permission(kind);
|
||||
context.getSharedPreferences("personal-data", 0).edit().putBoolean(kind, value).apply();
|
||||
observe();
|
||||
changed(kind);
|
||||
}
|
||||
|
||||
JSONObject status() throws Exception {
|
||||
JSONObject result = new JSONObject();
|
||||
for (String kind : new String[]{"contacts", "sms", "calls"}) result.put(kind, granted(kind));
|
||||
return result;
|
||||
}
|
||||
|
||||
JSONObject page(JSONObject params) throws Exception {
|
||||
String kind = params.getString("kind");
|
||||
if (!granted(kind)) throw new SecurityException("ANDROID_DATA_PERMISSION_DENIED");
|
||||
long after = params.optLong("after", 0);
|
||||
if (after < 0) throw new IllegalArgumentException("INVALID_CURSOR");
|
||||
Uri uri;
|
||||
String[] columns;
|
||||
switch (kind) {
|
||||
case "contacts":
|
||||
uri = ContactsContract.Contacts.CONTENT_URI;
|
||||
columns = new String[]{"_id", "display_name", "starred", "contact_last_updated_timestamp"};
|
||||
break;
|
||||
case "sms":
|
||||
uri = Telephony.Sms.CONTENT_URI;
|
||||
columns = new String[]{"_id", "thread_id", "address", "body", "date", "type", "read", "status"};
|
||||
break;
|
||||
default:
|
||||
uri = CallLog.Calls.CONTENT_URI;
|
||||
columns = new String[]{"_id", "number", "date", "duration", "type"};
|
||||
}
|
||||
JSONArray rows = new JSONArray();
|
||||
long last = after;
|
||||
boolean more = false;
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
// A key cursor avoids SQL LIMIT syntax differences between Android providers.
|
||||
try (Cursor cursor = resolver.query(uri, columns, "_id > ?", new String[]{Long.toString(after)}, "_id ASC")) {
|
||||
if (cursor == null) throw new IllegalStateException("ANDROID_PROVIDER_UNAVAILABLE");
|
||||
int bytes = 0;
|
||||
while (cursor.moveToNext()) {
|
||||
if (rows.length() >= 100 || bytes > 512 * 1024) { more = true; break; }
|
||||
JSONObject row = new JSONObject();
|
||||
for (int i = 0; i < columns.length; i++) {
|
||||
row.put(columns[i], cursor.isNull(i) ? JSONObject.NULL :
|
||||
cursor.getType(i) == Cursor.FIELD_TYPE_INTEGER ? cursor.getLong(i) : cursor.getString(i));
|
||||
}
|
||||
last = cursor.getLong(0);
|
||||
if (kind.equals("contacts")) contactDetails(last, row);
|
||||
rows.put(row);
|
||||
bytes += row.toString().length() * 2;
|
||||
}
|
||||
}
|
||||
return new JSONObject().put("rows", rows).put("next", more ? last : JSONObject.NULL);
|
||||
}
|
||||
|
||||
private void contactDetails(long id, JSONObject contact) throws Exception {
|
||||
JSONArray phones = new JSONArray(), emails = new JSONArray();
|
||||
contact.put("tel", phones).put("email", emails);
|
||||
try (Cursor c = context.getContentResolver().query(ContactsContract.Data.CONTENT_URI,
|
||||
new String[]{"mimetype", "data1", "data2", "data3", "is_primary"},
|
||||
"contact_id = ? AND mimetype IN (?, ?, ?)", new String[]{Long.toString(id),
|
||||
ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE,
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
|
||||
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}, "_id ASC")) {
|
||||
if (c == null) throw new IllegalStateException("ANDROID_PROVIDER_UNAVAILABLE");
|
||||
while (c.moveToNext()) {
|
||||
String mime = c.getString(0);
|
||||
if (mime.equals(ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)) {
|
||||
if (!contact.has("givenName")) contact.put("givenName", new JSONArray().put(c.getString(2)))
|
||||
.put("familyName", new JSONArray().put(c.getString(3)));
|
||||
} else {
|
||||
boolean phone = mime.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE);
|
||||
int type = c.getInt(2);
|
||||
String label = type == 0 ? c.getString(3) : phone ?
|
||||
(type == 2 ? "mobile" : type == 1 ? "home" : type == 3 ? "work" : "other") :
|
||||
(type == 1 ? "home" : type == 2 ? "work" : "other");
|
||||
(phone ? phones : emails).put(new JSONObject().put("value", c.getString(1))
|
||||
.put("type", new JSONArray().put(label == null ? "other" : label)).put("pref", c.getInt(4) != 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Intent open(JSONObject params) {
|
||||
String kind = params.optString("kind");
|
||||
if (kind.equals("contacts")) {
|
||||
String id = params.optString("id");
|
||||
if (!id.matches("[1-9][0-9]{0,15}")) throw new IllegalArgumentException("INVALID_CONTACT");
|
||||
if (!granted(kind)) throw new SecurityException("ANDROID_DATA_PERMISSION_DENIED");
|
||||
return new Intent(Intent.ACTION_VIEW, ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, Long.parseLong(id)));
|
||||
}
|
||||
if (kind.equals("sms")) return new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_APP_MESSAGING);
|
||||
if (kind.equals("calls")) return new Intent(Intent.ACTION_VIEW, CallLog.Calls.CONTENT_URI);
|
||||
throw new IllegalArgumentException("INVALID_DATA_KIND");
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ final class AndroidServices {
|
|||
CONTACT = 412;
|
||||
static final long MAX_MEDIA = 32L * 1024 * 1024;
|
||||
final VulpesApplication app;
|
||||
final AndroidPersonalData personal;
|
||||
final ExecutorService io = Executors.newSingleThreadExecutor();
|
||||
final Handler main = new Handler(Looper.getMainLooper());
|
||||
GeckoResult<Object> pending;
|
||||
|
|
@ -31,6 +32,7 @@ final class AndroidServices {
|
|||
|
||||
AndroidServices(VulpesApplication app) {
|
||||
this.app = app;
|
||||
personal = new AndroidPersonalData(app);
|
||||
}
|
||||
|
||||
GeckoResult<Object> request(JSONObject message) {
|
||||
|
|
@ -40,6 +42,30 @@ final class AndroidServices {
|
|||
JSONObject p = message.optJSONObject("params");
|
||||
if (p == null) p = new JSONObject();
|
||||
switch (op) {
|
||||
case "android.personal":
|
||||
JSONObject personalParams = p;
|
||||
String operation = p.optString("operation");
|
||||
if (operation.equals("permission")) {
|
||||
String kind = p.getString("kind");
|
||||
activity().permission(new String[]{AndroidPersonalData.permission(kind)}, granted -> {
|
||||
try { personal.enable(kind, granted); result.complete(personal.status()); } catch (Exception e) { fail(result, e); }
|
||||
});
|
||||
} else if (operation.equals("status")) result.complete(personal.status());
|
||||
else if (operation.equals("disconnect")) {
|
||||
personal.enable(p.getString("kind"), false);
|
||||
result.complete(personal.status());
|
||||
}
|
||||
else if (operation.equals("open")) {
|
||||
activity().startActivity(personal.open(p));
|
||||
result.complete(null);
|
||||
} else if (operation.equals("page")) io.execute(() -> {
|
||||
try {
|
||||
JSONObject data = personal.page(personalParams);
|
||||
main.post(() -> result.complete(data));
|
||||
} catch (Exception e) { fail(result, e); }
|
||||
});
|
||||
else throw new IllegalArgumentException("METHOD_NOT_SUPPORTED");
|
||||
break;
|
||||
case "android.notification":
|
||||
JSONObject request = p;
|
||||
if (p.optString("operation").equals("permission")) {
|
||||
|
|
@ -102,6 +128,18 @@ final class AndroidServices {
|
|||
);
|
||||
result.complete(null);
|
||||
break;
|
||||
case "android.storage":
|
||||
io.execute(() -> {
|
||||
try {
|
||||
File data = new File(app.getApplicationInfo().dataDir);
|
||||
File mediaRoot = new File(app.getFilesDir(), "media");
|
||||
JSONObject stats = new JSONObject().put("free", data.getUsableSpace())
|
||||
.put("apps", storageBytes(data, mediaRoot)).put("sdcard", storageBytes(mediaRoot, null))
|
||||
.put("scope", "vulpes-profile");
|
||||
main.post(() -> result.complete(stats));
|
||||
} catch (Exception e) { fail(result, e); }
|
||||
});
|
||||
break;
|
||||
case "android.media":
|
||||
JSONObject params = p;
|
||||
io.execute(() -> {
|
||||
|
|
@ -122,6 +160,15 @@ final class AndroidServices {
|
|||
return result;
|
||||
}
|
||||
|
||||
private static long storageBytes(File file, File skip) throws IOException {
|
||||
if (file.equals(skip) || !file.exists() || !file.getCanonicalFile().equals(file.getAbsoluteFile())) return 0;
|
||||
if (file.isFile()) return file.length();
|
||||
long size = 0;
|
||||
File[] children = file.listFiles();
|
||||
if (children != null) for (File child : children) size += storageBytes(child, skip);
|
||||
return size;
|
||||
}
|
||||
|
||||
private void fail(GeckoResult<Object> result, Exception e) {
|
||||
main.post(() -> {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ import java.util.zip.*;
|
|||
final class GaiaServer implements Closeable {
|
||||
|
||||
static final int PORT = 18765;
|
||||
final InstalledApps installed;
|
||||
private final ZipFile zip;
|
||||
private final ServerSocket socket;
|
||||
private final ExecutorService workers = Executors.newFixedThreadPool(4);
|
||||
|
||||
GaiaServer(Context context) throws IOException {
|
||||
try {installed=new InstalledApps(context);}catch(Exception e){throw new IOException(e);}
|
||||
File archive = new File(context.getCacheDir(), "gaia.zip");
|
||||
// Atomic refresh on every process start; the APK is the source of truth.
|
||||
File temp = new File(context.getCacheDir(), "gaia.zip.tmp");
|
||||
|
|
@ -94,7 +96,7 @@ final class GaiaServer implements Closeable {
|
|||
}
|
||||
String app = host.substring(0, host.indexOf('.'));
|
||||
if (app.equals("theme")) app = "default_theme";
|
||||
if (zip.getEntry(app + "/manifest.webapp") == null) {
|
||||
if (!InstalledApps.valid(app) && zip.getEntry(app + "/manifest.webapp") == null) {
|
||||
error(out, 404);
|
||||
return;
|
||||
}
|
||||
|
|
@ -109,6 +111,16 @@ final class GaiaServer implements Closeable {
|
|||
return;
|
||||
}
|
||||
if (path.endsWith("/")) path += "index.html";
|
||||
if (InstalledApps.valid(app)) {
|
||||
byte[] body=installed.read(app,path.substring(1));
|
||||
if(body==null){error(out,404);return;}
|
||||
String ext=path.substring(path.lastIndexOf('.')+1).toLowerCase(Locale.ROOT);
|
||||
String mime=MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
|
||||
if(ext.equals("js"))mime="text/javascript";
|
||||
if(ext.equals("webapp"))mime="application/json";
|
||||
if(mime==null)mime="application/octet-stream";
|
||||
out.write(("HTTP/1.1 200 OK\r\nContent-Type: "+mime+"\r\nContent-Length: "+body.length+"\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-store\r\nAccess-Control-Allow-Origin: *\r\nContent-Security-Policy: object-src 'none'; frame-ancestors http://*.localhost:"+PORT+"\r\n\r\n").getBytes(StandardCharsets.US_ASCII));out.write(body);return;
|
||||
}
|
||||
String key = path.startsWith("/_vulpes/") ? path.substring(1) : app + path;
|
||||
ZipEntry entry = zip.getEntry(key);
|
||||
if (entry == null && path.startsWith("/shared/")) entry = zip.getEntry(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package org.vulpes_os.preview;
|
||||
|
||||
import android.content.Context;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import org.json.*;
|
||||
|
||||
final class InstalledApps {
|
||||
private final File root;
|
||||
private final Map<String,JSONObject> items = new HashMap<>();
|
||||
InstalledApps(Context context) throws Exception {
|
||||
root=new File(context.getFilesDir(),"installed-apps");root.mkdirs();
|
||||
File[] files=root.listFiles((d,n)->n.matches("user-[a-f0-9]{24}\\.json"));
|
||||
if(files!=null)for(File file:files){JSONObject item=new JSONObject(new String(Files.readAllBytes(file.toPath()),StandardCharsets.UTF_8));items.put(item.getString("id"),item);}
|
||||
}
|
||||
static boolean valid(String id){return id!=null && id.matches("user-[a-f0-9]{24}");}
|
||||
private JSONObject record(JSONObject item) throws Exception {
|
||||
String id=item.getString("id"),origin="http://"+id+".localhost:"+GaiaServer.PORT;
|
||||
JSONObject manifest=new JSONObject(item.getJSONObject("manifest").toString());manifest.put("permissions",new JSONObject());manifest.put("type","web");
|
||||
return new JSONObject().put("id",id).put("origin",origin).put("manifestURL",origin+"/manifest.webapp").put("manifest",manifest).put("removable",true).put("installed",true);
|
||||
}
|
||||
synchronized JSONArray list() throws Exception {JSONArray result=new JSONArray();for(JSONObject item:items.values())result.put(record(item));return result;}
|
||||
synchronized void save(JSONObject item) throws Exception {
|
||||
String id=item.getString("id");if(!valid(id) || !item.has("files"))throw new IOException("INVALID_PACKAGE");
|
||||
byte[] data=item.toString().getBytes(StandardCharsets.UTF_8);if(data.length>70*1024*1024)throw new IOException("PACKAGE_TOO_LARGE");
|
||||
File stage=new File(root,id+".tmp");try(FileOutputStream out=new FileOutputStream(stage)){out.write(data);out.getFD().sync();}
|
||||
Files.move(stage.toPath(),new File(root,id+".json").toPath(),StandardCopyOption.REPLACE_EXISTING);items.put(id,item);
|
||||
}
|
||||
synchronized void remove(String id) throws Exception {if(!valid(id))throw new IOException("NOT_REMOVABLE");Files.deleteIfExists(new File(root,id+".json").toPath());items.remove(id);}
|
||||
synchronized byte[] read(String id,String path) throws Exception {
|
||||
JSONObject item=items.get(id);if(item==null)return null;
|
||||
if(path.equals("manifest.webapp"))return record(item).getJSONObject("manifest").toString().getBytes(StandardCharsets.UTF_8);
|
||||
String data=item.getJSONObject("files").optString(path,null);return data==null?null:android.util.Base64.decode(data,android.util.Base64.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
|
@ -45,8 +45,7 @@ public final class MainActivity extends Activity {
|
|||
root.setOnApplyWindowInsetsListener((v, insets) -> {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
android.graphics.Insets bars = insets.getInsets(
|
||||
WindowInsets.Type.systemBars() |
|
||||
WindowInsets.Type.ime() |
|
||||
WindowInsets.Type.ime() |
|
||||
WindowInsets.Type.displayCutout()
|
||||
);
|
||||
v.setPadding(bars.left, bars.top, bars.right, bars.bottom);
|
||||
|
|
@ -54,11 +53,12 @@ public final class MainActivity extends Activity {
|
|||
insets.getSystemWindowInsetLeft(),
|
||||
insets.getSystemWindowInsetTop(),
|
||||
insets.getSystemWindowInsetRight(),
|
||||
insets.getSystemWindowInsetBottom()
|
||||
insets.getSystemWindowInsetBottom() > dp(100) ? insets.getSystemWindowInsetBottom() : 0
|
||||
);
|
||||
return insets;
|
||||
});
|
||||
setContentView(root);
|
||||
immersive();
|
||||
// Respect Android's screen timeout; Gaia must not keep the phone awake indefinitely.
|
||||
float brightness = app.getSharedPreferences("android", 0).getFloat("brightness", -1);
|
||||
if (brightness >= 0) {
|
||||
|
|
@ -82,8 +82,8 @@ public final class MainActivity extends Activity {
|
|||
webPanel.addView(nav, new LinearLayout.LayoutParams(-1, dp(52)));
|
||||
Button back = new Button(this);
|
||||
back.setText("‹");
|
||||
back.setContentDescription("Retour au bureau");
|
||||
back.setOnClickListener(v -> closeBrowser());
|
||||
back.setContentDescription("Retour");
|
||||
back.setOnClickListener(v -> back());
|
||||
nav.addView(back, new LinearLayout.LayoutParams(dp(55), -1));
|
||||
address = new EditText(this);
|
||||
address.setSingleLine(true);
|
||||
|
|
@ -99,6 +99,9 @@ public final class MainActivity extends Activity {
|
|||
return true;
|
||||
});
|
||||
webView = new GeckoView(this);
|
||||
// The web panel is layered over Gaia and hidden when returning to an app.
|
||||
// A TextureView follows that visibility without leaving a separate surface.
|
||||
webView.setViewBackend(GeckoView.BACKEND_TEXTURE_VIEW);
|
||||
webPanel.addView(webView, new LinearLayout.LayoutParams(-1, 0, 1));
|
||||
home = new Button(this);
|
||||
home.setText("◯");
|
||||
|
|
@ -166,6 +169,28 @@ public final class MainActivity extends Activity {
|
|||
}
|
||||
}
|
||||
|
||||
private void immersive() {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
getWindow().setDecorFitsSystemWindows(false);
|
||||
WindowInsetsController controller = getWindow().getInsetsController();
|
||||
if (controller != null) {
|
||||
controller.setSystemBarsBehavior(WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
controller.hide(WindowInsets.Type.systemBars());
|
||||
}
|
||||
} else {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean focused) {
|
||||
super.onWindowFocusChanged(focused);
|
||||
if (focused) immersive();
|
||||
}
|
||||
|
||||
private int dp(int value) {
|
||||
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
|
@ -274,11 +299,13 @@ public final class MainActivity extends Activity {
|
|||
setHomeSurface(app.homeVisible);
|
||||
}
|
||||
|
||||
private void closeBrowser() {
|
||||
void closeBrowser() {
|
||||
boolean wasVisible = webPanel.getVisibility() == View.VISIBLE;
|
||||
webPanel.setVisibility(View.GONE);
|
||||
gaiaView.setVisibility(View.VISIBLE);
|
||||
setHomeSurface(app.homeVisible);
|
||||
syncActive();
|
||||
if (wasVisible) app.browserClosed();
|
||||
}
|
||||
|
||||
private void back() {
|
||||
|
|
@ -356,6 +383,7 @@ public final class MainActivity extends Activity {
|
|||
public void onResume() {
|
||||
super.onResume();
|
||||
resumed = true;
|
||||
immersive();
|
||||
app.activity = this;
|
||||
if (recoverGaia) {
|
||||
recoverGaia = false;
|
||||
|
|
@ -447,7 +475,7 @@ public final class MainActivity extends Activity {
|
|||
else callback.accept(granted);
|
||||
}
|
||||
|
||||
private void permission(String[] names, java.util.function.Consumer<Boolean> callback) {
|
||||
void permission(String[] names, java.util.function.Consumer<Boolean> callback) {
|
||||
boolean granted = true;
|
||||
for (String name : names)
|
||||
granted &= checkSelfPermission(name) == PackageManager.PERMISSION_GRANTED;
|
||||
|
|
@ -527,12 +555,17 @@ public final class MainActivity extends Activity {
|
|||
"http".equals(origin.getScheme()) &&
|
||||
"camera.localhost".equals(origin.getHost()) &&
|
||||
origin.getPort() == 18765;
|
||||
if (!camera || (audio != null && audio.length > 0) || video == null || video.length == 0) {
|
||||
boolean hasVideo = video != null && video.length > 0;
|
||||
boolean hasAudio = audio != null && audio.length > 0;
|
||||
if (!camera || (!hasVideo && !hasAudio)) {
|
||||
callback.reject();
|
||||
return;
|
||||
}
|
||||
permission(new String[] { android.Manifest.permission.CAMERA }, granted -> {
|
||||
if (granted) callback.grant(video[0], null);
|
||||
java.util.ArrayList<String> requested = new java.util.ArrayList<>();
|
||||
if (hasVideo) requested.add(android.Manifest.permission.CAMERA);
|
||||
if (hasAudio) requested.add(android.Manifest.permission.RECORD_AUDIO);
|
||||
permission(requested.toArray(new String[0]), granted -> {
|
||||
if (granted) callback.grant(hasVideo ? video[0] : null, hasAudio ? audio[0] : null);
|
||||
else callback.reject();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,32 @@ public final class VulpesApplication extends Application {
|
|||
GeckoSession gaiaSession;
|
||||
AndroidServices services;
|
||||
AndroidNotifications notifications;
|
||||
boolean pendingScreenLock;
|
||||
boolean homeVisible;
|
||||
boolean homeAtBottom;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
android.content.BroadcastReceiver receiver = new android.content.BroadcastReceiver() {
|
||||
@Override public void onReceive(android.content.Context context, android.content.Intent intent) {
|
||||
pendingScreenLock = true;
|
||||
sendScreenLock();
|
||||
}
|
||||
};
|
||||
android.content.IntentFilter filter = new android.content.IntentFilter(android.content.Intent.ACTION_SCREEN_OFF);
|
||||
if (android.os.Build.VERSION.SDK_INT >= 33) registerReceiver(receiver,filter,RECEIVER_NOT_EXPORTED);
|
||||
else registerReceiver(receiver,filter);
|
||||
}
|
||||
|
||||
void sendScreenLock() {
|
||||
if (port == null || !pendingScreenLock) return;
|
||||
try {
|
||||
port.postMessage(new JSONObject().put("type","android-screen-off"));
|
||||
pendingScreenLock = false;
|
||||
} catch (Exception error) {Log.w("Vulpes","Screen lock pending",error);}
|
||||
}
|
||||
|
||||
void prepare() throws IOException {
|
||||
if (runtime != null) return;
|
||||
server = new GaiaServer(this);
|
||||
|
|
@ -58,6 +81,7 @@ public final class VulpesApplication extends Application {
|
|||
@Override
|
||||
public void onConnect(WebExtension.Port p) {
|
||||
port = p;
|
||||
sendScreenLock();
|
||||
if (activity != null) notifications.click(activity.getIntent());
|
||||
p.setDelegate(
|
||||
new WebExtension.PortDelegate() {
|
||||
|
|
@ -80,6 +104,18 @@ public final class VulpesApplication extends Application {
|
|||
return GeckoResult.fromException(new SecurityException("EXTENSION_REQUIRED"));
|
||||
}
|
||||
JSONObject json = (JSONObject) message;
|
||||
if (json.optString("type").equals("marketplace.return") && activity!=null) {activity.runOnUiThread(()->activity.closeBrowser());return GeckoResult.fromValue(null);}
|
||||
if (json.optString("type").startsWith("packages.")) {
|
||||
GeckoResult<Object> result=new GeckoResult<>();
|
||||
new Thread(()->{try {
|
||||
String operation=json.getString("type");
|
||||
if(operation.equals("packages.save"))server.installed.save(json.getJSONObject("item"));
|
||||
else if(operation.equals("packages.remove"))server.installed.remove(json.getString("id"));
|
||||
else if(!operation.equals("packages.list"))throw new IOException("INVALID_OPERATION");
|
||||
result.complete(server.installed.list().toString());
|
||||
}catch(Exception error){result.completeExceptionally(error);}},"Vulpes-packages").start();
|
||||
return result;
|
||||
}
|
||||
if (json.optString("type").equals("homeScroll")) {
|
||||
homeAtBottom = json.optBoolean("bottom");
|
||||
if (activity != null) activity.setHomeSurface(homeVisible);
|
||||
|
|
@ -114,6 +150,15 @@ public final class VulpesApplication extends Application {
|
|||
);
|
||||
}
|
||||
|
||||
void browserClosed() {
|
||||
if (port == null) return;
|
||||
try {
|
||||
port.postMessage(new JSONObject().put("type", "browser-closed"));
|
||||
} catch (Exception e) {
|
||||
Log.w("Vulpes", "Browser close notification", e);
|
||||
}
|
||||
}
|
||||
|
||||
void home(boolean held) {
|
||||
if (port == null) return;
|
||||
try {
|
||||
|
|
@ -124,6 +169,8 @@ public final class VulpesApplication extends Application {
|
|||
}
|
||||
|
||||
void resumed() {
|
||||
if (services != null) services.personal.observe();
|
||||
sendScreenLock();
|
||||
if (port == null) return;
|
||||
try {
|
||||
port.postMessage(new JSONObject().put("type", "android-resume"));
|
||||
|
|
|
|||
|
|
@ -1,3 +1,7 @@
|
|||
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';
|
||||
|
|
@ -7,7 +11,12 @@ import { Messages } from './Messages.mjs';
|
|||
import { Connections } from './Connections.mjs';
|
||||
import { Alarms } from './Alarms.mjs';
|
||||
const peers = new Set();
|
||||
let apps, values, settings, dataStores, contacts, messages, connections, alarms, storage;
|
||||
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 {
|
||||
|
|
@ -26,7 +35,7 @@ function grants(app) {
|
|||
function broadcast(type, data, target) {
|
||||
for (const peer of peers) {
|
||||
if (target && peer.app.id !== target) continue;
|
||||
if (type === 'settingchange' && !grants(peer.app).includes('settings.read')) 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')
|
||||
|
|
@ -45,7 +54,7 @@ function broadcast(type, data, target) {
|
|||
notify(peer, type, data);
|
||||
}
|
||||
}
|
||||
function launch(target, entryPoint, path) {
|
||||
function launch(target, entryPoint, path, reload = false) {
|
||||
path ||=
|
||||
target.manifest.entry_points?.[entryPoint]?.launch_path ||
|
||||
target.manifest.launch_path ||
|
||||
|
|
@ -53,7 +62,7 @@ function launch(target, entryPoint, path) {
|
|||
const url = new URL(path, target.origin);
|
||||
if (url.origin !== target.origin) throw Error('INVALID_URL');
|
||||
broadcast(
|
||||
'launch',
|
||||
reload ? 'activity-launch' : 'launch',
|
||||
{
|
||||
manifestURL: target.manifestURL,
|
||||
origin: target.origin,
|
||||
|
|
@ -66,6 +75,8 @@ function launch(target, entryPoint, path) {
|
|||
}
|
||||
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 };
|
||||
|
|
@ -82,6 +93,9 @@ const ready = (async () => {
|
|||
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(
|
||||
{
|
||||
|
|
@ -91,7 +105,12 @@ const ready = (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 });
|
||||
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;
|
||||
|
|
@ -103,6 +122,7 @@ const ready = (async () => {
|
|||
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(
|
||||
|
|
@ -122,7 +142,14 @@ const ready = (async () => {
|
|||
})();
|
||||
// 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;
|
||||
|
|
@ -139,8 +166,27 @@ function connectNative() {
|
|||
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-resume') await refreshAndroid().catch(console.warn);
|
||||
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();
|
||||
|
|
@ -183,6 +229,12 @@ async function handle(peer, request) {
|
|||
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') : {};
|
||||
|
|
@ -206,6 +258,7 @@ async function handle(peer, request) {
|
|||
if (method === 'android.systemReady') {
|
||||
if (app.id !== 'system') throw Error('PERMISSION_DENIED');
|
||||
systemReady = true;
|
||||
deliverScreenLock();
|
||||
deliverNotification();
|
||||
return null;
|
||||
}
|
||||
|
|
@ -278,6 +331,10 @@ async function handle(peer, request) {
|
|||
}
|
||||
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 };
|
||||
|
|
@ -287,6 +344,14 @@ async function handle(peer, 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;
|
||||
|
|
@ -298,6 +363,10 @@ async function handle(peer, request) {
|
|||
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');
|
||||
|
|
@ -306,8 +375,8 @@ async function handle(peer, request) {
|
|||
}
|
||||
if (method === 'datastores.list') return dataStores.list(app, params.name);
|
||||
if (method === 'datastores.run') return dataStores.run(app, params);
|
||||
if (method === 'contacts.request') return contacts.request(app, params);
|
||||
if (method === 'messages.request') return messages.request(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);
|
||||
|
|
@ -319,9 +388,14 @@ async function handle(peer, request) {
|
|||
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 (app.id === 'gallery') {
|
||||
if (['gallery', 'system'].includes(app.id)) {
|
||||
const camera = apps.find((a) => a.id === 'camera');
|
||||
if (!camera) throw Error('ACTIVITY_NOT_SUPPORTED');
|
||||
launch(camera);
|
||||
|
|
@ -343,6 +417,7 @@ async function handle(peer, request) {
|
|||
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',
|
||||
|
|
@ -371,6 +446,7 @@ async function handle(peer, request) {
|
|||
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');
|
||||
}
|
||||
|
|
@ -408,6 +484,15 @@ browser.runtime.onConnect.addListener((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);
|
||||
});
|
||||
|
|
@ -415,6 +500,16 @@ browser.runtime.onConnect.addListener((port) => {
|
|||
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' } };
|
||||
|
|
@ -428,11 +523,27 @@ browser.runtime.onConnect.addListener((port) => {
|
|||
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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/* Extension world: the browser provides origin/tab/frame identity, never Gaia. */
|
||||
(() => {
|
||||
if (location.port !== '18765') return;
|
||||
if (location.port !== '18765' || location.hostname.startsWith('user-')) return;
|
||||
const win = window.wrappedJSObject;
|
||||
console.info('VULPES Android bridge', location.origin);
|
||||
let port;
|
||||
|
|
@ -25,7 +25,21 @@
|
|||
});
|
||||
}
|
||||
function onMessage(message) {
|
||||
if (message.edit) {
|
||||
try {
|
||||
const result=editor.edit(message.edit.params);
|
||||
result.inputMode=originalModes.get(editor.inputElement) ||
|
||||
(editor.inputElement.classList.contains('recipient')?'tel':'');
|
||||
port.postMessage({editReply:message.edit.id, result});
|
||||
}
|
||||
catch (error) { port.postMessage({editReply:message.edit.id, error:String(error.message)}); }
|
||||
return;
|
||||
}
|
||||
if (message.event) {
|
||||
if(message.event.type==='input-mode') {
|
||||
inputConfigurationRevision++;
|
||||
configureInput(message.event.data.enabled===true);
|
||||
}
|
||||
win.dispatchEvent(
|
||||
new window.CustomEvent('vulpes-service-event', { detail: cloneInto(message.event, win) }),
|
||||
);
|
||||
|
|
@ -37,6 +51,8 @@
|
|||
callback.resolve(cloneInto(message.reply, win));
|
||||
}
|
||||
function onDisconnect() {
|
||||
inputConfigurationRevision++;
|
||||
configureInput(false);
|
||||
for (const callback of pending.values())
|
||||
callback.reject(new window.Error('VULPES_HOST_CLOSED'));
|
||||
pending.clear();
|
||||
|
|
@ -44,12 +60,67 @@
|
|||
reconnect = setTimeout(() => {
|
||||
try {
|
||||
connect();
|
||||
refreshInputConfiguration();
|
||||
} catch (_) {
|
||||
onDisconnect();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
const editor = new InputEditor(window);
|
||||
let gaiaKeyboard = false;
|
||||
let inputConfigurationRevision = 0;
|
||||
const originalModes = new WeakMap();
|
||||
function restoreMode() {
|
||||
const field = editor.inputElement;
|
||||
if (!field || !originalModes.has(field)) return;
|
||||
const value = originalModes.get(field);
|
||||
if (value === null) field.removeAttribute('inputmode');
|
||||
else field.setAttribute('inputmode', value);
|
||||
originalModes.delete(field);
|
||||
}
|
||||
function configureInput(enabled) {
|
||||
gaiaKeyboard = enabled;
|
||||
if(!enabled) restoreMode();
|
||||
win.VulpesNativeInput = gaiaKeyboard;
|
||||
win.dispatchEvent(new window.Event('vulpes-input-configured'));
|
||||
if(enabled && !document.hidden && document.hasFocus()) reportFocus(document.activeElement);
|
||||
}
|
||||
function refreshInputConfiguration() {
|
||||
const revision = inputConfigurationRevision;
|
||||
request({version:1,id:0,method:'input.config',params:{}}).then(reply => {
|
||||
if(revision===inputConfigurationRevision) configureInput(reply.result===true);
|
||||
}).catch(error => console.error('Gaia input configuration:', error));
|
||||
}
|
||||
function reportFocus(element) {
|
||||
if (!gaiaKeyboard || editor.editing || location.hostname === 'keyboard.localhost') return false;
|
||||
if (element?.isContentEditable) element=element.closest('[contenteditable=true]') || element;
|
||||
const editable = element?.isContentEditable ||
|
||||
(['input','textarea'].includes(element?.localName) &&
|
||||
!['button','submit','range','checkbox','radio','date','time','datetime-local','file'].includes(element.type));
|
||||
if (!editable || element.disabled || element.readOnly) {
|
||||
return false;
|
||||
}
|
||||
if (element !== editor.inputElement) restoreMode();
|
||||
editor.focus(element);
|
||||
if (!originalModes.has(element)) originalModes.set(element, element.getAttribute('inputmode'));
|
||||
// Suppress Android's IME only in this editor; external web pages keep it.
|
||||
element.setAttribute('inputmode', 'none');
|
||||
const state=editor.state();
|
||||
state.inputMode=originalModes.get(element) || (element.classList.contains('recipient')?'tel':'');
|
||||
port.postMessage({inputFocus:state});
|
||||
return true;
|
||||
}
|
||||
connect();
|
||||
refreshInputConfiguration();
|
||||
for (const type of ['focusin', 'pointerdown', 'click']) addEventListener(type, event => {
|
||||
if (!gaiaKeyboard || editor.editing || location.hostname === 'keyboard.localhost') return;
|
||||
if (!reportFocus(event.target) && type === 'click' &&
|
||||
!event.target?.closest?.('[data-vulpes-keep-input]')) port.postMessage({inputFocus:null});
|
||||
}, true);
|
||||
addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && gaiaKeyboard) { restoreMode(); port.postMessage({inputFocus:null}); }
|
||||
else if (gaiaKeyboard && document.hasFocus()) reportFocus(document.activeElement);
|
||||
});
|
||||
addEventListener('unload', () => clearTimeout(reconnect));
|
||||
exportFunction(request, win.navigator, { defineAs: 'vulpesRequest' });
|
||||
const frames = new Map();
|
||||
|
|
|
|||
|
|
@ -1 +1,46 @@
|
|||
{"manifest_version":2,"name":"Vulpes Android host","version":"1.1","browser_specific_settings":{"gecko":{"id":"android-host@vulpes-os.org"}},"permissions":["storage","unlimitedStorage","nativeMessaging","geckoViewAddons","http://*.localhost/*"],"background":{"page":"background.html","persistent":true},"content_scripts":[{"matches":["http://*.localhost/*"],"js":["content.js"],"all_frames":true,"run_at":"document_start"}]}
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Vulpes Android host",
|
||||
"version": "1.1",
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "android-host@vulpes-os.org"
|
||||
}
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"unlimitedStorage",
|
||||
"nativeMessaging",
|
||||
"geckoViewAddons",
|
||||
"http://*.localhost/*",
|
||||
"https://vulpes-os.org/*"
|
||||
],
|
||||
"background": {
|
||||
"page": "background.html",
|
||||
"persistent": true
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"http://*.localhost/*"
|
||||
],
|
||||
"js": [
|
||||
"input-editor.js",
|
||||
"content.js"
|
||||
],
|
||||
"all_frames": true,
|
||||
"run_at": "document_start"
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"https://vulpes-os.org/marketplace/*"
|
||||
],
|
||||
"js": [
|
||||
"marketplace-page.js",
|
||||
"marketplace-content.js"
|
||||
],
|
||||
"all_frames": true,
|
||||
"run_at": "document_end"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
1
android/extension/marketplace-content.js
Normal file
1
android/extension/marketplace-content.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
installMarketplacePage(window,url=>browser.runtime.sendMessage({marketplaceDownload:url}).catch(console.error));
|
||||
88
android/extension/personal-data.mjs
Normal file
88
android/extension/personal-data.mjs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import {Contacts} from './Contacts.mjs';
|
||||
import {Messages} from './Messages.mjs';
|
||||
|
||||
export function authorizePersonal(app, kind, operation) {
|
||||
if (app.installed || !['settings', 'communications', 'sms', 'system'].includes(app.id))
|
||||
throw Error('PERMISSION_DENIED');
|
||||
if (operation === 'status' && app.id === 'settings') return;
|
||||
if (!['contacts', 'sms', 'calls'].includes(kind)) throw Error('INVALID_DATA_KIND');
|
||||
if (['permission', 'disconnect'].includes(operation)) {
|
||||
if (app.id !== 'settings') throw Error('PERMISSION_DENIED');
|
||||
return;
|
||||
}
|
||||
if (kind === 'contacts' && !app.manifest.permissions?.contacts?.access?.includes('read')) throw Error('PERMISSION_DENIED');
|
||||
if (kind === 'sms' && app.id !== 'sms') throw Error('PERMISSION_DENIED');
|
||||
if (kind === 'calls' && app.id !== 'communications') throw Error('PERMISSION_DENIED');
|
||||
}
|
||||
|
||||
export function contactFromAndroid(row) {
|
||||
return {id:'android:'+row._id, name:[row.display_name || ''], givenName:row.givenName || [],
|
||||
familyName:row.familyName || [], tel:row.tel || [], email:row.email || [],
|
||||
category:row.starred ? ['favorite'] : [], updated:new Date(row.contact_last_updated_timestamp || 0).toISOString()};
|
||||
}
|
||||
export function smsFromAndroid(row) {
|
||||
// Negative IDs keep Android provider rows separate from existing Vulpes messages.
|
||||
if (![row._id, row.thread_id].every(n => Number.isSafeInteger(n) && n > 0)) throw Error('INVALID_ANDROID_ID');
|
||||
const incoming = row.type === 1;
|
||||
return {id:-row._id, threadId:-row.thread_id, type:'sms',
|
||||
sender:incoming ? row.address : null, receiver:incoming ? null : row.address,
|
||||
body:row.body || '', timestamp:row.date, read:!!row.read,
|
||||
delivery:incoming ? 'received' : row.type === 2 ? 'sent' : row.type === 5 ? 'error' : 'sending',
|
||||
deliveryStatus:row.status === 0 ? 'success' : row.status >= 64 ? 'error' : 'not-applicable'};
|
||||
}
|
||||
|
||||
export class AndroidPersonalData {
|
||||
constructor(native, contacts, messages, storage) {
|
||||
Object.assign(this, {native, contacts, messages, storage});
|
||||
}
|
||||
async rows(kind) {
|
||||
const status = await this.native({operation:'status'});
|
||||
if (!status[kind]) return [];
|
||||
const rows = [];
|
||||
let after = 0;
|
||||
do {
|
||||
const page = await this.native({operation:'page', kind, after});
|
||||
rows.push(...page.rows);
|
||||
if (rows.length > 100000) throw Error('ANDROID_DATA_TOO_LARGE');
|
||||
if (page.next === null) break;
|
||||
if (!Number.isSafeInteger(page.next) || page.next <= after) throw Error('INVALID_CURSOR');
|
||||
after = page.next;
|
||||
} while (true);
|
||||
return rows;
|
||||
}
|
||||
async contactRequest(app, params) {
|
||||
const write = ['save', 'remove', 'clear'].includes(params.operation);
|
||||
const access = app.manifest.permissions?.contacts?.access || '';
|
||||
if (!access.includes(write ? 'write' : 'read')) throw Error('PERMISSION_DENIED');
|
||||
// Third-party apps keep the local address book and never inherit Android's grant.
|
||||
if (app.installed || !['communications','sms','system'].includes(app.id)) return this.contacts.request(app, params);
|
||||
if (write) {
|
||||
if (String(params.contact?.id || params.id || '').startsWith('android:') ||
|
||||
params.operation === 'clear' && (await this.native({operation:'status'})).contacts)
|
||||
throw Error('ANDROID_DATA_READ_ONLY');
|
||||
return this.contacts.request(app, params);
|
||||
}
|
||||
const remote = (await this.rows('contacts')).map(contactFromAndroid);
|
||||
const local = await this.contacts.request(app, {operation:'all'});
|
||||
const view = new Contacts(null, ()=>{});
|
||||
let revision = this.contacts.data.revision;
|
||||
for (const c of remote) for (const character of JSON.stringify(c)) revision = (revision * 31 + character.charCodeAt(0)) >>> 0;
|
||||
view.data = {revision, contacts:[...local, ...remote]};
|
||||
return view.request(app, params);
|
||||
}
|
||||
async messageRequest(app, params) {
|
||||
if (!app.manifest.permissions?.sms) throw Error('PERMISSION_DENIED');
|
||||
if (app.installed || app.id !== 'sms') return this.messages.request(app, params);
|
||||
const {operation, args=[]} = params;
|
||||
if (['delete','markMessageRead'].includes(operation) && [args[0]].flat().some(id => id < 0))
|
||||
throw Error('ANDROID_DATA_READ_ONLY');
|
||||
if (!['getMessages','getThreads','getMessage'].includes(operation)) return this.messages.request(app, params);
|
||||
const remote = (await this.rows('sms')).filter(row => row.type !== 3).map(smsFromAndroid);
|
||||
const local = await this.messages.request(app, {operation:'getMessages'});
|
||||
const view = new Messages({run:async(_store, op)=>{
|
||||
if (op !== 'get') throw Error('ANDROID_DATA_READ_ONLY');
|
||||
return {value:[...local, ...remote]};
|
||||
}});
|
||||
return view.request(app, params);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,10 +5,10 @@ import { SettingsService } from '../../services/settings.mjs';
|
|||
|
||||
const context = vm.createContext({ window: {}, Blob, Uint8Array, btoa, atob });
|
||||
vm.runInContext(
|
||||
await readFile(new URL('../ui/settings-codec.js', import.meta.url), 'utf8'),
|
||||
await readFile(new URL('../../host/settings-codec.js', import.meta.url), 'utf8'),
|
||||
context,
|
||||
);
|
||||
const { encode, decode } = context.window.VulpesAndroidSettingsCodec;
|
||||
const { encode, decode } = context.window.VulpesSettingsCodec;
|
||||
// A binary wallpaper larger than the original 1 MiB JSON ceiling, including
|
||||
// every byte value. Exercise the actual service, JSON storage and observer path.
|
||||
const pixels = Uint8Array.from({ length: 1200000 }, (_, i) => i % 256);
|
||||
|
|
|
|||
|
|
@ -125,11 +125,11 @@ result["camera_resumes_from_android_background"] = wait(
|
|||
"camera",
|
||||
'!document.hidden && !!app.camera.mozCamera && app.camera.mozCamera!==__streamBefore && app.camera.mozCamera.getVideoTracks().every(t=>t.readyState==="live") && !app.camera.isBusy',
|
||||
)
|
||||
result["unavailable_controls"] = ev(
|
||||
result["capability_controls"] = ev(
|
||||
"camera",
|
||||
'[".js-flash",".mode-switch"].every(s=>document.querySelector(s).getAttribute("aria-disabled")==="true")',
|
||||
'document.querySelector(".js-flash").getAttribute("aria-disabled")==="true" && (document.querySelector(".mode-switch").getAttribute("aria-disabled")!=="true") === !!Object.keys(app.camera.mozCamera.capabilities.recorderProfiles).length',
|
||||
)
|
||||
assert result["unavailable_controls"]
|
||||
assert result["capability_controls"]
|
||||
result["errors"] = ev(
|
||||
"camera", '__vulpesDiagnostics.filter(d=>d.kind==="error" || d.kind==="rejection")'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from pathlib import Path
|
|||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
from project import build_info, validate_assets, release_info
|
||||
from project import SOURCE_OVERRIDES, build_info, validate_assets, release_info, application_manifest
|
||||
|
||||
validate_assets()
|
||||
OUT = ROOT / "android/app/src/main/assets"
|
||||
|
|
@ -39,24 +39,15 @@ for manifest in sorted(APPS.glob("*/manifest.webapp")):
|
|||
id=name,
|
||||
origin=origin,
|
||||
manifestURL=origin + "/manifest.webapp",
|
||||
manifest=json.loads(translate(manifest.read_text())),
|
||||
manifest=application_manifest(name, json.loads(translate(manifest.read_text())), "--include-dev-apps" in sys.argv),
|
||||
)
|
||||
)
|
||||
info = build_info()
|
||||
release = release_info()
|
||||
revision = info["revision"]
|
||||
commit_time = str(info["timestamp"])
|
||||
source_overrides = {
|
||||
"system": [
|
||||
"js/launcher.js",
|
||||
"js/gaia_scheduler.js",
|
||||
"js/keyboard_manager.js",
|
||||
"js/core.js",
|
||||
"js/rocketbar.js",
|
||||
]
|
||||
}
|
||||
textsuffix = {".html", ".js", ".css", ".json", ".webapp", ".properties", ".svg"}
|
||||
head = b'<head><script src="/_vulpes/platform-config.js"></script><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><link rel="stylesheet" href="/_vulpes/desktop.css"><link rel="stylesheet" href="/_vulpes/android.css"><script src="/_vulpes/settings-codec.js"></script><script src="/_vulpes/gaia-compat.js"></script><script src="/_vulpes/gaia-wifi.js"></script><script src="/_vulpes/gaia-services.js"></script><script src="/_vulpes/gaia-styles.js"></script><script src="/_vulpes/android-ui.js"></script><script src="/_vulpes/gaia-restoration.js"></script><script src="/_vulpes/platform-ui.js"></script><script src="/_vulpes/gaia-camera.js"></script>'
|
||||
head = b'<head><script src="/_vulpes/platform-config.js"></script><meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"><link rel="stylesheet" href="/_vulpes/desktop.css"><link rel="stylesheet" href="/_vulpes/android.css"><script src="/_vulpes/settings-codec.js"></script><script src="/_vulpes/gaia-compat.js"></script><script src="/_vulpes/gaia-wifi.js"></script><script src="/_vulpes/gaia-mobile-settings.js"></script><script src="/_vulpes/gaia-bluetooth.js"></script><script src="/_vulpes/gaia-input.js"></script><script src="/_vulpes/gaia-services.js"></script><script src="/_vulpes/gaia-styles.js"></script><link rel="stylesheet" href="/_vulpes/gaia-theme.css"><script src="/_vulpes/gaia-theme.js"></script><script src="/_vulpes/gaia-select.js"></script><script src="/_vulpes/android-ui.js"></script><script src="/_vulpes/gaia-restoration.js"></script><script src="/_vulpes/platform-ui.js"></script><script src="/_vulpes/gaia-camera.js"></script>'
|
||||
if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists():
|
||||
with zipfile.ZipFile(OUT / "gaia.zip", "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
|
||||
hashes = {}
|
||||
|
|
@ -82,7 +73,7 @@ if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists():
|
|||
if p.is_file() and p.name != "manifest.webapp"
|
||||
}
|
||||
)
|
||||
for name in source_overrides.get(app["id"], []):
|
||||
for name in SOURCE_OVERRIDES.get(app["id"], []):
|
||||
files[name] = ROOT / "gaia/apps" / app["id"] / name
|
||||
for name, p in files.items():
|
||||
data = p.read_bytes()
|
||||
|
|
@ -120,13 +111,13 @@ if "--bridge-only" not in sys.argv or not (OUT / "gaia.zip").exists():
|
|||
data = translate(data.decode("utf8")).encode()
|
||||
put("_shared/" + p.relative_to(ROOT / "gaia/shared").as_posix(), data)
|
||||
put("_vulpes/platform-config.js", b'window.VulpesPlatform="android";')
|
||||
for name in ["gaia-compat.js", "gaia-services.js", "gaia-styles.js", "gaia-restoration.js", "gaia-wifi.js", "desktop.css", "platform-ui.js", "gaia-camera.js"]:
|
||||
for name in ["gaia-compat.js", "gaia-input.js", "gaia-services.js", "gaia-styles.js", "gaia-theme.js", "gaia-select.js", "gaia-mobile-settings.js", "gaia-theme.css", "gaia-restoration.js", "gaia-wifi.js", "gaia-bluetooth.js", "desktop.css", "platform-ui.js", "gaia-camera.js"]:
|
||||
text = (
|
||||
(ROOT / "host" / name)
|
||||
.read_text()
|
||||
)
|
||||
put("_vulpes/" + name, translate(text).encode())
|
||||
put("_vulpes/settings-codec.js", (ROOT / "android/ui/settings-codec.js").read_bytes())
|
||||
put("_vulpes/settings-codec.js", (ROOT / "host/settings-codec.js").read_bytes())
|
||||
put("_vulpes/android-ui.js", (ROOT / "android/ui/android-ui.js").read_bytes())
|
||||
put(
|
||||
"_vulpes/android.css",
|
||||
|
|
@ -145,6 +136,8 @@ for name in ["Connections", "Contacts", "Messages"]:
|
|||
)
|
||||
(ext / f"{name}.mjs").write_text(text)
|
||||
shutil.copy2(ROOT / "host/Alarms.sys.mjs", ext / "Alarms.mjs")
|
||||
(ext / "input-editor.js").write_text((ROOT / "services/input-editor.mjs").read_text().replace('export class InputEditor', 'class InputEditor'))
|
||||
shutil.copy2(ROOT / "host/marketplace-page.js", ext / "marketplace-page.js")
|
||||
(ext / "release.json").write_text(json.dumps(release))
|
||||
(ext / "registry.json").write_text(json.dumps(registry, ensure_ascii=False))
|
||||
settings = json.loads(translate((ROOT / "assets/settings-defaults.json").read_text()))
|
||||
|
|
|
|||
|
|
@ -3,14 +3,15 @@
|
|||
import json,sys,time
|
||||
from rdp import RDP
|
||||
|
||||
def evaluate(host, script):
|
||||
r=RDP()
|
||||
def evaluate(host, script, connection=None):
|
||||
hostname, _, path = host.partition('/')
|
||||
r=connection or RDP()
|
||||
try:
|
||||
for process in r.call('root','listProcesses')['processes']:
|
||||
if process['isParent']: continue
|
||||
target=r.call(process['actor'],'getTarget')['process']
|
||||
if 'webIsolated' not in target.get('remoteType',''): continue
|
||||
text='''(() => { const w=[...Services.ww.getWindowEnumerator()].find(w=>w.location.hostname===%s); if(!w)return null; return w.wrappedJSObject.eval(%s); })()''' % (json.dumps(host+'.localhost'),json.dumps(script))
|
||||
text='''(() => { const w=[...Services.ww.getWindowEnumerator()].find(w=>w.location.hostname===%s && (!%s || w.location.pathname.startsWith(%s))); if(!w)return null; return w.wrappedJSObject.eval(%s); })()''' % (json.dumps(hostname+'.localhost'),json.dumps(path),json.dumps('/'+path),json.dumps(script))
|
||||
response=r.call(target['consoleActor'],'evaluateJSAsync',text=text)
|
||||
if 'resultID' not in response: continue
|
||||
while True:
|
||||
|
|
@ -20,24 +21,32 @@ def evaluate(host, script):
|
|||
if value.get('result') is not None and value.get('result') != {'type':'null'}: return value.get('result')
|
||||
break
|
||||
raise RuntimeError('App not open: '+host)
|
||||
finally:r.s.close()
|
||||
finally:
|
||||
if connection is None:r.s.close()
|
||||
|
||||
def run(host,expression,timeout=30):
|
||||
def _run(host,expression,timeout,connection):
|
||||
start=time.monotonic()+timeout
|
||||
while True:
|
||||
try:
|
||||
evaluate(host,'window.__androidProbe=null; Promise.resolve().then(async()=>('+expression+')).then(value=>window.__androidProbe=JSON.stringify({value}),error=>window.__androidProbe=JSON.stringify({error:String(error),stack:error.stack})); "started"')
|
||||
evaluate(host,'window.__androidProbe=null; Promise.resolve().then(async()=>('+expression+')).then(value=>window.__androidProbe=JSON.stringify({value}),error=>window.__androidProbe=JSON.stringify({error:String(error),stack:error.stack})); "started"',connection)
|
||||
break
|
||||
except (RuntimeError, EOFError, OSError) as e:
|
||||
except RuntimeError as e:
|
||||
if time.monotonic()>=start:raise
|
||||
if isinstance(e, RuntimeError) and not str(e).startswith('App not open:'):raise
|
||||
if not str(e).startswith('App not open:'):raise
|
||||
time.sleep(.3)
|
||||
until=time.monotonic()+timeout
|
||||
while time.monotonic()<until:
|
||||
result=evaluate(host,'window.__androidProbe || "pending"')
|
||||
result=evaluate(host,'window.__androidProbe || "pending"',connection)
|
||||
if isinstance(result,str) and result != 'pending':return json.loads(result)
|
||||
time.sleep(.3)
|
||||
raise TimeoutError(host)
|
||||
|
||||
def run(host,expression,timeout=30):
|
||||
connection=RDP()
|
||||
try:
|
||||
return _run(host,expression,timeout,connection)
|
||||
finally:
|
||||
connection.s.close()
|
||||
|
||||
if __name__=='__main__':
|
||||
print(json.dumps(run(sys.argv[1],sys.argv[2]),ensure_ascii=False,indent=2))
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ class RDP:
|
|||
while True:
|
||||
r = self.recv()
|
||||
self.events.append(r)
|
||||
if str(r.get("type", "")).endswith("ListChanged"):
|
||||
continue
|
||||
if r.get("from") == to and (r.get("type") not in ["consoleAPICall", "pageError"]):
|
||||
return r
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1,202 @@
|
|||
// Home layout recovery now lives in the shared host/platform-ui.js.
|
||||
// Android-owned data stays in its provider; Gaia keeps its local data separately.
|
||||
(() => {
|
||||
const app = location.hostname.split('.')[0];
|
||||
const fr = () => (document.documentElement.lang || navigator.language).startsWith('fr');
|
||||
const text = (f,e) => fr() ? f : e;
|
||||
const call = params => VulpesCompat.call('android.personal', params);
|
||||
const notice = message => window.VulpesGaiaUI?.notice(message);
|
||||
const readOnly = () => notice(text('Ces données appartiennent à Android. Modifiez-les dans l’application Android.',
|
||||
'Android owns these records. Edit them in the Android app.'));
|
||||
const failure = () => notice(text('Accès aux données Android indisponible. Vérifiez les autorisations dans Paramètres.',
|
||||
'Android data is unavailable. Check permissions in Settings.'));
|
||||
let dirty = false;
|
||||
let selectedContact;
|
||||
const pendingCallChanges = new Set();
|
||||
function refreshCallLog() {
|
||||
if (document.hidden || !window.CallLog?.sticky) return;
|
||||
if (pendingCallChanges.has('calls')) {
|
||||
CallLog.callLogContainer.textContent = '';
|
||||
CallLog.render();
|
||||
} else if (pendingCallChanges.has('contacts')) {
|
||||
CallLog._contactCache = false;
|
||||
CallLog.updateListWithContactInfo('update');
|
||||
}
|
||||
pendingCallChanges.clear();
|
||||
}
|
||||
if(app==='communications' && location.pathname.startsWith('/contacts')) {
|
||||
// Gaia's first-paint HTML cache must not retain Android names after revocation.
|
||||
localStorage.removeItem('firstChunk');
|
||||
document.addEventListener('click',event=>{
|
||||
const row=event.target.closest?.('[data-uuid]');
|
||||
if(row) selectedContact=row.dataset.uuid;
|
||||
},true);
|
||||
let cache;
|
||||
Object.defineProperty(window,'Cache',{configurable:true,get:()=>cache,set:value=>{
|
||||
cache=value;
|
||||
Object.defineProperties(cache,{
|
||||
enabled:{get:()=>false},active:{get:()=>false},firstChunk:{set:()=>{}}
|
||||
});
|
||||
cache.apply=()=>Promise.reject();
|
||||
}});
|
||||
}
|
||||
function canRefresh() {
|
||||
if (document.hidden) return false;
|
||||
if (app === 'sms') return window.Navigation?.isCurrentPanel('thread-list');
|
||||
if (location.pathname.startsWith('/contacts')) return window.MainNavigation?.currentView() === 'view-contacts-list';
|
||||
return false;
|
||||
}
|
||||
let refreshingMessages = false;
|
||||
async function refreshMessages() {
|
||||
const view=window.ConversationView, id=view?.activeThread?.id;
|
||||
if(refreshingMessages || !(id < 0) || document.hidden || !window.Navigation?.isCurrentPanel('thread'))return;
|
||||
refreshingMessages=true;
|
||||
try {
|
||||
const rows=await VulpesCompat.call('messages.request',{operation:'getMessages',args:[{threadId:id}]});
|
||||
if(view.activeThread?.id!==id || document.hidden)return;
|
||||
const ids=new Set(rows.map(row=>String(row.id)));
|
||||
for(const node of document.querySelectorAll('[data-message-id]')) {
|
||||
if(Number(node.dataset.messageId)<0 && !ids.has(node.dataset.messageId))view.removeMessageDOM(node);
|
||||
}
|
||||
for(const row of rows) {
|
||||
const key=JSON.stringify(row),old=document.getElementById('message-'+row.id);
|
||||
if(old?.dataset.androidRecord===key)continue;
|
||||
const message={...row,timestamp:new Date(row.timestamp)};
|
||||
window.Threads?.registerMessage(message);
|
||||
await view.appendMessage(message,false);
|
||||
const node=document.getElementById('message-'+row.id);if(node)node.dataset.androidRecord=key;
|
||||
}
|
||||
dirty=false;
|
||||
} catch(error){failure();}finally{refreshingMessages=false;}
|
||||
}
|
||||
function refresh() {
|
||||
if (app === 'communications' && location.pathname.startsWith('/dialer')) {
|
||||
refreshCallLog();
|
||||
return;
|
||||
}
|
||||
if (dirty && selectedContact && !document.hidden && window.MainNavigation?.currentView()==='view-contact-details') {
|
||||
dirty = false;
|
||||
const id = selectedContact;
|
||||
VulpesCompat.call('contacts.request',{operation:'find',options:{filterBy:['id'],filterOp:'equals',filterValue:id}})
|
||||
.then(rows=>{if(rows.length)Contacts.updateContactDetail(id);else Contacts.goBack();}).catch(failure);
|
||||
return;
|
||||
}
|
||||
if(dirty && app==='sms')refreshMessages();
|
||||
if (!dirty || !canRefresh()) return;
|
||||
dirty = false;
|
||||
if(app==='sms' && window.InboxView?.container && !InboxView.inEditMode) InboxView.renderThreads();
|
||||
else if(window.contacts?.List?.load) contacts.List.load(undefined,true);
|
||||
}
|
||||
addEventListener('visibilitychange', refresh);
|
||||
addEventListener('hashchange', refresh);
|
||||
addEventListener('transitionend', refresh);
|
||||
addEventListener('vulpes-service-event', ({detail}) => {
|
||||
if (detail.type !== 'android-data-changed') return;
|
||||
if (app === 'communications' && location.pathname.startsWith('/dialer')) {
|
||||
pendingCallChanges.add(detail.data.kind);
|
||||
refreshCallLog();
|
||||
return;
|
||||
}
|
||||
dirty = true;
|
||||
refresh();
|
||||
});
|
||||
|
||||
// Keep Gaia's call grouping/rendering while reading the current Android log.
|
||||
if (app === 'communications' && location.pathname.startsWith('/dialer')) {
|
||||
let value;
|
||||
Object.defineProperty(window, 'CallLogDBManager', {configurable:true,
|
||||
get:()=>value, set:db=>{
|
||||
value=db;
|
||||
const original = Object.fromEntries(['getGroupList','getGroup','getGroupAtPosition'].map(k=>[k,db[k].bind(db)]));
|
||||
const groups = async () => {
|
||||
const rows = await call({operation:'page',kind:'calls'});
|
||||
const list = new Map();
|
||||
for (const row of rows) {
|
||||
const recent = {date:row.date,number:row.number || '',type:row.type===2 ? 'dialing':'incoming',
|
||||
status:row.type===1 ? 'connected':null};
|
||||
const id=db._getGroupId(recent), key=JSON.stringify(id);
|
||||
if(!list.has(key)) list.set(key,{id,number:recent.number,serviceId:0,lastEntryDate:row.date,retryCount:0,calls:[]});
|
||||
const g=list.get(key);g.lastEntryDate=Math.max(g.lastEntryDate,row.date);g.retryCount++;
|
||||
g.calls.push({date:row.date,duration:row.duration*1000});
|
||||
}
|
||||
return [...list.values()].map(g=>{g.calls.sort((a,b)=>b.date-a.date);return db._getGroupObject(g);});
|
||||
};
|
||||
let nativeCalls=false;
|
||||
const active=async()=> (nativeCalls=!!(await call({operation:'status',kind:'calls'})).calls);
|
||||
db.getGroupList=async(callback,sortedBy,prev,cursor,limit)=>{
|
||||
try {
|
||||
if(!await active()) return original.getGroupList(callback,sortedBy,prev,cursor,limit);
|
||||
// Native groups do not live in Gaia's IndexedDB contact cache.
|
||||
if(window.CallLog)CallLog._contactCache=false;
|
||||
let list=(await groups()).sort((a,b)=>(a.lastEntryDate-b.lastEntryDate)*(prev?-1:1));
|
||||
if(limit>0)list=list.slice(0,limit);
|
||||
if(!cursor)return callback(list);
|
||||
let index=0;
|
||||
const next=()=>queueMicrotask(()=>callback({value:list[index++] || null,continue:next}));next();
|
||||
} catch(error) { failure(); callback(cursor?{value:null}:[]); }
|
||||
};
|
||||
db.getGroup=async(number,date,type,status)=>{
|
||||
if(!await active())return original.getGroup(number,date,type,status);
|
||||
const key=db._getGroupId({number,date,type,status}).join('-');
|
||||
const found=(await groups()).find(g=>g.id===key);
|
||||
if(!found)throw Error('NotFoundError');return found;
|
||||
};
|
||||
db.getGroupAtPosition=async(position,sortedBy,prev,type,callback)=>{
|
||||
if(!await active())return original.getGroupAtPosition(position,sortedBy,prev,type,callback);
|
||||
try {const list=(await groups()).filter(g=>!type||g.type===type).sort((a,b)=>(a.lastEntryDate-b.lastEntryDate)*(prev?-1:1));callback(list[position-1]||null);}
|
||||
catch(error){failure();callback(null);}
|
||||
};
|
||||
document.addEventListener('click', event=>{
|
||||
if(!nativeCalls || !event.target.closest?.('#call-log-icon-edit'))return;
|
||||
// Editing Android logs is intentionally delegated in this first integration.
|
||||
event.preventDefault();event.stopImmediatePropagation();readOnly();
|
||||
call({operation:'open',kind:'calls'}).catch(failure);
|
||||
},true);
|
||||
}});
|
||||
}
|
||||
if(app==='communications' && location.pathname.startsWith('/contacts')) {
|
||||
const timer=setInterval(()=>{
|
||||
if(!window.Contacts?.showForm)return;
|
||||
clearInterval(timer);
|
||||
const original=Contacts.showForm;
|
||||
Contacts.showForm=function(edit,contact) {
|
||||
if(edit && String(contact?.id).startsWith('android:')) {
|
||||
selectedContact=contact.id;
|
||||
call({operation:'open',kind:'contacts',id:contact.id.slice(8)}).catch(failure);
|
||||
return;
|
||||
}
|
||||
return original.apply(this,arguments);
|
||||
};
|
||||
},100);
|
||||
addEventListener('pagehide',()=>clearInterval(timer),{once:true});
|
||||
}
|
||||
function initialize() {
|
||||
if (app !== 'settings') return;
|
||||
const root=document.querySelector('#root > div');
|
||||
if(!root || document.querySelector(".vulpes-android-data"))return;
|
||||
const header=document.createElement('header');header.className='vulpes-android-data';
|
||||
const title=document.createElement('h2');title.textContent=text('Données Android','Android data');header.append(title);
|
||||
const list=document.createElement('ul');list.className='vulpes-android-data';
|
||||
const hint=document.createElement('p');hint.style.cssText='margin:1rem 1.5rem;font-size:1.4rem;line-height:1.5';
|
||||
hint.textContent=text('Lecture des contacts, SMS (hors MMS) et appels Android. Envoi, modification et suppression dans Android. Les contacts créés dans Gaia restent locaux.',
|
||||
'Read Android contacts, SMS (not MMS) and calls. Send, edit and delete in Android. Contacts created in Gaia stay local.');
|
||||
root.append(header,hint,list);
|
||||
for(const [kind,french,english] of [['contacts','Contacts','Contacts'],['sms','SMS','SMS'],['calls','Journal d’appels','Call history']]) {
|
||||
const li=document.createElement('li'),button=document.createElement('button');
|
||||
li.append(button);list.append(li);
|
||||
let enabled=false;
|
||||
const update=state=>{enabled=state[kind];button.textContent=text(french,english)+' — '+(enabled?text('Déconnecter','Disconnect'):text('Autoriser la lecture','Allow reading'));};
|
||||
call({operation:'status'}).then(update).catch(failure);
|
||||
button.onclick=async()=>{
|
||||
button.disabled=true;
|
||||
try {
|
||||
const disconnect=enabled;
|
||||
const state=await call({operation:disconnect?'disconnect':'permission',kind});update(state);
|
||||
if(!disconnect && !state[kind])notice(text('Autorisation refusée ou restreinte par Android. L’application Android reste disponible.',
|
||||
'Permission denied or restricted by Android. Your Android app is still available.'));
|
||||
} catch(error){failure();} finally{button.disabled=false;}
|
||||
};
|
||||
}
|
||||
}
|
||||
if(app==='settings') new MutationObserver(initialize).observe(document.documentElement,{childList:true,subtree:true});
|
||||
if(document.readyState==='loading')addEventListener('DOMContentLoaded',initialize,{once:true});else initialize();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -5447,6 +5447,9 @@ define('views/viewfinder', [
|
|||
if (!preview) {
|
||||
return;
|
||||
}
|
||||
// Recompute when the embedded app becomes visible or rotates.
|
||||
this.getSize();
|
||||
if (!this.width || !this.height) { return; }
|
||||
var aspect;
|
||||
if (sensorAngle % 180 === 0) {
|
||||
this.container = {
|
||||
|
|
@ -5697,6 +5700,7 @@ define('controllers/viewfinder', [
|
|||
this.views.viewfinder.set('grid', 'off');
|
||||
};
|
||||
ViewfinderController.prototype.bindEvents = function () {
|
||||
window.addEventListener('resize', this.configurePreview);
|
||||
this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible'));
|
||||
this.views.viewfinder.on('fadedout', this.app.firer('viewfinder:hidden'));
|
||||
this.views.viewfinder.on('click', this.app.firer('viewfinder:click'));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -405,7 +405,7 @@ window.performance.measure('callLogReady','callLogStart');return;}
|
|||
self._empty=false;var currDate=new Date(cursor.value.date);batchGroupCounter++;if(!prevDate||(currDate.getTime()==prevDate.getTime())){chunk.push(cursor.value);}else{daysToRender.push(chunk);chunk=[cursor.value];var renderNow=false;if(batchGroupCounter>=MAX_GROUPS_FOR_FIRST_RENDER&&!screenRendered){renderNow=true;screenRendered=true;window.performance.mark('firstChunkReady');}else if(batchGroupCounter>=MAX_GROUPS_TO_BATCH_RENDER){renderNow=true;}
|
||||
if(renderNow){self.renderSeveralDays(daysToRender);daysToRender=[];batchGroupCounter=0;}}
|
||||
prevDate=currDate;cursor.continue();},'lastEntryDate',true,true);},renderSeveralDays:function cl_renderSeveralDays(chunks){for(var i=0,l=chunks.length;i<l;i++){this.renderChunk(chunks[i]);}},renderChunk:function cl_renderChunk(chunk){var callLogSection=this.createSectionForGroup(chunk[0]);var phoneNumbers=[];var logGroupContainer=callLogSection.getElementsByTagName('ol')[0];for(var i=0;i<chunk.length;i++){var current=chunk[i];var logGroupDOM=this.createGroup(current);logGroupContainer.appendChild(logGroupDOM);phoneNumbers.push(current.number);}
|
||||
this.callLogContainer.appendChild(callLogSection);if(!this._contactCache){this.updateListWithContactInfo(null,null,phoneNumbers,logGroupContainer);}},renderEmptyCallLog:function cl_renderEmptyCallLog(isEmptyMissedCallsGroup){this.disableEditModeButton();if(!isEmptyMissedCallsGroup){this._empty=true;}
|
||||
this.callLogContainer.appendChild(callLogSection);if(!this._contactCache){this.updateListWithContactInfo(null,null,logGroupContainer);}},renderEmptyCallLog:function cl_renderEmptyCallLog(isEmptyMissedCallsGroup){this.disableEditModeButton();if(!isEmptyMissedCallsGroup){this._empty=true;}
|
||||
var noResultContainer=document.getElementById('no-result-container');noResultContainer.hidden=false;var allCallsMsg1=document.getElementById('no-result-msg1');var allCallsMsg2=document.getElementById('no-result-msg2');var noMissedCallsMsg=document.getElementById('no-result-msg3');allCallsMsg1.hidden=isEmptyMissedCallsGroup||false;allCallsMsg2.hidden=isEmptyMissedCallsGroup||false;noMissedCallsMsg.hidden=!isEmptyMissedCallsGroup;if(!isEmptyMissedCallsGroup){var noResultContainerClone=noResultContainer.cloneNode(true);this.callLogContainer.innerHTML='';this.callLogContainer.appendChild(noResultContainerClone);}},appendGroup:function cl_appendGroup(group){if(!this._initialized){return;}
|
||||
this.unfilter();this.enableEditModeButton();var logGroupDOM=this.createGroup(group);var dayIndex=group.date;var container=this.callLogContainer;var previousLogGroup=document.getElementById(group.id);if(previousLogGroup){var parent=previousLogGroup.parentNode;parent.removeChild(previousLogGroup);this.insertInSection(logGroupDOM,parent);return logGroupDOM;}
|
||||
var groupSelector='[data-timestamp="'+dayIndex+'"]';var sectionExists=container.querySelector(groupSelector);if(sectionExists){var section=sectionExists.getElementsByTagName('ol')[0];this.insertInSection(logGroupDOM,section);return logGroupDOM;}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ window.performance.measure('callLogReady','callLogStart');return;}
|
|||
self._empty=false;var currDate=new Date(cursor.value.date);batchGroupCounter++;if(!prevDate||(currDate.getTime()==prevDate.getTime())){chunk.push(cursor.value);}else{daysToRender.push(chunk);chunk=[cursor.value];var renderNow=false;if(batchGroupCounter>=MAX_GROUPS_FOR_FIRST_RENDER&&!screenRendered){renderNow=true;screenRendered=true;window.performance.mark('firstChunkReady');}else if(batchGroupCounter>=MAX_GROUPS_TO_BATCH_RENDER){renderNow=true;}
|
||||
if(renderNow){self.renderSeveralDays(daysToRender);daysToRender=[];batchGroupCounter=0;}}
|
||||
prevDate=currDate;cursor.continue();},'lastEntryDate',true,true);},renderSeveralDays:function cl_renderSeveralDays(chunks){for(var i=0,l=chunks.length;i<l;i++){this.renderChunk(chunks[i]);}},renderChunk:function cl_renderChunk(chunk){var callLogSection=this.createSectionForGroup(chunk[0]);var phoneNumbers=[];var logGroupContainer=callLogSection.getElementsByTagName('ol')[0];for(var i=0;i<chunk.length;i++){var current=chunk[i];var logGroupDOM=this.createGroup(current);logGroupContainer.appendChild(logGroupDOM);phoneNumbers.push(current.number);}
|
||||
this.callLogContainer.appendChild(callLogSection);if(!this._contactCache){this.updateListWithContactInfo(null,null,phoneNumbers,logGroupContainer);}},renderEmptyCallLog:function cl_renderEmptyCallLog(isEmptyMissedCallsGroup){this.disableEditModeButton();if(!isEmptyMissedCallsGroup){this._empty=true;}
|
||||
this.callLogContainer.appendChild(callLogSection);if(!this._contactCache){this.updateListWithContactInfo(null,null,logGroupContainer);}},renderEmptyCallLog:function cl_renderEmptyCallLog(isEmptyMissedCallsGroup){this.disableEditModeButton();if(!isEmptyMissedCallsGroup){this._empty=true;}
|
||||
var noResultContainer=document.getElementById('no-result-container');noResultContainer.hidden=false;var allCallsMsg1=document.getElementById('no-result-msg1');var allCallsMsg2=document.getElementById('no-result-msg2');var noMissedCallsMsg=document.getElementById('no-result-msg3');allCallsMsg1.hidden=isEmptyMissedCallsGroup||false;allCallsMsg2.hidden=isEmptyMissedCallsGroup||false;noMissedCallsMsg.hidden=!isEmptyMissedCallsGroup;if(!isEmptyMissedCallsGroup){var noResultContainerClone=noResultContainer.cloneNode(true);this.callLogContainer.innerHTML='';this.callLogContainer.appendChild(noResultContainerClone);}},appendGroup:function cl_appendGroup(group){if(!this._initialized){return;}
|
||||
this.unfilter();this.enableEditModeButton();var logGroupDOM=this.createGroup(group);var dayIndex=group.date;var container=this.callLogContainer;var previousLogGroup=document.getElementById(group.id);if(previousLogGroup){var parent=previousLogGroup.parentNode;parent.removeChild(previousLogGroup);this.insertInSection(logGroupDOM,parent);return logGroupDOM;}
|
||||
var groupSelector='[data-timestamp="'+dayIndex+'"]';var sectionExists=container.querySelector(groupSelector);if(sectionExists){var section=sectionExists.getElementsByTagName('ol')[0];this.insertInSection(logGroupDOM,section);return logGroupDOM;}
|
||||
|
|
|
|||
|
|
@ -846,7 +846,7 @@ Apps.prototype={get iconSize(){if(!this._iconSize){var children=this.icons.child
|
|||
return this._iconSize;},toggleSmall:function(small){if(this.icons.classList.contains('small')===small){return;}
|
||||
this.icons.classList.toggle('small',small);this.icons.synchronise();this.refreshGridSize();this.snapScrollPosition();},toggleScrollSnapping:function(scrollSnapping){if(this.scrollable.classList.contains('snapping')===scrollSnapping){return;}
|
||||
this.scrollable.classList.toggle('snapping',scrollSnapping);this.snapScrollPosition();},onVisualLoad:function(){this.visualLoadComplete=true;this.icons.thaw();this.icons.classList.remove('loading');window.performance.mark('visuallyLoaded');window.performance.mark('contentInteractive');},addApp:function(app){var manifest=app.manifest||app.updateManifest;if(!manifest){return;}
|
||||
if(BLACKLIST.includes(app.origin)){return;}
|
||||
if(HIDDEN_ROLES.includes(manifest.role)||BLACKLIST.includes(app.origin)){return;}
|
||||
if(manifest.entry_points){for(var entryPoint in manifest.entry_points){this.addAppIcon(app,entryPoint);}}else{this.addAppIcon(app);}},addIconContainer:function(icon,entry){var container=document.createElement('div');container.classList.add('icon-container');container.order=-1;container.appendChild(icon);if(entry!==-1&&this.startupMetadata[entry].order>=0){container.order=this.startupMetadata[entry].order;var children=this.icons.children;for(var i=0,iLen=children.length;i<iLen;i++){var child=children[i];if(child.order!==-1&&child.order<container.order){continue;}
|
||||
this.icons.insertBefore(container,child);if(this.startupMetadata===null){this.iconAdded(container);}
|
||||
break;}}
|
||||
|
|
|
|||
BIN
assets/webapps/marketplace.gaiamobile.org/icon.png
Normal file
BIN
assets/webapps/marketplace.gaiamobile.org/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
5
assets/webapps/marketplace.gaiamobile.org/index.html
Normal file
5
assets/webapps/marketplace.gaiamobile.org/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
assets/webapps/marketplace.gaiamobile.org/manifest.webapp
Normal file
19
assets/webapps/marketplace.gaiamobile.org/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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
38
assets/webapps/marketplace.gaiamobile.org/marketplace.js
Normal file
38
assets/webapps/marketplace.gaiamobile.org/marketplace.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
(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);
|
||||
frame.addEventListener('mozbrowserclose',()=>{
|
||||
if(busy)return;
|
||||
status(text('Le catalogue est fermé. Vous pouvez le rouvrir ci-dessous.',
|
||||
'The catalogue is closed. You can reopen it below.'));
|
||||
document.getElementById('open').hidden=true;
|
||||
});
|
||||
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);
|
||||
})();
|
||||
|
|
@ -471,7 +471,7 @@ this.continue();}else{resolve(results);}};cursor.onerror=function(){if(this.erro
|
|||
return{remove:remove,get:get,list:list};})();;'use strict';function TonePlayer(){this._currentTone=null;this._isValid=true;this._player=new Audio();this._player.addEventListener('loadedmetadata',function(){if(this._player.src){this._isValid=true;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('error',function(){if(this._player.src){this._isValid=false;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('ended',function(){this._firePlayingCallback(false);}.bind(this));window.addEventListener('visibilitychange',function(){if(document.hidden){this.stop();this._setExclusiveMode(false);}}.bind(this));}
|
||||
TonePlayer.prototype={setTone:function(tone,callback){if(tone!==this._currentTone){this._firePlayingCallback(false);this._currentTone=tone;this._playingCallback=callback;if(tone&&tone.url){this._isValid=undefined;this._player.src=tone.url;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._isValid=true;this._player.removeAttribute('src');this._player.load();}}else{if(!this._isValid){return;}
|
||||
if(this._player.paused||this._player.ended){this._player.currentTime=0;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._player.pause();this._firePlayingCallback(false);}}},stop:function(){this._player.pause();this._firePlayingCallback(false);},get currentTone(){return this._currentTone;},isValid:function(callback){if(this._isValid!==undefined){callback(this._isValid);return;}
|
||||
this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext('ringer');this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;}
|
||||
this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext();this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;}
|
||||
var aName=a.name.toLocaleLowerCase();var bName=b.name.toLocaleLowerCase();var cmp=aName.localeCompare(bName);if(cmp){return cmp;}
|
||||
var aSubtitle=(a.subtitle||'').toLocaleLowerCase();var bSubtitle=(b.subtitle||'').toLocaleLowerCase();return aSubtitle.localeCompare(bSubtitle);}
|
||||
function domify(htmlText){var dummyDiv=document.createElement('div');dummyDiv.innerHTML=htmlText;var element=dummyDiv.firstElementChild;return element;}
|
||||
|
|
|
|||
|
|
@ -447,7 +447,7 @@ this.continue();}else{resolve(results);}};cursor.onerror=function(){if(this.erro
|
|||
return{remove:remove,get:get,list:list};})();;;'use strict';function TonePlayer(){this._currentTone=null;this._isValid=true;this._player=new Audio();this._player.addEventListener('loadedmetadata',function(){if(this._player.src){this._isValid=true;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('error',function(){if(this._player.src){this._isValid=false;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('ended',function(){this._firePlayingCallback(false);}.bind(this));window.addEventListener('visibilitychange',function(){if(document.hidden){this.stop();this._setExclusiveMode(false);}}.bind(this));}
|
||||
TonePlayer.prototype={setTone:function(tone,callback){if(tone!==this._currentTone){this._firePlayingCallback(false);this._currentTone=tone;this._playingCallback=callback;if(tone&&tone.url){this._isValid=undefined;this._player.src=tone.url;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._isValid=true;this._player.removeAttribute('src');this._player.load();}}else{if(!this._isValid){return;}
|
||||
if(this._player.paused||this._player.ended){this._player.currentTime=0;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._player.pause();this._firePlayingCallback(false);}}},stop:function(){this._player.pause();this._firePlayingCallback(false);},get currentTone(){return this._currentTone;},isValid:function(callback){if(this._isValid!==undefined){callback(this._isValid);return;}
|
||||
this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext('ringer');this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;}
|
||||
this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext();this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};;;'use strict';var ToneList=(function(){var NONE_ID='none:none';function toneCompare(a,b){if(a.id===NONE_ID&&b.id===NONE_ID){return 0;}else if(a.id===NONE_ID){return-1;}else if(b.id===NONE_ID){return 1;}
|
||||
var aName=a.name.toLocaleLowerCase();var bName=b.name.toLocaleLowerCase();var cmp=aName.localeCompare(bName);if(cmp){return cmp;}
|
||||
var aSubtitle=(a.subtitle||'').toLocaleLowerCase();var bSubtitle=(b.subtitle||'').toLocaleLowerCase();return aSubtitle.localeCompare(bSubtitle);}
|
||||
function domify(htmlText){var dummyDiv=document.createElement('div');dummyDiv.innerHTML=htmlText;var element=dummyDiv.firstElementChild;return element;}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
;;'use strict';function TonePlayer(){this._currentTone=null;this._isValid=true;this._player=new Audio();this._player.addEventListener('loadedmetadata',function(){if(this._player.src){this._isValid=true;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('error',function(){if(this._player.src){this._isValid=false;this._player.dispatchEvent(new CustomEvent('validated',{detail:this._isValid}));}}.bind(this));this._player.addEventListener('ended',function(){this._firePlayingCallback(false);}.bind(this));window.addEventListener('visibilitychange',function(){if(document.hidden){this.stop();this._setExclusiveMode(false);}}.bind(this));}
|
||||
TonePlayer.prototype={setTone:function(tone,callback){if(tone!==this._currentTone){this._firePlayingCallback(false);this._currentTone=tone;this._playingCallback=callback;if(tone&&tone.url){this._isValid=undefined;this._player.src=tone.url;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._isValid=true;this._player.removeAttribute('src');this._player.load();}}else{if(!this._isValid){return;}
|
||||
if(this._player.paused||this._player.ended){this._player.currentTime=0;this._player.play();this._firePlayingCallback(true);this._setExclusiveMode(true);}else{this._player.pause();this._firePlayingCallback(false);}}},stop:function(){this._player.pause();this._firePlayingCallback(false);},get currentTone(){return this._currentTone;},isValid:function(callback){if(this._isValid!==undefined){callback(this._isValid);return;}
|
||||
this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext('ringer');this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};
|
||||
this._player.addEventListener('validated',function validated(event){this.removeEventListener('validated',validated);callback(event.detail);});},_firePlayingCallback:function(playing){if(this._playingCallback){this._playingCallback(playing);}},_setExclusiveMode:function(exclusive){if(exclusive){if(!this._source){this._context=new AudioContext();this._source=this._context.createMediaElementSource(this._player);this._source.connect(this._context.destination);}}else{if(this._source){this._source.disconnect();this._context=null;this._source=null;}}}};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
define('modules/navigator/mozPermissionSettings',[],function(){'use strict';return window.navigator.mozPermissionSettings;});define('panels/app_permissions_list/app_permissions_list',['require','modules/settings_service','shared/manifest_helper','modules/apps_cache','modules/navigator/mozApps','modules/navigator/mozPermissionSettings'],function(require){'use strict';var SettingsService=require('modules/settings_service');var ManifestHelper=require('shared/manifest_helper');var AppsCache=require('modules/apps_cache');var mozApps=require('modules/navigator/mozApps');var mozPerms=require('modules/navigator/mozPermissionSettings');var PermissionsList=function pl(){this._listRoot=null;this._permissionsTable=null;this._permissionTableHaveProcessed=false;this._apps=null;this._enabled=false;};PermissionsList.prototype={init:function pl_init(listRoot){this._listRoot=listRoot;this._boundOnAppChoose=this._onAppChoose.bind(this);this._boundOnApplicationInstall=this._onApplicationInstall.bind(this);this._boundOnApplicationUninstall=this._onApplicationUninstall.bind(this);},set enabled(value){if(value!==this._enabled){this._enabled=value;if(this._enabled){this._bindEvents();}else{this._unbindEvents();}}},_bindEvents:function pl__bindEvents(){this._listRoot.addEventListener('click',this._boundOnAppChoose);AppsCache.addEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.addEventListener('onuninstall',this._boundOnApplicationUninstall);},_unbindEvents:function pl__unbindEvents(){this._listRoot.removeEventListener('click',this._boundOnAppChoose);AppsCache.removeEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.removeEventListener('onuninstall',this._boundOnApplicationUninstall);},setPermissionsTable:function pl_setPermissionsTable(permissionTable){this._permissionTable=permissionTable;},refresh:function pl_refresh(){var self=this;this._apps=[];if(this._permissionTableHaveProcessed){return this.loadApps();}else{return mozApps.getSelf().then(function(app){return self._initExplicitPermissionsTable(app);}).then(function(){return self.loadApps();});}},_onAppChoose:function pl__onAppChoose(evt){if(evt.target.dataset&&evt.target.dataset.appIndex){SettingsService.navigate('appPermissions-details',{app:this._apps[evt.target.dataset.appIndex],permissionsTable:this._permissionTable});}},_onApplicationInstall:function pl__onApplicationInstall(evt){var app=evt.application;this._apps.push(app);this._sortApps();this.renderList();},_onApplicationUninstall:function pl__onApplicationUninstall(evt){var app;var appIndex;this._apps.some(function findApp(anApp,index){if(anApp.origin===evt.application.origin){app=anApp;appIndex=index;return true;}
|
||||
define('modules/navigator/mozPermissionSettings',[],function(){'use strict';return window.navigator.mozPermissionSettings;});define('panels/app_permissions_list/app_permissions_list',['require','modules/settings_service','shared/manifest_helper','modules/apps_cache','modules/navigator/mozApps','modules/navigator/mozPermissionSettings'],function(require){'use strict';var SettingsService=require('modules/settings_service');var ManifestHelper=require('shared/manifest_helper');var AppsCache=require('modules/apps_cache');var mozApps=require('modules/navigator/mozApps');var mozPerms=require('modules/navigator/mozPermissionSettings');var PermissionsList=function pl(){this._listRoot=null;this._permissionsTable=null;this._permissionTableHaveProcessed=false;this._apps=null;this._enabled=false;};PermissionsList.prototype={init:function pl_init(listRoot){this._listRoot=listRoot;this._boundOnAppChoose=this._onAppChoose.bind(this);this._boundOnApplicationInstall=this._onApplicationInstall.bind(this);this._boundOnApplicationUninstall=this._onApplicationUninstall.bind(this);},set enabled(value){if(value!==this._enabled){this._enabled=value;if(this._enabled){this._bindEvents();}else{this._unbindEvents();}}},_bindEvents:function pl__bindEvents(){this._listRoot.addEventListener('click',this._boundOnAppChoose);AppsCache.addEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.addEventListener('onuninstall',this._boundOnApplicationUninstall);},_unbindEvents:function pl__unbindEvents(){this._listRoot.removeEventListener('click',this._boundOnAppChoose);AppsCache.removeEventListener('oninstall',this._boundOnApplicationInstall);AppsCache.removeEventListener('onuninstall',this._boundOnApplicationUninstall);},setPermissionsTable:function pl_setPermissionsTable(permissionTable){this._permissionTable=permissionTable;},refresh:function pl_refresh(){var self=this;this._apps=[];if(this._permissionTableHaveProcessed){return this.loadApps();}else{return mozApps.getSelf().then(function(app){return self._initExplicitPermissionsTable(app);}).then(function(){return self.loadApps();});}},_onAppChoose:function pl__onAppChoose(evt){var target=evt.target.closest('[data-app-index]');if(target&&this._listRoot.contains(target)){SettingsService.navigate('appPermissions-details',{app:this._apps[target.dataset.appIndex],permissionsTable:this._permissionTable});}},_onApplicationInstall:function pl__onApplicationInstall(evt){var app=evt.application;this._apps.push(app);this._sortApps();this.renderList();},_onApplicationUninstall:function pl__onApplicationUninstall(evt){var app;var appIndex;this._apps.some(function findApp(anApp,index){if(anApp.origin===evt.application.origin){app=anApp;appIndex=index;return true;}
|
||||
return false;});if(!app){return;}
|
||||
SettingsService.navigate('appPermissions');this._apps.splice(appIndex,1);this.renderList();},_sortApps:function pl__sortApps(){this._apps.sort(function alphabeticalSort(app,otherApp){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var otherManifest=new ManifestHelper(otherApp.manifest?otherApp.manifest:otherApp.updateManifest);return manifest.name>otherManifest.name;});},_genAppItemTemplate:function pl__genAppItemTemplate(itemData){var icon=document.createElement('img');var item=document.createElement('li');var link=document.createElement('a');var span=document.createElement('span');span.textContent=itemData.name;span.dir='auto';icon.src=itemData.iconSrc;link.dataset.appIndex=itemData.index;link.href='#';link.classList.add('menu-item');link.appendChild(icon);link.appendChild(span);item.appendChild(link);return item;},renderList:function pl_renderList(){this._listRoot.innerHTML='';var listFragment=document.createDocumentFragment();this._apps.forEach(function appIterator(app,index){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var li=this._genAppItemTemplate({name:manifest.displayName,index:index,iconSrc:this._getBestIconURL(app,manifest.icons)});listFragment.appendChild(li);}.bind(this));this._listRoot.appendChild(listFragment);},_initExplicitPermissionsTable:function pl__initExplicitPermissionsTable(app){this._currentApp=app;var table=this._permissionTable;table.explicitCertifiedPermissions=[];table.plainPermissions.forEach(function plainPermIterator(perm){if(this._isExplicitPerm(perm)){table.explicitCertifiedPermissions.push({explicitPermission:perm,permission:perm});}}.bind(this));table.composedPermissions.forEach(function permIterator(perm){table.accessModes.forEach(function modeIterator(mode){var composedPerm=perm+'-'+mode;if(this._isExplicitPerm(composedPerm)){table.explicitCertifiedPermissions.push({explicitPermission:composedPerm,permission:perm});}}.bind(this));}.bind(this));this._permissionTableHaveProcessed=true;},_isExplicitPerm:function pl_isExplicitPerm(perm){return mozPerms.isExplicit(perm,this._currentApp.manifestURL,this._currentApp.origin,false);},loadApps:function pl_loadApps(){var self=this;return AppsCache.apps().then(function(apps){self._loadApps(apps);});},_loadApps:function pl__loadApps(apps){var table=this._permissionTable;apps.forEach(function(app){var manifest=app.manifest?app.manifest:app.updateManifest;if(manifest.type!='certified'){this._apps.push(app);return;}
|
||||
SettingsService.navigate('appPermissions');this._apps.splice(appIndex,1);this.renderList();},_sortApps:function pl__sortApps(){this._apps.sort(function alphabeticalSort(app,otherApp){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var otherManifest=new ManifestHelper(otherApp.manifest?otherApp.manifest:otherApp.updateManifest);return manifest.name>otherManifest.name;});},_genAppItemTemplate:function pl__genAppItemTemplate(itemData){var icon=document.createElement('img');var item=document.createElement('li');var link=document.createElement('a');var span=document.createElement('span');span.textContent=itemData.name;span.dir='auto';icon.src=itemData.iconSrc;link.dataset.appIndex=itemData.index;link.href='#';link.classList.add('menu-item');link.appendChild(icon);link.appendChild(span);item.appendChild(link);return item;},renderList:function pl_renderList(){this._listRoot.innerHTML='';var listFragment=document.createDocumentFragment();this._apps.forEach(function appIterator(app,index){var manifest=new ManifestHelper(app.manifest?app.manifest:app.updateManifest);var li=this._genAppItemTemplate({name:manifest.displayName,index:index,iconSrc:this._getBestIconURL(app,manifest.icons)});listFragment.appendChild(li);}.bind(this));this._listRoot.appendChild(listFragment);},_initExplicitPermissionsTable:function pl__initExplicitPermissionsTable(app){this._currentApp=app;var table=this._permissionTable;table.explicitCertifiedPermissions=[];table.plainPermissions.forEach(function plainPermIterator(perm){if(this._isExplicitPerm(perm)){table.explicitCertifiedPermissions.push({explicitPermission:perm,permission:perm});}}.bind(this));table.composedPermissions.forEach(function permIterator(perm){table.accessModes.forEach(function modeIterator(mode){var composedPerm=perm+'-'+mode;if(this._isExplicitPerm(composedPerm)){table.explicitCertifiedPermissions.push({explicitPermission:composedPerm,permission:perm});}}.bind(this));}.bind(this));this._permissionTableHaveProcessed=true;},_isExplicitPerm:function pl_isExplicitPerm(perm){return mozPerms&&mozPerms.isExplicit(perm,this._currentApp.manifestURL,this._currentApp.origin,false);},loadApps:function pl_loadApps(){var self=this;return AppsCache.apps().then(function(apps){self._loadApps(apps);});},_loadApps:function pl__loadApps(apps){var table=this._permissionTable;apps.forEach(function(app){var manifest=app.manifest?app.manifest:app.updateManifest;if(manifest.type!='certified'){this._apps.push(app);return;}
|
||||
var display=table.explicitCertifiedPermissions.some(function iterator(perm){var permInfo=mozPerms.get(perm.explicitPermission,app.manifestURL,app.origin,false);return permInfo!='unknown';}.bind(this));if(display){this._apps.push(app);}}.bind(this));this._sortApps();this.renderList();},_getBestIconURL:function pl__getBestIconURL(app,icons){if(!icons||!Object.keys(icons).length){return'../style/images/default.png';}
|
||||
var preferredIconSize=30*(window.devicePixelRatio||1);var preferredSize=Number.MAX_VALUE;var max=0;for(var size in icons){size=parseInt(size,10);if(size>max){max=size;}
|
||||
if(size>=preferredIconSize&&size<preferredSize){preferredSize=size;}}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
|
||||
define('panels/screen_lock_passcode/panel',['require','modules/settings_utils','modules/dialog_panel','shared/passcode_helper'],function(require){'use strict';var SettingsUtils=require('modules/settings_utils');var DialogPanel=require('modules/dialog_panel');var PasscodeHelper=require('shared/passcode_helper');return function ctor_screenlockPasscode(){var mozSettings=window.navigator.mozSettings;return DialogPanel({_elements:{},_mode:'create',_leftApp:false,_passcodeBuffer:'',_checkingLength:{'create':8,'new':8,'edit':4,'confirm':4,'confirmLock':4},_headerL10nIdMapping:{'create':'new-passcode','edit':'current-passcode','new':'new-passcode','confirm':'enter-passcode','confirmLock':'enter-passcode'},_submitButtonL10nIdMapping:{'new':'change','create':'create','edit':'','confirm':'','confirmLock':''},onInit:function(panel){this._elements.panel=panel;this._elements.gaiaHeader=panel.querySelector('gaia-header');this._elements.header=panel.querySelector('gaia-header h1');this._elements.passcodeInput=panel.querySelector('.passcode-input');this._elements.passcodeDigits=panel.querySelectorAll('.passcode-digit');this._elements.passcodeContainer=panel.querySelector('.passcode-container');this._elements.submitButton=panel.querySelector('button[type="submit"]');this._elements.submitButtonText=this._elements.submitButton.querySelector('span');this._bindEvents();},_bindEvents:function(){this._elements.passcodeInput.onkeypress=this._onPasscodeInputKeypress.bind(this);this._elements.passcodeContainer.onclick=this._onPasscodeContainerClick.bind(this);},_onPasscodeContainerClick:function(evt){evt.preventDefault();this._elements.passcodeInput.focus();},_onPasscodeInputKeypress:function(evt){var keyCode=evt.keyCode;if(keyCode!==0&&keyCode!==8){return;}
|
||||
evt.preventDefault();if(this._passcodeBuffer===''){this._hideErrorMessage();}
|
||||
var code=evt.charCode;if(code!==0&&(code<0x30||code>0x39)){return;}
|
||||
var key=String.fromCharCode(code);if(evt.charCode===0){if(this._passcodeBuffer.length>0){this._passcodeBuffer=this._getPasscodeBuffer('all');if(this._elements.panel.dataset.passcodeStatus==='success'){this._resetPasscodeStatus();}}}else if(this._passcodeBuffer.length<8){this._passcodeBuffer+=key;}
|
||||
this._updatePassCodeUI();this._processPasscode();},_updateHeaderAndButtonUI:function(){var headerL10nId=this._headerL10nIdMapping[this._mode];var buttonL10nId=this._submitButtonL10nIdMapping[this._mode];if(headerL10nId){this._elements.header.setAttribute('data-l10n-id',headerL10nId);}
|
||||
define('panels/screen_lock_passcode/panel',['require','modules/settings_utils','modules/dialog_panel','shared/passcode_helper'],function(require){'use strict';var SettingsUtils=require('modules/settings_utils');var DialogPanel=require('modules/dialog_panel');var PasscodeHelper=require('shared/passcode_helper');return function ctor_screenlockPasscode(){var mozSettings=window.navigator.mozSettings;return DialogPanel({_elements:{},_mode:'create',_leftApp:false,_passcodeBuffer:'',_checkingLength:{'create':8,'new':8,'edit':4,'confirm':4,'confirmLock':4},_headerL10nIdMapping:{'create':'new-passcode','edit':'current-passcode','new':'new-passcode','confirm':'enter-passcode','confirmLock':'enter-passcode'},_submitButtonL10nIdMapping:{'new':'change','create':'create','edit':'','confirm':'','confirmLock':''},onInit:function(panel){this._elements.panel=panel;this._elements.gaiaHeader=panel.querySelector('gaia-header');this._elements.header=panel.querySelector('gaia-header h1');this._elements.passcodeInput=panel.querySelector('.passcode-input');this._elements.passcodeDigits=panel.querySelectorAll('.passcode-digit');this._elements.passcodeContainer=panel.querySelector('.passcode-container');this._elements.submitButton=panel.querySelector('button[type="submit"]');this._elements.submitButtonText=this._elements.submitButton.querySelector('span');this._bindEvents();},_bindEvents:function(){this._elements.passcodeInput.type='password';this._elements.passcodeInput.inputMode='numeric';this._elements.passcodeInput.maxLength=8;this._elements.passcodeInput.oninput=this._onPasscodeInput.bind(this);this._elements.passcodeContainer.onclick=this._onPasscodeContainerClick.bind(this);},_onPasscodeContainerClick:function(evt){evt.preventDefault();this._elements.passcodeInput.focus();},_onPasscodeInput: function() {
|
||||
// Android IMEs commit text without legacy keypress/charCode events.
|
||||
var value = this._elements.passcodeInput.value.replace(/[^0-9]/g, '').slice(0, 8);
|
||||
this._hideErrorMessage();
|
||||
if (this._elements.panel.dataset.passcodeStatus === 'success') {
|
||||
this._resetPasscodeStatus();
|
||||
}
|
||||
this._passcodeBuffer = value;
|
||||
this._updatePassCodeUI();
|
||||
this._processPasscode();
|
||||
},_updateHeaderAndButtonUI:function(){var headerL10nId=this._headerL10nIdMapping[this._mode];var buttonL10nId=this._submitButtonL10nIdMapping[this._mode];if(headerL10nId){this._elements.header.setAttribute('data-l10n-id',headerL10nId);}
|
||||
if(buttonL10nId===''){this._elements.submitButton.disabled=true;this._elements.submitButtonText.removeAttribute('data-l10n-id');this._elements.submitButtonText.textContent='';}else if(buttonL10nId){this._elements.submitButton.disabled=false;this._elements.submitButtonText.setAttribute('data-l10n-id',buttonL10nId);}
|
||||
SettingsUtils.runHeaderFontFit(this._elements.gaiaHeader);},_updatePassCodeUI:function(){for(var i=0;i<8;i++){if(i<this._passcodeBuffer.length){this._elements.passcodeDigits[i].dataset.dot=true;}else{delete this._elements.passcodeDigits[i].dataset.dot;}}},_processPasscode:function(){if(this._passcodeBuffer.length===this._checkingLength[this._mode]){switch(this._mode){case'create':case'new':var originalPasscode=this._getPasscodeBuffer('original');var confirmPasscode=this._getPasscodeBuffer('confirm');if(originalPasscode!==confirmPasscode){this._passcodeBuffer='';this._updatePassCodeUI();this._showErrorMessage();}else{this._showSuccessPasscodeStatus();}
|
||||
SettingsUtils.runHeaderFontFit(this._elements.gaiaHeader);},_updatePassCodeUI:function(){this._elements.passcodeInput.value=this._passcodeBuffer;for(var i=0;i<8;i++){if(i<this._passcodeBuffer.length){this._elements.passcodeDigits[i].dataset.dot=true;}else{delete this._elements.passcodeDigits[i].dataset.dot;}}},_processPasscode:function(){if(this._passcodeBuffer.length===this._checkingLength[this._mode]){switch(this._mode){case'create':case'new':var originalPasscode=this._getPasscodeBuffer('original');var confirmPasscode=this._getPasscodeBuffer('confirm');if(originalPasscode!==confirmPasscode){this._passcodeBuffer='';this._updatePassCodeUI();this._showErrorMessage();}else{this._showSuccessPasscodeStatus();}
|
||||
break;case'confirm':this._checkPasscode().then((result)=>{if(result){mozSettings.createLock().set({'lockscreen.passcode-lock.enabled':false}).then(()=>{this._showSuccessPasscodeStatus();this.cancel();}).catch((error)=>{console.log(error);});}else{this._passcodeBuffer='';this._updatePassCodeUI();}});break;case'confirmLock':this._checkPasscode().then((result)=>{if(result){mozSettings.createLock().set({'lockscreen.enabled':false,'lockscreen.passcode-lock.enabled':false}).then(()=>{this._showSuccessPasscodeStatus();this.cancel();}).catch((error)=>{console.log(error);});}else{this._passcodeBuffer='';this._updatePassCodeUI();}});break;case'edit':this._checkPasscode().then((result)=>{if(result){this._passcodeBuffer='';this._updatePassCodeUI();this._showDialogInMode('new');}else{this._passcodeBuffer='';this._updatePassCodeUI();}});break;}}},_showDialogInMode:function(mode){this._mode=mode;this._elements.panel.dataset.mode=mode;this._hideErrorMessage();this._updatePassCodeUI();this._updateHeaderAndButtonUI();},onSubmit:function(){if(this._elements.panel.dataset.passcodeStatus!=='success'){this._showErrorMessage();this._elements.passcodeInput.focus();return Promise.reject();}else{var originalPasscode=this._getPasscodeBuffer('original');return mozSettings.createLock().set({'lockscreen.passcode-lock.enabled':true}).then(()=>{return PasscodeHelper.set(originalPasscode);});}},onCancel:function(){this._elements.passcodeInput.blur();},onBeforeShow:function(panel,options){this._mode=options.mode;if(!this._leftApp){this._showDialogInMode(this._mode);}
|
||||
this._leftApp=false;},onShow:function(){this._elements.passcodeInput.focus();},onHide:function(){this._leftApp=document.hidden;if(!this._leftApp){this._passcodeBuffer='';}},_checkPasscode:function(){return PasscodeHelper.check(this._passcodeBuffer).then((result)=>{if(result){this._hideErrorMessage();}else{this._showErrorMessage();}
|
||||
return result;}).catch(()=>{this._showErrorMessage();});},_showErrorMessage:function(){this._elements.panel.dataset.passcodeStatus='error';},_hideErrorMessage:function(){this._elements.panel.dataset.passcodeStatus='';},_resetPasscodeStatus:function(){this._elements.panel.dataset.passcodeStatus='';},_showSuccessPasscodeStatus:function(){this._elements.panel.dataset.passcodeStatus='success';},_getPasscodeBuffer:function(part){if(part==='original'){return this._passcodeBuffer.substring(0,4);}else if(part==='confirm'){return this._passcodeBuffer.substring(4,8);}else if(part==='all'){return this._passcodeBuffer.substring(0,this._passcodeBuffer.length-1);}else{console.log('You are using _getPasscodeBuffer wrongly, '+'please check your argument again - '+part);}}});};});
|
||||
|
|
@ -399,7 +399,7 @@ function iteratorSV(value){this.applicationSource.addPreviouslyInstalledSvApp(va
|
|||
loadTable(DB_SV_APP_STORE_NAME,'indexSV',iteratorSV.bind(this));loadTable(DB_ITEM_STORE,'index',iterator,finish.bind(this));function addIfUnique(item){if(!cached[item.identifier]){cached[item.identifier]=true;this._allItems.push(item);}}
|
||||
function finish(){for(var i=0,iLen=collected.length;i<iLen;i++){var thisItem=collected[i];if(thisItem.type==='app'){var itemObj=this.applicationSource.mapToApp(thisItem);addIfUnique.call(this,itemObj);}else if(thisItem.type==='divider'){var divider=new GaiaGrid.Divider(thisItem);this._allItems.push(divider);}else if(thisItem.type==='bookmark'){var bookmark=new GaiaGrid.Bookmark(thisItem);addIfUnique.call(this,bookmark);}}
|
||||
this.notifyReady();if(callback&&typeof callback==='function'){callback();}}},synchronize:function(){this.sources.forEach(function eachSource(source){source.synchronize();});},initSources:function(callback){var pending=this.sources.length;var allEntries=[];var self=this;var current=0;function handleSource(){var source=self.sources[current];current++;source.populate(next);}
|
||||
handleSource();function next(entries){allEntries=allEntries.concat(entries);if(!(--pending)){callback(allEntries);}else{handleSource();}}},populate:function(callback){this.initSources(function(entries){this.deferredSave(entries,callback);}.bind(this));},notifyReady:function(){this.ready=true;dispatchEvent(new CustomEvent('databaseready'));},getNextPosition:function(){var nextPosition=this.nextPosition;this.nextPosition++;return nextPosition;},handleIAC:function(connectionRequest){if(connectionRequest.keyword!=='verticalhome-migrate'){return;}
|
||||
handleSource();function next(entries){allEntries=allEntries.concat(entries);if(!(--pending)){callback(allEntries);}else{handleSource();}}},populate:function(callback){this.initSources(function(entries){this.save(entries,callback);}.bind(this));},notifyReady:function(){this.ready=true;dispatchEvent(new CustomEvent('databaseready'));},getNextPosition:function(){var nextPosition=this.nextPosition;this.nextPosition++;return nextPosition;},handleIAC:function(connectionRequest){if(connectionRequest.keyword!=='verticalhome-migrate'){return;}
|
||||
var port=connectionRequest.port;port.onmessage=(event)=>{if(event.data.action!=='migrate'){console.error('Unhandled migration action requested:',event.data.action);return;}
|
||||
this.migrate(event.data.manifestURL);};port.start();},migrate:function(manifestURL){this.migratingTo=manifestURL;console.info('verticalhome migrating to '+this.migratingTo);if(this.migrating){return;}
|
||||
this.migrating=true;(datastore?Promise.resolve():navigator.getDataStores(DATASTORE_NAME).then(stores=>{datastore=stores[0];return Promise.resolve();},(e)=>{return Promise.reject(e);})).then(()=>{this.all(entries=>{this.saveMigrationData(entries,'detail',true).then(()=>{this.migrating=false;window.navigator.mozSettings.createLock().set({'homescreen.manifestURL':this.migratingTo});navigator.getDataStores(PREFS_STORE).then(stores=>{if(stores.length<1){console.error('Error opening verticalhome prefs datastore');return;}
|
||||
|
|
|
|||
|
|
@ -70,3 +70,6 @@ html[dir="rtl"] .wallpaper {
|
|||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
/* Keep preview tiles aligned when source wallpapers have different ratios. */
|
||||
#wallpapers .wallpaper { aspect-ratio: 2 / 3; object-fit: cover; display: block; }
|
||||
|
|
|
|||
938
changelog.json
Normal file
938
changelog.json
Normal file
|
|
@ -0,0 +1,938 @@
|
|||
{
|
||||
"schema": 1,
|
||||
"preamble": {
|
||||
"fr": "# Journal des modifications — Vulpes OS\n\n[English](CHANGELOG.en.md)\n\nLes nouveautés, par date et par plateforme. Dans chaque groupe : Add, Change, Fix, Note, Test. Les entrées décrivent la situation à leur date ; une correction en développement nécessite une nouvelle version installée.\n",
|
||||
"en": "# Changelog — Vulpes OS\n\n[Français](CHANGELOG.md)\n\nUpdates by date and platform. Within each group: Add, Change, Fix, Note, Test. Entries describe the state at their date; a fix in development requires an updated installed version.\n"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"id": "2.7-preview.21",
|
||||
"text": {
|
||||
"fr": "## 14/10/2026 : 2.7-preview.21\n\n\n\n### \n\n-   : Messages des fonctions indisponibles plus lisibles en thèmes clair et sombre.\n-   : Dictionnaires supplémentaires disponibles hors ligne ; activation et reprise du clavier Gaia fiabilisées. La vérification sur les appareils concernés reste nécessaire.\n-   : Affichage du nom des contacts rétabli au premier chargement du journal.\n-   : Panneaux Stockage rétablis. Une erreur de chargement ne bloque plus les autres menus.\n\n### \n\n-   : Lecture des contacts, SMS et journaux Android dans Gaia, après autorisation. Les données locales de Vulpes sont conservées.\n-   : Retour vers Vulpes sans écran blanc persistant ; un catalogue fermé peut être rouvert.\n-   : Le rectangle blanc signalé reste à reproduire sur les appareils concernés.\n-   : Première étape en lecture seule, hors MMS. Envoi et édition des données Android dans les applications Android ; les nouveaux contacts Gaia restent locaux. Accès SMS et journal selon les autorisations de la ROM et de l’installateur. Essais sur Android 9 ; autres versions à confirmer.\n\n### \n\n-   : Les corrections communes de Gaia sont incluses dans les images complètes et les mises à jour de démarrage.\n-   : Nouvelle image d’installation construite ; démarrage et flash sur Pixel à vérifier avant publication.\n",
|
||||
"en": "## 14/10/2026 : 2.7-preview.21\n\n\n\n### \n\n-   : Unavailable-feature messages are easier to read in light and dark themes.\n-   : Contact names now appear when the call history first loads.\n-   : Additional dictionaries available offline; improved Gaia keyboard activation and recovery. Verification on affected devices is still needed.\n-   : Storage panels restored. A loading error no longer locks the other menus.\n\n### \n\n-   : Read Android contacts, SMS and call history in Gaia after granting access. Existing local Vulpes data is preserved.\n-   : Return to Vulpes without a persistent white overlay; a closed catalogue can be reopened.\n-   : The reported blank keyboard still needs reproduction on the affected devices.\n-   : First, read-only integration, excluding MMS. Sending and editing Android records use Android apps; new Gaia contacts stay local. SMS and call log access depend on ROM and installer permissions. Tested on Android 9; other versions need verification.\n\n### \n\n-   : Shared Gaia fixes are included in full installation images and boot updates.\n-   : New installation image built; Pixel boot and flashing must be tested before publication.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.20",
|
||||
"text": {
|
||||
"fr": "## 13/10/2026 : 2.7-preview.20\n\n\n\n### \n\n-   : Mode vidéo, avec miniature et lecture dans Galerie et Vidéo.\n-   : Améliorations des paramètres, de l’apparence, des sélecteurs, du clavier, des contacts et des médias.\n-   : Changement de capteur, arrêt et reprise fiabilisés.\n-   : Compte Mozilla, Sync, localisation distante, extensions et transfert USB restent incomplets.\n\n### \n\n-   : APK Preview 20 disponible.\n-   : Appels et SMS utilisent Android. Cette Preview n’a pas encore été validée en utilisation sur Android.\n\n### \n\n-   : Aperçu autour de 16 images par seconde.\n-   : Mise à jour du Wi-Fi, du Bluetooth, des appels, des SMS/MMS et des réglages réseau.\n-   : Son des vidéos et conversation téléphonique à vérifier séparément.\n-   : Preview 20 installée sur le Pixel 3a, données conservées. Wi-Fi connecté et réseau 4G détecté.\n\n### \n\n-   : Historique FR/EN simplifié, badges et filtres par plateforme. Même contenu pour le site et les releases.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20)\n",
|
||||
"en": "## 13/10/2026 : 2.7-preview.20\n\n\n\n### \n\n-   : Video mode, with thumbnails and playback in Gallery and Video.\n-   : Improvements to settings, appearance, pickers, keyboard, contacts and media.\n-   : More reliable camera switching, shutdown and resume.\n-   : Mozilla Account, Sync, remote location, extensions and USB transfer remain incomplete.\n\n### \n\n-   : Preview 20 APK available.\n-   : Calls and SMS use Android. This Preview has not yet been validated in use on Android.\n\n### \n\n-   : Preview runs at around 16 frames per second.\n-   : Updates to Wi-Fi, Bluetooth, calls, SMS/MMS and network settings.\n-   : Video sound and two-way call audio still need separate testing.\n-   : Preview 20 installed on Pixel 3a with data preserved. Wi-Fi connected and 4G registration observed.\n\n### \n\n-   : Simpler French and English history, with badges and platform filters. Shared content for the website and releases.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20)\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models",
|
||||
"web"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-12-video-clock",
|
||||
"text": {
|
||||
"fr": "## 12/10/2026 : Vidéo et heure\n\n\n\n### \n\n-   : Enregistrement vidéo avec son, miniature et lecture dans Galerie et Vidéo.\n-   : « Améliorer Vulpes » mène aux tickets du projet.\n-   : Sélecteurs de date et d’heure rétablis ; retour du menu Clavier et texte des listes corrigés.\n\n### \n\n-   : Enregistrement vérifié avec une caméra simulée.\n\n### \n\n-   : Changements préparés pour un prochain APK ; vidéo non encore testée sur Android.\n\n### \n\n-   : Aperçu amélioré, autour de 16 images par seconde.\n-   : Heure automatique selon les informations du réseau ; fuseau horaire encore manuel.\n-   : Vidéo et changement de capteur vérifiés. Son des vidéos encore à écouter.\n",
|
||||
"en": "## 12/10/2026 : Video and time\n\n\n\n### \n\n-   : Video recording with sound, thumbnails and playback in Gallery and Video.\n-   : “Improve Vulpes” opens the project’s issues.\n-   : Date and time pickers restored; Keyboard back navigation and list text fixed.\n\n### \n\n-   : Recording checked with a simulated camera.\n\n### \n\n-   : Changes prepared for a future APK; video not yet tested on Android.\n\n### \n\n-   : Improved preview, at around 16 frames per second.\n-   : Automatic time depends on network information; time zone remains manual.\n-   : Video recording and camera switching checked. Video sound still needs listening tests.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-12-settings-calls-hotspot",
|
||||
"text": {
|
||||
"fr": "## 12/10/2026 : Appels et partage Wi-Fi\n\n\n\n### \n\n-   : Fermeture des dialogues et nouvelle tentative après une erreur corrigées.\n\n### \n\n-   : Réglages du double appel, de l’affichage du numéro et du renvoi d’appel.\n-   : Partage de connexion avec ou sans mot de passe.\n-   : Les réglages lents ne bloquent plus les appels et les SMS.\n-   : Activation du point d’accès vérifiée ; accès Internet d’un autre appareil encore à tester.\n",
|
||||
"en": "## 12/10/2026 : Calls and hotspot\n\n\n\n### \n\n-   : Dialog closing and retry after an error fixed.\n\n### \n\n-   : Settings for call waiting, caller ID and call forwarding.\n-   : Hotspot with or without a password.\n-   : Slow settings no longer block calls and SMS.\n-   : Hotspot activation checked; Internet access from another device still needs testing.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-12-settings-network",
|
||||
"text": {
|
||||
"fr": "## 12/10/2026 : Paramètres réseau\n\n\n\n### \n\n-   : Adresse IP et vitesse de connexion affichées correctement.\n\n### \n\n-   : Choix du type de réseau et modification des points d’accès mobiles.\n-   : Réglages réseau plus fiables lorsque le modem répond lentement.\n-   : Ajout, suppression et réinitialisation des points d’accès encore incomplets.\n",
|
||||
"en": "## 12/10/2026 : Network settings\n\n\n\n### \n\n-   : IP address and connection speed displayed correctly.\n\n### \n\n-   : Network type selection and mobile access point editing.\n-   : More reliable network settings when the modem responds slowly.\n-   : Adding, deleting and resetting access points remain incomplete.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-12-settings-audit",
|
||||
"text": {
|
||||
"fr": "## 12/10/2026 : Revue des paramètres\n\n\n\n### \n\n-   : Navigation, accessibilité et aide corrigées. Les panneaux incomplets restent visibles.\n-   : Menus parcourus ; certaines actions dépendent encore du matériel ou de l’opérateur.\n\n### \n\n-   : Informations de stockage et téléchargements raccordés.\n\n### \n\n-   : Affichage de l’espace utilisé par Vulpes.\n\n### \n\n-   : Données mobiles, itinérance, heure, réseaux Wi-Fi masqués et panneau Son corrigés.\n",
|
||||
"en": "## 12/10/2026 : Settings review\n\n\n\n### \n\n-   : Navigation, accessibility and help fixed. Incomplete panels remain visible.\n-   : Menus reviewed; some actions still depend on hardware or the carrier.\n\n### \n\n-   : Storage information and downloads connected.\n\n### \n\n-   : Display of storage used by Vulpes.\n\n### \n\n-   : Mobile data, roaming, time, hidden Wi-Fi networks and Sound panel fixed.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-12-settings-navigation",
|
||||
"text": {
|
||||
"fr": "## 12/10/2026 : Navigation des paramètres\n\n\n\n### \n\n-   : Choix Clair/Sombre et validation ou annulation des sélecteurs rétablis.\n-   : Gestionnaire adapté aux téléphones à une seule SIM.\n\n### \n\n-   : Informations SIM, centre de messagerie et accusés de réception.\n-   : Chargement plus rapide des réseaux enregistrés.\n-   : Certificats Wi-Fi, sécurité SIM et stockage USB encore incomplets.\n",
|
||||
"en": "## 12/10/2026 : Settings navigation\n\n\n\n### \n\n-   : Light/Dark selection and picker confirmation or cancellation restored.\n-   : Manager adapted to single-SIM phones.\n\n### \n\n-   : SIM information, message centre and delivery reports.\n-   : Faster loading of saved networks.\n-   : Wi-Fi certificates, SIM security and USB storage remain incomplete.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-12-sargo-appearance",
|
||||
"text": {
|
||||
"fr": "## 12/10/2026 : Apparence sur Pixel 3a\n\n\n\n### \n\n-   : Modes clair et sombre communs disponibles sur le Pixel 3a.\n-   : Correctifs des paramètres et polices Firefox OS intégrés.\n-   : Pas de nouvel essai des appels, SMS ou Bluetooth à cette étape.\n-   : Thèmes et affichage vérifiés sur le téléphone.\n",
|
||||
"en": "## 12/10/2026 : Appearance on Pixel 3a\n\n\n\n### \n\n-   : Shared light and dark modes available on Pixel 3a.\n-   : Settings fixes and Firefox OS fonts included.\n-   : No new call, SMS or Bluetooth test at this stage.\n-   : Themes and display checked on the phone.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-11-appearance-input",
|
||||
"text": {
|
||||
"fr": "## 11/10/2026 : Apparence et clavier\n\n\n\n### \n\n-   : Modes Clair, Sombre et Système, conservés au redémarrage.\n-   : Sélection et suppression de texte corrigées.\n\n### \n\n-   : Thèmes et saisie de SMS vérifiés.\n\n### \n\n-   : Clavier Gaia expérimental ; le clavier Android reste proposé par défaut.\n-   : Nouvelle tentative possible quand la caméra est occupée.\n-   : Preview 19 construite ; thèmes, clavier et caméra avant encore à vérifier sur Android.\n",
|
||||
"en": "## 11/10/2026 : Appearance and keyboard\n\n\n\n### \n\n-   : Light, Dark and System modes, preserved after restart.\n-   : Text selection and deletion fixed.\n\n### \n\n-   : Themes and SMS typing checked.\n\n### \n\n-   : Experimental Gaia keyboard; Android keyboard remains the default.\n-   : Retry available when the camera is busy.\n-   : Preview 19 built; themes, keyboard and front camera still need testing on Android.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-11-settings",
|
||||
"text": {
|
||||
"fr": "## 11/10/2026 : Paramètres et accueil\n\n\n\n### \n\n-   : Second écran d’accueil, aperçus de fonds d’écran et bandeau d’installation corrigés.\n-   : Fermeture du message d’erreur rétablie.\n-   : Sonneries, code de verrouillage, permissions et libellés FR/EN corrigés.\n\n### \n\n-   : Ouverture du volet en touchant la barre de statut.\n-   : Preview 18 construite localement.\n\n### \n\n-   : Retour à la version précédente après un redémarrage instable de l’interface.\n-   : Fonctionnement confirmé par l’utilisateur.\n",
|
||||
"en": "## 11/10/2026 : Settings and home screen\n\n\n\n### \n\n-   : Second home screen, wallpaper previews and installation banner fixed.\n-   : Error message dismissal restored.\n-   : Ringtones, lock code, permissions and French/English labels fixed.\n\n### \n\n-   : Open the notification tray by tapping the status bar.\n-   : Preview 18 built locally.\n\n### \n\n-   : Returned to the previous version after an unstable interface restart.\n-   : Operation confirmed by the user.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-09-mms-bluetooth",
|
||||
"text": {
|
||||
"fr": "## 09/10/2026 : MMS et Bluetooth\n\n\n\n### \n\n-   : Panneaux Gaia reliés aux capacités de chaque plateforme.\n\n### \n\n-   : Recherche, association, connexion, oubli d’appareils et sortie audio.\n-   : Envoi conservé et repris après une interruption.\n-   : MMS accepté par l’opérateur, réception non confirmée. Audio d’appel sur casque à tester.\n-   : Lecture audio Bluetooth vers un PC vérifiée.\n",
|
||||
"en": "## 09/10/2026 : MMS and Bluetooth\n\n\n\n### \n\n-   : Gaia panels connected to each platform’s capabilities.\n\n### \n\n-   : Discovery, pairing, connection, forgetting devices and audio output.\n-   : Sending preserved and resumed after interruption.\n-   : MMS accepted by the carrier, delivery not confirmed. Headset call audio still needs testing.\n-   : Bluetooth audio playback to a PC checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-08-camera-mms",
|
||||
"text": {
|
||||
"fr": "## 08/10/2026 : Caméra et MMS\n\n\n\n### \n\n-   : Changement de capteur, arrêt et affichage après rotation corrigés.\n\n### \n\n-   : Pièces jointes conservées et envoi affiché en attente jusqu’à confirmation.\n\n### \n\n-   : Aperçu mesuré autour de 16 images/s à l’arrière et 25 à 27 à l’avant.\n-   : Envoi réel encore bloqué par la connexion aux données mobiles.\n-   : Mise au point, zoom et prise de photo vérifiés.\n",
|
||||
"en": "## 08/10/2026 : Camera and MMS\n\n\n\n### \n\n-   : Camera switching, shutdown and display after rotation fixed.\n\n### \n\n-   : Attachments preserved and sending shown as pending until confirmation.\n\n### \n\n-   : Preview measured at around 16 fps on the rear camera and 25–27 on the front.\n-   : Real sending still blocked by the mobile data connection.\n-   : Focus, zoom and photo capture checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-08-call-log-dialogs",
|
||||
"text": {
|
||||
"fr": "## 08/10/2026 : Journal d’appel et dialogues\n\n### \n\n-   : Messages d’erreur lisibles, avec des boutons utilisables.\n\n### \n\n-   : Message clair si l’envoi est indisponible, sans perdre le brouillon.\n-   : Rappel depuis le journal d’appel rétabli.\n\n### \n\n-   : Parcours vérifié avec des appels simulés ; audio réel non testé.\n",
|
||||
"en": "## 08/10/2026 : Call history and dialogs\n\n### \n\n-   : Readable error messages with usable buttons.\n\n### \n\n-   : Clear message when sending is unavailable, without losing the draft.\n-   : Calling back from call history restored.\n\n### \n\n-   : Flow checked with simulated calls; real audio not tested.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-08-sms-audio",
|
||||
"text": {
|
||||
"fr": "## 08/10/2026 : SMS et audio\n\n\n\n### \n\n-   : Affichage du volume, verrouillage et balayage des notifications corrigés.\n-   : Envoi en un appui, sans fermer d’abord le clavier. Menu des pièces jointes rétabli.\n\n### \n\n-   : Corrections du routage du son des appels.\n-   : Le réseau a empêché de vérifier une conversation réelle ; son dans les deux sens non confirmé.\n",
|
||||
"en": "## 08/10/2026 : SMS and audio\n\n\n\n### \n\n-   : Volume display, lock screen and notification swiping fixed.\n-   : Send with one tap without first closing the keyboard. Attachment menu restored.\n\n### \n\n-   : Call audio routing fixes.\n-   : Network issues prevented testing a real conversation; two-way audio not confirmed.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-08-radio-reliability",
|
||||
"text": {
|
||||
"fr": "## 08/10/2026 : Fiabilité du réseau\n\n\n\n### \n\n-   : Messages associés à la bonne conversation.\n\n### \n\n-   : Récupération du modem améliorée ; les nouvelles tentatives évitent de doubler appels et SMS.\n-   : Stabilité du réseau et audio dans les deux sens encore à confirmer.\n-   : Retour sur le réseau 4G après redémarrage du modem vérifié.\n",
|
||||
"en": "## 08/10/2026 : Network reliability\n\n\n\n### \n\n-   : Messages linked to the correct conversation.\n\n### \n\n-   : Improved modem recovery; retries avoid duplicate calls and SMS.\n-   : Network stability and two-way audio still need confirmation.\n-   : Return to the 4G network after modem restart checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2026-10-08-source-publication",
|
||||
"text": {
|
||||
"fr": "## 08/10/2026 : Sources publiques\n\n### \n\n-   : Publication des corrections caméra, contacts, SMS et Marketplace, avec présentations FR/EN à jour.\n\n### \n\n-   : Construction depuis une copie neuve du dépôt corrigée.\n-   : L’APK public reste en Preview 17 ; ces corrections sont dans les sources.\n\n### \n\n-   : Recherche de réseau encore instable à cette étape ; pas de nouvelle image publiée.\n",
|
||||
"en": "## 08/10/2026 : Public source code\n\n### \n\n-   : Camera, contacts, SMS and Marketplace fixes published, with updated French/English project overviews.\n\n### \n\n-   : Building from a fresh repository copy fixed.\n-   : The public APK remains Preview 17; these fixes are in the source code.\n\n### \n\n-   : Network search still unstable at this stage; no new image published.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-08-camera-lifecycle",
|
||||
"text": {
|
||||
"fr": "## 08/10/2026 : Caméra et contacts\n\n\n\n### \n\n-   : Ouverture et changement de capteur fiabilisés.\n-   : Photos, dates, modifications et choix d’un numéro depuis SMS ou Téléphone corrigés.\n-   : Sélection des médias corrigée.\n-   : Correctifs dans les sources ; APK public inchangé à cette étape.\n\n### \n\n-   : Numéro et texte transmis correctement à l’application Android.\n\n### \n\n-   : Recherche disponible ; association et usages audio encore à vérifier.\n-   : Photo, mise au point, zoom et changements de capteur vérifiés.\n",
|
||||
"en": "## 08/10/2026 : Camera and contacts\n\n\n\n### \n\n-   : More reliable opening and camera switching.\n-   : Photos, dates, edits and number selection from Messages or Phone fixed.\n-   : Media selection fixed.\n-   : Fixes in source code; public APK unchanged at this stage.\n\n### \n\n-   : Number and text passed correctly to the Android app.\n\n### \n\n-   : Discovery available; pairing and audio use still need testing.\n-   : Photo capture, focus, zoom and camera switching checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-06-marketplace-theme",
|
||||
"text": {
|
||||
"fr": "## 06/10/2026 : Thème Marketplace\n\n\n\n### \n\n-   : Intégration prévue dans un prochain APK.\n\n### \n\n-   : Thème intégré : police Fira, navigation compacte, boutons tactiles et captures défilantes.\n-   : Affichage intégré conservé pendant la recherche, les filtres et les changements de langue.\n-   : Thème préparé localement, pas encore déployé sur le site.\n-   : Pages françaises et anglaises vérifiées sur petits écrans.\n",
|
||||
"en": "## 06/10/2026 : Marketplace theme\n\n\n\n### \n\n-   : Integration planned for a future APK.\n\n### \n\n-   : Embedded theme: Fira font, compact navigation, touch buttons and scrollable screenshots.\n-   : Embedded display preserved during search, filtering and language changes.\n-   : Theme prepared locally, not yet deployed to the website.\n-   : French and English pages checked on small screens.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"apk",
|
||||
"web"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-06-system-banner",
|
||||
"text": {
|
||||
"fr": "## 06/10/2026 : Bandeaux de notification\n\n\n\n### \n\n-   : Le bandeau « Application installée » disparaît automatiquement et se remplace correctement.\n\n### \n\n-   : Affichage et fermeture des bandeaux vérifiés.\n\n### \n\n-   : Correctif préparé pour le prochain APK.\n\n### \n\n-   : Disparition du bandeau après cinq secondes vérifiée.\n",
|
||||
"en": "## 06/10/2026 : Notification banners\n\n\n\n### \n\n-   : The “Application installed” banner disappears automatically and is replaced correctly.\n\n### \n\n-   : Banner display and dismissal checked.\n\n### \n\n-   : Fix prepared for the next APK.\n\n### \n\n-   : Banner disappearance after five seconds checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-06-wifi",
|
||||
"text": {
|
||||
"fr": "## 06/10/2026 : Wi-Fi et paramètres\n\n\n\n### \n\n-   : Ouverture des panneaux rétablie quand une animation ne se termine pas normalement.\n\n### \n\n-   : Correctifs encore absents de l’APK public.\n\n### \n\n-   : Réseaux détectés et déconnexions affichés correctement.\n-   : Recherche, connexion et accès Internet vérifiés.\n",
|
||||
"en": "## 06/10/2026 : Wi-Fi and settings\n\n\n\n### \n\n-   : Panels open even when an animation does not finish normally.\n\n### \n\n-   : Fixes not yet included in the public APK.\n\n### \n\n-   : Detected networks and disconnections displayed correctly.\n-   : Search, connection and Internet access checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-10-06-marketplace-views",
|
||||
"text": {
|
||||
"fr": "## 06/10/2026 : Navigation Marketplace\n\n\n\n### \n\n-   : Message local avec bouton Réessayer si le catalogue est inaccessible.\n-   : Le catalogue reste sous la barre de statut et se masque en revenant à l’accueil ou en le fermant.\n-   : Une ancienne erreur ne masque plus une nouvelle tentative.\n\n### \n\n-   : Navigation, erreurs, retour à l’accueil et fermeture vérifiés.\n\n### \n\n-   : Preview 17 reconstruite localement, sans nouveau test Android.\n\n### \n\n-   : Interface testée avec une page locale ; catalogue distant non vérifié sans Internet.\n",
|
||||
"en": "## 06/10/2026 : Marketplace navigation\n\n\n\n### \n\n-   : Local message with a Retry button when the catalogue is unavailable.\n-   : The catalogue stays below the status bar and hides when returning home or closing it.\n-   : An earlier error no longer hides a new attempt.\n\n### \n\n-   : Navigation, errors, returning home and closing checked.\n\n### \n\n-   : Preview 17 rebuilt locally, without a new Android test.\n\n### \n\n-   : Interface tested with a local page; remote catalogue not checked without Internet.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.17",
|
||||
"text": {
|
||||
"fr": "## 05/10/2026 : 2.7-preview.17\n\n\n\n### \n\n-   : Installation, lancement et désinstallation d’applications web empaquetées.\n-   : Catalogue Vulpes sur l’accueil, en français et en anglais, avec son icône originale.\n-   : Applications installées séparées des applications système.\n-   : Certaines anciennes applications et API Firefox OS ne sont pas encore prises en charge.\n\n### \n\n-   : Installation et utilisation de 2048 vérifiées.\n\n### \n\n-   : 2048 installé, conservé après redémarrage et désinstallé sur un émulateur Android.\n\n### \n\n-   : Accueil vérifié ; installation d’applications encore à valider sur le Pixel.\n",
|
||||
"en": "## 05/10/2026 : 2.7-preview.17\n\n\n\n### \n\n-   : Install, launch and uninstall packaged web apps.\n-   : Vulpes catalogue on the home screen, in French and English, with its original icon.\n-   : Installed apps kept separate from system apps.\n-   : Some old Firefox OS apps and APIs are not yet supported.\n\n### \n\n-   : Installation and use of 2048 checked.\n\n### \n\n-   : 2048 installed, preserved after restart and uninstalled on an Android emulator.\n\n### \n\n-   : Home screen checked; app installation still needs validation on Pixel.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.16",
|
||||
"text": {
|
||||
"fr": "## 05/10/2026 : 2.7-preview.16\n\n\n\n### \n\n-   : Logo Vulpes au démarrage.\n-   : Défilement horizontal involontaire supprimé ; applications de test masquées.\n-   : Choix des fonds d’écran et des sons rétabli. Langue appliquée à l’accueil et conservée au redémarrage.\n-   : APK construit, non publié ; image Pixel essayée sans installation permanente.\n\n### \n\n-   : Barres Android masquées, accessibles par balayage ; mode lanceur vérifié.\n-   : Retour après veille et depuis la caméra du verrouillage corrigé.\n\n### \n\n-   : Les anciens fichiers ne remplacent plus les mises à jour au démarrage.\n",
|
||||
"en": "## 05/10/2026 : 2.7-preview.16\n\n\n\n### \n\n-   : Vulpes startup logo.\n-   : Unwanted horizontal scrolling removed; test apps hidden.\n-   : Wallpaper and sound selection restored. Home screen language applied and preserved after restart.\n-   : APK built, not published; Pixel image tried without permanent installation.\n\n### \n\n-   : Android bars hidden and accessible by swiping; launcher mode checked.\n-   : Return from sleep and the lock screen camera fixed.\n\n### \n\n-   : Old files no longer overwrite updates at startup.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2026-10-04-sargo-standalone",
|
||||
"text": {
|
||||
"fr": "## 04/10/2026 : Installation autonome\n\n\n\n### \n\n-   : Installateur avec vérification du modèle et des fichiers avant écriture.\n-   : Installation autonome de Vulpes, sans passer par Droidian.\n-   : Paquet non publié ; un seul Pixel testé. L’installation remplace les données du téléphone.\n-   : Sauvegardes vérifiées ; restauration complète encore à essayer.\n-   : Démarrages installés, conservation des réglages, caméra, clavier et notifications vérifiés.\n",
|
||||
"en": "## 04/10/2026 : Standalone installation\n\n\n\n### \n\n-   : Installer with device and file checks before writing.\n-   : Standalone Vulpes installation without going through Droidian.\n-   : Package not published; tested on one Pixel only. Installation replaces phone data.\n-   : Backups checked; full restoration still needs testing.\n-   : Installed boots, saved settings, camera, keyboard and notifications checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2026-10-03-sargo-per-device-installation",
|
||||
"text": {
|
||||
"fr": "## 03/10/2026 : Installation par appareil\n\n\n\n### \n\n-   : Préparation adaptée à chaque Pixel, avec sauvegardes propres à l’appareil.\n-   : Mise au point parfois trop lente ; problème encore ouvert.\n-   : Installation directe depuis Android d’origine pas encore disponible.\n-   : Démarrages temporaires, conservation des réglages et retour à l’installation précédente vérifiés.\n",
|
||||
"en": "## 03/10/2026 : Per-device installation\n\n\n\n### \n\n-   : Preparation adapted to each Pixel, with device-specific backups.\n-   : Focus sometimes times out; issue still open.\n-   : Direct installation from stock Android not yet available.\n-   : Temporary boots, saved settings and return to the previous installation checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2026-10-03-sargo-installation-tools",
|
||||
"text": {
|
||||
"fr": "## 03/10/2026 : Installation et secours\n\n\n\n### \n\n-   : Préparation des paquets et kit de secours avec sauvegarde du démarrage.\n-   : Restauration possible sans démarrer l’outil de diagnostic.\n-   : Préparation vérifiée localement ; installation sur un autre Pixel encore à finaliser.\n\n### \n\n-   : Historique commun inclus dans les prochaines publications de sources.\n",
|
||||
"en": "## 03/10/2026 : Installation and recovery\n\n\n\n### \n\n-   : Package preparation and recovery kit with a boot backup.\n-   : Restoration possible without booting the diagnostic tool.\n-   : Preparation checked locally; installation on another Pixel still needs completion.\n\n### \n\n-   : Shared history included in upcoming source releases.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"models",
|
||||
"web"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2026-10-03-sargo-modem-persistence",
|
||||
"text": {
|
||||
"fr": "## 03/10/2026 : Modem et premier démarrage\n\n\n\n### \n\n-   : Récupération du modem sans redémarrer tout le téléphone.\n-   : Polices et initialisation du modem corrigées sur une installation neuve.\n-   : Redémarrages intermittents du modem encore à résoudre ; paquet public incomplet.\n-   : Caméras, focus, zoom, clavier, notifications et recherche Wi-Fi vérifiés.\n",
|
||||
"en": "## 03/10/2026 : Modem and first startup\n\n\n\n### \n\n-   : Recover the modem without restarting the whole phone.\n-   : Fonts and modem initialization fixed on a fresh installation.\n-   : Intermittent modem restarts still need fixing; public package incomplete.\n-   : Cameras, focus, zoom, keyboard, notifications and Wi-Fi search checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2026-10-01-sargo-distribution-preparation",
|
||||
"text": {
|
||||
"fr": "## 01/10/2026 : Préparation de la distribution\n\n\n\n### \n\n-   : Préparation d’une image avec des données vierges.\n-   : Affichage des polices au premier démarrage corrigé.\n-   : Image non publiée. Appels, SMS et données mobiles encore à vérifier de bout en bout.\n-   : Redémarrage intermittent du modem identifié.\n-   : SIM, 4G, recherche Wi-Fi, caméras, clavier et notifications vérifiés en démarrage temporaire.\n",
|
||||
"en": "## 01/10/2026 : Preparing distribution\n\n\n\n### \n\n-   : Preparation of an image with clean user data.\n-   : Font display on first boot fixed.\n-   : Image not published. Calls, SMS and mobile data still need end-to-end testing.\n-   : Intermittent modem restart identified.\n-   : SIM, 4G, Wi-Fi search, cameras, keyboard and notifications checked in a temporary boot.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.15",
|
||||
"text": {
|
||||
"fr": "## 30/09/2026 : 2.7-preview.15\n\n\n\n### \n\n-   : Première publication des sources PC, Android et Tundra, avec présentation FR/EN.\n-   : Arrêt et remplacement du flux caméra fiabilisés.\n-   : Dates et états d’envoi corrigés dans les conversations.\n\n### \n\n-   : Navigation, réglages, gestes et conservation des données vérifiés.\n\n### \n\n-   : Mise à jour, Photo, Galerie et retours d’arrière-plan vérifiés sur un émulateur Android.\n\n### \n\n-   : Sources incluses ; installation universelle encore indisponible à cette étape.\n-   : Flash, vidéo, rotation, veille et audio d’appel encore incomplets ou à vérifier.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)\n",
|
||||
"en": "## 30/09/2026 : 2.7-preview.15\n\n\n\n### \n\n-   : First publication of Desktop, Android and Tundra source code, with French/English project overviews.\n-   : More reliable camera stream shutdown and replacement.\n-   : Dates and sending statuses fixed in conversations.\n\n### \n\n-   : Navigation, settings, gestures and saved data checked.\n\n### \n\n-   : Update, Camera, Gallery and returns from the background checked on an Android emulator.\n\n### \n\n-   : Source code included; universal installation still unavailable at this stage.\n-   : Flash, video, rotation, sleep and call audio still incomplete or awaiting tests.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15)\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-30-sms-camera-followup",
|
||||
"text": {
|
||||
"fr": "## 30/09/2026 : Retours SMS et caméra\n\n\n\n### \n\n-   : Libération du capteur et changement de caméra corrigés.\n-   : Conversations vides corrigées.\n\n### \n\n-   : Sources communes mises à jour, sans nouvel APK.\n\n### \n\n-   : Arrêt du vibreur rétabli.\n-   : Dialogue après envoi SMS et fluidité caméra encore à corriger.\n-   : Focus et zoom confirmés par l’utilisateur.\n-   : Envoi et réception confirmés par l’utilisateur ; conversations affichées.\n",
|
||||
"en": "## 30/09/2026 : SMS and camera follow-up\n\n\n\n### \n\n-   : Camera release and switching fixed.\n-   : Empty conversations fixed.\n\n### \n\n-   : Shared sources updated, without a new APK.\n\n### \n\n-   : Vibration now stops correctly.\n-   : Dialog after sending SMS and camera smoothness still need fixing.\n-   : Focus and zoom confirmed by the user.\n-   : Sending and receiving confirmed by the user; conversations displayed.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-30-sms-camera",
|
||||
"text": {
|
||||
"fr": "## 30/09/2026 : SMS et photo\n\n\n\n### \n\n-   : Messages en échec conservés et états d’envoi mis à jour.\n\n### \n\n-   : APK Preview 14 inchangé à cette étape.\n\n### \n\n-   : Mise au point au toucher et zoom numérique. Photos jusqu’à 12 Mpx à l’arrière et 8 Mpx à l’avant.\n-   : Retour de l’aperçu après une photo corrigé.\n-   : Réception par le destinataire non encore confirmée.\n",
|
||||
"en": "## 30/09/2026 : SMS and photos\n\n\n\n### \n\n-   : Failed messages preserved and sending statuses updated.\n\n### \n\n-   : Preview 14 APK unchanged at this stage.\n\n### \n\n-   : Tap to focus and digital zoom. Photos up to 12 MP at the rear and 8 MP at the front.\n-   : Preview return after taking a photo fixed.\n-   : Delivery to the recipient not yet confirmed.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.14",
|
||||
"text": {
|
||||
"fr": "## 30/09/2026 : 2.7-preview.14\n\n\n\n### \n\n-   : Polices, informations de version, verrouillage et notifications harmonisés.\n\n### \n\n-   : Clavier agrandi, édition de texte et saisie du numéro de destinataire améliorées.\n\n### \n\n-   : Démarrage et polices vérifiés sur un émulateur Android.\n\n### \n\n-   : Arrêt de la vibration corrigé.\n-   : Notifications conservées, ouverture de la conversation et doublons corrigés.\n-   : Réseaux enregistrés, reconnexion et oubli d’un réseau corrigés.\n-   : Contrôles audio testés hors appel ; conversation et son des notifications encore à vérifier.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)\n",
|
||||
"en": "## 30/09/2026 : 2.7-preview.14\n\n\n\n### \n\n-   : Fonts, version information, lock screen and notifications aligned.\n\n### \n\n-   : Larger keyboard, improved text editing and recipient number entry.\n\n### \n\n-   : Startup and fonts checked on an Android emulator.\n\n### \n\n-   : Vibration stopping fixed.\n-   : Notifications preserved, conversation opening and duplicate messages fixed.\n-   : Saved networks, reconnection and forgetting a network fixed.\n-   : Audio controls tested outside calls; conversation audio and notification sounds still need testing.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14)\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.13",
|
||||
"text": {
|
||||
"fr": "## 27/09/2026 : 2.7-preview.13\n\n\n\n### \n\n-   : Rappels des événements corrigés, y compris après suppression.\n-   : Premier affichage et fermeture de l’éditeur corrigés.\n-   : Filtres des conversations corrigés.\n\n### \n\n-   : Photo, vidéo, calendrier et conversations vérifiés.\n\n### \n\n-   : Caméra, Galerie, Calendrier et reprise après arrière-plan vérifiés.\n\n### \n\n-   : Caméra, Wi-Fi et luminosité vérifiés sur la version installée.\n",
|
||||
"en": "## 27/09/2026 : 2.7-preview.13\n\n\n\n### \n\n-   : Event reminders fixed, including after deletion.\n-   : Initial display and closing the editor fixed.\n-   : Conversation filters fixed.\n\n### \n\n-   : Photo, video, calendar and conversations checked.\n\n### \n\n-   : Camera, Gallery, Calendar and background resume checked.\n\n### \n\n-   : Camera, Wi-Fi and brightness checked on the installed version.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.12",
|
||||
"text": {
|
||||
"fr": "## 27/09/2026 : 2.7-preview.12\n\n\n\n### \n\n-   : Page d’accueil locale, en français ou en anglais selon la langue.\n-   : Conversations SMS et traductions du Calendrier corrigées.\n-   : Changement de capteur et zoom de l’interface corrigés ; flash grisé s’il est indisponible.\n-   : Icônes plus nettes, appui long sur Accueil et gestes corrigés.\n\n### \n\n-   : Aperçu plus fluide, autour de 23 images par seconde lors des essais.\n-   : Luminosité plus réactive et connexion Wi-Fi corrigée.\n-   : Vidéo, flash, Bluetooth et réglages d’appel avancés encore incomplets.\n",
|
||||
"en": "## 27/09/2026 : 2.7-preview.12\n\n\n\n### \n\n-   : Local start page, in French or English according to language.\n-   : SMS conversations and Calendar translations fixed.\n-   : Camera switching and interface zoom fixed; flash disabled when unavailable.\n-   : Sharper icons, long press on Home and gestures fixed.\n\n### \n\n-   : Smoother preview, at around 23 frames per second in testing.\n-   : More responsive brightness and Wi-Fi connection fixed.\n-   : Video, flash, Bluetooth and advanced call settings remain incomplete.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-27-4",
|
||||
"text": {
|
||||
"fr": "## 27/09/2026 : Interface et matériel\n\n\n\n### \n\n-   : Première page d’accueil locale.\n-   : Animation d’extinction conservée, avec des ronds bleus.\n-   : Textes des listes et de Vidéo corrigés ; barre Accueil transparente et adaptée au paysage.\n-   : En-têtes et icônes du journal d’appel corrigés.\n\n### \n\n-   : Réglage de la luminosité raccordé.\n-   : Appels audio, vidéo, pleine résolution photo et rotation encore à vérifier.\n",
|
||||
"en": "## 27/09/2026 : Interface and hardware\n\n\n\n### \n\n-   : First local start page.\n-   : Original shutdown animation retained, with blue circles.\n-   : List and Video text fixed; transparent Home bar adapted to landscape.\n-   : Call history headers and icons fixed.\n\n### \n\n-   : Brightness adjustment connected.\n-   : Call audio, video, full-resolution photos and rotation still need testing.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-27-3",
|
||||
"text": {
|
||||
"fr": "## 27/09/2026 : Stabilité Wi-Fi\n\n\n\n### \n\n-   : Preview 9 construite avec les corrections communes ; Wi-Fi géré par Android.\n\n### \n\n-   : Recherche des réseaux stabilisée pour éviter les blocages du téléphone.\n-   : Caméra et appels encore incomplets.\n-   : Verrouillage, polices, date, batterie et Wi-Fi vérifiés.\n",
|
||||
"en": "## 27/09/2026 : Wi-Fi stability\n\n\n\n### \n\n-   : Preview 9 built with shared fixes; Wi-Fi handled by Android.\n\n### \n\n-   : Network discovery stabilized to avoid phone freezes.\n-   : Camera and calls remain incomplete.\n-   : Lock screen, fonts, date, battery and Wi-Fi checked.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-27-2",
|
||||
"text": {
|
||||
"fr": "## 27/09/2026 : Retour aux composants Gaia\n\n\n\n### \n\n-   : Remerciements, écran de démarrage et nom du navigateur mis à jour.\n-   : Panneaux et styles Gaia d’origine rétablis dans Paramètres, Suivi conso, Radio FM et les imports.\n-   : Bouton Accueil dégagé des commandes ; date, batterie et écran de verrouillage rétablis.\n\n### \n\n-   : Mode lanceur, accès aux paramètres Android et reprise après arrière-plan.\n\n### \n\n-   : Première intégration instable ; retour à la version précédente.\n",
|
||||
"en": "## 27/09/2026 : Restoring Gaia components\n\n\n\n### \n\n-   : Credits, startup screen and browser name updated.\n-   : Original Gaia panels and styles restored in Settings, Data Usage, FM Radio and imports.\n-   : Home button no longer overlaps controls; date, battery and lock screen restored.\n\n### \n\n-   : Launcher mode, access to Android settings and background resume.\n\n### \n\n-   : Initial integration unstable; returned to the previous version.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-27-1",
|
||||
"text": {
|
||||
"fr": "## 27/09/2026 : Appels et boutons physiques\n\n\n\n### \n\n-   : Interfaces Gaia d’appel entrant et sortant, avec journal d’appel.\n-   : Polices Fira, contacts et affichage de la batterie corrigés.\n\n### \n\n-   : Prise en charge des touches Marche/Arrêt et Volume, du menu d’extinction et du réveil de l’écran.\n-   : Caméra native encore indisponible.\n-   : Sonnerie et raccrochage vérifiés ; conversation audio encore à tester.\n",
|
||||
"en": "## 27/09/2026 : Calls and hardware buttons\n\n\n\n### \n\n-   : Gaia incoming and outgoing call interfaces, with call history.\n-   : Fira fonts, contacts and battery display fixed.\n\n### \n\n-   : Support for Power and Volume keys, shutdown menu and waking the screen.\n-   : Native camera still unavailable.\n-   : Ringing and hanging up checked; conversation audio still needs testing.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-26-1",
|
||||
"text": {
|
||||
"fr": "## 26/09/2026 : Démarrage et polices\n\n\n\n### \n\n-   : Polices Fira et informations de batterie corrigées ; boutons de test retirés des paramètres.\n\n### \n\n-   : Conservation des données lors du démarrage installé.\n-   : Plein écran stable, sans gestes ni ouverture automatique d’applications.\n-   : Son et micro testés séparément ; audio d’appel, caméra et veille non validés.\n",
|
||||
"en": "## 26/09/2026 : Startup and fonts\n\n\n\n### \n\n-   : Fira fonts and battery information fixed; test buttons removed from settings.\n\n### \n\n-   : Data preserved in the installed boot.\n-   : Stable fullscreen, without automatic gestures or app launches.\n-   : Sound and microphone tested separately; call audio, camera and sleep not validated.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "development-2026-09-25-1",
|
||||
"text": {
|
||||
"fr": "## 25/09/2026 : Accueil et premiers essais natifs\n\n\n\n### \n\n-   : Bouton rond, fond transparent sur l’accueil et bleu dans les applications.\n-   : État appuyé du bouton, dégradé, polices et informations système corrigés.\n\n### \n\n-   : Reprise de l’application modifiée dans les sources, pas encore testée sur Android.\n\n### \n\n-   : Premiers essais d’une image native avec affichage accéléré et batterie.\n-   : Essais temporaires ; de nombreuses fonctions matérielles restent à raccorder.\n",
|
||||
"en": "## 25/09/2026 : Home screen and first native trials\n\n\n\n### \n\n-   : Round button, transparent background on Home and blue background in apps.\n-   : Pressed button state, gradient, fonts and system information fixed.\n\n### \n\n-   : App resume changed in source code, not yet tested on Android.\n\n### \n\n-   : First native image trials with accelerated display and battery support.\n-   : Temporary trials; many hardware features still need connecting.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "development",
|
||||
"status": "development",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.7",
|
||||
"text": {
|
||||
"fr": "## 2.7-preview.7\n\n\n\n### \n\n-   : Versions de Vulpes, Gaia et Gecko distinguées.\n\n### \n\n-   : Gestion des autorisations et messages d’erreur plus précis.\n\n### \n\n-   : Reprise après la première demande d’autorisation corrigée.\n",
|
||||
"en": "## 2.7-preview.7\n\n\n\n### \n\n-   : Vulpes, Gaia and Gecko versions shown separately.\n\n### \n\n-   : Improved permission handling and clearer error messages.\n\n### \n\n-   : Resume after the first permission request fixed.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.6",
|
||||
"text": {
|
||||
"fr": "## 2.7-preview.6\n\n\n\n### \n\n-   : Application Photo Gaia reliée à la webcam.\n-   : Import de fichiers et photos disponibles dans Galerie.\n-   : Changement de capteur selon le matériel ; flash et vidéo indisponibles.\n\n### \n\n-   : Application Photo Gaia reliée aux caméras Android.\n-   : Import de fichiers et photos disponibles dans Galerie.\n-   : Changement de capteur selon le matériel ; flash et vidéo indisponibles.\n",
|
||||
"en": "## 2.7-preview.6\n\n\n\n### \n\n-   : Gaia Camera app connected to the webcam.\n-   : File import and photos available in Gallery.\n-   : Camera switching depends on hardware; flash and video unavailable.\n\n### \n\n-   : Gaia Camera app connected to Android cameras.\n-   : File import and photos available in Gallery.\n-   : Camera switching depends on hardware; flash and video unavailable.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.5",
|
||||
"text": {
|
||||
"fr": "## 2.7-preview.5\n\n\n\n### \n\n-   : Réglages système, imports de médias et de contacts, notifications et essai webcam.\n-   : Interface et numérotation des versions alignées avec Android.\n\n### \n\n-   : Interface et numérotation des versions alignées avec le PC.\n-   : Reprise après arrière-plan et récupération de l’affichage améliorées.\n",
|
||||
"en": "## 2.7-preview.5\n\n\n\n### \n\n-   : System settings, media and contact imports, notifications and webcam test.\n-   : Interface and version numbering aligned with Android.\n\n### \n\n-   : Interface and version numbering aligned with Desktop.\n-   : Improved background resume and display recovery.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"apk"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-156.0.2",
|
||||
"text": {
|
||||
"fr": "## 2.7-156.0.2\n\n\n\n### \n\n-   : Réglages Android, son, luminosité, imports et notifications.\n-   : Appels, SMS, horloge et caméra confiés aux applications Android.\n-   : Écrans blancs et reprise après arrière-plan corrigés.\n-   : Ancienne numérotation : ce numéro ne correspond pas à une mise à jour de Gecko.\n",
|
||||
"en": "## 2.7-156.0.2\n\n\n\n### \n\n-   : Android settings, sound, brightness, imports and notifications.\n-   : Calls, SMS, clock and camera handled by Android apps.\n-   : White screens and background resume fixed.\n-   : Historical numbering: this number does not represent a Gecko update.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "built",
|
||||
"platforms": [
|
||||
"apk"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-156.0.1",
|
||||
"text": {
|
||||
"fr": "## 2.7-156.0.1\n\n\n\n### \n\n-   : Nom « Vulpes OS – Preview » et icône du projet.\n-   : Clignotement du fond d’écran corrigé.\n-   : Ancienne numérotation, conservée pour retrouver cette publication.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)\n",
|
||||
"en": "## 2.7-156.0.1\n\n\n\n### \n\n-   : “Vulpes OS – Preview” name and project icon.\n-   : Wallpaper flickering fixed.\n-   : Historical numbering retained to identify this release.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"apk"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.2",
|
||||
"text": {
|
||||
"fr": "## 2.7-preview.2\n\n\n\n### \n\n-   : Clignotement du fond d’écran corrigé.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)\n",
|
||||
"en": "## 2.7-preview.2\n\n\n\n### \n\n-   : Wallpaper flickering fixed.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2)\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"apk"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "2.7-preview.1",
|
||||
"text": {
|
||||
"fr": "## 2.7-preview.1\n\n\n\n### \n\n-   : Première Preview Android pour découvrir Vulpes sans flasher le téléphone.\n-   : Fond d’écran clignotant connu dans cette première version.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)\n",
|
||||
"en": "## 2.7-preview.1\n\n\n\n### \n\n-   : First Android Preview to try Vulpes without flashing the phone.\n-   : Known wallpaper flickering in this first version.\n\n[Release](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1)\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "version",
|
||||
"status": "published",
|
||||
"platforms": [
|
||||
"apk"
|
||||
],
|
||||
"models": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "history",
|
||||
"text": {
|
||||
"fr": "## Premières étapes\n\n### \n\n-   : Adaptation des contacts, messages, paramètres, alarmes et du stockage.\n-   : Création de Vulpes : identité visuelle, traductions et informations système.\n-   : Passage des anciens moteurs Firefox OS à un Gecko moderne.\n\n### \n\n-   : Premiers démarrages natifs et essais matériels sur le Pixel 3a.\n",
|
||||
"en": "## Early milestones\n\n### \n\n-   : Adaptation of contacts, messages, settings, alarms and storage.\n-   : Creation of Vulpes: visual identity, translations and system information.\n-   : Move from old Firefox OS engines to a modern Gecko.\n\n### \n\n-   : First native boots and hardware trials on Pixel 3a.\n"
|
||||
},
|
||||
"metadata": {
|
||||
"kind": "history",
|
||||
"status": "history",
|
||||
"platforms": [
|
||||
"pc",
|
||||
"models"
|
||||
],
|
||||
"models": [
|
||||
"sargo"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"releases": {
|
||||
"android-2.7-preview.14": {
|
||||
"version": "2.7-preview.14",
|
||||
"after": "2.7-156.0.1",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.14",
|
||||
"summary": "# Vulpes OS — 2.7-preview.14\n\n🇫🇷 Preview expérimentale. L’APK peut être installé par-dessus la Preview précédente. Les appels et SMS passent par Android. Les avancées Sargo ci-dessous sont distinctes de l’APK ; cette release ne fournit pas de nouvelle image pour le Pixel.\n\n🇬🇧 Experimental Preview. Install the APK over the previous Preview. Calls and SMS use Android. Sargo progress below is separate from the APK; this release does not provide a new Pixel image.\n\nAndroid 8 minimum · Essais sur émulateur Android. / Tested on an Android emulator.",
|
||||
"include_entry": true
|
||||
},
|
||||
"v2.7-preview.15": {
|
||||
"version": "2.7-preview.15",
|
||||
"after": "2.7-preview.14",
|
||||
"summary": "# Vulpes OS — 2.7-preview.15\n\n🇫🇷 Preview expérimentale. L’APK peut être installé par-dessus la Preview précédente. Les appels et SMS passent par Android. Les avancées Sargo ci-dessous sont distinctes de l’APK ; cette release ne fournit pas de nouvelle image pour le Pixel.\n\n🇬🇧 Experimental Preview. Install the APK over the previous Preview. Calls and SMS use Android. Sargo progress below is separate from the APK; this release does not provide a new Pixel image.\n\nAndroid 8 minimum · Essais sur émulateur Android. / Tested on an Android emulator.",
|
||||
"include_entry": true,
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.15"
|
||||
},
|
||||
"v2.7-preview.17": {
|
||||
"version": "2.7-preview.17",
|
||||
"after": "2.7-preview.15",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.17",
|
||||
"summary": "# Vulpes OS — 2.7-preview.17\n\n🇫🇷 Preview expérimentale. L’APK peut être installé par-dessus la Preview précédente. Les appels et SMS passent par Android. Les avancées Sargo ci-dessous sont distinctes de l’APK ; cette release ne fournit pas de nouvelle image pour le Pixel.\n\n🇬🇧 Experimental Preview. Install the APK over the previous Preview. Calls and SMS use Android. Sargo progress below is separate from the APK; this release does not provide a new Pixel image.\n\nAndroid 8 minimum · Essais sur émulateur Android. / Tested on an Android emulator.",
|
||||
"include_entry": true
|
||||
},
|
||||
"v2.7-preview.20": {
|
||||
"version": "2.7-preview.20",
|
||||
"after": "2.7-preview.17",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.20",
|
||||
"summary": "# Vulpes OS — 2.7-preview.20\n\n🇫🇷 Preview expérimentale. L’APK peut être installé par-dessus la Preview précédente. Les appels et SMS passent par Android. Les avancées Sargo ci-dessous sont distinctes de l’APK ; cette release ne fournit pas de nouvelle image pour le Pixel.\n\n🇬🇧 Experimental Preview. Install the APK over the previous Preview. Calls and SMS use Android. Sargo progress below is separate from the APK; this release does not provide a new Pixel image.\n\nAndroid 8 minimum · Exécution de cette Preview sur Android encore à valider. / Android execution of this Preview still needs validation.",
|
||||
"include_entry": true
|
||||
},
|
||||
"android-2.7-156.0.1": {
|
||||
"version": "2.7-156.0.1",
|
||||
"after": "2.7-preview.2",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1",
|
||||
"summary": "# Vulpes OS — 2.7-156.0.1\n\n🇫🇷 Ancienne Preview Android expérimentale. Une version plus récente est disponible dans les releases du projet.\n\n🇬🇧 Older experimental Android Preview. A newer version is available in the project releases.",
|
||||
"include_entry": true
|
||||
},
|
||||
"android-2.7-preview.2": {
|
||||
"version": "2.7-preview.2",
|
||||
"after": "2.7-preview.1",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2",
|
||||
"summary": "# Vulpes OS — 2.7-preview.2\n\n🇫🇷 Ancienne Preview Android expérimentale. Une version plus récente est disponible dans les releases du projet.\n\n🇬🇧 Older experimental Android Preview. A newer version is available in the project releases.",
|
||||
"include_entry": true
|
||||
},
|
||||
"android-2.7-preview.1": {
|
||||
"version": "2.7-preview.1",
|
||||
"after": "history",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1",
|
||||
"summary": "# Vulpes OS — 2.7-preview.1\n\n🇫🇷 Ancienne Preview Android expérimentale. Une version plus récente est disponible dans les releases du projet.\n\n🇬🇧 Older experimental Android Preview. A newer version is available in the project releases.",
|
||||
"include_entry": true
|
||||
},
|
||||
"v2.7-preview.21": {
|
||||
"version": "2.7-preview.21",
|
||||
"after": "2.7-preview.20",
|
||||
"url": "https://nnsprod.com/git/oversu/VulpesOS/releases/tag/v2.7-preview.21",
|
||||
"include_entry": true,
|
||||
"summary": "# Vulpes OS — 2.7-preview.21\n\n🇫🇷 **Preview Android expérimentale.** Contacts, SMS et journal d’appels Android accessibles dans Gaia après autorisation, en lecture seule. Les données locales Vulpes sont conservées. Les MMS ne sont pas synchronisés ; appels, envoi et modification des données Android passent par les applications Android.\n\nInstaller l’APK par-dessus une Preview précédente signée avec la même clé. Android 8 minimum déclaré ; ARM64, ARMv7 et x86_64. Intégration vérifiée sur émulateur Android 9 avec des données de test ; les Motorola signalés et les versions Android récentes restent à qualifier. Le problème de clavier blanc n’est pas considéré comme résolu sur tous les appareils.\n\nBuild de développement signé, débogage via ADB autorisé. Gaia 2.7.0 · GeckoView Android 156.0. Vérification du téléchargement : `sha256sum -c SHA256SUMS.txt`. **Aucune nouvelle image Pixel n’est publiée ici : son flash et ses tests restent à effectuer.**\n\n🇬🇧 **Experimental Android Preview.** Android contacts, SMS and call history can be read in Gaia after granting permission. Local Vulpes data is preserved. MMS are not synchronized; calls, sending and editing Android records use Android apps.\n\nInstall over a previous Preview signed with the same key. Declared minimum Android 8; ARM64, ARMv7 and x86_64. Integration checked on an Android 9 emulator with test data; reported Motorola devices and recent Android versions still need qualification. The blank keyboard issue is not considered fixed on every device.\n\nSigned development build, debugging through authorized ADB. Gaia 2.7.0 · Android GeckoView 156.0. Verify the download with `sha256sum -c SHA256SUMS.txt`. **No new Pixel image is published here: flashing and hardware tests are still pending.**\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 769 KiB After Width: | Height: | Size: 1.3 MiB |
|
|
@ -272,7 +272,7 @@ ActivityController.prototype.onActivityConfirmed = function(newMedia) {
|
|||
|
||||
// Video
|
||||
if (newMedia.isVideo) {
|
||||
media.type = 'video/3gpp';
|
||||
media.type = media.blob.type || 'video/3gpp';
|
||||
media.poster = newMedia.poster.blob;
|
||||
activity.postResult(media);
|
||||
this.app.clearSpinner();
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ OverlayController.prototype.updateOverlay = function(type, enabled, reason) {
|
|||
return;
|
||||
}
|
||||
|
||||
var closable = self.activity.pick && type !== 'request-fail';
|
||||
var closable = self.activity.pick || reason === 'request-fail';
|
||||
var view = new OverlayView({
|
||||
type: reason,
|
||||
closable: closable,
|
||||
|
|
@ -118,6 +118,10 @@ OverlayController.prototype.updateOverlay = function(type, enabled, reason) {
|
|||
|
||||
overlay.view = view.appendTo(document.body)
|
||||
.on('click:close-btn', function() {
|
||||
if (reason === 'request-fail' && !self.activity.pick) {
|
||||
self.updateOverlay(type, false);
|
||||
return;
|
||||
}
|
||||
self.app.emit('activitycanceled');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ StorageController.prototype.storeVideo = function(video) {
|
|||
var self = this;
|
||||
|
||||
// Add the poster image to the image storage
|
||||
poster.filepath = video.filepath.replace('.3gp', '.jpg');
|
||||
poster.filepath = video.filepath.replace(/\.(?:3gp|mp4|webm)$/, '.jpg');
|
||||
video.isVideo = true;
|
||||
|
||||
this.storage.addPicture(
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ ViewfinderController.prototype.hideGrid = function() {
|
|||
* @private
|
||||
*/
|
||||
ViewfinderController.prototype.bindEvents = function() {
|
||||
window.addEventListener('resize', this.configurePreview);
|
||||
|
||||
// View
|
||||
this.views.viewfinder.on('fadedin', this.app.firer('viewfinder:visible'));
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ var dcfConfig = {
|
|||
seq: null,
|
||||
postFix: 'MZLLA',
|
||||
prefix: {video: 'VID_', image: 'IMG_'},
|
||||
ext: {video: '3gp', image: 'jpg'}
|
||||
ext: {video: window.VulpesVideoExtension || '3gp', image: 'jpg'}
|
||||
};
|
||||
|
||||
exports.init = function() {
|
||||
|
|
|
|||
|
|
@ -252,7 +252,7 @@ Storage.prototype.checkFilepath = function(filepath) {
|
|||
// image and get the video filepath by changing '.jpg' to '.3gp'
|
||||
if (filepath.indexOf('VID') != -1 &&
|
||||
filepath.lastIndexOf('.jpg') === filepath.length - 4) {
|
||||
filepath = filepath.replace('.jpg', '.3gp');
|
||||
filepath = filepath.replace('.jpg', '.' + (window.VulpesVideoExtension || '3gp'));
|
||||
}
|
||||
|
||||
return filepath;
|
||||
|
|
@ -380,7 +380,7 @@ Storage.prototype.deletePicture = function(filepath, done) {
|
|||
* @param {String} filepath
|
||||
*/
|
||||
Storage.prototype.deleteVideo = function(filepath) {
|
||||
var poster = filepath.replace('.3gp', '.jpg');
|
||||
var poster = filepath.replace(/\.(?:3gp|mp4|webm)$/, '.jpg');
|
||||
|
||||
this.video.delete(filepath).onerror = function(e) {
|
||||
console.warn(
|
||||
|
|
|
|||
|
|
@ -205,6 +205,9 @@ module.exports = View.extend({
|
|||
*/
|
||||
updatePreview: function(preview, sensorAngle, mirrored) {
|
||||
if (!preview) { return; }
|
||||
// An embedded app can initialize before its window is visible.
|
||||
this.getSize();
|
||||
if (!this.width || !this.height) { return; }
|
||||
var aspect;
|
||||
|
||||
// Invert dimensions if the camera's `sensorAngle` is
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -265,8 +265,7 @@ var CallLog = {
|
|||
// If the contacts cache is not valid, we retrieve the contacts information
|
||||
// directly from the contacts API and update the DOM accordingly.
|
||||
if (!this._contactCache) {
|
||||
this.updateListWithContactInfo(null, null, phoneNumbers,
|
||||
logGroupContainer);
|
||||
this.updateListWithContactInfo(null, null, logGroupContainer);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,19 @@ var TelephonyHelper = (function() {
|
|||
return Promise.reject();
|
||||
}
|
||||
|
||||
// The platform adapter owns network checks on Tundra and Android.
|
||||
if (window.VulpesPlatformDial) {
|
||||
return window.VulpesPlatformDial(sanitizedNumber.split(',')[0]).then(function(obj) {
|
||||
if (window.TelephonyCall && obj instanceof TelephonyCall) {
|
||||
installHandlers(obj, sanitizedNumber, false, cardIndex,
|
||||
oncall, onconnected, ondisconnected);
|
||||
} else if (oncall) {
|
||||
oncall();
|
||||
}
|
||||
return obj;
|
||||
});
|
||||
}
|
||||
|
||||
var conn = navigator.mozMobileConnections &&
|
||||
navigator.mozMobileConnections[cardIndex];
|
||||
|
||||
|
|
@ -170,11 +183,13 @@ var TelephonyHelper = (function() {
|
|||
call.addEventListener('connected', onconnected);
|
||||
|
||||
call.addEventListener('disconnected', function handleDisconnect(evt) {
|
||||
loadTelephonyMessages(function() {
|
||||
TelephonyMessages.handleDisconnect(
|
||||
evt.call.disconnectedReason, number, emergencyOnly
|
||||
);
|
||||
});
|
||||
if (evt.call.disconnectedReason) {
|
||||
loadTelephonyMessages(function() {
|
||||
TelephonyMessages.handleDisconnect(
|
||||
evt.call.disconnectedReason, number, emergencyOnly
|
||||
);
|
||||
});
|
||||
}
|
||||
ondisconnected && ondisconnected();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -651,25 +651,30 @@ var metadataParser = (function() {
|
|||
|
||||
function videoMetadataParser(file, metadataCallback, errorCallback) {
|
||||
var metadata = {};
|
||||
var videofilename = file.name.replace('.jpg', '.3gp');
|
||||
metadata.video = videofilename;
|
||||
|
||||
var getreq = videostorage.get(videofilename);
|
||||
getreq.onerror = function() {
|
||||
errorCallback('cannot get video file: ' + videofilename);
|
||||
};
|
||||
getreq.onsuccess = function() {
|
||||
var videofile = getreq.result;
|
||||
getVideoRotation(videofile, function(rotation) {
|
||||
if (typeof rotation === 'number') {
|
||||
metadata.rotation = rotation;
|
||||
var extensions = ['webm', '3gp', 'mp4'];
|
||||
function findVideo() {
|
||||
var videofilename = file.name.replace(/\.jpg$/, '.' + extensions.shift());
|
||||
var getreq = videostorage.get(videofilename);
|
||||
getreq.onerror = function() {
|
||||
if (extensions.length) findVideo();
|
||||
else errorCallback('cannot get video file: ' + videofilename);
|
||||
};
|
||||
getreq.onsuccess = function() {
|
||||
metadata.video = videofilename;
|
||||
if (/\.webm$/.test(videofilename)) {
|
||||
metadata.rotation = 0;
|
||||
getVideoThumbnailAndSize();
|
||||
return;
|
||||
}
|
||||
else if (typeof rotation === 'string') {
|
||||
errorCallback('Video rotation:', rotation);
|
||||
}
|
||||
});
|
||||
};
|
||||
getVideoRotation(getreq.result, function(rotation) {
|
||||
if (typeof rotation === 'number') {
|
||||
metadata.rotation = rotation;
|
||||
getVideoThumbnailAndSize();
|
||||
} else errorCallback('Video rotation: ' + rotation);
|
||||
});
|
||||
};
|
||||
}
|
||||
findVideo();
|
||||
|
||||
function getVideoThumbnailAndSize() {
|
||||
var url = URL.createObjectURL(file);
|
||||
|
|
|
|||
|
|
@ -408,6 +408,10 @@
|
|||
return;
|
||||
}
|
||||
|
||||
if (HIDDEN_ROLES.includes(manifest.role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not add blacklisted apps
|
||||
if (BLACKLIST.includes(app.origin)) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -9,10 +9,9 @@ var LayoutDictionaryDownloader = function(imEngineId, dictFilename) {
|
|||
this._xhr = null;
|
||||
};
|
||||
|
||||
// Remote URLs to download the data. We require the remote server to have
|
||||
// CORS turned on since we are not asking for SystemXHR permission.
|
||||
// Dictionaries ship with Vulpes so adding a layout also works offline.
|
||||
LayoutDictionaryDownloader.prototype.REMOTE_URL =
|
||||
'https://fxos.cdn.mozilla.net/dictionaries/%imEngineId/%ver/%dictFilename';
|
||||
'/js/imes/%imEngineId/dictionaries/%dictFilename';
|
||||
|
||||
// If the data schema changes in an incompatible way, we must bump the
|
||||
// version-to-download here.
|
||||
|
|
|
|||
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}
|
||||
38
gaia/apps/marketplace/marketplace.js
Normal file
38
gaia/apps/marketplace/marketplace.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
(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);
|
||||
frame.addEventListener('mozbrowserclose',()=>{
|
||||
if(busy)return;
|
||||
status(text('Le catalogue est fermé. Vous pouvez le rouvrir ci-dessous.',
|
||||
'The catalogue is closed. You can reopen it below.'));
|
||||
document.getElementById('open').hidden=true;
|
||||
});
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,10 +139,6 @@ define(function(require) {
|
|||
*/
|
||||
DateTime.prototype._init = function() {
|
||||
this._mozTime = window.navigator.mozTime;
|
||||
if (!this._mozTime) {
|
||||
console.error('Could not get window.navigator.mozTime');
|
||||
return;
|
||||
}
|
||||
|
||||
this._boundSetTimeAutoEnabled = function(event) {
|
||||
this._clockAutoEnabled = event.settingValue;
|
||||
|
|
@ -320,9 +316,7 @@ define(function(require) {
|
|||
switch (type) {
|
||||
case 'date':
|
||||
tokens = value.split('-');
|
||||
newDate.setYear(tokens[0]);
|
||||
newDate.setMonth(tokens[1] - 1);
|
||||
newDate.setDate(tokens[2]);
|
||||
newDate.setFullYear(Number(tokens[0]), Number(tokens[1]) - 1, Number(tokens[2]));
|
||||
break;
|
||||
|
||||
case 'time':
|
||||
|
|
@ -331,7 +325,8 @@ define(function(require) {
|
|||
newDate.setMinutes(parseInt(tokens[1]));
|
||||
break;
|
||||
}
|
||||
this._mozTime.set(newDate);
|
||||
if (!this._mozTime) return;
|
||||
return this._mozTime.set(newDate);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -99,37 +99,35 @@ define(function(require) {
|
|||
_transit: function dm__transit(method, dialog, options) {
|
||||
var promise = new Promise(function(resolve) {
|
||||
var panel = dialog.panel;
|
||||
|
||||
panel.addEventListener('transitionend', function paintWait(evt) {
|
||||
if ((method === 'close' || method === 'open') &&
|
||||
evt.propertyName === 'visibility') {
|
||||
// After transition, we have to `hide` the panel, otherwise
|
||||
// the panel would still exist on the layer and would block
|
||||
// the scrolling event.
|
||||
if (method === 'close') {
|
||||
panel.hidden = true;
|
||||
}
|
||||
panel.removeEventListener('transitionend', paintWait);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
// Before transition, we have to `show` the panel, otherwise
|
||||
// the panel before applying transition class.
|
||||
if (method === 'open') {
|
||||
panel.hidden = false;
|
||||
var completed = false;
|
||||
var fallback;
|
||||
function finish(event) {
|
||||
if (event && (event.target !== panel || event.propertyName !== 'visibility')) return;
|
||||
if (completed) return;
|
||||
completed = true;
|
||||
clearTimeout(fallback);
|
||||
panel.removeEventListener('transitionend', finish);
|
||||
panel.removeEventListener('transitioncancel', finish);
|
||||
if (method === 'close') panel.hidden = true;
|
||||
resolve();
|
||||
}
|
||||
|
||||
// We need to apply class later otherwise Gecko can't apply
|
||||
// this transition and 150ms is an approximate number after doing
|
||||
// several rounds of manual tests.
|
||||
if (method === 'open') panel.hidden = false;
|
||||
setTimeout(function() {
|
||||
if (method === 'open') {
|
||||
// Let's unhide the panel first
|
||||
panel.classList.add('current');
|
||||
} else {
|
||||
panel.classList.remove('current');
|
||||
panel.addEventListener('transitionend', finish);
|
||||
panel.addEventListener('transitioncancel', finish);
|
||||
panel.classList.toggle('current', method === 'open');
|
||||
var style = window.getComputedStyle(panel);
|
||||
function milliseconds(value) {
|
||||
return parseFloat(value) * (value.trim().endsWith('ms') ? 1 : 1000) || 0;
|
||||
}
|
||||
var durations = style.transitionDuration.split(',').map(milliseconds);
|
||||
var delays = style.transitionDelay.split(',').map(milliseconds);
|
||||
var duration = Math.max.apply(Math, durations.map(function(value, index) {
|
||||
return value + delays[index % delays.length];
|
||||
}));
|
||||
// Reduced motion, display changes or interrupted transitions may
|
||||
// omit the visibility event. Always release the dialog queue.
|
||||
fallback = setTimeout(finish, Math.max(0, duration) + 100);
|
||||
}, 150);
|
||||
});
|
||||
return promise;
|
||||
|
|
|
|||
|
|
@ -190,29 +190,30 @@ define(function(require) {
|
|||
if ('#' + panelId === currentPanel) {
|
||||
defer.reject('You are showing the same panel #' + panelId);
|
||||
} else {
|
||||
options.onWrapSubmit = function() {
|
||||
DialogManager.close(dialog, 'submit', options)
|
||||
.then(function(result) {
|
||||
defer.resolve({
|
||||
type: 'submit',
|
||||
value: result
|
||||
});
|
||||
var opening;
|
||||
var closing = false;
|
||||
function close(type) {
|
||||
if (closing) return;
|
||||
closing = true;
|
||||
// A tap during the opening animation must not run hide/show
|
||||
// concurrently, or a queued dialog can inherit stale form values.
|
||||
Promise.resolve(opening).then(function() {
|
||||
return DialogManager.close(dialog, type, options);
|
||||
}).then(function(result) {
|
||||
self._navigating = false;
|
||||
defer.resolve({type: type, value: result});
|
||||
self._execPendingRequest();
|
||||
}, function(error) {
|
||||
// Panel validation rejects to keep the form open. Allow a
|
||||
// correction or cancellation without rejecting the show promise.
|
||||
closing = false;
|
||||
dialogDOM.dispatchEvent(new CustomEvent('dialogvalidationfailed', {
|
||||
detail: error
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
options.onWrapCancel = function() {
|
||||
DialogManager.close(dialog, 'cancel', options)
|
||||
.then(function(result) {
|
||||
defer.resolve({
|
||||
type: 'cancel',
|
||||
value: result
|
||||
});
|
||||
self._navigating = false;
|
||||
self._execPendingRequest();
|
||||
});
|
||||
};
|
||||
}
|
||||
options.onWrapSubmit = function() { close('submit'); };
|
||||
options.onWrapCancel = function() { close('cancel'); };
|
||||
|
||||
switch (options.type) {
|
||||
case 'alert':
|
||||
|
|
@ -229,7 +230,17 @@ define(function(require) {
|
|||
break;
|
||||
}
|
||||
this._navigating = true;
|
||||
DialogManager.open(dialog, options);
|
||||
opening = DialogManager.open(dialog, options);
|
||||
opening.catch(function(error) {
|
||||
// A missing host API must not lock every later Settings dialog.
|
||||
dialogDOM.classList.remove('current');
|
||||
dialogDOM.hidden = true;
|
||||
var overlay = document.querySelector('.settings-dialog-overlay');
|
||||
if (overlay) overlay.hidden = true;
|
||||
self._navigating = false;
|
||||
defer.reject(error);
|
||||
self._execPendingRequest();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -86,7 +86,16 @@ define(function(require) {
|
|||
var path = pathElement ? pathElement.dataset.path : null;
|
||||
|
||||
var panelFuncLoaded = function(panelFunc) {
|
||||
var panel = panelFunc();
|
||||
var panel;
|
||||
try {
|
||||
panel = panelFunc();
|
||||
if (!panel || typeof panel.beforeShow !== 'function') {
|
||||
throw new TypeError('Invalid settings panel: ' + panelId);
|
||||
}
|
||||
} catch (error) {
|
||||
if (callback) callback(null, error);
|
||||
return;
|
||||
}
|
||||
_panelCache[panelId] = panel;
|
||||
if (callback) {
|
||||
callback(panel);
|
||||
|
|
@ -97,6 +106,8 @@ define(function(require) {
|
|||
require([path], function(panelFunc) {
|
||||
// Create a new panel object for static panels.
|
||||
panelFuncLoaded(panelFunc ? panelFunc : SettingsPanel);
|
||||
}, function(error) {
|
||||
if (callback) callback(null, error);
|
||||
});
|
||||
} else {
|
||||
panelFuncLoaded(SettingsPanel);
|
||||
|
|
|
|||
|
|
@ -187,7 +187,12 @@ define(function(require) {
|
|||
}
|
||||
|
||||
_loadPanel(panelId, function() {
|
||||
PanelCache.get(panelId, function(panel) {
|
||||
PanelCache.get(panelId, function(panel, loadError) {
|
||||
if (loadError || !panel) {
|
||||
console.error('Settings panel ' + panelId + ':', loadError);
|
||||
callback(loadError || new Error('Missing settings panel: ' + panelId));
|
||||
return;
|
||||
}
|
||||
var newPanelElement = document.getElementById(panelId);
|
||||
var currentPanelId =
|
||||
_currentNavigation && _currentNavigation.panelId;
|
||||
|
|
@ -246,6 +251,17 @@ define(function(require) {
|
|||
// directly
|
||||
Settings._currentPanel = '#' + panelId;
|
||||
callback();
|
||||
}).catch(function(error) {
|
||||
console.error('Settings panel ' + panelId + ':', error);
|
||||
newPanelElement.classList.remove('current');
|
||||
if (currentPanelElement) currentPanelElement.classList.add('current');
|
||||
// Release the navigation lock even when a hardware query fails.
|
||||
callback(error);
|
||||
if (window.VulpesGaiaUI) {
|
||||
window.VulpesGaiaUI.notice(document.documentElement.lang.indexOf('fr') === 0 ?
|
||||
'Ce panneau n’a pas pu être chargé. Vous pouvez revenir aux autres paramètres.' :
|
||||
'This panel could not be loaded. You can return to the other settings.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -103,9 +103,10 @@ define(function(require) {
|
|||
* @access public
|
||||
*/
|
||||
_onAppChoose: function pl__onAppChoose(evt) {
|
||||
if (evt.target.dataset && evt.target.dataset.appIndex) {
|
||||
var target = evt.target.closest('[data-app-index]');
|
||||
if (target && this._listRoot.contains(target)) {
|
||||
SettingsService.navigate('appPermissions-details', {
|
||||
app: this._apps[evt.target.dataset.appIndex],
|
||||
app: this._apps[target.dataset.appIndex],
|
||||
permissionsTable: this._permissionTable
|
||||
});
|
||||
}
|
||||
|
|
@ -262,7 +263,7 @@ define(function(require) {
|
|||
* @return {Bool}
|
||||
*/
|
||||
_isExplicitPerm: function pl_isExplicitPerm(perm) {
|
||||
return mozPerms.isExplicit(perm, this._currentApp.manifestURL,
|
||||
return mozPerms && mozPerms.isExplicit(perm, this._currentApp.manifestURL,
|
||||
this._currentApp.origin, false);
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ define(function(require) {
|
|||
* refer to the JSON File Data.
|
||||
*/
|
||||
this._callSupportInfo = null;
|
||||
var url = 'http://support.mozilla.org/products/firefox-os';
|
||||
var url = 'https://vulpes-os.org/';
|
||||
this._elements.userGuide.addEventListener('click', () => {
|
||||
this._elements.userGuide.blur();
|
||||
var activity = new window.MozActivity({
|
||||
|
|
@ -55,7 +55,10 @@ define(function(require) {
|
|||
.then(function loadSupportInfo(data) {
|
||||
this._supportInfo = data;
|
||||
callback(this._supportInfo);
|
||||
}.bind(this));
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.warn('Support information:', error);
|
||||
callback({onlinesupport: {title: 'Vulpes OS', href: 'https://vulpes-os.org/'}, callsupport: []});
|
||||
});
|
||||
},
|
||||
/**
|
||||
* create support url items
|
||||
|
|
@ -103,7 +106,7 @@ define(function(require) {
|
|||
_renderSupportInfo: function support_renderSupportInfo(result) {
|
||||
var onlineSupportInfo = null;
|
||||
var onlineSupportTitle = result['support.onlinesupport.title'];
|
||||
if (onlineSupportTitle !== '') {
|
||||
if (typeof onlineSupportTitle === 'string' && onlineSupportTitle !== '') {
|
||||
onlineSupportInfo = { title: onlineSupportTitle };
|
||||
onlineSupportInfo.href = result['support.onlinesupport.href'];
|
||||
this._setOnlineSupportInfo(onlineSupportInfo);
|
||||
|
|
@ -114,7 +117,7 @@ define(function(require) {
|
|||
// Check to see if we have a title for the first support number.
|
||||
var callSupport1Title = result['support.callsupport1.title'];
|
||||
// If we have a title we'll go ahead and load the href for it too.
|
||||
if (callSupport1Title !== '') {
|
||||
if (typeof callSupport1Title === 'string' && callSupport1Title !== '') {
|
||||
this._callSupportInfo = [
|
||||
{
|
||||
'title': callSupport1Title,
|
||||
|
|
@ -125,7 +128,7 @@ define(function(require) {
|
|||
// Now check to see if we have a title for the second
|
||||
// support number. If we do, we'll load it's href as well.
|
||||
var callSupport2Title = result['support.callsupport2.title'];
|
||||
if (callSupport2Title !== '') {
|
||||
if (typeof callSupport2Title === 'string' && callSupport2Title !== '') {
|
||||
this._callSupportInfo.push({
|
||||
'title': callSupport2Title,
|
||||
'href': result['support.callsupport2.href']
|
||||
|
|
|
|||
|
|
@ -66,8 +66,11 @@ define(function(require) {
|
|||
},
|
||||
|
||||
_bindEvents: function() {
|
||||
this._elements.passcodeInput.onkeypress =
|
||||
this._onPasscodeInputKeypress.bind(this);
|
||||
this._elements.passcodeInput.type = 'password';
|
||||
this._elements.passcodeInput.inputMode = 'numeric';
|
||||
this._elements.passcodeInput.maxLength = 8;
|
||||
this._elements.passcodeInput.oninput =
|
||||
this._onPasscodeInput.bind(this);
|
||||
this._elements.passcodeContainer.onclick =
|
||||
this._onPasscodeContainerClick.bind(this);
|
||||
},
|
||||
|
|
@ -77,37 +80,14 @@ define(function(require) {
|
|||
this._elements.passcodeInput.focus();
|
||||
},
|
||||
|
||||
_onPasscodeInputKeypress: function(evt) {
|
||||
// key code for key strokes from the keypad are 0 (numbers) and 8
|
||||
// (backspace). Filter out the events that are not from the keypad.
|
||||
var keyCode = evt.keyCode;
|
||||
if (keyCode !== 0 && keyCode !== 8) {
|
||||
return;
|
||||
_onPasscodeInput: function() {
|
||||
// Android IMEs commit text without legacy keypress/charCode events.
|
||||
var value = this._elements.passcodeInput.value.replace(/[^0-9]/g, '').slice(0, 8);
|
||||
this._hideErrorMessage();
|
||||
if (this._elements.panel.dataset.passcodeStatus === 'success') {
|
||||
this._resetPasscodeStatus();
|
||||
}
|
||||
|
||||
evt.preventDefault();
|
||||
|
||||
if (this._passcodeBuffer === '') {
|
||||
this._hideErrorMessage();
|
||||
}
|
||||
|
||||
var code = evt.charCode;
|
||||
if (code !== 0 && (code < 0x30 || code > 0x39)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var key = String.fromCharCode(code);
|
||||
if (evt.charCode === 0) {
|
||||
if (this._passcodeBuffer.length > 0) {
|
||||
this._passcodeBuffer = this._getPasscodeBuffer('all');
|
||||
if (this._elements.panel.dataset.passcodeStatus === 'success') {
|
||||
this._resetPasscodeStatus();
|
||||
}
|
||||
}
|
||||
} else if (this._passcodeBuffer.length < 8) {
|
||||
this._passcodeBuffer += key;
|
||||
}
|
||||
|
||||
this._passcodeBuffer = value;
|
||||
this._updatePassCodeUI();
|
||||
this._processPasscode();
|
||||
},
|
||||
|
|
@ -134,6 +114,7 @@ define(function(require) {
|
|||
},
|
||||
|
||||
_updatePassCodeUI: function() {
|
||||
this._elements.passcodeInput.value = this._passcodeBuffer;
|
||||
for (var i = 0; i < 8; i++) {
|
||||
if (i < this._passcodeBuffer.length) {
|
||||
this._elements.passcodeDigits[i].dataset.dot = true;
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ define(function(require) {
|
|||
*/
|
||||
_updateSimSettingsUI: function scm__updateSimSettingsUI() {
|
||||
var firstCardInfo = this._simcards[0].getInfo();
|
||||
var secondCardInfo = this._simcards[1].getInfo();
|
||||
var secondCardInfo = (this._simcards[1] ? this._simcards[1].getInfo() : {absent: true});
|
||||
var hidden = firstCardInfo.absent && secondCardInfo.absent ||
|
||||
this._isAirplaneMode;
|
||||
|
||||
|
|
@ -322,7 +322,7 @@ define(function(require) {
|
|||
*/
|
||||
_updateSelectOptionsUI: function scm__updateSelectOptionsUI() {
|
||||
var firstCardInfo = this._simcards[0].getInfo();
|
||||
var secondCardInfo = this._simcards[1].getInfo();
|
||||
var secondCardInfo = (this._simcards[1] ? this._simcards[1].getInfo() : {absent: true});
|
||||
|
||||
// two cards all are not absent, we have to update separately
|
||||
if (!firstCardInfo.absent && !secondCardInfo.absent) {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ define(function(require) {
|
|||
var info = wifiManager.connectionInformation || {};
|
||||
elements.ip.textContent = info.ipAddress || '';
|
||||
document.l10n.setAttributes(elements.speed,
|
||||
'linkSpeedMbs', { linkSpeed: info.linkSpeed });
|
||||
Number.isFinite(info.linkSpeed) ? 'linkSpeedMbs' : 'unavailable',
|
||||
Number.isFinite(info.linkSpeed) ? { linkSpeed: info.linkSpeed } : {});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -408,14 +408,13 @@
|
|||
|
||||
var appStarter = AppStarter();
|
||||
|
||||
if (document.readyState !== 'loading') {
|
||||
// Deferred Gaia components contain UMD wrappers. Finish loading them before
|
||||
// Alameda starts, otherwise an anonymous component can become a panel module.
|
||||
if (document.readyState === 'complete') {
|
||||
appStarter.start();
|
||||
} else {
|
||||
document.addEventListener('readystatechange', function readyStateChange() {
|
||||
if (document.readyState == 'interactive') {
|
||||
document.removeEventListener('readystatechange', readyStateChange);
|
||||
appStarter.start();
|
||||
}
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
appStarter.start();
|
||||
}, { once: true });
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -439,7 +439,8 @@
|
|||
*/
|
||||
populate: function(callback) {
|
||||
this.initSources(function(entries) {
|
||||
this.deferredSave(entries, callback);
|
||||
// The first render waits for this write; deferring it waits for that render.
|
||||
this.save(entries, callback);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ var currentLayoutMode;
|
|||
var pendingUpdateTitleText = false;
|
||||
|
||||
// Videos recorded by our own camera have filenames of this form
|
||||
var FROMCAMERA = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.3gp$/;
|
||||
var FROMCAMERA = /DCIM\/\d{3}MZLLA\/VID_\d{4}\.(?:3gp|mp4|webm)$/;
|
||||
|
||||
// We have a single instance of the loading checker because it is used
|
||||
// across functions
|
||||
|
|
@ -664,7 +664,7 @@ function deleteFile(filename) {
|
|||
// If we're deleting a video file recorded by our camera,
|
||||
// we also need to delete the poster image associated with
|
||||
// that video.
|
||||
var postername = filename.replace('.3gp', '.jpg');
|
||||
var postername = filename.replace(/\.(?:3gp|mp4|webm)$/, '.jpg');
|
||||
navigator.getDeviceStorage('pictures'). delete(postername);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,3 +70,6 @@ html[dir="rtl"] .wallpaper {
|
|||
margin: 0 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
/* Keep preview tiles aligned when source wallpapers have different ratios. */
|
||||
#wallpapers .wallpaper { aspect-ratio: 2 / 3; object-fit: cover; display: block; }
|
||||
|
|
|
|||
|
|
@ -113,6 +113,10 @@ var DownloadHelper = (function() {
|
|||
*/
|
||||
function getBlob(download) {
|
||||
var req = new Request();
|
||||
if (download.vulpesDownload) {
|
||||
download.getBlob().then(function(blob) { req.done(blob); }, function(error) { req.failed(error); });
|
||||
return req;
|
||||
}
|
||||
var storage = getVolume(download.storageName);
|
||||
var storeAvailableReq = storage.available();
|
||||
|
||||
|
|
@ -394,6 +398,10 @@ var DownloadHelper = (function() {
|
|||
*/
|
||||
function remove(download) {
|
||||
var req = new Request();
|
||||
if (download.vulpesDownload) {
|
||||
navigator.mozDownloadManager.remove(download).then(function() { req.done(download); }, function(error) { req.failed(error); });
|
||||
return req;
|
||||
}
|
||||
var incompleteDownload = download.state !== 'succeeded';
|
||||
// If is not done, use download manager to remove it,
|
||||
// otherwise, deal with the datastore.
|
||||
|
|
|
|||
30
host/Downloads.sys.mjs
Normal file
30
host/Downloads.sys.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Download identities belong to Gecko; paths supplied by content are never used.
|
||||
export class GaiaDownloads {
|
||||
constructor() {this.ids=new WeakMap();this.items=new Map();}
|
||||
async request(app, params={}) {
|
||||
if(!['settings','system'].includes(app.id))throw Error('PERMISSION_DENIED');
|
||||
if(!['list','pause','resume','remove','blob'].includes(params.operation))throw Error('INVALID_VALUE');
|
||||
const {Downloads}=ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs');
|
||||
const list=await Downloads.getList(Downloads.PUBLIC);
|
||||
const entries=await list.getAll();
|
||||
this.items.clear();
|
||||
const rows=entries.map(item=>{
|
||||
if(!this.ids.has(item))this.ids.set(item,Services.uuid.generateUUID().toString());
|
||||
const id=this.ids.get(item);this.items.set(id,item);
|
||||
return {id,url:item.source.url,path:item.target.path,startTime:+item.startTime,
|
||||
contentType:item.contentType||'application/octet-stream',currentBytes:item.currentBytes,totalBytes:item.totalBytes,
|
||||
state:item.succeeded?'succeeded':item.stopped?'stopped':'downloading'};
|
||||
});
|
||||
if(params.operation==='list')return rows;
|
||||
const item=this.items.get(params.id);if(!item)throw Error('DOWNLOAD_NOT_FOUND');
|
||||
if(params.operation==='blob') {
|
||||
if(!item.succeeded)throw Error('DOWNLOAD_NOT_COMPLETE');
|
||||
return File.createFromFileName(item.target.path);
|
||||
}
|
||||
if(params.operation==='pause')await item.cancel();
|
||||
if(params.operation==='resume')await item.start();
|
||||
if(params.operation==='remove') {await item.cancel();await item.finalize(true);if(item.succeeded)await IOUtils.remove(item.target.path,{ignoreAbsent:true});await list.remove(item);}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1 @@
|
|||
// Only the bundled keyboard can edit the field reported by a content actor.
|
||||
export class Input {
|
||||
constructor(runtime) { this.runtime=runtime; this.focus=null; this.sequence=0; }
|
||||
changed(actor, state) {
|
||||
const app=this.runtime.identity(actor);
|
||||
if (!app || app.id==='keyboard') return;
|
||||
if (!state) { if(this.focus?.actor===actor) this.hide(); return; }
|
||||
this.focus={actor,state:{...state,id:++this.sequence}};
|
||||
this.runtime.broadcast('input-focus',this.focus.state,'system');
|
||||
this.runtime.broadcast('input-focus',this.focus.state,'keyboard');
|
||||
}
|
||||
hide() {
|
||||
this.focus=null;
|
||||
this.runtime.broadcast('input-focus',null,'system');
|
||||
this.runtime.broadcast('input-focus',null,'keyboard');
|
||||
}
|
||||
cleanup(actor) { if(this.focus?.actor===actor) this.hide(); }
|
||||
async request(actor, p) {
|
||||
const app=this.runtime.identity(actor);
|
||||
if (!app || !['keyboard','system'].includes(app.id)) throw Error('PERMISSION_DENIED');
|
||||
if (p.operation==='hide') { this.hide();return null; }
|
||||
if(app.id!=='keyboard') throw Error('PERMISSION_DENIED');
|
||||
if(p.operation==='state') return this.focus?.state || null;
|
||||
if(p.operation==='height') {
|
||||
if(!Number.isFinite(p.height) || p.height<0 || p.height>600) throw Error('INVALID_VALUE');
|
||||
this.runtime.broadcast('input-height',{height:p.height},'system');return null;
|
||||
}
|
||||
if(!this.focus || p.id!==this.focus.state.id) throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
const context=this.focus;
|
||||
const result=await context.actor.sendQuery('Vulpes:Edit',p);
|
||||
// An application can close or focus another field while the edit is pending.
|
||||
if(this.focus!==context) throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
Object.assign(context.state,result);
|
||||
return context.state;
|
||||
}
|
||||
}
|
||||
export {Input} from 'resource://vulpes/services/input.mjs';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Desktop message store. Radio operations fail explicitly; no synthetic SMS.
|
||||
export class Messages {
|
||||
constructor(storage, radio = null, notify = () => {}, radioStatus = null) {
|
||||
constructor(storage, radio = null, notify = () => {}, radioStatus = null, mms = null) {
|
||||
this.storage = storage;
|
||||
this.radio = radio;
|
||||
this.notify = notify;
|
||||
this.radioStatus = radioStatus;
|
||||
this.mms = mms;
|
||||
this.queue = Promise.resolve();
|
||||
}
|
||||
receive(incoming) {
|
||||
|
|
@ -14,7 +15,8 @@ export class Messages {
|
|||
if (existing) return existing;
|
||||
const id=Math.max(Date.now(),...records.map(m=>Number(m.id)+1 || 0));
|
||||
const record={id,threadId:records.find(m=>(m.sender||m.receiver)===incoming.sender)?.threadId || id+1,
|
||||
receipt:incoming.receipt,type:'sms',sender:incoming.sender,receiver:null,
|
||||
receipt:incoming.receipt,type:incoming.type || 'sms',sender:incoming.sender,receiver:null,
|
||||
...(incoming.type==='mms' ? {receivers:incoming.receivers || [],subject:incoming.subject || '',smil:incoming.smil || '',attachments:incoming.attachments || [],deliveryInfo:[]} : {}),
|
||||
body:incoming.body,timestamp:incoming.timestamp,delivery:'received',read:false};
|
||||
records.push(record);
|
||||
await this.storage.run('vulpes-private-messages','put',[records,'records']);
|
||||
|
|
@ -24,14 +26,16 @@ export class Messages {
|
|||
return work;
|
||||
}
|
||||
refreshOutgoing() {
|
||||
if (!this.radioStatus) return Promise.resolve();
|
||||
if (!this.radioStatus && !this.mms) return Promise.resolve();
|
||||
const work=this.queue.then(async()=>{
|
||||
const records=(await this.storage.run('vulpes-private-messages','get',['records'])).value || [];
|
||||
for (const message of records.filter(m=>m.delivery==='sending' && m.radioPath)) {
|
||||
const result=await this.radioStatus(message.radioPath);
|
||||
const result=await (message.type==='mms' ? this.mms.status(message.radioPath) : this.radioStatus(message.radioPath));
|
||||
if (!['sent','failed'].includes(result.state)) continue;
|
||||
message.delivery=result.state==='sent'?'sent':'error';
|
||||
message.deliveryStatus=result.state==='sent'?'not-applicable':'error';
|
||||
if(result.error) message.error=result.error;
|
||||
if(message.type==='mms') message.deliveryInfo=message.receivers.map(receiver=>({receiver,deliveryStatus:message.deliveryStatus,readStatus:'not-applicable'}));
|
||||
await this.storage.run('vulpes-private-messages','put',[records,'records']);
|
||||
this.notify(result.state,structuredClone(message));
|
||||
}
|
||||
|
|
@ -104,7 +108,34 @@ export class Messages {
|
|||
this.notify('sent', structuredClone(record));
|
||||
return record;
|
||||
}
|
||||
if (['send', 'sendMMS', 'retrieveMMS', 'getSmscAddress', 'setSmscAddress'].includes(operation))
|
||||
if (operation === 'sendMMS' && this.mms) {
|
||||
const payload=args[0];
|
||||
this.mms.validate(payload);
|
||||
const receivers=payload.receivers.slice();
|
||||
const participants=receivers.slice().sort().join(',');
|
||||
const existing=records.find(m=>(m.type==='mms' && m.delivery!=='received' ? m.receivers.slice().sort().join(',') : m.receiver || m.sender)===participants);
|
||||
const record={id:nextId++,threadId:existing?.threadId || nextId++,type:'mms',sender:null,
|
||||
receivers,subject:payload.subject || '',smil:payload.smil || '',attachments:payload.attachments || [],body:payload.subject || '',
|
||||
timestamp:Date.now(),delivery:'sending',deliveryStatus:'pending',read:true,
|
||||
deliveryInfo:receivers.map(receiver=>({receiver,deliveryStatus:'pending',readStatus:'not-applicable'}))};
|
||||
records.push(record);
|
||||
await this.storage.run('vulpes-private-messages','put',[records,'records']);
|
||||
this.notify('sending',structuredClone(record));
|
||||
try {
|
||||
const submitted=await this.mms.send(payload);
|
||||
record.radioPath=submitted.path;
|
||||
await this.storage.run('vulpes-private-messages','put',[records,'records']);
|
||||
return record;
|
||||
} catch(error) {
|
||||
record.delivery='error';record.deliveryStatus='error';record.error=error.message;
|
||||
record.deliveryInfo=receivers.map(receiver=>({receiver,deliveryStatus:'error',readStatus:'not-applicable'}));
|
||||
await this.storage.run('vulpes-private-messages','put',[records,'records']);
|
||||
this.notify('failed',structuredClone(record));throw error;
|
||||
}
|
||||
}
|
||||
if (['sendMMS', 'retrieveMMS'].includes(operation))
|
||||
throw new Error('NotSupportedError');
|
||||
if (['send', 'getSmscAddress', 'setSmscAddress'].includes(operation))
|
||||
throw new Error('RadioDisabledError');
|
||||
if (operation === 'getThreads') {
|
||||
const threads = new Map();
|
||||
|
|
@ -115,7 +146,7 @@ export class Messages {
|
|||
id: message.threadId,
|
||||
body: message.body,
|
||||
timestamp: message.timestamp,
|
||||
participants: [message.sender || message.receiver],
|
||||
participants: message.delivery==='received' ? [message.sender] : message.type==='mms' ? message.receivers : [message.receiver],
|
||||
unreadCount: records.filter((m) => m.threadId === message.threadId && !m.read).length,
|
||||
lastMessageType: message.type,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ export class NativeCamera {
|
|||
return await Promise.race([
|
||||
(async () => {
|
||||
const pipe=session.process.stdout;
|
||||
const sequence=metadata ? null : await pipe.readUint32();
|
||||
const length=await pipe.readUint32();
|
||||
const header=new DataView(await pipe.read(metadata ? 4 : 8));
|
||||
const sequence=metadata ? null : header.getUint32(0,true);
|
||||
const length=header.getUint32(metadata ? 0 : 4,true);
|
||||
if(metadata) {
|
||||
if(length>65536) throw Error('CAMERA_INVALID_FORMAT');
|
||||
return pipe.readJSON(length);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import {receivedMms} from 'resource://vulpes/services/mms.mjs';
|
||||
import { authorizePlatform } from 'resource://vulpes/services/platform.mjs';
|
||||
import {
|
||||
tundraCapabilities,
|
||||
tundraBluetooth,
|
||||
tundraCallAudio,
|
||||
tundraHardware,
|
||||
tundraRadio,
|
||||
tundraMms,
|
||||
tundraSnapshot,
|
||||
tundraWifi,
|
||||
} from 'resource://vulpes/host/Tundra.sys.mjs';
|
||||
|
|
@ -48,6 +51,25 @@ export class Platform {
|
|||
}
|
||||
async init() {
|
||||
this.radioQueue = Promise.resolve();
|
||||
if (Services.env.get('VULPES_TUNDRA') === '1') {
|
||||
let shown = null;
|
||||
const pollBluetooth = async () => {
|
||||
try {
|
||||
const state = await tundraBluetooth({operation:'status'});
|
||||
const pending = state.pairing?.[0];
|
||||
const app = this.runtime.apps.find(a=>a.id==='bluetooth');
|
||||
if (pending && app && pending.id!==shown) {
|
||||
shown=pending.id;
|
||||
this.runtime.broadcast('launch',{manifestURL:app.manifestURL,origin:app.origin,
|
||||
url:app.origin+'/onpair.html',timestamp:Date.now()},'system');
|
||||
}
|
||||
if (!pending) shown=null;
|
||||
} catch (_) {}
|
||||
setTimeout(pollBluetooth,1500);
|
||||
};
|
||||
pollBluetooth();
|
||||
}
|
||||
|
||||
this.notificationPath = PathUtils.join(PathUtils.profileDir,'vulpes-notifications.json');
|
||||
let notifications = [];
|
||||
try { notifications = await IOUtils.readJSON(this.notificationPath); } catch (_) {}
|
||||
|
|
@ -80,6 +102,16 @@ export class Platform {
|
|||
await tundraRadio('ackMessages',{ids:[row.receipt]});
|
||||
}
|
||||
} catch(error) {console.error('SMS inbox:',error.message);}
|
||||
try {
|
||||
const rows=await tundraMms('incoming');
|
||||
const app=this.runtime.apps.find(a=>a.id==='sms');
|
||||
for(const row of rows) {
|
||||
const message=await this.runtime.messages.receive(receivedMms(row));
|
||||
await this.gaiaNotification(app,{operation:'show',title:message.sender,body:message.body || 'MMS',tag:'sms-'+message.id,data:{threadId:message.threadId}});
|
||||
this.runtime.broadcast('sms-received',message,'sms');
|
||||
await tundraMms('ack',{ids:[row.receipt]});
|
||||
}
|
||||
} catch(error) {console.error('MMS inbox:',error.message);}
|
||||
setTimeout(inbox,2000);
|
||||
};
|
||||
inbox();
|
||||
|
|
@ -136,48 +168,39 @@ export class Platform {
|
|||
!request.secure ||
|
||||
request.sharingScreen ||
|
||||
request.sharingAudio ||
|
||||
request.audioInputDevices?.length ||
|
||||
!request.videoInputDevices?.length
|
||||
!(request.videoInputDevices?.length || request.audioInputDevices?.length)
|
||||
) {
|
||||
deny();
|
||||
return;
|
||||
}
|
||||
const principal = global.documentPrincipal;
|
||||
let permission = Services.perms.testExactPermissionFromPrincipal(principal, 'camera');
|
||||
if (permission === Ci.nsIPermissionManager.UNKNOWN_ACTION) {
|
||||
const allowed = Services.prompt.confirm(
|
||||
browser.ownerDocument.defaultView,
|
||||
'Vulpes — Photo',
|
||||
'Autoriser Photo à utiliser la webcam ? / Allow Camera to use your webcam?',
|
||||
);
|
||||
permission = allowed
|
||||
? Ci.nsIPermissionManager.ALLOW_ACTION
|
||||
: Ci.nsIPermissionManager.DENY_ACTION;
|
||||
Services.perms.addFromPrincipal(principal, 'camera', permission);
|
||||
const video = request.videoInputDevices?.[0], audio = request.audioInputDevices?.[0];
|
||||
for (const [device, kind, message] of [
|
||||
[video, 'camera', 'Autoriser Photo à utiliser la caméra ? / Allow Camera to use your camera?'],
|
||||
[audio, 'microphone', 'Autoriser Photo à enregistrer le son ? / Allow Camera to record audio?']
|
||||
]) {
|
||||
if (!device) continue;
|
||||
let permission = Services.perms.testExactPermissionFromPrincipal(principal, kind);
|
||||
if (permission === Ci.nsIPermissionManager.UNKNOWN_ACTION) {
|
||||
permission = Services.prompt.confirm(browser.ownerDocument.defaultView, 'Vulpes — Photo', message)
|
||||
? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION;
|
||||
Services.perms.addFromPrincipal(principal, kind, permission);
|
||||
}
|
||||
if (permission !== Ci.nsIPermissionManager.ALLOW_ACTION) { deny(); return; }
|
||||
}
|
||||
if (permission !== Ci.nsIPermissionManager.ALLOW_ACTION) {
|
||||
deny();
|
||||
return;
|
||||
}
|
||||
if (!(await actor.checkOSPermission(true, false, false))) {
|
||||
deny();
|
||||
return;
|
||||
}
|
||||
// Gecko consumes this one-shot grant when allocating the real device.
|
||||
// Use its top-level principal, as Firefox's WebRTC permission handler does.
|
||||
if (!(await actor.checkOSPermission(!!video, !!audio, false))) { deny(); return; }
|
||||
const topPrincipal = browser.browsingContext.currentWindowGlobal.documentPrincipal;
|
||||
Services.perms.addFromPrincipal(
|
||||
topPrincipal,
|
||||
'MediaManagerVideo',
|
||||
Ci.nsIPermissionManager.ALLOW_ACTION,
|
||||
Ci.nsIPermissionManager.EXPIRE_SESSION,
|
||||
);
|
||||
const device = request.videoInputDevices[0];
|
||||
actor.activateDevicePerm(request.windowID, device.mediaSource, device.rawId);
|
||||
const devices = [];
|
||||
for (const [device, kind] of [[video, 'MediaManagerVideo'], [audio, 'MediaManagerAudio']]) {
|
||||
if (!device) continue;
|
||||
Services.perms.addFromPrincipal(topPrincipal, kind, Ci.nsIPermissionManager.ALLOW_ACTION, Ci.nsIPermissionManager.EXPIRE_SESSION);
|
||||
actor.activateDevicePerm(request.windowID, device.mediaSource, device.rawId);
|
||||
devices.push(device.deviceIndex);
|
||||
}
|
||||
actor.sendAsyncMessage('webrtc:Allow', {
|
||||
callID: request.callID,
|
||||
windowID: request.windowID,
|
||||
devices: [request.videoInputDevices[0].deviceIndex],
|
||||
devices,
|
||||
});
|
||||
}
|
||||
async request(actor, app, operation, params) {
|
||||
|
|
@ -193,6 +216,9 @@ export class Platform {
|
|||
if (operation === 'snapshot') return tundraSnapshot();
|
||||
if (operation === 'power') return tundraHardware({operation:'power', action:params.action});
|
||||
if (operation === 'wifi') return tundraWifi(params);
|
||||
if (operation === 'deviceSettings') return tundraRadio('deviceSettings', params);
|
||||
if (operation === 'mobileSettings') return tundraRadio('mobileSettings', params);
|
||||
if (operation === 'bluetooth') return tundraBluetooth(params);
|
||||
if (['callHistory','ackCallHistory'].includes(operation)) return this.radio(operation, params);
|
||||
if (operation === 'dial') return this.radio('dial', params);
|
||||
if (operation === 'hangup') return tundraRadio('hangup', params);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { setInterval } from 'resource://gre/modules/Timer.sys.mjs';
|
||||
import {mmsPayload} from 'resource://vulpes/services/mms.mjs';
|
||||
import { setInterval, setTimeout, clearTimeout } from 'resource://gre/modules/Timer.sys.mjs';
|
||||
import {Input} from 'resource://vulpes/host/Input.sys.mjs';
|
||||
import { ScreenPower } from 'resource://vulpes/services/screen-power.mjs';
|
||||
import { Packages } from 'resource://vulpes/services/packages.mjs';
|
||||
import { Activities, isGaiaActivity } from 'resource://vulpes/services/activities.mjs';
|
||||
import { SettingsService } from 'resource://vulpes/services/settings.mjs';
|
||||
import { DataStoreService } from 'resource://vulpes/services/datastores.mjs';
|
||||
import { DataStoresIndexedDB } from 'resource://vulpes/adapters/datastores-indexeddb.mjs';
|
||||
|
|
@ -11,7 +14,7 @@ import { Messages } from 'resource://vulpes/host/Messages.sys.mjs';
|
|||
import { Alarms } from 'resource://vulpes/host/Alarms.sys.mjs';
|
||||
import { Platform } from 'resource://vulpes/host/Platform.sys.mjs';
|
||||
import { Media } from 'resource://vulpes/host/Media.sys.mjs';
|
||||
import { tundraCallAudio, tundraHardware, tundraRadio } from 'resource://vulpes/host/Tundra.sys.mjs';
|
||||
import { tundraCallAudio, tundraHardware, tundraRadio, tundraMms } from 'resource://vulpes/host/Tundra.sys.mjs';
|
||||
Cu.importGlobalProperties(['indexedDB']);
|
||||
|
||||
// This is the only privileged owner of application identity and settings data.
|
||||
|
|
@ -29,6 +32,7 @@ export const Runtime = {
|
|||
wakeLocks: new Map(),
|
||||
idleListeners: new Map(),
|
||||
cleanup(actor) {
|
||||
this.activities?.cleanup(actor);
|
||||
this.input?.cleanup(actor);
|
||||
this.platform?.nativeCamera.cleanup(actor);
|
||||
this.connections?.cleanup(actor);
|
||||
|
|
@ -45,6 +49,22 @@ export const Runtime = {
|
|||
this.input = new Input(this);
|
||||
const root = Services.env.get('VULPES_WORKSPACE_ROOT');
|
||||
this.apps = await IOUtils.readJSON(PathUtils.join(root, 'assets', 'registry.json'));
|
||||
const storeURL = 'http://system.localhost:8765/_vulpes/packages/';
|
||||
const store = async (operation, item) => {
|
||||
const token = await IOUtils.readUTF8(PathUtils.join(root,'profiles','installed-apps','.token'));
|
||||
const response = await fetch(storeURL+operation,{method:'POST',headers:{'X-Vulpes-Install':token,'Content-Type':'application/json'},body:JSON.stringify(item)});
|
||||
if(!response.ok)throw Error('PACKAGE_STORAGE_FAILED');
|
||||
return response.json();
|
||||
};
|
||||
this.refreshInstalled = async () => {
|
||||
const response=await fetch(storeURL+'list');if(!response.ok)throw Error('PACKAGE_STORAGE_FAILED');
|
||||
const installed=await response.json();
|
||||
this.apps.splice(0,this.apps.length,...this.apps.filter(a=>!a.installed),...installed);
|
||||
return installed;
|
||||
};
|
||||
this.packages = new Packages({schedule:setTimeout,unschedule:clearTimeout,save:item=>store('save',item),changed:()=>this.refreshInstalled(),uuid:()=>Services.uuid.generateUUID().toString()});
|
||||
this.removePackage = async id => {const app=this.apps.find(a=>a.id===id && a.installed);if(!app)throw Error('NOT_REMOVABLE');
|
||||
await store('remove',{id});await this.refreshInstalled();this.broadcast('apps-uninstalled',app);};
|
||||
// Installed Gaia audio apps must be able to ring when an alarm fires. This
|
||||
// permission is scoped to their origins in this profile, never web pages.
|
||||
for (const app of this.apps)
|
||||
|
|
@ -91,6 +111,12 @@ export const Runtime = {
|
|||
this.values['vulpes.gaia-restoration.v1'] = true;
|
||||
}
|
||||
Services.prefs.setStringPref('intl.accept_languages', this.values['language.current']);
|
||||
this.activities = new Activities({identity:actor=>this.identity(actor),apps:()=>this.apps,
|
||||
choose:data=>this.broadcast('activity-choice',data,'system'),
|
||||
uuid:()=>Services.uuid.generateUUID().toString(),
|
||||
notify:(actor,type,data)=>actor.sendAsyncMessage('Vulpes:Event',{type,data}),
|
||||
launch:(target,path,{reload=false,entryPoint}={})=>this.broadcast(reload?'activity-launch':'launch',{manifestURL:target.manifestURL,origin:target.origin,
|
||||
entryPoint,url:new URL(path || target.manifest.launch_path || '/index.html',target.origin).href,timestamp:Date.now()},'system')});
|
||||
this.writeQueue = Promise.resolve();
|
||||
this.settings = new SettingsService({
|
||||
get: async (key) => this.values[key],
|
||||
|
|
@ -99,6 +125,19 @@ export const Runtime = {
|
|||
const values = { ...this.values, ...Object.fromEntries(entries) };
|
||||
await IOUtils.writeJSON(path, values, { tmpPath: path + '.tmp' });
|
||||
this.values = values;
|
||||
const prefMap={
|
||||
'ui.click_hold_context_menus.delay':'ui.click_hold_context_menus.delay',
|
||||
'accessibility.monoaudio.enable':'accessibility.monoaudio.enable',
|
||||
'geolocation.enabled':'geo.enabled',
|
||||
'privacy.donottrackheader.enabled':'privacy.donottrackheader.enabled',
|
||||
'debug.paint-flashing.enabled':'nglayout.debug.paint_flashing',
|
||||
'apz.overscroll.enabled':'apz.overscroll.enabled'
|
||||
};
|
||||
for(const [key,value] of entries) {
|
||||
const pref=prefMap[key];if(!pref)continue;
|
||||
if(typeof value==='boolean')Services.prefs.setBoolPref(pref,value);
|
||||
else if(key==='ui.click_hold_context_menus.delay' && [400,800,1600].includes(value))Services.prefs.setIntPref(pref,value);
|
||||
}
|
||||
if (entries.some(([key]) => key === 'language.current'))
|
||||
Services.prefs.setStringPref('intl.accept_languages', values['language.current']);
|
||||
if (Services.env.get('VULPES_TUNDRA') === '1') {
|
||||
|
|
@ -115,7 +154,7 @@ export const Runtime = {
|
|||
this.writeQueue = write.catch(() => {});
|
||||
return write;
|
||||
},
|
||||
});
|
||||
}, {maxBytes:32 * 1024 * 1024});
|
||||
const storage = new DataStoresIndexedDB(indexedDB, IDBKeyRange, () =>
|
||||
Services.uuid.generateUUID().toString(),
|
||||
);
|
||||
|
|
@ -132,6 +171,8 @@ export const Runtime = {
|
|||
: null,
|
||||
(type, message) => this.broadcast('sms-' + type, message, 'sms'),
|
||||
path => tundraRadio('messageState', {path}),
|
||||
Services.env.get('VULPES_TUNDRA_RADIO') === '1' ? {validate:mmsPayload,
|
||||
send:message=>tundraMms('send',mmsPayload(message)), status:path=>tundraMms('state',{path})} : null,
|
||||
);
|
||||
this.media = new Media((data) => this.broadcast('mediachange', data));
|
||||
this.platform = new Platform(this);
|
||||
|
|
@ -202,6 +243,16 @@ export const Runtime = {
|
|||
this.actors.add(actor);
|
||||
const { method, params } = request;
|
||||
if (typeof method !== 'string') return { error: { code: 'INVALID_REQUEST' } };
|
||||
if(method.startsWith('marketplace.')) {
|
||||
if(app.id!=='marketplace')return {error:{code:'PERMISSION_DENIED'}};
|
||||
try {
|
||||
if(method==='marketplace.poll'){const url=this.marketplaceDownload;this.marketplaceDownload=null;return {result:url||null};}
|
||||
if(method==='marketplace.cancel'){this.packages.cancel();return {result:true};}
|
||||
if(method==='marketplace.prepare')return {result:await this.packages.prepare(params.url)};
|
||||
if(method==='marketplace.install') {const id=await this.packages.install(params.token);const installed=this.apps.find(a=>a.id===id);this.broadcast('apps-installed',installed);return {result:installed};}
|
||||
} catch(error) {return {error:{code:error.message}};}
|
||||
return {error:{code:'METHOD_NOT_SUPPORTED'}};
|
||||
}
|
||||
if(method==='hardware.vibrate') {
|
||||
if(app.id!=='system' || Services.env.get('VULPES_TUNDRA')!=='1') return {error:{code:'NOT_SUPPORTED'}};
|
||||
const pattern=params?.pattern;
|
||||
|
|
@ -267,10 +318,20 @@ export const Runtime = {
|
|||
}
|
||||
}
|
||||
if (typeof method !== 'string') return { error: { code: 'INVALID_REQUEST' } };
|
||||
if (['activities.subscribe','activities.finish','activities.cancel','activities.select'].includes(method) || (method === 'activities.start' && isGaiaActivity(params))) {
|
||||
try {
|
||||
const result = method === 'activities.select' ? this.activities.select(actor,params)
|
||||
: method === 'activities.cancel' ? this.activities.cancel(actor)
|
||||
: method === 'activities.subscribe' ? this.activities.subscribe(actor)
|
||||
: method === 'activities.finish' ? this.activities.finish(actor,params)
|
||||
: await this.activities.start(actor,params);
|
||||
return {result:result ?? null};
|
||||
} catch(error) {return {error:{code:error.message}};}
|
||||
}
|
||||
if (method === 'activities.start') {
|
||||
if (params?.name === 'record' && params.data?.type === 'photos') {
|
||||
// Gallery's Photo button opens the shared Gaia UI; it expects no result file.
|
||||
if (app.id === 'gallery') {
|
||||
// Gallery and the lockscreen open Gaia Camera without expecting a file result.
|
||||
if (['gallery', 'system'].includes(app.id)) {
|
||||
const camera = this.apps.find((a) => a.id === 'camera');
|
||||
if (!camera) return { error: { code: 'ACTIVITY_NOT_SUPPORTED' } };
|
||||
this.broadcast(
|
||||
|
|
@ -316,10 +377,28 @@ export const Runtime = {
|
|||
return { error: { code: error.message || 'VIEW_ERROR' } };
|
||||
}
|
||||
}
|
||||
if (method === 'storage.stats') {
|
||||
try {const {storageStats}=ChromeUtils.importESModule('resource://vulpes/host/StorageStats.sys.mjs');return {result:await storageStats(app)};}
|
||||
catch(error) {return {error:{code:error.message || 'STORAGE_ERROR'}};}
|
||||
}
|
||||
if (method === 'downloads.request') {
|
||||
try {
|
||||
const {GaiaDownloads}=ChromeUtils.importESModule('resource://vulpes/host/Downloads.sys.mjs');
|
||||
this.downloads ||= new GaiaDownloads();
|
||||
return {result:await this.downloads.request(app,params)};
|
||||
} catch(error) {return {error:{code:error.message || 'DOWNLOAD_ERROR'}};}
|
||||
}
|
||||
if (method === 'appearance.current') return { result: this.values['vulpes.theme'] || 'light' };
|
||||
if (method === 'locale.current') return { result: this.values['language.current'] || 'en-US' };
|
||||
if (method === 'settings.snapshot')
|
||||
return this.grants(app).includes('settings.read')
|
||||
? { result: { ...this.values } }
|
||||
: { error: { code: 'PERMISSION_DENIED' } };
|
||||
if (method === 'settings.clockChanged') {
|
||||
if(app.id !== 'settings' || Services.env.get('VULPES_TUNDRA') !== '1') return {error:{code:'PERMISSION_DENIED'}};
|
||||
Services.obs.notifyObservers(null, 'system-timezone-changed');
|
||||
return {result:true};
|
||||
}
|
||||
if (method.startsWith('settings.'))
|
||||
return this.settings.bindSession(app.id, this.grants(app))(request);
|
||||
if (method === 'desktop.idle') {
|
||||
|
|
@ -441,6 +520,10 @@ export const Runtime = {
|
|||
if (method === 'apps.self') return { result: app };
|
||||
if (method === 'apps.list')
|
||||
return manage ? { result: this.apps } : { error: { code: 'PERMISSION_DENIED' } };
|
||||
if(method==='apps.uninstall') {
|
||||
if(!manage)return {error:{code:'PERMISSION_DENIED'}};
|
||||
try {await this.removePackage(params.id);return {result:true};}catch(error){return {error:{code:error.message}};}
|
||||
}
|
||||
if (method === 'apps.launch') {
|
||||
const target = this.apps.find((a) => a.manifestURL === params?.manifestURL);
|
||||
if (!target || (!manage && app.id !== target.id))
|
||||
|
|
@ -469,7 +552,7 @@ export const Runtime = {
|
|||
try {
|
||||
const app = this.identity(actor);
|
||||
if (!app || (target && app.id !== target)) continue;
|
||||
if (type === 'settingchange' && !this.grants(app).includes('settings.read')) continue;
|
||||
if (type === 'settingchange' && !['language.current', 'vulpes.theme'].includes(data.key) && !this.grants(app).includes('settings.read')) continue;
|
||||
if (
|
||||
type === 'mediachange' &&
|
||||
!app.manifest.permissions?.['device-storage:' + data.type]?.access?.includes('read')
|
||||
|
|
|
|||
133
host/SettingsAudit.sys.mjs
Normal file
133
host/SettingsAudit.sys.mjs
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Settings navigation and bounded readback tests, enabled by a private marker.
|
||||
export async function auditSettings({evaluate,wait,sleep,save}) {
|
||||
const context=new Set(['wifi-status','wifi-auth','wifi-joinHidden','wifi-wps','wifi-enterCertificateNickname','call-cf-not-reachable-settings','call-cf-no-reply-settings','call-cf-mobile-busy-settings','call-cf-unconditional-settings','call-voiceMailSettings','hotspot-wifiSettings','screenLock-passcode','apn-editor','apn-list','appPermissions-details','addon-details','homescreen-details','frame','simpin-dialog','call-fdnList-add','call-barring-passcode-change','settings-alert-dialog','settings-confirm-dialog','settings-prompt-dialog','ums-partial-warning','template-messaging','full-developer-mode-warning','full-developer-mode-final-warning']);
|
||||
await evaluate('system','ScreenManager.turnScreenOn(true);if(Service.query("locked"))await Service.request("unlock",{forcibly:true});await VulpesCompat.call("apps.launch",{manifestURL:"http://settings.localhost:8765/manifest.webapp"});return true;');
|
||||
await wait('settings','return document.readyState==="complete" && typeof require==="function";');
|
||||
const ids=await evaluate('settings','return Array.from(document.querySelectorAll("section[id]"),e=>e.id);');
|
||||
const report={scope:'navigation only; no destructive actions, SIM PIN attempts or subscriptions',panels:{}};
|
||||
for(const id of ids) {
|
||||
if(context.has(id)){report.panels[id]={contextRequired:true};continue;}
|
||||
try {
|
||||
await evaluate('settings',`window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate(${JSON.stringify(id)},${JSON.stringify(id==='messaging-details'?{cardIndex:0}:{})}));return true;`);
|
||||
await wait('settings',`return document.querySelector('section.current')?.id===${JSON.stringify(id)};`,25000);
|
||||
// Slow carrier queries continue after the panel appears. Wait for those
|
||||
// controls before leaving, rather than flooding the radio worker while
|
||||
// sweeping every panel in a few milliseconds.
|
||||
if(id==='call')await wait('settings',"return !document.querySelector('#ril-callerId').disabled;",30000);
|
||||
if(id==='call-forwarding')await wait('settings',"return !!document.querySelector('#call-forwarding .callForwardingUnconditional[aria-disabled=false]');",30000);
|
||||
await sleep(450);
|
||||
report.panels[id]=await evaluate('settings',`const p=document.getElementById(${JSON.stringify(id)});return {opened:true,title:p.querySelector('h1')?.textContent,unavailable:!!p.querySelector('[data-vulpes-unavailable]'),links:Array.from(p.querySelectorAll('a[href^="#"]'),e=>e.getAttribute('href')),controls:Array.from(p.querySelectorAll('input,select,gaia-switch,gaia-checkbox'),e=>({name:e.name||e.id||e.getAttribute('name'),disabled:!!e.disabled,hidden:!e.checkVisibility()})),errors:window.__vulpesDiagnostics};`);
|
||||
} catch(error) {
|
||||
report.panels[id]={opened:false,error:String(error),errors:await evaluate('settings','return window.__vulpesDiagnostics;')};
|
||||
await evaluate('settings','location.reload();return true;');await sleep(1500);
|
||||
await wait('settings','return document.readyState==="complete" && typeof require==="function";');
|
||||
}
|
||||
await save(report);
|
||||
}
|
||||
report.dialogs={};
|
||||
for(const [id,method,options] of [
|
||||
['settings-confirm-dialog','confirm',{}],['settings-prompt-dialog','prompt',{}],['settings-alert-dialog','alert',{}],
|
||||
['wifi-auth','show',{network:{ssid:'Audit — not connected',security:['WPA-PSK']},security:'WPA-PSK',sl:3}],
|
||||
['wifi-status','show',{network:{ssid:'Audit — not connected',security:[]},security:'',sl:3}],
|
||||
['wifi-joinHidden','show',{}],['screenLock-passcode','show',{mode:'create'}]
|
||||
]) {
|
||||
try {
|
||||
await evaluate('settings',`window.__dialogResult=null;window.__vulpesDiagnostics=[];require(['modules/dialog_service'],d=>d.${method}(${JSON.stringify(method==='show'?id:'cancel')},${JSON.stringify(options)}).then(r=>window.__dialogResult=r,e=>window.__dialogResult={error:String(e)}));return true;`);
|
||||
await wait('settings',`return document.getElementById(${JSON.stringify(id)}).classList.contains('current');`,3000);
|
||||
await evaluate('settings',`const p=document.getElementById(${JSON.stringify(id)});const button=p.querySelector('button[type=reset]')||p.querySelector('button[type=submit]');if(button)button.click();else p.querySelector('gaia-header')?.dispatchEvent(new CustomEvent('action'));return true;`);
|
||||
await wait('settings',`return !!window.__dialogResult;`,3000);
|
||||
report.dialogs[id]=await evaluate('settings',`return {result:window.__dialogResult,errors:window.__vulpesDiagnostics,closed:!document.getElementById(${JSON.stringify(id)}).classList.contains('current')};`);
|
||||
} catch(error) {
|
||||
report.dialogs[id]={error:String(error),errors:await evaluate('settings','return window.__vulpesDiagnostics;')};
|
||||
await evaluate('settings','location.reload();return true;');await sleep(1500);
|
||||
await wait('settings','return document.readyState==="complete" && typeof require==="function";');
|
||||
}
|
||||
await save(report);
|
||||
}
|
||||
report.effects={};
|
||||
for(const [name,code] of Object.entries({
|
||||
storage:'return await VulpesCompat.call("storage.stats",{});',
|
||||
modemReadback:'const read=await VulpesCompat.call("platform.mobileSettings",{action:"network"});if(typeof read.enabled!=="boolean")throw Error("Unknown data state");const after=await VulpesCompat.call("platform.mobileSettings",{action:"dataEnabled",enabled:read.enabled});return {same:read.enabled===after.enabled,registered:after.status,technology:after.technology};',
|
||||
clockReadback:'const read=await VulpesCompat.call("platform.deviceSettings",{action:"clock"});const after=await VulpesCompat.call("platform.deviceSettings",{action:"setTimezone",timezone:read.timezone});return {same:read.timezone===after.timezone,timezone:after.timezone,automaticAvailable:after.automaticAvailable};'
|
||||
})) {
|
||||
try {report.effects[name]=await evaluate('settings',code);}
|
||||
catch(error){report.effects[name]={error:String(error)};}
|
||||
}
|
||||
report.networkUi={};
|
||||
try {
|
||||
await evaluate('settings',"window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate('operator-settings'));return true;");
|
||||
await wait('settings',"return document.querySelector('#operator-settings.current select:not([disabled])')?.options.length>0;",7000);
|
||||
report.networkUi.radio=await evaluate('settings',"const s=document.querySelector('#operator-settings select');s.dispatchEvent(new Event('change',{bubbles:true}));return {mode:s.value,choices:Array.from(s.options,o=>o.value)};");
|
||||
await wait('settings',"return !document.querySelector('#operator-settings select').disabled;",7000);
|
||||
report.networkUi.radio.same=await evaluate('settings',`return document.querySelector('#operator-settings select').value===${JSON.stringify(report.networkUi.radio.mode)};`);
|
||||
for(const type of ['default','mms']) {
|
||||
await evaluate('settings',`require(['modules/settings_service'],s=>s.navigate('apn-settings'));return true;`);
|
||||
await wait('settings',"return !!document.querySelector('#apn-settings.current [data-apn-type]');",4000);
|
||||
await evaluate('settings',`document.querySelector('#apn-settings [data-apn-type=${type}]').click();return true;`);
|
||||
await wait('settings',"return !!document.querySelector('#apn-list.current label.name');",7000);
|
||||
await evaluate('settings',"document.querySelector('#apn-list label.name').dispatchEvent(new MouseEvent('mouseup',{bubbles:true}));return true;");
|
||||
await wait('settings',"return !!document.querySelector('#apn-editor.current .apn:not([disabled])');",7000);
|
||||
report.networkUi[type]=await evaluate('settings',"const p=document.querySelector('#apn-editor');return {opened:true,apnPopulated:!!p.querySelector('.apn').value,passwordBlank:p.querySelector('.password').value==='',protocol:p.querySelector('.protocol').value,authentication:p.querySelector('.authtype').value,mmscEditable:!p.querySelector('.mmsc').disabled};");
|
||||
// Submit the current APN unchanged, through Gaia. Never change operator
|
||||
// credentials, activate a bearer or expose any APN/user/password in logs.
|
||||
await evaluate('settings',"const p=document.querySelector('#apn-editor');p.querySelector('.apn').dispatchEvent(new Event('input',{bubbles:true}));p.querySelector('.ok').click();return true;");
|
||||
await wait('settings',"return !!document.querySelector('#apn-list.current label.name');",7000);
|
||||
report.networkUi[type].savedUnchanged=true;
|
||||
}
|
||||
report.networkUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');
|
||||
} catch(error) {report.networkUi.error=String(error);report.networkUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');}
|
||||
report.supplementaryUi={};
|
||||
try {
|
||||
await evaluate('settings',"window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate('call'));return true;");
|
||||
await wait('settings',"return !!document.querySelector('#call.current #ril-callerId:not([disabled])');",30000);
|
||||
report.supplementaryUi.calls=await evaluate('settings',"return {waitingEnabled:!document.querySelector('#menuItem-callWaiting gaia-switch').disabled,callerEnabled:!document.querySelector('#ril-callerId').disabled};");
|
||||
await evaluate('settings',"document.querySelector('#ril-callerId').dispatchEvent(new Event('change',{bubbles:true}));return true;");
|
||||
await wait('settings',"return !document.querySelector('#ril-callerId').disabled;",50000);
|
||||
report.supplementaryUi.callerSavedUnchanged=true;
|
||||
await evaluate('settings',"require(['modules/settings_service'],s=>s.navigate('call-forwarding'));return true;");
|
||||
await wait('settings',"return !!document.querySelector('#call-forwarding.current .callForwardingUnconditional[aria-disabled=false]');",30000);
|
||||
report.supplementaryUi.dialogs={};
|
||||
for(const [selector,id] of [
|
||||
['.callForwardingUnconditional','call-cf-unconditional-settings'],['.callForwardingMobileBusy','call-cf-mobile-busy-settings'],
|
||||
['.callForwardingNoReply','call-cf-no-reply-settings'],['.callForwardingNotReachable','call-cf-not-reachable-settings']
|
||||
]) {
|
||||
await evaluate('settings',`document.querySelector('#call-forwarding ${selector}').click();return true;`);
|
||||
await wait('settings',`return document.getElementById('${id}').classList.contains('current');`,4000);
|
||||
await evaluate('settings',`document.querySelector('#${id} button[type=reset]').click();return true;`);
|
||||
await wait('settings',"return document.getElementById('call-forwarding').classList.contains('current');",4000);
|
||||
report.supplementaryUi.dialogs[id]={opened:true,cancelled:true};
|
||||
}
|
||||
report.supplementaryUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');
|
||||
} catch(error) {report.supplementaryUi.error=String(error);report.supplementaryUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');}
|
||||
report.hotspotUi={};
|
||||
try {
|
||||
await evaluate('settings',"window.__vulpesDiagnostics=[];require(['modules/settings_service'],s=>s.navigate('hotspot'));return true;");
|
||||
await wait('settings',"return !!document.querySelector('#hotspot.current #tethering-wifi-enabled:not([disabled])');",10000);
|
||||
await evaluate('settings',"document.querySelector('#hotspot-settings-section a').click();return true;");
|
||||
await wait('settings',"return document.getElementById('hotspot-wifiSettings').classList.contains('current');",4000);
|
||||
report.hotspotUi=await evaluate('settings',"const p=document.getElementById('hotspot-wifiSettings');return {opened:true,securityChoices:p.querySelector('select').options.length,passwordMasked:p.querySelector('input[name=password]').type==='password'};");
|
||||
await evaluate('settings',"document.querySelector('#hotspot-wifiSettings button[type=reset]').click();return true;");
|
||||
await wait('settings',"return document.getElementById('hotspot').classList.contains('current') && !document.getElementById('hotspot-wifiSettings').classList.contains('current') && !document.querySelector('#tethering-wifi-enabled').disabled;",4000);
|
||||
report.hotspotUi.cancelled=true;
|
||||
const unconfigured=await evaluate('settings',"const s=await VulpesCompat.call('platform.wifi',{operation:'hotspotStatus'});return !s.configured && !s.enabled;");
|
||||
if(unconfigured) {
|
||||
await evaluate('settings',"document.querySelector('#hotspot-settings-section a').click();return true;");
|
||||
await wait('settings',"return document.getElementById('hotspot-wifiSettings').classList.contains('current');",4000);
|
||||
await evaluate('settings',"const p=document.getElementById('hotspot-wifiSettings');p.querySelector('input[type=text]').value='Vulpes-qualification';p.querySelector('select').value='wpa2-psk';p.querySelector('input[name=password]').value=crypto.randomUUID();p.querySelector('button[type=submit]').click();return true;");
|
||||
await wait('settings',"return !!document.querySelector('#hotspot.current #tethering-wifi-enabled:not([disabled])') && !document.getElementById('hotspot-wifiSettings').classList.contains('current');",10000);
|
||||
report.hotspotUi.configuredByAudit=true;
|
||||
for(const enabled of [true,false]) {
|
||||
await evaluate('settings',`const c=document.querySelector('#tethering-wifi-enabled');c.checked=${enabled};c.dispatchEvent(new Event('change',{bubbles:true}));return true;`);
|
||||
await wait('settings',`const c=document.querySelector('#tethering-wifi-enabled');return !c.disabled && c.checked===${enabled};`,90000);
|
||||
const actual=await evaluate('settings',"return (await VulpesCompat.call('platform.wifi',{operation:'hotspotStatus'})).enabled;");
|
||||
if(actual!==enabled)throw Error('Hotspot UI/readback mismatch');
|
||||
report.hotspotUi[enabled?'started':'stopped']=true;
|
||||
}
|
||||
}
|
||||
report.hotspotUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');
|
||||
} catch(error) {report.hotspotUi.error=String(error);report.hotspotUi.errors=await evaluate('settings','return window.__vulpesDiagnostics;');report.hotspotUi.formState=await evaluate('settings',"const p=document.getElementById('hotspot-wifiSettings');return {current:p.classList.contains('current'),message:p.querySelector('[role=status]')?.textContent,submitDisabled:p.querySelector('button[type=submit]')?.disabled};");}
|
||||
report.scope='panel navigation plus storage read and idempotent data/timezone/radio/APN write/readback; temporary hotspot start/stop when initially unconfigured; no destructive actions, PIN attempts or subscriptions';
|
||||
await evaluate('settings','require(["modules/settings_service"],s=>s.navigate("root"));return true;');
|
||||
return report;
|
||||
}
|
||||
56
host/SettingsFollowupAudit.sys.mjs
Normal file
56
host/SettingsFollowupAudit.sys.mjs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Private qualification entry point, invoked only by a test marker/profile.
|
||||
export async function auditFollowup({evaluate,wait,sleep,save,tap}) {
|
||||
const report={checks:{},errors:{},mediaRetained:false};
|
||||
const launch=async app=>{
|
||||
await evaluate('system',`ScreenManager.turnScreenOn(true);if(Service.query('locked'))await Service.request('unlock',{forcibly:true});await VulpesCompat.call('apps.launch',{manifestURL:'http://${app}.localhost:8765/manifest.webapp'});return true;`);
|
||||
await wait(app,'return document.readyState==="complete" && !document.hidden;');
|
||||
await wait('system',`return Service.query('getActiveApp')?.origin==='http://${app}.localhost:8765';`);
|
||||
};
|
||||
await wait('system',`return document.body?.getAttribute('ready-state')==='fullyLoaded';`,30000);
|
||||
await sleep(1000);
|
||||
try {
|
||||
await launch('settings');await wait('settings','return typeof require==="function";');
|
||||
await evaluate('settings',`const s=await new Promise(r=>require(['modules/settings_service'],r));await s.navigate('dateTime');return true;`);
|
||||
await sleep(800);
|
||||
// Test the actual Gaia picker without changing the user's system clock.
|
||||
await evaluate('settings',`if(!navigator.mozTime){window.__testClockInstalled=true;Object.defineProperty(navigator,'mozTime',{value:{},configurable:true});}window.__dateModule=await new Promise(r=>require(['modules/date_time'],r));window.__oldMozTime=__dateModule._mozTime;window.__pickedTime=null;__dateModule._mozTime={set:d=>{window.__pickedTime=+d;return Promise.resolve();}};document.querySelector('#dateTime .time-manual').classList.remove('disabled');for(const e of document.querySelectorAll('#dateTime input'))e.disabled=false;return true;`);
|
||||
for(const type of ['date','time']) {
|
||||
await evaluate('settings',`const e=document.querySelector('#dateTime .${type}-picker');e.disabled=false;e.click();return true;`);
|
||||
await wait('system',`const p=Service.query('getActiveApp')?.valueSelector;return !!(p?.isShown && p._${type}Picker);`);
|
||||
report.checks[type+'Picker']=await evaluate('system',`const p=Service.query('getActiveApp').valueSelector;const e=p.elements.${type==='date'?'spinDate':'time'}PickerPopup;return {visible:e.checkVisibility(),height:e.getBoundingClientRect().height};`);
|
||||
await evaluate('system',`Service.query('getActiveApp').valueSelector.confirm();return true;`);
|
||||
await wait('settings','return Number.isFinite(window.__pickedTime);');
|
||||
report.checks[type+'Commit']=true;
|
||||
await evaluate('settings','window.__pickedTime=null;return true;');
|
||||
}
|
||||
await evaluate('settings',`__dateModule._mozTime=__oldMozTime;const s=await new Promise(r=>require(['modules/settings_service'],r));await s.navigate('keyboard');return true;`);
|
||||
await launch('keyboard');
|
||||
await wait('keyboard','return !!document.querySelector("#general gaia-header");');
|
||||
await evaluate('keyboard',`document.querySelector('#general gaia-header').dispatchEvent(new CustomEvent('action'));return true;`);
|
||||
await wait('system',`return Service.query('getActiveApp')?.origin==='http://settings.localhost:8765';`);
|
||||
report.checks.keyboardBack=true;
|
||||
} catch(error){report.errors.settings=String(error);}
|
||||
finally {
|
||||
await evaluate('settings','if(window.__dateModule)__dateModule._mozTime=window.__oldMozTime;if(window.__testClockInstalled)delete navigator.mozTime;return true;').catch(()=>{});
|
||||
}
|
||||
await save(report);
|
||||
try {
|
||||
await launch('camera');
|
||||
await wait('camera','return !!window.app?.camera?.mozCamera && !app.camera.isBusy;');
|
||||
await evaluate('camera',`window.__vulpesDiagnostics=[];window.__lastVideo=null;app.once('newmedia',v=>{window.__lastVideo=v;});app.settings.mode.select('video');return true;`);
|
||||
await wait('camera','return app.camera.mode==="video" && !app.camera.isBusy;');
|
||||
await tap('camera','.js-capture');
|
||||
await wait('camera','return app.camera.get("recording")==="started";');
|
||||
await sleep(3000);
|
||||
await tap('camera','.js-capture');
|
||||
await wait('camera','return !!window.__lastVideo?.blob;',20000);
|
||||
report.checks.video=await evaluate('camera',`const v=window.__lastVideo;const el=document.createElement('video');el.muted=true;el.src=URL.createObjectURL(v.blob);try{await new Promise((ok,fail)=>{el.onloadeddata=ok;el.onerror=()=>fail(Error('Recorded video cannot be decoded'));});return {bytes:v.blob.size,mime:v.blob.type,path:v.filepath,width:el.videoWidth,height:el.videoHeight,poster:v.poster.blob.size};}finally{URL.revokeObjectURL(el.src);el.remove();}`);
|
||||
report.errors.camera=await evaluate('camera','return __vulpesDiagnostics.filter(x=>x.kind==="error" || x.kind==="rejection");');
|
||||
} catch(error){report.errors.video=String(error);report.errors.camera=await evaluate('camera','return window.__vulpesDiagnostics;').catch(()=>[]);}
|
||||
finally {
|
||||
await evaluate('camera',`await app.camera.mozCamera?.vulpesFinishRecording?.();if(window.__lastVideo){await navigator.getDeviceStorage('videos').delete(__lastVideo.filepath);await navigator.getDeviceStorage('pictures').delete(__lastVideo.poster.filepath);}app.settings.mode.select('picture');return true;`).catch(()=>{});
|
||||
await evaluate('system',"dispatchEvent(new Event('home'));return true;").catch(()=>{});
|
||||
}
|
||||
report.passed=!Object.values(report.errors).some(x=>Array.isArray(x)?x.length:!!x) && !!report.checks.video;
|
||||
await save(report);return report;
|
||||
}
|
||||
28
host/StorageStats.sys.mjs
Normal file
28
host/StorageStats.sys.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// SPDX-License-Identifier: MPL-2.0
|
||||
// Only byte counts for Vulpes-owned storage cross the application boundary.
|
||||
let pending, cached;
|
||||
export async function storageStats(app) {
|
||||
if(app.id!=='settings')throw Error('PERMISSION_DENIED');
|
||||
if(cached && Date.now()-cached.at<5000)return cached.value;
|
||||
if(pending)return pending;
|
||||
pending=(async()=>{
|
||||
async function bytes(path,skip='') {
|
||||
if(path===skip || !await IOUtils.exists(path))return 0;
|
||||
const file=Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);file.initWithPath(path);
|
||||
if(file.isSymlink())return 0;
|
||||
const info=await IOUtils.stat(path);
|
||||
if(info.type==='regular')return info.size;
|
||||
if(info.type!=='directory')return 0;
|
||||
let total=0;
|
||||
for(const child of await IOUtils.getChildren(path)) {
|
||||
try {total+=await bytes(child,skip);} catch(error) {if(error.name!=='NotFoundError')throw error;}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
const profile=PathUtils.profileDir,media=PathUtils.join(profile,'vulpes-media');
|
||||
const file=Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsIFile);file.initWithPath(profile);
|
||||
const value={free:file.diskSpaceAvailable,apps:await bytes(profile,media),sdcard:await bytes(media),scope:'vulpes-profile'};
|
||||
cached={at:Date.now(),value};return value;
|
||||
})().finally(()=>{pending=null;});
|
||||
return pending;
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ export function tundraCapabilities(release) {
|
|||
dial: radio,
|
||||
callControl: radio,
|
||||
sms: radio,
|
||||
mms: radio,
|
||||
alarms: false,
|
||||
hardwareStatus: true,
|
||||
wifiControl: Services.env?.get('VULPES_TUNDRA_WIFI') === '1',
|
||||
|
|
@ -50,7 +51,7 @@ export function tundraCapabilities(release) {
|
|||
|
||||
export async function tundraRadio(operation, params = {}) {
|
||||
if (Services.env.get('VULPES_TUNDRA_RADIO') !== '1') throw Error('RADIO_UNAVAILABLE');
|
||||
if (!['status', 'calls', 'dial', 'hangup', 'answer', 'sms', 'incomingMessages', 'ackMessages', 'messageState'].includes(operation)) throw Error('RADIO_INVALID_REQUEST');
|
||||
if (!['deviceSettings', 'mobileSettings', 'status', 'calls', 'dial', 'hangup', 'answer', 'sms', 'incomingMessages', 'ackMessages', 'messageState'].includes(operation)) throw Error('RADIO_INVALID_REQUEST');
|
||||
const { Subprocess } = ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
|
||||
const process = await Subprocess.call({
|
||||
command: '/usr/bin/python3',
|
||||
|
|
@ -78,8 +79,8 @@ export async function tundraRadio(operation, params = {}) {
|
|||
throw Error('RADIO_INVALID_REPLY');
|
||||
}
|
||||
if (reply?.ok === false) {
|
||||
const errors = ['RADIO_NOT_REGISTERED', 'RADIO_BUSY', 'RADIO_NOT_READY',
|
||||
'RADIO_REQUEST_FAILED', 'INVALID_NUMBER', 'INVALID_SMS', 'INVALID_CALL'];
|
||||
const errors = ['RADIO_NOT_REGISTERED', 'RADIO_BUSY', 'RADIO_NOT_READY', 'RADIO_TIMEOUT',
|
||||
'RADIO_REQUEST_FAILED', 'RADIO_READBACK_FAILED', 'INVALID_REQUEST', 'APN_IN_USE', 'APN_NOT_FOUND', 'APN_WRITE_FAILED', 'APN_ROLLBACK_FAILED', 'CLOCK_REQUEST_FAILED', 'CLOCK_UNAVAILABLE', 'INVALID_NUMBER', 'INVALID_SMS', 'INVALID_CALL'];
|
||||
throw Error(errors.includes(reply.error) ? reply.error : 'RADIO_REQUEST_FAILED');
|
||||
}
|
||||
if (reply?.ok !== true) throw Error('RADIO_INVALID_REPLY');
|
||||
|
|
@ -92,11 +93,11 @@ const WIFI_SCAN_CACHE_MS = 5000;
|
|||
|
||||
export async function tundraWifi(params) {
|
||||
if (Services.env.get('VULPES_TUNDRA_WIFI') !== '1') throw Error('NOT_SUPPORTED');
|
||||
if (!['scan', 'connect', 'disconnect', 'status', 'setEnabled', 'known', 'forget'].includes(params.operation))
|
||||
if (!['scan', 'connect', 'connectHidden', 'disconnect', 'status', 'setEnabled', 'known', 'forget','hotspotStatus','hotspotSave','hotspotPower'].includes(params.operation))
|
||||
throw Error('WIFI_INVALID_REQUEST');
|
||||
if (params.operation !== 'scan') {
|
||||
try { return await wifiCommand(params); }
|
||||
finally { wifiScanCache = null; }
|
||||
finally { if (['connect','connectHidden','disconnect','forget','setEnabled','hotspotPower'].includes(params.operation)) wifiScanCache = null; }
|
||||
}
|
||||
// System and Settings share a broker: at most one Python scan client exists.
|
||||
if (wifiScanPending) return wifiScanPending;
|
||||
|
|
@ -176,3 +177,33 @@ export async function tundraCallAudio(params) {
|
|||
if (reply.error) throw Error(reply.error);
|
||||
return reply.result;
|
||||
}
|
||||
|
||||
|
||||
// Separate socket and bounded payload so MMS traffic cannot block call control.
|
||||
export async function tundraMms(operation, params={}) {
|
||||
if(Services.env.get('VULPES_TUNDRA_RADIO')!=='1') throw Error('NotSupportedError');
|
||||
if(!['send','state','incoming','ack'].includes(operation)) throw Error('INVALID_REQUEST');
|
||||
const {Subprocess}=ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
|
||||
const process=await Subprocess.call({command:'/usr/bin/python3',arguments:['/etc/tundra/mms-control.py','--client'],stderr:'ignore'});
|
||||
await process.stdin.write(JSON.stringify({version:1,operation,...params}));await process.stdin.close();
|
||||
let output='';for(;;){const chunk=await process.stdout.readString();if(!chunk)break;output+=chunk;if(output.length>1228800){process.kill();throw Error('MMS_TOO_LARGE');}}
|
||||
const {exitCode}=await process.wait();if(exitCode)throw Error('MMS_UNAVAILABLE');
|
||||
const reply=JSON.parse(output);if(reply.ok!==true)throw Error(reply.error || 'MMS_UNAVAILABLE');return reply.result;
|
||||
}
|
||||
|
||||
// A persistent daemon owns BlueZ discovery and pending pairing requests.
|
||||
export async function tundraBluetooth(params) {
|
||||
if (Services.env.get('VULPES_TUNDRA') !== '1') throw Error('NotSupportedError');
|
||||
const {Subprocess}=ChromeUtils.importESModule('resource://gre/modules/Subprocess.sys.mjs');
|
||||
const process=await Subprocess.call({command:'/usr/bin/python3',
|
||||
arguments:['/etc/tundra/bluetooth-control.py','--client'],stderr:'ignore'});
|
||||
await process.stdin.write(JSON.stringify({...params,version:1}));await process.stdin.close();
|
||||
let output='';
|
||||
for (;;) {
|
||||
const chunk=await process.stdout.readString();if(!chunk)break;
|
||||
output+=chunk;if(output.length>65536){process.kill();throw Error('BLUETOOTH_INVALID_REPLY');}
|
||||
}
|
||||
const {exitCode}=await process.wait();if(exitCode)throw Error('BLUETOOTH_UNAVAILABLE');
|
||||
const reply=JSON.parse(output);if(reply.error)throw Error(reply.error);
|
||||
return reply.result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,12 +15,52 @@ export const Views = {
|
|||
);
|
||||
Services.ppmm.sharedData.flush();
|
||||
},
|
||||
hideAll() {
|
||||
for (const entry of this.entries.values()) entry.browser.style.visibility='hidden';
|
||||
},
|
||||
async layout(entry) {
|
||||
const geometry=entry.geometry;
|
||||
if (!geometry) return;
|
||||
let {left,top,width,height,visible}=geometry;
|
||||
let right=left+width,bottom=top+height;
|
||||
try {
|
||||
// A child document can remain visible while its owning Gaia app is hidden.
|
||||
// Check every embedding frame in its own process, up to the system app.
|
||||
for (let child=entry.actor.browsingContext;child.parent;child=child.parent) {
|
||||
const state=await child.parent.currentWindowGlobal.getActor('Vulpes').sendQuery(
|
||||
'Vulpes:ViewState',{contextId:child.id});
|
||||
visible=visible && state.visible;
|
||||
if (!visible) break;
|
||||
left=Math.max(left,state.left);top=Math.max(top,state.top);
|
||||
right=Math.min(right,state.right);bottom=Math.min(bottom,state.bottom);
|
||||
}
|
||||
} catch (_) {visible=false;}
|
||||
if (entry.geometry!==geometry || !entry.browser.isConnected) return;
|
||||
const shell=entry.browser.ownerDocument.defaultView;
|
||||
const bounds=shell.document.getElementById('screen').getBoundingClientRect();
|
||||
const x=shell.mozInnerScreenX+bounds.left,y=shell.mozInnerScreenY+bounds.top;
|
||||
left=Math.max(x,left);top=Math.max(y,top);
|
||||
right=Math.min(x+bounds.width,right);bottom=Math.min(y+bounds.height,bottom);
|
||||
Object.assign(entry.browser.style,{left:(left-x)+'px',top:(top-y)+'px',
|
||||
width:Math.max(0,right-left)+'px',height:Math.max(0,bottom-top)+'px',
|
||||
visibility:visible && right>left && bottom>top?'visible':'hidden'});
|
||||
},
|
||||
prune() {
|
||||
for (const [id, entry] of this.entries) {
|
||||
if (!BrowsingContext.get(id)?.parent) {
|
||||
this.remove(id, entry);
|
||||
continue;
|
||||
}
|
||||
this.layout(entry);
|
||||
const document=entry.browser.browsingContext?.currentWindowGlobal;
|
||||
const page=document?.documentURI.spec || '';
|
||||
// A retry keeps the previous error document until the next navigation commits.
|
||||
if (document?.innerWindowId!==entry.previousDocumentId &&
|
||||
/^about:(neterror|certerror|tabcrashed)/.test(page) && entry.errorPage!==page) {
|
||||
entry.errorPage=page;
|
||||
try {entry.actor.sendAsyncMessage('Vulpes:Event',{type:'view',
|
||||
data:{contextId:id,type:'error',detail:{type:'other'}}});} catch (_) {}
|
||||
}
|
||||
// A detached iframe's BrowsingContext can outlive its DOM element.
|
||||
// Ask the trusted child actor about actual ownership, not just the BC tree.
|
||||
if (entry.checking) continue;
|
||||
|
|
@ -123,7 +163,9 @@ export const Views = {
|
|||
if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
|
||||
emit('loadend', { backgroundColor: 'rgb(255, 255, 255)' });
|
||||
emit('titlechange', browser.contentTitle || browser.currentURI.spec);
|
||||
if (!Components.isSuccessCode(status)) emit('error', { type: 'other', status });
|
||||
// Replacing a navigation cancels the previous request normally.
|
||||
if (!Components.isSuccessCode(status) && status !== Cr.NS_BINDING_ABORTED)
|
||||
emit('error', { type: 'other', status });
|
||||
}
|
||||
},
|
||||
onProgressChange() {},
|
||||
|
|
@ -143,6 +185,8 @@ export const Views = {
|
|||
this.entries.set(contextId, entry);
|
||||
}
|
||||
entry.url = url;
|
||||
entry.errorPage=null;
|
||||
entry.previousDocumentId=entry.browser.browsingContext?.currentWindowGlobal?.innerWindowId;
|
||||
entry.browser.loadURI(uri, {
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
|
|
@ -153,22 +197,8 @@ export const Views = {
|
|||
if (operation === 'geometry') {
|
||||
const { left, top, width, height, visible } = geometry || {};
|
||||
if (![left, top, width, height].every(Number.isFinite)) throw new Error('INVALID_GEOMETRY');
|
||||
const shell = browser.ownerDocument.defaultView,
|
||||
screen = shell.document.getElementById('screen');
|
||||
const bounds = screen.getBoundingClientRect();
|
||||
const x = left - shell.mozInnerScreenX - bounds.left;
|
||||
const y = top - shell.mozInnerScreenY - bounds.top;
|
||||
const right = Math.min(480, x + width),
|
||||
bottom = Math.min(800, y + height);
|
||||
const clippedX = Math.max(0, x),
|
||||
clippedY = Math.max(0, y);
|
||||
Object.assign(browser.style, {
|
||||
left: clippedX + 'px',
|
||||
top: clippedY + 'px',
|
||||
width: Math.max(0, right - clippedX) + 'px',
|
||||
height: Math.max(0, bottom - clippedY) + 'px',
|
||||
visibility: visible && right > clippedX && bottom > clippedY ? 'visible' : 'hidden',
|
||||
});
|
||||
entry.geometry={left,top,width,height,visible};
|
||||
await this.layout(entry);
|
||||
return null;
|
||||
}
|
||||
if (operation === 'back') {
|
||||
|
|
|
|||
7
host/actors/MarketplaceChild.sys.mjs
Normal file
7
host/actors/MarketplaceChild.sys.mjs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export class VulpesMarketplaceChild extends JSWindowActorChild {
|
||||
handleEvent() {
|
||||
const scope={};
|
||||
Services.scriptloader.loadSubScript('resource://vulpes/host/marketplace-page.js',scope);
|
||||
scope.installMarketplacePage(this.contentWindow,url=>this.sendAsyncMessage('Marketplace:Download',{url}));
|
||||
}
|
||||
}
|
||||
13
host/actors/MarketplaceParent.sys.mjs
Normal file
13
host/actors/MarketplaceParent.sys.mjs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Runtime } from 'resource://vulpes/host/Runtime.sys.mjs';
|
||||
import { Views } from 'resource://vulpes/host/Views.sys.mjs';
|
||||
import { packageURL } from 'resource://vulpes/services/packages.mjs';
|
||||
export class VulpesMarketplaceParent extends JSWindowActorParent {
|
||||
receiveMessage(message) {
|
||||
if(message.name!=='Marketplace:Download' || this.manager.documentPrincipal.originNoSuffix!=='https://vulpes-os.org')return;
|
||||
const view=[...Views.entries.values()].find(v=>v.browser.browsingContext.top.id===this.browsingContext.top.id);
|
||||
if(!view || !Runtime.identity(view.actor))return;
|
||||
Runtime.marketplaceDownload=packageURL(message.data.url);
|
||||
const app=Runtime.apps.find(a=>a.id==='marketplace');
|
||||
Runtime.broadcast('launch',{manifestURL:app.manifestURL,origin:app.origin,url:app.origin+'/index.html',timestamp:Date.now()},'system');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
import {InputEditor} from 'resource://vulpes/services/input-editor.mjs';
|
||||
export class VulpesChild extends JSWindowActorChild {
|
||||
handleEvent(event) {
|
||||
if(['focusin','pointerdown'].includes(event.type)) {
|
||||
if(this.editing) return;
|
||||
if(['focusin','pointerdown','click'].includes(event.type)) {
|
||||
if(this._editor?.editing) return;
|
||||
if(!Services.cpmm.sharedData.get('vulpes:native-input')) return;
|
||||
let element=event.target;
|
||||
if(element?.isContentEditable) element=element.closest('[contenteditable=true]') || element;
|
||||
const editable=element?.isContentEditable || (['input','textarea'].includes(element?.localName) && !['button','submit','range','checkbox','radio','date','time','datetime-local','file'].includes(element.type));
|
||||
if(!editable || element.disabled || element.readOnly) {
|
||||
if(event.type==='pointerdown') this.sendAsyncMessage('Vulpes:InputFocus',null);
|
||||
// Dismissing on pointerdown moves buttons before pointerup and loses
|
||||
// the click. Composer actions may also explicitly retain the editor.
|
||||
if(event.type==='click' && !element?.closest?.('[data-vulpes-keep-input]'))
|
||||
this.sendAsyncMessage('Vulpes:InputFocus',null);
|
||||
return;
|
||||
}
|
||||
this.inputElement=element;
|
||||
this.editor.focus(element);
|
||||
this.sendAsyncMessage('Vulpes:InputFocus',this.inputState());
|
||||
return;
|
||||
}
|
||||
|
|
@ -76,71 +80,39 @@ export class VulpesChild extends JSWindowActorChild {
|
|||
{ defineAs: 'vulpesFrameForContext' },
|
||||
);
|
||||
}
|
||||
inputState() {
|
||||
const e=this.inputElement;
|
||||
const text=e.isContentEditable ? e.textContent : e.value;
|
||||
const selection=this.contentWindow.getSelection();
|
||||
let start=e.selectionStart ?? text.length, end=e.selectionEnd ?? start;
|
||||
if(e.isContentEditable && selection.rangeCount && e.contains(selection.anchorNode)) {
|
||||
const range=this.document.createRange();range.selectNodeContents(e);
|
||||
range.setEnd(selection.anchorNode,selection.anchorOffset);start=range.toString().length;
|
||||
range.setEnd(selection.focusNode,selection.focusOffset);end=range.toString().length;
|
||||
[start,end]=[Math.min(start,end),Math.max(start,end)];
|
||||
}
|
||||
return {type:e.isContentEditable?'contenteditable':e.localName,inputType:e.type || (e.classList.contains('recipient')?'tel':'text'),
|
||||
inputMode:e.inputMode || (e.classList.contains('recipient')?'tel':''),
|
||||
text:e.type==='password'?'':text,selectionStart:start,selectionEnd:end};
|
||||
}
|
||||
edit(p) {
|
||||
const e=this.inputElement,win=this.contentWindow;
|
||||
if(!e?.isConnected || this.document.hidden) throw Error('INPUT_CONTEXT_EXPIRED');
|
||||
this.editing=true;
|
||||
try {e.focus();} finally {this.editing=false;}
|
||||
// A touch can focus a new contenteditable while Gecko retains the previous
|
||||
// field's selection. execCommand follows that selection, not activeElement.
|
||||
if(e.isContentEditable) {
|
||||
const selection=win.getSelection();
|
||||
if(!e.contains(selection.anchorNode) || !e.contains(selection.focusNode)) {
|
||||
const range=this.document.createRange();
|
||||
range.selectNodeContents(e);range.collapse(false);
|
||||
selection.removeAllRanges();selection.addRange(range);
|
||||
}
|
||||
}
|
||||
if(p.operation==='key') {
|
||||
let key=typeof p.key==='object'?p.key.key:p.key;
|
||||
if(key===8 || key==='Backspace') this.document.execCommand('delete');
|
||||
else if(key===13 || key==='Enter') {
|
||||
const options={key:'Enter',code:'Enter',keyCode:13,charCode:13,which:13,bubbles:true,cancelable:true};
|
||||
const accepted=e.dispatchEvent(new win.KeyboardEvent('keydown',options)) && e.dispatchEvent(new win.KeyboardEvent('keypress',options));
|
||||
if(accepted && (e.isContentEditable || e.localName==='textarea')) this.document.execCommand('insertLineBreak');
|
||||
e.dispatchEvent(new win.KeyboardEvent('keyup',options));
|
||||
} else {
|
||||
const text=typeof key==='string'?key:String.fromCodePoint(p.charCode || key);
|
||||
if(text.length>16) throw Error('INVALID_VALUE');
|
||||
this.document.execCommand('insertText',false,text);
|
||||
}
|
||||
} else if(['selection','replace'].includes(p.operation)) {
|
||||
const state=this.inputState();
|
||||
const start=p.operation==='selection'?p.start:state.selectionStart+(p.offset||0);
|
||||
const length=p.length||0;
|
||||
if(!Number.isInteger(start)||!Number.isInteger(length)||start<0||length<0||start+length>(e.value ?? e.textContent).length) throw Error('INVALID_VALUE');
|
||||
if(e.isContentEditable) {
|
||||
const walker=this.document.createTreeWalker(e,win.NodeFilter.SHOW_TEXT);
|
||||
const points=[];let offset=0,node;
|
||||
while((node=walker.nextNode())) {points.push({node,start:offset,end:offset+node.length});offset+=node.length;}
|
||||
const point=index=>{const part=points.find(p=>index<=p.end);return part?[part.node,index-part.start]:[e,0];};
|
||||
const range=this.document.createRange();range.setStart(...point(start));range.setEnd(...point(start+length));
|
||||
win.getSelection().removeAllRanges();win.getSelection().addRange(range);
|
||||
} else e.setSelectionRange(start,start+length);
|
||||
if(p.operation==='replace') {
|
||||
if(typeof p.text!=='string'||p.text.length>4096) throw Error('INVALID_VALUE');
|
||||
this.document.execCommand('insertText',false,p.text);
|
||||
}
|
||||
} else throw Error('METHOD_NOT_SUPPORTED');
|
||||
return this.inputState();
|
||||
}
|
||||
get editor() { return this._editor ||= new InputEditor(this.contentWindow); }
|
||||
inputState() { return this.editor.state(); }
|
||||
edit(params) { return this.editor.edit(params); }
|
||||
receiveMessage(message) {
|
||||
if(message.name==='Vulpes:Edit') return this.edit(message.data);
|
||||
if (message.name === 'Vulpes:ViewState') {
|
||||
const frame = Array.from(this.document.querySelectorAll('iframe')).find(
|
||||
f => f.browsingContext?.id === message.data.contextId);
|
||||
if (!frame?.isConnected) return {visible:false};
|
||||
const win = this.contentWindow, rect = frame.getBoundingClientRect();
|
||||
// App chrome (the status bar and browser controls) legitimately covers
|
||||
// the corners of a mozbrowser iframe. Requiring every corner to hit the
|
||||
// iframe hid the external browser completely, leaving a white page for
|
||||
// sites such as YouTube. The centre plus one side sample is sufficient
|
||||
// to prove that the content viewport is visible.
|
||||
const samples = [[.5,.5],[.05,.5],[.95,.5],[.5,.05],[.5,.95]];
|
||||
const hits = samples.reduce((count,[x,y]) => count +
|
||||
(this.document.elementFromPoint(rect.left+rect.width*x,rect.top+rect.height*y)===frame ? 1 : 0), 0);
|
||||
const visible = !this.document.hidden && frame.checkVisibility({checkOpacity:true,checkVisibilityCSS:true}) &&
|
||||
hits >= 2;
|
||||
let top = Math.max(0,rect.top);
|
||||
// Gaia's status bar belongs above all app content, including native web views.
|
||||
if (this.document.location.hostname === 'system.localhost') {
|
||||
const bar=this.document.getElementById('statusbar');
|
||||
if (bar?.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) {
|
||||
const r=bar.getBoundingClientRect();
|
||||
if (r.width>0 && r.bottom>top && r.top<=top) top=r.bottom;
|
||||
}
|
||||
}
|
||||
return {visible,left:Math.max(0,rect.left)+win.mozInnerScreenX,
|
||||
top:top+win.mozInnerScreenY,right:Math.min(win.innerWidth,rect.right)+win.mozInnerScreenX,
|
||||
bottom:Math.min(win.innerHeight,rect.bottom)+win.mozInnerScreenY};
|
||||
}
|
||||
if (message.name === 'Vulpes:ViewAlive') {
|
||||
return Array.from(this.document.querySelectorAll('iframe')).some(
|
||||
(frame) => frame.isConnected && frame.browsingContext?.id === message.data.contextId,
|
||||
|
|
|
|||
|
|
@ -146,6 +146,12 @@ h1, h2, h3, h4, h5, h6, #statusbar-time,
|
|||
/* Gaia owns edge gestures; avoid a second host overscroll effect. */
|
||||
html[data-vulpes-app] { overscroll-behavior:none; }
|
||||
|
||||
/* padding-box is no longer supported. Content-box made the full-width
|
||||
section wrap below the dialog's spacer, hiding its title and message. */
|
||||
form[role='dialog'][data-type='confirm'] > section {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
/* Native selects now paint their own arrow; the old Gecko arrow-hiding offset
|
||||
clips Fira descenders on current Gecko. */
|
||||
|
|
@ -196,3 +202,51 @@ html[data-vulpes-app='communications'] body {
|
|||
|
||||
/* Camera gestures must not magnify the application chrome. */
|
||||
html[data-vulpes-app='camera'], html[data-vulpes-app='camera'] body { touch-action:none; }
|
||||
|
||||
/* Homescreen pages must settle at their boundary after a drag (Forgejo #3). */
|
||||
html[data-vulpes-app="homescreen"] #panels { scroll-snap-type:x mandatory; }
|
||||
html[data-vulpes-app="homescreen"] #panels > * { scroll-snap-align:start; }
|
||||
|
||||
/* System gestures are handled by Gaia, not by the page's pan/zoom handler.
|
||||
App frames keep their own scrolling; the notification list scrolls vertically. */
|
||||
html[data-vulpes-app='system'],
|
||||
html[data-vulpes-app='system'] body {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
html[data-vulpes-app='system'] #lockscreen {
|
||||
touch-action: none;
|
||||
}
|
||||
html[data-vulpes-app='system'] #utility-tray {
|
||||
touch-action: pan-y;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* Keep the original Gaia value selector inside the application's viewport. */
|
||||
html[data-vulpes-app] #vulpes-value-selector {
|
||||
position: fixed; inset: 0; z-index: 2147483000; margin: 0;
|
||||
font-family: FiraSans, sans-serif; box-sizing: border-box;
|
||||
}
|
||||
html[data-vulpes-app] #vulpes-value-selector > section { box-sizing: border-box; }
|
||||
html[data-vulpes-app] #vulpes-value-selector [role=option][aria-disabled=true] { opacity: .45; }
|
||||
|
||||
html[data-vulpes-app] #vulpes-value-selector-layer {
|
||||
position: fixed; inset: 0; z-index: 2147483000;
|
||||
}
|
||||
|
||||
html[data-vulpes-app] :is(.button,.bb-button) select.vulpes-select-label-owner {
|
||||
opacity: 0;
|
||||
}
|
||||
html[data-vulpes-app] .vulpes-select-label {
|
||||
position: absolute;
|
||||
inset: 0 3.5rem 0 1.1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
font: italic 1.7rem/1.5 FiraSans, sans-serif;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
color: var(--text-color, #333);
|
||||
text-shadow: none;
|
||||
}
|
||||
html[dir=rtl][data-vulpes-app] .vulpes-select-label { inset: 0 1.1rem 0 3.5rem; }
|
||||
html[data-vulpes-theme=dark] .vulpes-select-label { color: #eee; }
|
||||
|
|
|
|||
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});
|
||||
})();
|
||||
|
|
@ -1,11 +1,44 @@
|
|||
// The Gaia camera UI consumes mozCameras. Only live MediaStream capabilities are exposed here.
|
||||
(() => {
|
||||
if (location.hostname !== 'camera.localhost') return;
|
||||
const videoMime = typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported('video/webm;codecs=vp8,opus') ? 'video/webm;codecs=vp8,opus' : null;
|
||||
window.VulpesVideoExtension = videoMime ? 'webm' : null;
|
||||
let cameras = ['back'],
|
||||
devices = new Map(),
|
||||
current = null,
|
||||
generation = 0;
|
||||
let cameraError = null;
|
||||
let opening = Promise.resolve(), closing = Promise.resolve();
|
||||
const releases = new WeakMap();
|
||||
const nativeViews = new WeakMap();
|
||||
function releaseStream(stream, preview) {
|
||||
if (releases.has(stream)) return releases.get(stream);
|
||||
if (current === stream) current = null;
|
||||
const release = Promise.resolve().then(async () => {
|
||||
await stream.vulpesFinishRecording?.();
|
||||
emit(stream, 'previewstatechange', {newState:'stopped'});
|
||||
// Stop the capture producer before detaching its consumers. Gecko can
|
||||
// otherwise block in srcObject=null while a GPU-backed frame is in use.
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
for (const video of document.querySelectorAll('video')) {
|
||||
if (nativeViews.get(video) === stream) {
|
||||
nativeViews.delete(video);
|
||||
stream.vulpesPreviewCanvas.remove();
|
||||
video.style.display = '';
|
||||
}
|
||||
if (video.srcObject === stream) {
|
||||
video.pause(); video.srcObject = null;
|
||||
if ('vulpesCapture' in video.dataset) video.remove();
|
||||
}
|
||||
}
|
||||
preview?.remove();
|
||||
try { await stream.vulpesCameraClose?.(); }
|
||||
finally { stream.getTracks().forEach(track => track.stop()); }
|
||||
});
|
||||
releases.set(stream, release);
|
||||
closing = release.catch(recordError);
|
||||
return release;
|
||||
}
|
||||
function recordError(error) {
|
||||
cameraError = { name: error.name || 'Error', message: error.message || '' };
|
||||
console.warn('Vulpes Camera:', cameraError.name, cameraError.message);
|
||||
|
|
@ -76,18 +109,39 @@
|
|||
Object.defineProperty(HTMLMediaElement.prototype, 'mozSrcObject', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return nativeSource.get.call(this);
|
||||
return nativeViews.get(this) || nativeSource.get.call(this);
|
||||
},
|
||||
set(stream) {
|
||||
nativeSource.set.call(this, stream);
|
||||
const old = nativeViews.get(this);
|
||||
if (old) {
|
||||
old.vulpesPreviewCanvas.remove();
|
||||
nativeViews.delete(this);
|
||||
this.style.display = '';
|
||||
}
|
||||
if (stream?.vulpesPreviewCanvas) {
|
||||
// The HAL already delivers NV21 frames. Render them directly in Gaia's
|
||||
// viewfinder instead of capturing the WebGL canvas into a second GPU
|
||||
// video pipeline, which stalls on repeated switches on Sargo.
|
||||
nativeViews.set(this, stream);
|
||||
const canvas = stream.vulpesPreviewCanvas;
|
||||
canvas.className = this.className;
|
||||
canvas.dataset.vulpesNativePreview = '';
|
||||
canvas.style.cssText = 'display:block;width:100%;height:100%;pointer-events:none';
|
||||
this.style.display = 'none';
|
||||
this.parentNode.append(canvas);
|
||||
} else nativeSource.set.call(this, stream);
|
||||
},
|
||||
});
|
||||
const playVideo = HTMLMediaElement.prototype.play;
|
||||
HTMLMediaElement.prototype.play = function() {
|
||||
return nativeViews.has(this) ? Promise.resolve() : playVideo.call(this);
|
||||
};
|
||||
function emit(target, type, detail = {}) {
|
||||
const event = new Event(type);
|
||||
Object.assign(event, detail);
|
||||
target.dispatchEvent(event);
|
||||
}
|
||||
async function discover(stream) {
|
||||
async function discover(stream, name) {
|
||||
if (stream.vulpesNative) {
|
||||
cameras = ['back', 'front'];
|
||||
return;
|
||||
|
|
@ -104,24 +158,32 @@
|
|||
const active = list.find((d) => d.deviceId === settings.deviceId) || list[0];
|
||||
const back = list.find((d) => /back|rear|environment/i.test(d.label));
|
||||
const front = list.find((d) => /front|user|face/i.test(d.label));
|
||||
const first =
|
||||
back ||
|
||||
(settings.facingMode === 'user' && list.length > 1 ? list.find((d) => d !== active) : active);
|
||||
// Unlabelled webcams must not exchange identities every time we switch.
|
||||
const previousBack = list.find(d => d.deviceId === devices.get('back'));
|
||||
const previousFront = list.find(d => d.deviceId === devices.get('front'));
|
||||
const first = back || previousBack ||
|
||||
((settings.facingMode === 'user' || name === 'front') && list.length > 1
|
||||
? list.find(d => d !== active) : active);
|
||||
devices = new Map();
|
||||
if (first) devices.set('back', first.deviceId);
|
||||
const second = front && front !== first ? front : list.find((d) => d !== first);
|
||||
const second = [front, previousFront].find(d => d && d !== first) || list.find(d => d !== first);
|
||||
if (second) devices.set('front', second.deviceId);
|
||||
cameras = devices.size ? [...devices.keys()] : ['back'];
|
||||
}
|
||||
// Keep one graphics context per app. Losing/recreating it at every lens switch
|
||||
// can exhaust the mobile driver while captured video frames are still in use.
|
||||
let nativeCanvas = null;
|
||||
async function nativeStream(name) {
|
||||
let session;
|
||||
const call = (params) => VulpesCompat.call('platform.cameraPreview', {...params, session});
|
||||
const meta = await call({action:'start', camera:name === 'front' ? 'front' : 'back'});
|
||||
session = meta.session;
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvas = nativeCanvas || (nativeCanvas = document.createElement('canvas'));
|
||||
const sideways = meta.orientation % 180 !== 0;
|
||||
canvas.width = sideways ? meta.height : meta.width;
|
||||
canvas.height = sideways ? meta.width : meta.height;
|
||||
const width = sideways ? meta.height : meta.width;
|
||||
const height = sideways ? meta.width : meta.height;
|
||||
if (canvas.width !== width) canvas.width = width;
|
||||
if (canvas.height !== height) canvas.height = height;
|
||||
// Convert NV21 in a shader instead of blocking Gaia's UI with one JS loop
|
||||
// per pixel. The two textures contain Y and interleaved V/U respectively.
|
||||
const gl = canvas.getContext('webgl', {alpha:false, antialias:false, preserveDrawingBuffer:true});
|
||||
|
|
@ -129,81 +191,100 @@
|
|||
await call({action:'stop'});
|
||||
throw new DOMException('Camera preview requires WebGL', 'NotSupportedError');
|
||||
}
|
||||
function shader(type, source) {
|
||||
const value = gl.createShader(type);
|
||||
gl.shaderSource(value, source); gl.compileShader(value);
|
||||
if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw Error('Camera shader compilation failed');
|
||||
return value;
|
||||
}
|
||||
const program = gl.createProgram();
|
||||
gl.attachShader(program, shader(gl.VERTEX_SHADER, `
|
||||
attribute vec2 position; varying vec2 coord;
|
||||
void main() { gl_Position=vec4(position,0.,1.); coord=vec2(position.x*.5+.5,.5-position.y*.5); }
|
||||
`));
|
||||
gl.attachShader(program, shader(gl.FRAGMENT_SHADER, `
|
||||
precision mediump float; varying vec2 coord;
|
||||
uniform sampler2D luma; uniform sampler2D chroma; uniform float rotation;
|
||||
void main() {
|
||||
vec2 p=coord;
|
||||
if(rotation==90.) p=vec2(coord.y,1.-coord.x);
|
||||
else if(rotation==270.) p=vec2(1.-coord.y,coord.x);
|
||||
else if(rotation==180.) p=1.-coord;
|
||||
float y=1.16438356*(texture2D(luma,p).r-16./255.);
|
||||
vec4 vu=texture2D(chroma,p);
|
||||
float v=vu.r-128./255.; float u=vu.a-128./255.;
|
||||
gl_FragColor=vec4(y+1.59602678*v,y-.39176229*u-.81296764*v,y+2.01723214*u,1.);
|
||||
}
|
||||
`));
|
||||
gl.linkProgram(program);
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw Error('Camera shader link failed');
|
||||
gl.useProgram(program);
|
||||
const buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
|
||||
const position=gl.getAttribLocation(program,'position');
|
||||
gl.enableVertexAttribArray(position);gl.vertexAttribPointer(position,2,gl.FLOAT,false,0,0);
|
||||
gl.uniform1f(gl.getUniformLocation(program,'rotation'),meta.orientation);
|
||||
const textures = ['luma','chroma'].map((name,index) => {
|
||||
const texture=gl.createTexture();gl.activeTexture(gl.TEXTURE0+index);gl.bindTexture(gl.TEXTURE_2D,texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
|
||||
gl.uniform1i(gl.getUniformLocation(program,name),index);return texture;
|
||||
});
|
||||
const timing = {frames:0, startedAt:performance.now(), conversionMs:0};
|
||||
let stopped = false, timer, stream, sequence = -1, closing;
|
||||
const timing = {frames:0, startedAt:performance.now(), conversionMs:0, transferMs:0, sensorFrames:0};
|
||||
let stopped = false, timer, stream, wakeLock, sequence = -1, closing, program, buffer;
|
||||
const textures = [], shaders = [];
|
||||
const stop = () => {
|
||||
if (closing) return closing;
|
||||
stopped = true; clearTimeout(timer);
|
||||
wakeLock?.unlock(); wakeLock = null;
|
||||
closing = call({action:'stop'}).finally(() => {
|
||||
if (stream) stream.vulpesClosed = true;
|
||||
textures.forEach(texture => gl.deleteTexture(texture));
|
||||
gl.deleteBuffer(buffer); gl.deleteProgram(program);
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
shaders.forEach(value => gl.deleteShader(value));
|
||||
if (buffer) gl.deleteBuffer(buffer);
|
||||
if (program) gl.deleteProgram(program);
|
||||
// The next lens reuses this context after its old stream has stopped.
|
||||
gl.clearColor(0,0,0,1); gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
});
|
||||
return closing;
|
||||
};
|
||||
const draw = async () => {
|
||||
const frame = await call({action:'frame'});
|
||||
if (stopped || !frame.data || frame.sequence === sequence) return false;
|
||||
const data = new Uint8Array(frame.data), w = meta.width, h = meta.height;
|
||||
if (data.length !== w * h * 3 / 2) throw Error('Invalid native camera frame');
|
||||
sequence = frame.sequence;
|
||||
const start=performance.now();
|
||||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]);
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,w,h,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,w*h));
|
||||
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]);
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE_ALPHA,w/2,h/2,0,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(w*h));
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
timing.frames++;timing.conversionMs+=performance.now()-start;
|
||||
return true;
|
||||
};
|
||||
try {
|
||||
function shader(type, source) {
|
||||
const value = gl.createShader(type);
|
||||
shaders.push(value);
|
||||
gl.shaderSource(value, source); gl.compileShader(value);
|
||||
if (!gl.getShaderParameter(value, gl.COMPILE_STATUS)) throw Error('Camera shader compilation failed');
|
||||
return value;
|
||||
}
|
||||
program = gl.createProgram();
|
||||
gl.attachShader(program, shader(gl.VERTEX_SHADER, `
|
||||
attribute vec2 position; varying vec2 coord;
|
||||
void main() { gl_Position=vec4(position,0.,1.); coord=vec2(position.x*.5+.5,.5-position.y*.5); }
|
||||
`));
|
||||
gl.attachShader(program, shader(gl.FRAGMENT_SHADER, `
|
||||
precision mediump float; varying vec2 coord;
|
||||
uniform sampler2D luma; uniform sampler2D chroma; uniform float rotation;
|
||||
void main() {
|
||||
vec2 p=coord;
|
||||
if(rotation==90.) p=vec2(coord.y,1.-coord.x);
|
||||
else if(rotation==270.) p=vec2(1.-coord.y,coord.x);
|
||||
else if(rotation==180.) p=1.-coord;
|
||||
float y=1.16438356*(texture2D(luma,p).r-16./255.);
|
||||
vec4 vu=texture2D(chroma,p);
|
||||
float v=vu.r-128./255.; float u=vu.a-128./255.;
|
||||
gl_FragColor=vec4(y+1.59602678*v,y-.39176229*u-.81296764*v,y+2.01723214*u,1.);
|
||||
}
|
||||
`));
|
||||
gl.linkProgram(program);
|
||||
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) throw Error('Camera shader link failed');
|
||||
gl.useProgram(program);
|
||||
buffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);
|
||||
const position=gl.getAttribLocation(program,'position');
|
||||
gl.enableVertexAttribArray(position);gl.vertexAttribPointer(position,2,gl.FLOAT,false,0,0);
|
||||
gl.uniform1f(gl.getUniformLocation(program,'rotation'),meta.orientation);
|
||||
['luma','chroma'].forEach((name,index) => {
|
||||
const texture=gl.createTexture();textures.push(texture);gl.activeTexture(gl.TEXTURE0+index);gl.bindTexture(gl.TEXTURE_2D,texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);
|
||||
gl.uniform1i(gl.getUniformLocation(program,name),index);
|
||||
});
|
||||
gl.viewport(0,0,canvas.width,canvas.height);
|
||||
const pixels = meta.width * meta.height;
|
||||
gl.activeTexture(gl.TEXTURE0);
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE,meta.width,meta.height,0,gl.LUMINANCE,gl.UNSIGNED_BYTE,null);
|
||||
gl.activeTexture(gl.TEXTURE1);
|
||||
gl.texImage2D(gl.TEXTURE_2D,0,gl.LUMINANCE_ALPHA,meta.width/2,meta.height/2,0,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,null);
|
||||
const draw = async () => {
|
||||
const requested = performance.now();
|
||||
const frame = await call({action:'frame'});
|
||||
timing.transferMs += performance.now()-requested;
|
||||
if (stopped || !frame.data || frame.sequence === sequence) return false;
|
||||
const data = new Uint8Array(frame.data), w = meta.width, h = meta.height;
|
||||
if (data.length !== w * h * 3 / 2) throw Error('Invalid native camera frame');
|
||||
sequence = frame.sequence; timing.sensorFrames = sequence;
|
||||
const start=performance.now();
|
||||
gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D,textures[0]);
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w,h,gl.LUMINANCE,gl.UNSIGNED_BYTE,data.subarray(0,pixels));
|
||||
gl.activeTexture(gl.TEXTURE1); gl.bindTexture(gl.TEXTURE_2D,textures[1]);
|
||||
gl.texSubImage2D(gl.TEXTURE_2D,0,0,0,w/2,h/2,gl.LUMINANCE_ALPHA,gl.UNSIGNED_BYTE,data.subarray(pixels));
|
||||
gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||||
timing.frames++;timing.conversionMs+=performance.now()-start;
|
||||
return true;
|
||||
};
|
||||
const deadline = Date.now() + 6000;
|
||||
while (!(await draw())) {
|
||||
if (Date.now() > deadline) throw new DOMException('No camera frames', 'NotReadableError');
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
stream = canvas.captureStream(24);
|
||||
stream = new MediaStream();
|
||||
stream.vulpesPreviewCanvas = canvas;
|
||||
stream.vulpesClosed = false;
|
||||
// A canvas has no video element's implicit screen wake lock.
|
||||
wakeLock = navigator.requestWakeLock?.('screen');
|
||||
stream.vulpesTiming = timing;
|
||||
stream.vulpesNative = true;
|
||||
stream.vulpesCameraMetadata = meta;
|
||||
|
|
@ -218,20 +299,41 @@
|
|||
const started = performance.now();
|
||||
try { await draw(); }
|
||||
catch (error) {
|
||||
if (!stopped) { recordError(error); stream.getTracks().forEach(track => track.stop()); }
|
||||
if (!stopped) { recordError(error); await stop().catch(recordError); }
|
||||
return;
|
||||
}
|
||||
if (!stopped) timer = setTimeout(next, Math.max(0, 1000/24-(performance.now()-started)));
|
||||
if (!stopped) timer = setTimeout(next, Math.max(0, 1000/30-(performance.now()-started)));
|
||||
};
|
||||
timer = setTimeout(next, 1000/24);
|
||||
timer = setTimeout(next, 1000/30);
|
||||
return stream;
|
||||
} catch (error) { await stop().catch(() => {}); throw error; }
|
||||
}
|
||||
async function getCamera(name, config = {}) {
|
||||
async function captureVideo(constraints, ticket, platform) {
|
||||
// track.stop() returns before some Android camera drivers finish closing.
|
||||
// Retry the same sensor, never replace a refused/missing camera silently.
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
if (document.hidden || ticket !== generation)
|
||||
throw new DOMException('Camera request superseded or hidden', 'AbortError');
|
||||
try { return await navigator.mediaDevices.getUserMedia(constraints); }
|
||||
catch (error) {
|
||||
if (platform !== 'android' || error.name !== 'NotReadableError' || attempt >= 2) throw error;
|
||||
await new Promise(resolve => setTimeout(resolve, 200 * (attempt + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
function getCamera(name, config = {}) {
|
||||
const ticket = ++generation;
|
||||
const request = opening.then(() => openCamera(name, config, ticket));
|
||||
opening = request.catch(() => {});
|
||||
return request;
|
||||
}
|
||||
async function openCamera(name, config, ticket) {
|
||||
if (config.mode && config.mode !== 'picture')
|
||||
throw new DOMException('Video capture is not ported yet', 'NotSupportedError');
|
||||
const ticket = ++generation;
|
||||
await current?.release?.();
|
||||
if (current) await releaseStream(current);
|
||||
await closing;
|
||||
if (document.hidden || ticket !== generation)
|
||||
throw new DOMException('Camera request superseded or hidden', 'AbortError');
|
||||
const selected = devices.get(name);
|
||||
|
||||
cameraError = null;
|
||||
|
|
@ -240,22 +342,22 @@
|
|||
const platform = await VulpesCompat.call('platform.capabilities', {});
|
||||
if (platform.platform === 'tundra' && !platform.camera)
|
||||
throw new DOMException('Native camera adapter unavailable', 'NotSupportedError');
|
||||
stream = platform.platform === 'tundra' ? await nativeStream(name) : await navigator.mediaDevices.getUserMedia({
|
||||
stream = platform.platform === 'tundra' ? await nativeStream(name) : await captureVideo({
|
||||
audio: false,
|
||||
video: {
|
||||
...(selected
|
||||
? { deviceId: { exact: selected } }
|
||||
: { facingMode: { ideal: 'environment' } }),
|
||||
: { facingMode: { ideal: name === 'front' ? 'user' : 'environment' } }),
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1080 },
|
||||
},
|
||||
});
|
||||
}, ticket, platform.platform);
|
||||
} catch (error) {
|
||||
recordError(error);
|
||||
throw error;
|
||||
}
|
||||
if (document.hidden || ticket !== generation) {
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
await releaseStream(stream);
|
||||
const error = new DOMException('Camera hidden', 'AbortError');
|
||||
recordError(error);
|
||||
throw error;
|
||||
|
|
@ -264,49 +366,60 @@
|
|||
current = stream;
|
||||
let preview;
|
||||
try {
|
||||
await discover(stream);
|
||||
await discover(stream, name);
|
||||
if (document.hidden || ticket !== generation)
|
||||
throw new DOMException('Camera hidden', 'AbortError');
|
||||
|
||||
const track = stream.getVideoTracks()[0];
|
||||
const settings = track.getSettings();
|
||||
const size = { width: settings.width, height: settings.height };
|
||||
preview = document.createElement('video');
|
||||
preview.muted = true;
|
||||
preview.playsInline = true;
|
||||
preview.srcObject = stream;
|
||||
preview.dataset.vulpesCapture = '';
|
||||
preview.style.cssText = 'position:fixed;width:1px;height:1px;opacity:0;pointer-events:none';
|
||||
document.body.append(preview);
|
||||
let previewTimeout;
|
||||
await Promise.race([
|
||||
preview.play(),
|
||||
new Promise((_, reject) => {
|
||||
previewTimeout = setTimeout(
|
||||
() => reject(new DOMException('Camera preview timed out', 'NotReadableError')),
|
||||
10000,
|
||||
);
|
||||
}),
|
||||
]).finally(() => clearTimeout(previewTimeout));
|
||||
const native = stream.vulpesCameraMetadata;
|
||||
let size;
|
||||
if (native) {
|
||||
// Preview dimensions and still photos come directly from the HAL.
|
||||
// The native canvas needs no hidden capture video.
|
||||
const sideways = native.orientation % 180 !== 0;
|
||||
size = {width:sideways ? native.height : native.width,
|
||||
height:sideways ? native.width : native.height};
|
||||
} else {
|
||||
preview = document.createElement('video');
|
||||
preview.muted = true;
|
||||
preview.playsInline = true;
|
||||
preview.srcObject = stream;
|
||||
preview.dataset.vulpesCapture = '';
|
||||
preview.style.cssText = 'position:fixed;width:1px;height:1px;opacity:0;pointer-events:none';
|
||||
document.body.append(preview);
|
||||
let previewTimeout;
|
||||
await Promise.race([
|
||||
preview.play(),
|
||||
new Promise((_, reject) => {
|
||||
previewTimeout = setTimeout(
|
||||
() => reject(new DOMException('Camera preview timed out', 'NotReadableError')),
|
||||
10000,
|
||||
);
|
||||
}),
|
||||
]).finally(() => clearTimeout(previewTimeout));
|
||||
const settings = stream.getVideoTracks()[0].getSettings();
|
||||
size = {width:preview.videoWidth || settings.width,
|
||||
height:preview.videoHeight || settings.height};
|
||||
}
|
||||
if (document.hidden || ticket !== generation)
|
||||
throw new DOMException('Camera hidden', 'AbortError');
|
||||
|
||||
const native = stream.vulpesCameraMetadata;
|
||||
const pictureSizes = native?.pictureSizes || [size];
|
||||
let pictureSize = pictureSizes.reduce((a,b)=>a.width*a.height>b.width*b.height?a:b);
|
||||
let focusArea = null, zoom = 1, releasing;
|
||||
let focusArea = null, zoom = 1, mode = 'picture', recording, startingRecording = false;
|
||||
const profile = {containerFormat:'webm',video:{width:size.width,height:size.height,codec:'vp8',bitrate:1500000,framerate:30},audio:{codec:'opus',samplerate:48000,channels:1,bitrate:64000}};
|
||||
const configuration = () => ({
|
||||
mode: 'picture',
|
||||
mode,
|
||||
previewSize: size,
|
||||
pictureSize,
|
||||
recorderProfile: null,
|
||||
recorderProfile: videoMime ? 'webm' : null,
|
||||
});
|
||||
Object.assign(stream, {
|
||||
capabilities: {
|
||||
pictureSizes,
|
||||
previewSizes: [size],
|
||||
thumbnailSizes: [],
|
||||
recorderProfiles: {},
|
||||
recorderProfiles: videoMime ? {webm:profile} : {},
|
||||
flashModes: ['off'],
|
||||
focusModes: native ? (native.focusModes.includes('auto') ? ['auto','infinity'] : ['fixed']) : ['fixed'],
|
||||
sceneModes: ['auto'],
|
||||
|
|
@ -324,8 +437,9 @@
|
|||
zoom: 1,
|
||||
async setConfiguration(value) {
|
||||
if (value.pictureSize) stream.setPictureSize(value.pictureSize);
|
||||
if (value.mode && value.mode !== 'picture')
|
||||
if (value.mode && !['picture',...(videoMime ? ['video'] : [])].includes(value.mode))
|
||||
throw new DOMException('Video unavailable', 'NotSupportedError');
|
||||
if(value.mode)mode=value.mode;
|
||||
queueMicrotask(() => emit(stream, 'previewstatechange', { newState: 'started' }));
|
||||
return configuration();
|
||||
},
|
||||
|
|
@ -378,24 +492,68 @@
|
|||
emit(stream, 'previewstatechange', { newState: 'started' });
|
||||
},
|
||||
release() {
|
||||
if (releasing) return releasing;
|
||||
// Detach first: visibility changes and Gaia may release the same stream
|
||||
// again while its hardware worker is still shutting down.
|
||||
if (current === stream) current = null;
|
||||
releasing = Promise.resolve().then(async () => {
|
||||
for (const video of document.querySelectorAll('video')) {
|
||||
if (video.srcObject === stream) { video.pause(); video.srcObject = null; }
|
||||
}
|
||||
preview.remove();
|
||||
await stream.vulpesCameraClose?.();
|
||||
stream.getTracks().forEach(t => t.stop());
|
||||
});
|
||||
return releasing;
|
||||
return releaseStream(stream, preview);
|
||||
},
|
||||
async startRecording() {
|
||||
throw new DOMException('Video unavailable', 'NotSupportedError');
|
||||
async startRecording(options, storage, filepath) {
|
||||
if(!videoMime)throw new DOMException('Video unavailable','NotSupportedError');
|
||||
if(recording || startingRecording)throw new DOMException('Recording in progress','InvalidStateError');
|
||||
startingRecording=true;
|
||||
let audio, captured;
|
||||
try {
|
||||
audio=await navigator.mediaDevices.getUserMedia({audio:true,video:false});
|
||||
if(current!==stream || document.hidden)throw new DOMException('Camera closed','AbortError');
|
||||
// Only recording captures the native canvas. The preview keeps its
|
||||
// direct WebGL surface and the capture track is stopped before a switch.
|
||||
captured=native ? stream.vulpesPreviewCanvas.captureStream(30) : new MediaStream(stream.getVideoTracks().map(t=>t.clone()));
|
||||
for(const track of audio.getAudioTracks())captured.addTrack(track);
|
||||
const recorder=new MediaRecorder(captured,{mimeType:videoMime,videoBitsPerSecond:1500000,audioBitsPerSecond:64000});
|
||||
const chunks=[];let bytes=0,failed=false,finish;
|
||||
const done=new Promise(resolve=>{finish=resolve;});
|
||||
const cap=Math.min(options.maxFileSizeBytes || Infinity,32*1024*1024);
|
||||
recorder.ondataavailable=event=>{
|
||||
if(event.data.size) {chunks.push(event.data);bytes+=event.data.size;}
|
||||
if(bytes>=cap && recorder.state==='recording') {
|
||||
emit(stream,'recorderstatechange',{newState:'FileSizeLimitReached'});
|
||||
recorder.stop();
|
||||
}
|
||||
};
|
||||
recorder.onerror=()=>{failed=true;emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'});};
|
||||
recorder.onstop=async()=>{
|
||||
try {
|
||||
if(failed || !bytes)throw Error('Empty or failed recording');
|
||||
const blob=new Blob(chunks,{type:'video/webm'});
|
||||
await storage.addNamed(blob,filepath);
|
||||
const canvas=document.createElement('canvas');
|
||||
const source=native ? stream.vulpesPreviewCanvas : preview;
|
||||
canvas.width=native ? source.width : preview.videoWidth;
|
||||
canvas.height=native ? source.height : preview.videoHeight;
|
||||
canvas.getContext('2d').drawImage(source,0,0,canvas.width,canvas.height);
|
||||
const poster=await new Promise(resolve=>canvas.toBlob(resolve,'image/jpeg',0.9));
|
||||
if(!poster)throw Error('Video poster failed');
|
||||
emit(stream,'poster',{data:poster});
|
||||
} catch(error) {
|
||||
recordError(error);emit(stream,'recorderstatechange',{newState:'MediaRecorderFailed'});
|
||||
} finally {
|
||||
captured.getTracks().forEach(t=>t.stop());
|
||||
chunks.length=0;recording=null;finish();
|
||||
emit(stream,'recorderstatechange',{newState:'Stopped'});
|
||||
}
|
||||
};
|
||||
recording={recorder,done};
|
||||
recorder.start(500);
|
||||
emit(stream,'recorderstatechange',{newState:'Started'});
|
||||
} catch(error) {
|
||||
captured?.getTracks().forEach(t=>t.stop());audio?.getTracks().forEach(t=>t.stop());
|
||||
recording=null;throw error;
|
||||
} finally {startingRecording=false;}
|
||||
},
|
||||
stopRecording() {
|
||||
if(recording?.recorder.state==='recording')recording.recorder.stop();
|
||||
},
|
||||
async vulpesFinishRecording() {
|
||||
if(!recording)return;
|
||||
const done=recording.done;stream.stopRecording();await done;
|
||||
},
|
||||
stopRecording() {},
|
||||
});
|
||||
if (native) Object.defineProperty(stream,'zoom',{
|
||||
configurable:true, get:()=>zoom,
|
||||
|
|
@ -410,18 +568,13 @@
|
|||
});
|
||||
dispatchEvent(
|
||||
new CustomEvent('vulpes-camera-capabilities', {
|
||||
detail: { cameras: [...cameras], flash: false, video: false },
|
||||
detail: { cameras: [...cameras], flash: false, video: !!videoMime },
|
||||
}),
|
||||
);
|
||||
return { camera: stream, configuration: configuration() };
|
||||
} catch (error) {
|
||||
recordError(error);
|
||||
stream.getTracks().forEach((t) => t.stop());
|
||||
if (preview) {
|
||||
preview.srcObject = null;
|
||||
preview.remove();
|
||||
}
|
||||
if (current === stream) current = null;
|
||||
await releaseStream(stream, preview).catch(recordError);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -433,8 +586,7 @@
|
|||
// Keep that request valid; its completion still checks visibility.
|
||||
if (!current) return;
|
||||
++generation;
|
||||
if (current?.release) current.release().catch(recordError);
|
||||
else current?.getTracks().forEach((t) => t.stop());
|
||||
releaseStream(current).catch(recordError);
|
||||
};
|
||||
addEventListener('pagehide', stop);
|
||||
addEventListener('visibilitychange', () => {
|
||||
|
|
@ -462,7 +614,7 @@
|
|||
'Only one camera available',
|
||||
'toggle-camera-rear',
|
||||
],
|
||||
['.mode-switch', true, 'Vidéo pas encore disponible', 'Video is not available yet', null],
|
||||
['.mode-switch', !videoMime, 'Vidéo pas encore disponible', 'Video is not available yet', null],
|
||||
];
|
||||
const fr = (document.documentElement.lang || navigator.language).startsWith('fr');
|
||||
for (const [selector, disabled, french, english, icon] of controls) {
|
||||
|
|
@ -472,7 +624,7 @@
|
|||
e.setAttribute('aria-disabled', String(disabled));
|
||||
if (!disabled) {
|
||||
e.removeAttribute('title');
|
||||
e.setAttribute('aria-label', fr ? 'Changer de caméra' : 'Switch camera');
|
||||
if(selector==='.js-camera')e.setAttribute('aria-label', fr ? 'Changer de caméra' : 'Switch camera');
|
||||
}
|
||||
if (disabled) {
|
||||
e.removeAttribute('data-l10n-id');
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
addEventListener('error', (event) =>
|
||||
record('error', [event.message, event.filename, event.lineno]),
|
||||
);
|
||||
// Desktop transports Blobs directly; Android installs a JSON-compatible codec.
|
||||
// Binary settings use the same persistence format on every host.
|
||||
const battery = Object.assign(new EventTarget(), {level:NaN, charging:false, chargingTime:Infinity, dischargingTime:Infinity});
|
||||
Object.defineProperty(navigator, 'battery', {value:battery});
|
||||
async function refreshBattery() {
|
||||
|
|
@ -67,7 +67,25 @@
|
|||
const unsupported = () => Promise.reject(new DOMException('Not available on this adapter', 'NotSupportedError'));
|
||||
telephony.conferenceGroup = Object.assign(new EventTarget(), {calls:[], state:'', add:unsupported, remove:unsupported, hangUp:unsupported});
|
||||
telephony.ready = Promise.resolve();
|
||||
telephony.dial = number => call('platform.dial',{number});
|
||||
class TelephonyCall extends EventTarget {}
|
||||
Object.defineProperty(window,'TelephonyCall',{value:TelephonyCall});
|
||||
function makeCall(item) {
|
||||
return Object.assign(new TelephonyCall(),{path:item.path,id:{number:item.number},serviceId:0,
|
||||
state:({active:'connected',waiting:'incoming'})[item.state] || item.state,
|
||||
emergency:false,group:null,switchable:false,mergeable:false,
|
||||
answer:()=>call('platform.answer',{path:item.path}),hangUp:()=>call('platform.hangup',{path:item.path}),
|
||||
hold:unsupported,resume:unsupported});
|
||||
}
|
||||
telephony.dial = async number => {
|
||||
const result = await call('platform.dial',{number});
|
||||
let current = telephony.calls.find(c=>c.path===result.path);
|
||||
if (!current) {
|
||||
current = makeCall({path:result.path,number,state:'dialing'});
|
||||
telephony.calls.push(current);
|
||||
emit(telephony,'callschanged');
|
||||
}
|
||||
return current;
|
||||
};
|
||||
telephony.dialEmergency = unsupported;
|
||||
telephony.sendTones = unsupported;
|
||||
telephony.startTone = unsupported;
|
||||
|
|
@ -94,14 +112,17 @@
|
|||
const state = ({active:'connected',waiting:'incoming'})[item.state] || item.state;
|
||||
if (!c) {
|
||||
changed = true;
|
||||
c = Object.assign(new EventTarget(),{path:item.path,id:{number:item.number}, serviceId:0,
|
||||
state, emergency:false, group:null, switchable:false, mergeable:false,
|
||||
answer:()=>call('platform.answer',{path:item.path}), hangUp:()=>call('platform.hangup',{path:item.path}),
|
||||
hold:unsupported,resume:unsupported});
|
||||
c = makeCall(item);
|
||||
} else if (c.state !== state) { c.state=state;changes.push(c); }
|
||||
return c;
|
||||
});
|
||||
for (const c of old) if (!telephony.calls.includes(c)) {c.state='disconnected';changes.push(c);changed=true;}
|
||||
for (const c of old) if (!telephony.calls.includes(c)) {
|
||||
c.state='disconnected';
|
||||
// The broker currently supplies no modem disconnect cause. Avoid
|
||||
// inventing an error when a call simply disappears after hang-up.
|
||||
c.disconnectedReason=null;
|
||||
changes.push(c);changed=true;
|
||||
}
|
||||
for (const c of changes) {emit(c,'statechange',{call:c});emit(c,c.state,{call:c});}
|
||||
if (changed) {
|
||||
if (location.hostname === 'system.localhost' && live.some(c => ['incoming','waiting'].includes(c.state))) window.Service?.request('turnScreenOn');
|
||||
|
|
@ -128,15 +149,16 @@
|
|||
Object.defineProperty(navigator, 'mozPhoneNumberService', {value:{
|
||||
normalize: number => String(number || '').normalize('NFKC').replace(/[^0-9+*#]/g, '')
|
||||
}});
|
||||
const settingsCodec = window.VulpesAndroidSettingsCodec;
|
||||
const settingsCodec = window.VulpesSettingsCodec;
|
||||
async function call(method, params) {
|
||||
if (!navigator.vulpesRequest) return Promise.reject(new Error('VULPES_HOST_UNAVAILABLE'));
|
||||
if (settingsCodec && ['settings.set', 'media.request'].includes(method))
|
||||
if (settingsCodec && (method === 'settings.set' || method === 'messages.request' ||
|
||||
(window.VulpesPlatform === 'android' && ['media.request', 'activities.finish', 'contacts.request'].includes(method))))
|
||||
params = await settingsCodec.encode(params);
|
||||
return navigator.vulpesRequest({ version: 1, id: ++sequence, method, params }).then((reply) => {
|
||||
if (reply.error) throw new DOMException(reply.error.code, reply.error.code);
|
||||
return settingsCodec &&
|
||||
['settings.get', 'settings.snapshot', 'media.request'].includes(method)
|
||||
['messages.request', 'settings.get', 'settings.snapshot', 'media.request', 'activities.start', 'contacts.request'].includes(method)
|
||||
? settingsCodec.decode(reply.result)
|
||||
: reply.result;
|
||||
});
|
||||
|
|
@ -165,6 +187,16 @@
|
|||
}
|
||||
return r;
|
||||
}
|
||||
let language = navigator.language;
|
||||
function setLanguage(value) {
|
||||
if (typeof value !== 'string' || !/^[a-z]{2,3}(?:-[a-zA-Z0-9]{2,8})*$/.test(value)) return;
|
||||
const changed = language !== value;
|
||||
language = value;
|
||||
if (changed) dispatchEvent(new Event('languagechange'));
|
||||
}
|
||||
Object.defineProperty(navigator, 'language', {get:() => language});
|
||||
Object.defineProperty(navigator, 'languages', {get:() => Object.freeze([language])});
|
||||
call('locale.current').then(setLanguage).catch(() => {});
|
||||
const observers = new Map();
|
||||
const settings = new EventTarget();
|
||||
settings.createLock = () => ({
|
||||
|
|
@ -196,6 +228,7 @@
|
|||
addEventListener('vulpes-service-event', (event) => {
|
||||
const { type, data } = event.detail;
|
||||
if (type === 'settingchange') {
|
||||
if (data.key === 'language.current') setLanguage(data.value);
|
||||
const change = {
|
||||
settingName: data.key,
|
||||
settingValue: settingsCodec ? settingsCodec.decode(data.value) : data.value,
|
||||
|
|
@ -205,17 +238,34 @@
|
|||
}
|
||||
if (type === 'callscreen-closed') document.querySelector('iframe[name="call_screen"]')?.dispatchEvent(new CustomEvent('mozbrowserclose',{bubbles:true}));
|
||||
if (type === 'hardware-key') dispatchEvent(new CustomEvent('softwareButtonEvent', {detail:data}));
|
||||
if (type === 'activity-choice') dispatchEvent(new CustomEvent('mozChromeEvent', {detail:{...data,type:'activity-choice'}}));
|
||||
if (type === 'activity-launch') dispatchEvent(new CustomEvent('open-app', {detail:{...data,showApp:true,onlyShowApp:false}}));
|
||||
if (type === 'launch') dispatchEvent(new CustomEvent('webapps-launch', { detail: data }));
|
||||
if (type === 'home' || type === 'holdhome') dispatchEvent(new CustomEvent(type));
|
||||
if (type === 'screen-off') {
|
||||
settings.createLock().get('lockscreen.enabled').then(values => {
|
||||
if (values['lockscreen.enabled'] === false) return;
|
||||
dispatchEvent(new CustomEvent('secure-killapps'));
|
||||
window.Service?.request('lock').catch(console.error);
|
||||
}).catch(console.error);
|
||||
}
|
||||
if (type === 'screen-on') window.ScreenManager?.turnScreenOn(true);
|
||||
if (type === 'home' || type === 'holdhome') {
|
||||
call('activities.cancel', {}).catch(console.error);
|
||||
dispatchEvent(new CustomEvent(type));
|
||||
}
|
||||
if (type === 'activity-view') dispatchEvent(new CustomEvent('activity-view', { detail: data }));
|
||||
});
|
||||
if (location.hostname==='system.localhost') addEventListener('mozContentEvent', event=>{
|
||||
if (event.detail?.type==='activity-choice')
|
||||
call('activities.select',{id:event.detail.id,value:event.detail.value}).catch(console.error);
|
||||
});
|
||||
function application(record) {
|
||||
// DOMApplication never exposed the registry's internal id. Gaia uses `id`
|
||||
// to distinguish a bookmark from an installed application.
|
||||
const { id, ...publicRecord } = record;
|
||||
const app = Object.assign(new EventTarget(), publicRecord, {
|
||||
installState: 'installed',
|
||||
removable: false,
|
||||
removable: record.removable === true,
|
||||
installTime: 0,
|
||||
updateTime: 0,
|
||||
downloadAvailable: false,
|
||||
|
|
@ -238,6 +288,14 @@
|
|||
}
|
||||
const mgmt = new EventTarget();
|
||||
mgmt.getAll = () => request(call('apps.list').then((apps) => apps.map(application)));
|
||||
mgmt.uninstall = app => request(call('apps.uninstall',{id:new URL(app.origin).hostname.split('.')[0]}));
|
||||
addEventListener('vulpes-service-event',event=>{
|
||||
const {type,data}=event.detail;
|
||||
if(!['apps-installed','apps-uninstalled'].includes(type))return;
|
||||
const name=type==='apps-installed'?'install':'uninstall';
|
||||
const change=new Event(name);Object.defineProperty(change,'application',{value:application(data)});
|
||||
mgmt.dispatchEvent(change);if(typeof mgmt['on'+name]==='function')mgmt['on'+name](change);
|
||||
});
|
||||
mgmt.getNotInstalled = () => request(Promise.resolve([]));
|
||||
mgmt.getIcon = async (app, size, entry) => {
|
||||
const manifest = app.manifest.entry_points?.[entry] || app.manifest;
|
||||
|
|
@ -319,10 +377,19 @@
|
|||
const handlers = new Map();
|
||||
navigator.mozSetMessageHandler = (name, fn) => {
|
||||
handlers.set(name, fn);
|
||||
if (name === 'activity') call('activities.subscribe', {}).catch(console.error);
|
||||
if (name === 'connection') call('iac.subscribe', {}).catch(console.error);
|
||||
if (name === 'alarm') call('alarms.request', { operation: 'subscribe' }).catch(console.error);
|
||||
};
|
||||
navigator.mozHasPendingMessage = () => false;
|
||||
addEventListener('vulpes-service-event', ({detail}) => {
|
||||
if (detail.type !== 'activity') return;
|
||||
const {id,source} = detail.data;
|
||||
setTimeout(() => handlers.get('activity')?.({source,
|
||||
postResult: result => call('activities.finish',{id,result}).catch(console.error),
|
||||
postError: error => call('activities.finish',{id,error:String(error)}).catch(console.error)
|
||||
}),0);
|
||||
});
|
||||
navigator.mozHasPendingMessage = name => name === 'activity' && (new URLSearchParams(location.search).has('activity') || /\/(pick|share)\.html$/.test(location.pathname));
|
||||
navigator.mozSetMessageHandlerPromise = (promise) => promise;
|
||||
window.VulpesCompat = { call, request, handlers };
|
||||
window.MozActivity = function (source) {
|
||||
|
|
@ -482,7 +549,7 @@
|
|||
frames.add(frame);
|
||||
// Set delegation before navigation; a MutationObserver runs too late on Android.
|
||||
if (location.hostname === 'system.localhost')
|
||||
frame.setAttribute('allow', 'camera http://camera.localhost:' + location.port);
|
||||
frame.setAttribute('allow', 'camera http://camera.localhost:' + location.port + '; microphone http://camera.localhost:' + location.port);
|
||||
frame.addEventListener('load', () => {
|
||||
if (!frame.hasAttribute('mozbrowser')) return;
|
||||
if (webViews.has(frame)) return;
|
||||
|
|
@ -657,13 +724,29 @@
|
|||
Object.setPrototypeOf(GaiaElement.prototype, prototype);
|
||||
customElements.define(name, GaiaElement);
|
||||
registered.add(name);
|
||||
return GaiaElement;
|
||||
function LegacyGaiaElement() {
|
||||
return initialize(nativeCreate(name));
|
||||
}
|
||||
LegacyGaiaElement.prototype = GaiaElement.prototype;
|
||||
return LegacyGaiaElement;
|
||||
};
|
||||
document.createElement = function (name, options) {
|
||||
const element = nativeCreate(name, options);
|
||||
if (name.toLowerCase() === 'iframe') wire(element);
|
||||
return registered.has(name) ? initialize(element) : element;
|
||||
};
|
||||
// v0 created callbacks ran for detached fragments too. Gaia reads component
|
||||
// state immediately after parsing its templates, before attaching them.
|
||||
const innerHTML = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML');
|
||||
Object.defineProperty(Element.prototype, 'innerHTML', {
|
||||
...innerHTML,
|
||||
set(value) {
|
||||
innerHTML.set.call(this, value);
|
||||
for (const element of this.querySelectorAll('*')) {
|
||||
if (registered.has(element.localName)) initialize(element);
|
||||
}
|
||||
},
|
||||
});
|
||||
HTMLElement.prototype.createShadowRoot = function () {
|
||||
const host = this,
|
||||
root = this.attachShadow({ mode: 'open' });
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue