Compare commits

..
26057 changed files with 367 additions and 10010197 deletions

24
.gitignore vendored
View file

@ -1,24 +0,0 @@
/engines/
/downloads/
/profiles/
/logs/
__pycache__/
*.pyc
# Installed app payload is versioned; these indexes are generated by the server.
/assets/registry.json
/assets/build-info.json
/assets/settings-current-defaults.json
/node_modules/
/npm-debug.log*
/.venv/
*.keystore
*.jks
.env
.env.*
/local.properties
# 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/

1
.nvmrc
View file

@ -1 +0,0 @@
22

View file

@ -1,13 +0,0 @@
gaia/
assets/
engines/
downloads/
profiles/
logs/
android/toolchain/
android/.gradle/
android/build/
android/app/build/
android/app/src/main/assets/
android/dist/
android/keys/

View file

@ -1,7 +0,0 @@
{
"singleQuote": true,
"printWidth": 100,
"trailingComma": "all",
"tabWidth": 2,
"plugins": ["prettier-plugin-java"]
}

View file

@ -1,904 +0,0 @@
# 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
![APK published](https://img.shields.io/badge/APK%20published-2F855A?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Appearance](https://img.shields.io/badge/Appearance-475569?style=flat-square) : Unavailable-feature messages are easier to read in light and dark themes.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Call history](https://img.shields.io/badge/Call%20history-475569?style=flat-square) : Contact names now appear when the call history first loads.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Keyboard](https://img.shields.io/badge/Keyboard-475569?style=flat-square) : Additional dictionaries available offline; improved Gaia keyboard activation and recovery. Verification on affected devices is still needed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : Storage panels restored. A loading error no longer locks the other menus.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-2F855A?style=flat-square) ![Android data](https://img.shields.io/badge/Android%20data-475569?style=flat-square) : Read Android contacts, SMS and call history in Gaia after granting access. Existing local Vulpes data is preserved.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Return to Vulpes without a persistent white overlay; a closed catalogue can be reopened.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Keyboard](https://img.shields.io/badge/Keyboard-475569?style=flat-square) : The reported blank keyboard still needs reproduction on the affected devices.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Android data](https://img.shields.io/badge/Android%20data-475569?style=flat-square) : 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.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Shared Gaia fixes are included in full installation images and boot updates.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : 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
![Published](https://img.shields.io/badge/Published-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Video mode, with thumbnails and playback in Gallery and Video.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Improvements to settings, appearance, pickers, keyboard, contacts and media.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : More reliable camera switching, shutdown and resume.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Services](https://img.shields.io/badge/Services-475569?style=flat-square) : Mozilla Account, Sync, remote location, extensions and USB transfer remain incomplete.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Release](https://img.shields.io/badge/Release-475569?style=flat-square) : Preview 20 APK available.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Compatibility](https://img.shields.io/badge/Compatibility-475569?style=flat-square) : Calls and SMS use Android. This Preview has not yet been validated in use on Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Preview runs at around 16 frames per second.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : Updates to Wi-Fi, Bluetooth, calls, SMS/MMS and network settings.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Audio](https://img.shields.io/badge/Audio-475569?style=flat-square) : Video sound and two-way call audio still need separate testing.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Preview 20 installed on Pixel 3a with data preserved. Wi-Fi connected and 4G registration observed.
### ![Web](https://img.shields.io/badge/Web-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Changelog](https://img.shields.io/badge/Changelog-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Video recording with sound, thumbnails and playback in Gallery and Video.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Help](https://img.shields.io/badge/Help-475569?style=flat-square) : “Improve Vulpes” opens the project’s issues.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : Date and time pickers restored; Keyboard back navigation and list text fixed.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Recording checked with a simulated camera.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Changes prepared for a future APK; video not yet tested on Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Improved preview, at around 16 frames per second.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Clock](https://img.shields.io/badge/Clock-475569?style=flat-square) : Automatic time depends on network information; time zone remains manual.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : Dialog closing and retry after an error fixed.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Calls](https://img.shields.io/badge/Calls-475569?style=flat-square) : Settings for call waiting, caller ID and call forwarding.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Hotspot with or without a password.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : Slow settings no longer block calls and SMS.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : IP address and connection speed displayed correctly.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : Network type selection and mobile access point editing.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : More reliable network settings when the modem responds slowly.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : Adding, deleting and resetting access points remain incomplete.
<a id="development-2026-10-12-settings-audit"></a>
## 12/10/2026 : Settings review
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : Navigation, accessibility and help fixed. Incomplete panels remain visible.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Testing](https://img.shields.io/badge/Testing-475569?style=flat-square) : Menus reviewed; some actions still depend on hardware or the carrier.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Storage](https://img.shields.io/badge/Storage-475569?style=flat-square) : Storage information and downloads connected.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Storage](https://img.shields.io/badge/Storage-475569?style=flat-square) : Display of storage used by Vulpes.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Appearance](https://img.shields.io/badge/Appearance-475569?style=flat-square) : Light/Dark selection and picker confirmation or cancellation restored.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SIM](https://img.shields.io/badge/SIM-475569?style=flat-square) : Manager adapted to single-SIM phones.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : SIM information, message centre and delivery reports.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Faster loading of saved networks.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Appearance](https://img.shields.io/badge/Appearance-475569?style=flat-square) : Shared light and dark modes available on Pixel 3a.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Settings fixes and Firefox OS fonts included.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Testing](https://img.shields.io/badge/Testing-475569?style=flat-square) : No new call, SMS or Bluetooth test at this stage.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Appearance](https://img.shields.io/badge/Appearance-475569?style=flat-square) : Themes and display checked on the phone.
<a id="development-2026-10-11-appearance-input"></a>
## 11/10/2026 : Appearance and keyboard
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Appearance](https://img.shields.io/badge/Appearance-475569?style=flat-square) : Light, Dark and System modes, preserved after restart.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Keyboard](https://img.shields.io/badge/Keyboard-475569?style=flat-square) : Text selection and deletion fixed.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Themes and SMS typing checked.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Keyboard](https://img.shields.io/badge/Keyboard-475569?style=flat-square) : Experimental Gaia keyboard; Android keyboard remains the default.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Retry available when the camera is busy.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Testing](https://img.shields.io/badge/Testing-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Home](https://img.shields.io/badge/Home-475569?style=flat-square) : Second home screen, wallpaper previews and installation banner fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Error message dismissal restored.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : Ringtones, lock code, permissions and French/English labels fixed.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Open the notification tray by tapping the status bar.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Preview 18 built locally.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stability](https://img.shields.io/badge/Stability-475569?style=flat-square) : Returned to the previous version after an unstable interface restart.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Operation confirmed by the user.
<a id="development-2026-10-09-mms-bluetooth"></a>
## 09/10/2026 : MMS and Bluetooth
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Gaia panels connected to each platform’s capabilities.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Discovery, pairing, connection, forgetting devices and audio output.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Sending preserved and resumed after interruption.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Testing](https://img.shields.io/badge/Testing-475569?style=flat-square) : MMS accepted by the carrier, delivery not confirmed. Headset call audio still needs testing.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Bluetooth audio playback to a PC checked.
<a id="development-2026-10-08-camera-mms"></a>
## 08/10/2026 : Camera and MMS
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Camera switching, shutdown and display after rotation fixed.
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Attachments preserved and sending shown as pending until confirmation.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Preview measured at around 16 fps on the rear camera and 25–27 on the front.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Real sending still blocked by the mobile data connection.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Focus, zoom and photo capture checked.
<a id="development-2026-10-08-call-log-dialogs"></a>
## 08/10/2026 : Call history and dialogs
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Readable error messages with usable buttons.
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Clear message when sending is unavailable, without losing the draft.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Phone](https://img.shields.io/badge/Phone-475569?style=flat-square) : Calling back from call history restored.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Calls](https://img.shields.io/badge/Calls-475569?style=flat-square) : Flow checked with simulated calls; real audio not tested.
<a id="development-2026-10-08-sms-audio"></a>
## 08/10/2026 : SMS and audio
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Volume display, lock screen and notification swiping fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Send with one tap without first closing the keyboard. Attachment menu restored.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Audio](https://img.shields.io/badge/Audio-475569?style=flat-square) : Call audio routing fixes.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Calls](https://img.shields.io/badge/Calls-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Messages linked to the correct conversation.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : Improved modem recovery; retries avoid duplicate calls and SMS.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Calls](https://img.shields.io/badge/Calls-475569?style=flat-square) : Network stability and two-way audio still need confirmation.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : Return to the 4G network after modem restart checked.
<a id="2026-10-08-source-publication"></a>
## 08/10/2026 : Public source code
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Source code](https://img.shields.io/badge/Source%20code-475569?style=flat-square) : Camera, contacts, SMS and Marketplace fixes published, with updated French/English project overviews.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Build](https://img.shields.io/badge/Build-475569?style=flat-square) : Building from a fresh repository copy fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : The public APK remains Preview 17; these fixes are in the source code.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : More reliable opening and camera switching.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Contacts](https://img.shields.io/badge/Contacts-475569?style=flat-square) : Photos, dates, edits and number selection from Messages or Phone fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Gallery](https://img.shields.io/badge/Gallery-475569?style=flat-square) : Media selection fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Fixes in source code; public APK unchanged at this stage.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Number and text passed correctly to the Android app.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Discovery available; pairing and audio use still need testing.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Photo capture, focus, zoom and camera switching checked.
<a id="development-2026-10-06-marketplace-theme"></a>
## 06/10/2026 : Marketplace theme
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Integration planned for a future APK.
### ![Web](https://img.shields.io/badge/Web-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Embedded theme: Fira font, compact navigation, touch buttons and scrollable screenshots.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Embedded display preserved during search, filtering and language changes.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Theme prepared locally, not yet deployed to the website.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Display](https://img.shields.io/badge/Display-475569?style=flat-square) : French and English pages checked on small screens.
<a id="development-2026-10-06-system-banner"></a>
## 06/10/2026 : Notification banners
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : The “Application installed” banner disappears automatically and is replaced correctly.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Banner display and dismissal checked.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Fix prepared for the next APK.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Banner disappearance after five seconds checked.
<a id="development-2026-10-06-wifi"></a>
## 06/10/2026 : Wi-Fi and settings
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Settings](https://img.shields.io/badge/Settings-475569?style=flat-square) : Panels open even when an animation does not finish normally.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Fixes not yet included in the public APK.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Detected networks and disconnections displayed correctly.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Search, connection and Internet access checked.
<a id="development-2026-10-06-marketplace-views"></a>
## 06/10/2026 : Marketplace navigation
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Local message with a Retry button when the catalogue is unavailable.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : The catalogue stays below the status bar and hides when returning home or closing it.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Navigation](https://img.shields.io/badge/Navigation-475569?style=flat-square) : An earlier error no longer hides a new attempt.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Navigation, errors, returning home and closing checked.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Preview 17 rebuilt locally, without a new Android test.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : 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
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Apps](https://img.shields.io/badge/Apps-475569?style=flat-square) : Install, launch and uninstall packaged web apps.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Vulpes catalogue on the home screen, in French and English, with its original icon.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Security](https://img.shields.io/badge/Security-475569?style=flat-square) : Installed apps kept separate from system apps.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Compatibility](https://img.shields.io/badge/Compatibility-475569?style=flat-square) : Some old Firefox OS apps and APIs are not yet supported.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Installation and use of 2048 checked.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : 2048 installed, preserved after restart and uninstalled on an Android emulator.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Home screen checked; app installation still needs validation on Pixel.
<a id="2.7-preview.16"></a>
## 05/10/2026 : 2.7-preview.16
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : Vulpes startup logo.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Home](https://img.shields.io/badge/Home-475569?style=flat-square) : Unwanted horizontal scrolling removed; test apps hidden.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Customization](https://img.shields.io/badge/Customization-475569?style=flat-square) : Wallpaper and sound selection restored. Home screen language applied and preserved after restart.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : APK built, not published; Pixel image tried without permanent installation.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Android bars hidden and accessible by swiping; launcher mode checked.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Lock screen](https://img.shields.io/badge/Lock%20screen-475569?style=flat-square) : Return from sleep and the lock screen camera fixed.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : Old files no longer overwrite updates at startup.
<a id="2026-10-04-sargo-standalone"></a>
## 04/10/2026 : Standalone installation
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Installer with device and file checks before writing.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Standalone Vulpes installation without going through Droidian.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Package not published; tested on one Pixel only. Installation replaces phone data.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Recovery](https://img.shields.io/badge/Recovery-475569?style=flat-square) : Backups checked; full restoration still needs testing.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Preparation adapted to each Pixel, with device-specific backups.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Focus sometimes times out; issue still open.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Direct installation from stock Android not yet available.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Package preparation and recovery kit with a boot backup.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Recovery](https://img.shields.io/badge/Recovery-475569?style=flat-square) : Restoration possible without booting the diagnostic tool.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Preparation checked locally; installation on another Pixel still needs completion.
### ![Web](https://img.shields.io/badge/Web-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Changelog](https://img.shields.io/badge/Changelog-475569?style=flat-square) : Shared history included in upcoming source releases.
<a id="2026-10-03-sargo-modem-persistence"></a>
## 03/10/2026 : Modem and first startup
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Network](https://img.shields.io/badge/Network-475569?style=flat-square) : Recover the modem without restarting the whole phone.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : Fonts and modem initialization fixed on a fresh installation.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stability](https://img.shields.io/badge/Stability-475569?style=flat-square) : Intermittent modem restarts still need fixing; public package incomplete.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : Cameras, focus, zoom, keyboard, notifications and Wi-Fi search checked.
<a id="2026-10-01-sargo-distribution-preparation"></a>
## 01/10/2026 : Preparing distribution
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Preparation of an image with clean user data.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Fonts](https://img.shields.io/badge/Fonts-475569?style=flat-square) : Font display on first boot fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Image not published. Calls, SMS and mobile data still need end-to-end testing.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stability](https://img.shields.io/badge/Stability-475569?style=flat-square) : Intermittent modem restart identified.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : 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
![Published](https://img.shields.io/badge/Published-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Source code](https://img.shields.io/badge/Source%20code-475569?style=flat-square) : First publication of Desktop, Android and Tundra source code, with French/English project overviews.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : More reliable camera stream shutdown and replacement.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Dates and sending statuses fixed in conversations.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Apps](https://img.shields.io/badge/Apps-475569?style=flat-square) : Navigation, settings, gestures and saved data checked.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Stability](https://img.shields.io/badge/Stability-475569?style=flat-square) : Update, Camera, Gallery and returns from the background checked on an Android emulator.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Source code included; universal installation still unavailable at this stage.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Camera release and switching fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Empty conversations fixed.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Shared sources updated, without a new APK.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Vibration now stops correctly.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Remaining work](https://img.shields.io/badge/Remaining%20work-475569?style=flat-square) : Dialog after sending SMS and camera smoothness still need fixing.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Focus and zoom confirmed by the user.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Sending and receiving confirmed by the user; conversations displayed.
<a id="development-2026-09-30-sms-camera"></a>
## 30/09/2026 : SMS and photos
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Failed messages preserved and sending statuses updated.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Preview 14 APK unchanged at this stage.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Tap to focus and digital zoom. Photos up to 12 MP at the rear and 8 MP at the front.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Preview return after taking a photo fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Delivery to the recipient not yet confirmed.
<a id="2.7-preview.14"></a>
## 30/09/2026 : 2.7-preview.14
![Published](https://img.shields.io/badge/Published-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Fonts, version information, lock screen and notifications aligned.
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Keyboard](https://img.shields.io/badge/Keyboard-475569?style=flat-square) : Larger keyboard, improved text editing and recipient number entry.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : Startup and fonts checked on an Android emulator.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Vibration stopping fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Notifications preserved, conversation opening and duplicate messages fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Saved networks, reconnection and forgetting a network fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Audio](https://img.shields.io/badge/Audio-475569?style=flat-square) : 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
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Calendar](https://img.shields.io/badge/Calendar-475569?style=flat-square) : Event reminders fixed, including after deletion.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Gallery](https://img.shields.io/badge/Gallery-475569?style=flat-square) : Initial display and closing the editor fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Conversation filters fixed.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Apps](https://img.shields.io/badge/Apps-475569?style=flat-square) : Photo, video, calendar and conversations checked.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Apps](https://img.shields.io/badge/Apps-475569?style=flat-square) : Camera, Gallery, Calendar and background resume checked.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : Camera, Wi-Fi and brightness checked on the installed version.
<a id="2.7-preview.12"></a>
## 27/09/2026 : 2.7-preview.12
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Browser](https://img.shields.io/badge/Browser-475569?style=flat-square) : Local start page, in French or English according to language.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Apps](https://img.shields.io/badge/Apps-475569?style=flat-square) : SMS conversations and Calendar translations fixed.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Camera switching and interface zoom fixed; flash disabled when unavailable.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Sharper icons, long press on Home and gestures fixed.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Smoother preview, at around 23 frames per second in testing.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : More responsive brightness and Wi-Fi connection fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : Video, flash, Bluetooth and advanced call settings remain incomplete.
<a id="development-2026-09-27-4"></a>
## 27/09/2026 : Interface and hardware
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Browser](https://img.shields.io/badge/Browser-475569?style=flat-square) : First local start page.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Shutdown](https://img.shields.io/badge/Shutdown-475569?style=flat-square) : Original shutdown animation retained, with blue circles.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : List and Video text fixed; transparent Home bar adapted to landscape.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Phone](https://img.shields.io/badge/Phone-475569?style=flat-square) : Call history headers and icons fixed.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Brightness](https://img.shields.io/badge/Brightness-475569?style=flat-square) : Brightness adjustment connected.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Preview 9 built with shared fixes; Wi-Fi handled by Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Network discovery stabilized to avoid phone freezes.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : Camera and calls remain incomplete.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Lock screen, fonts, date, battery and Wi-Fi checked.
<a id="development-2026-09-27-2"></a>
## 27/09/2026 : Restoring Gaia components
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![About](https://img.shields.io/badge/About-475569?style=flat-square) : Credits, startup screen and browser name updated.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Original Gaia panels and styles restored in Settings, Data Usage, FM Radio and imports.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Home button no longer overlaps controls; date, battery and lock screen restored.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Integration](https://img.shields.io/badge/Integration-475569?style=flat-square) : Launcher mode, access to Android settings and background resume.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Initial integration unstable; returned to the previous version.
<a id="development-2026-09-27-1"></a>
## 27/09/2026 : Calls and hardware buttons
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Phone](https://img.shields.io/badge/Phone-475569?style=flat-square) : Gaia incoming and outgoing call interfaces, with call history.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Fira fonts, contacts and battery display fixed.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Buttons](https://img.shields.io/badge/Buttons-475569?style=flat-square) : Support for Power and Volume keys, shutdown menu and waking the screen.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Native camera still unavailable.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Calls](https://img.shields.io/badge/Calls-475569?style=flat-square) : Ringing and hanging up checked; conversation audio still needs testing.
<a id="development-2026-09-26-1"></a>
## 26/09/2026 : Startup and fonts
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Fira fonts and battery information fixed; test buttons removed from settings.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Data preserved in the installed boot.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : Stable fullscreen, without automatic gestures or app launches.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Hardware](https://img.shields.io/badge/Hardware-475569?style=flat-square) : 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
![In development](https://img.shields.io/badge/In%20development-334155?style=flat-square)
### ![All Platforms](https://img.shields.io/badge/All%20Platforms-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Home](https://img.shields.io/badge/Home-475569?style=flat-square) : Round button, transparent background on Home and blue background in apps.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Pressed button state, gradient, fonts and system information fixed.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stability](https://img.shields.io/badge/Stability-475569?style=flat-square) : App resume changed in source code, not yet tested on Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : First native image trials with accelerated display and battery support.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Availability](https://img.shields.io/badge/Availability-475569?style=flat-square) : Temporary trials; many hardware features still need connecting.
<a id="2.7-preview.7"></a>
## 2.7-preview.7
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![Desktop + Android](https://img.shields.io/badge/Desktop%20%2B%20Android-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![About](https://img.shields.io/badge/About-475569?style=flat-square) : Vulpes, Gaia and Gecko versions shown separately.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Improved permission handling and clearer error messages.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Resume after the first permission request fixed.
<a id="2.7-preview.6"></a>
## 2.7-preview.6
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Gaia Camera app connected to the webcam.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Gallery](https://img.shields.io/badge/Gallery-475569?style=flat-square) : File import and photos available in Gallery.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Camera switching depends on hardware; flash and video unavailable.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Gaia Camera app connected to Android cameras.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Gallery](https://img.shields.io/badge/Gallery-475569?style=flat-square) : File import and photos available in Gallery.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Camera](https://img.shields.io/badge/Camera-475569?style=flat-square) : Camera switching depends on hardware; flash and video unavailable.
<a id="2.7-preview.5"></a>
## 2.7-preview.5
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Integration](https://img.shields.io/badge/Integration-475569?style=flat-square) : System settings, media and contact imports, notifications and webcam test.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Interface and version numbering aligned with Android.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Interface and version numbering aligned with Desktop.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Stability](https://img.shields.io/badge/Stability-475569?style=flat-square) : Improved background resume and display recovery.
<a id="2.7-156.0.2"></a>
## 2.7-156.0.2
![APK built · not published](https://img.shields.io/badge/APK%20built%20%C2%B7%20not%20published-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Integration](https://img.shields.io/badge/Integration-475569?style=flat-square) : Android settings, sound, brightness, imports and notifications.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Apps](https://img.shields.io/badge/Apps-475569?style=flat-square) : Calls, SMS, clock and camera handled by Android apps.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Stability](https://img.shields.io/badge/Stability-475569?style=flat-square) : White screens and background resume fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Version](https://img.shields.io/badge/Version-475569?style=flat-square) : Historical numbering: this number does not represent a Gecko update.
<a id="2.7-156.0.1"></a>
## 2.7-156.0.1
![Published](https://img.shields.io/badge/Published-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Branding](https://img.shields.io/badge/Branding-475569?style=flat-square) : “Vulpes OS – Preview” name and project icon.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Home](https://img.shields.io/badge/Home-475569?style=flat-square) : Wallpaper flickering fixed.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Version](https://img.shields.io/badge/Version-475569?style=flat-square) : 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
![Published](https://img.shields.io/badge/Published-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Home](https://img.shields.io/badge/Home-475569?style=flat-square) : 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
![Published](https://img.shields.io/badge/Published-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Preview](https://img.shields.io/badge/Preview-475569?style=flat-square) : First Android Preview to try Vulpes without flashing the phone.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Home](https://img.shields.io/badge/Home-475569?style=flat-square) : 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
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Apps](https://img.shields.io/badge/Apps-475569?style=flat-square) : Adaptation of contacts, messages, settings, alarms and storage.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Branding](https://img.shields.io/badge/Branding-475569?style=flat-square) : Creation of Vulpes: visual identity, translations and system information.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Engine](https://img.shields.io/badge/Engine-475569?style=flat-square) : Move from old Firefox OS engines to a modern Gecko.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Startup](https://img.shields.io/badge/Startup-475569?style=flat-square) : First native boots and hardware trials on Pixel 3a.

View file

@ -1,904 +0,0 @@
# 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
![APK publié](https://img.shields.io/badge/APK%20publi%C3%A9-2F855A?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Affichage](https://img.shields.io/badge/Affichage-475569?style=flat-square) : Messages des fonctions indisponibles plus lisibles en thèmes clair et sombre.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Clavier](https://img.shields.io/badge/Clavier-475569?style=flat-square) : 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.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Journal d’appels](https://img.shields.io/badge/Journal%20d%E2%80%99appels-475569?style=flat-square) : Affichage du nom des contacts rétabli au premier chargement du journal.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : Panneaux Stockage rétablis. Une erreur de chargement ne bloque plus les autres menus.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-2F855A?style=flat-square) ![Données Android](https://img.shields.io/badge/Donn%C3%A9es%20Android-475569?style=flat-square) : Lecture des contacts, SMS et journaux Android dans Gaia, après autorisation. Les données locales de Vulpes sont conservées.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Retour vers Vulpes sans écran blanc persistant ; un catalogue fermé peut être rouvert.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Clavier](https://img.shields.io/badge/Clavier-475569?style=flat-square) : Le rectangle blanc signalé reste à reproduire sur les appareils concernés.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Données Android](https://img.shields.io/badge/Donn%C3%A9es%20Android-475569?style=flat-square) : 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.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Les corrections communes de Gaia sont incluses dans les images complètes et les mises à jour de démarrage.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : 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
![Publié](https://img.shields.io/badge/Publi%C3%A9-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Mode vidéo, avec miniature et lecture dans Galerie et Vidéo.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Améliorations des paramètres, de l’apparence, des sélecteurs, du clavier, des contacts et des médias.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Changement de capteur, arrêt et reprise fiabilisés.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Services](https://img.shields.io/badge/Services-475569?style=flat-square) : Compte Mozilla, Sync, localisation distante, extensions et transfert USB restent incomplets.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Publication](https://img.shields.io/badge/Publication-475569?style=flat-square) : APK Preview 20 disponible.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Compatibilité](https://img.shields.io/badge/Compatibilit%C3%A9-475569?style=flat-square) : Appels et SMS utilisent Android. Cette Preview n’a pas encore été validée en utilisation sur Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Aperçu autour de 16 images par seconde.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : Mise à jour du Wi-Fi, du Bluetooth, des appels, des SMS/MMS et des réglages réseau.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Audio](https://img.shields.io/badge/Audio-475569?style=flat-square) : Son des vidéos et conversation téléphonique à vérifier séparément.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Preview 20 installée sur le Pixel 3a, données conservées. Wi-Fi connecté et réseau 4G détecté.
### ![Web](https://img.shields.io/badge/Web-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Changelog](https://img.shields.io/badge/Changelog-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Enregistrement vidéo avec son, miniature et lecture dans Galerie et Vidéo.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Aide](https://img.shields.io/badge/Aide-475569?style=flat-square) : « Améliorer Vulpes » mène aux tickets du projet.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : Sélecteurs de date et d’heure rétablis ; retour du menu Clavier et texte des listes corrigés.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Enregistrement vérifié avec une caméra simulée.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Changements préparés pour un prochain APK ; vidéo non encore testée sur Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Aperçu amélioré, autour de 16 images par seconde.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Horloge](https://img.shields.io/badge/Horloge-475569?style=flat-square) : Heure automatique selon les informations du réseau ; fuseau horaire encore manuel.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : Fermeture des dialogues et nouvelle tentative après une erreur corrigées.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Appels](https://img.shields.io/badge/Appels-475569?style=flat-square) : Réglages du double appel, de l’affichage du numéro et du renvoi d’appel.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Partage de connexion avec ou sans mot de passe.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : Les réglages lents ne bloquent plus les appels et les SMS.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Adresse IP et vitesse de connexion affichées correctement.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : Choix du type de réseau et modification des points d’accès mobiles.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : Réglages réseau plus fiables lorsque le modem répond lentement.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : Navigation, accessibilité et aide corrigées. Les panneaux incomplets restent visibles.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Vérification](https://img.shields.io/badge/V%C3%A9rification-475569?style=flat-square) : Menus parcourus ; certaines actions dépendent encore du matériel ou de l’opérateur.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Stockage](https://img.shields.io/badge/Stockage-475569?style=flat-square) : Informations de stockage et téléchargements raccordés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Stockage](https://img.shields.io/badge/Stockage-475569?style=flat-square) : Affichage de l’espace utilisé par Vulpes.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Apparence](https://img.shields.io/badge/Apparence-475569?style=flat-square) : Choix Clair/Sombre et validation ou annulation des sélecteurs rétablis.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SIM](https://img.shields.io/badge/SIM-475569?style=flat-square) : Gestionnaire adapté aux téléphones à une seule SIM.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Informations SIM, centre de messagerie et accusés de réception.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Chargement plus rapide des réseaux enregistrés.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Apparence](https://img.shields.io/badge/Apparence-475569?style=flat-square) : Modes clair et sombre communs disponibles sur le Pixel 3a.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Correctifs des paramètres et polices Firefox OS intégrés.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Vérification](https://img.shields.io/badge/V%C3%A9rification-475569?style=flat-square) : Pas de nouvel essai des appels, SMS ou Bluetooth à cette étape.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Apparence](https://img.shields.io/badge/Apparence-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Apparence](https://img.shields.io/badge/Apparence-475569?style=flat-square) : Modes Clair, Sombre et Système, conservés au redémarrage.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Clavier](https://img.shields.io/badge/Clavier-475569?style=flat-square) : Sélection et suppression de texte corrigées.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Thèmes et saisie de SMS vérifiés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Clavier](https://img.shields.io/badge/Clavier-475569?style=flat-square) : Clavier Gaia expérimental ; le clavier Android reste proposé par défaut.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Nouvelle tentative possible quand la caméra est occupée.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Vérification](https://img.shields.io/badge/V%C3%A9rification-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Accueil](https://img.shields.io/badge/Accueil-475569?style=flat-square) : Second écran d’accueil, aperçus de fonds d’écran et bandeau d’installation corrigés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Fermeture du message d’erreur rétablie.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : Sonneries, code de verrouillage, permissions et libellés FR/EN corrigés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Ouverture du volet en touchant la barre de statut.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Preview 18 construite localement.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stabilité](https://img.shields.io/badge/Stabilit%C3%A9-475569?style=flat-square) : Retour à la version précédente après un redémarrage instable de l’interface.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Fonctionnement confirmé par l’utilisateur.
<a id="development-2026-10-09-mms-bluetooth"></a>
## 09/10/2026 : MMS et Bluetooth
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Panneaux Gaia reliés aux capacités de chaque plateforme.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Recherche, association, connexion, oubli d’appareils et sortie audio.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Envoi conservé et repris après une interruption.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Vérification](https://img.shields.io/badge/V%C3%A9rification-475569?style=flat-square) : MMS accepté par l’opérateur, réception non confirmée. Audio d’appel sur casque à tester.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Changement de capteur, arrêt et affichage après rotation corrigés.
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Pièces jointes conservées et envoi affiché en attente jusqu’à confirmation.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Aperçu mesuré autour de 16 images/s à l’arrière et 25 à 27 à l’avant.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Envoi réel encore bloqué par la connexion aux données mobiles.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : 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
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Messages d’erreur lisibles, avec des boutons utilisables.
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![MMS](https://img.shields.io/badge/MMS-475569?style=flat-square) : Message clair si l’envoi est indisponible, sans perdre le brouillon.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Téléphone](https://img.shields.io/badge/T%C3%A9l%C3%A9phone-475569?style=flat-square) : Rappel depuis le journal d’appel rétabli.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Appels](https://img.shields.io/badge/Appels-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Affichage du volume, verrouillage et balayage des notifications corrigés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Envoi en un appui, sans fermer d’abord le clavier. Menu des pièces jointes rétabli.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Audio](https://img.shields.io/badge/Audio-475569?style=flat-square) : Corrections du routage du son des appels.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Appels](https://img.shields.io/badge/Appels-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Messages associés à la bonne conversation.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : Récupération du modem améliorée ; les nouvelles tentatives évitent de doubler appels et SMS.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Appels](https://img.shields.io/badge/Appels-475569?style=flat-square) : Stabilité du réseau et audio dans les deux sens encore à confirmer.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : 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
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Sources](https://img.shields.io/badge/Sources-475569?style=flat-square) : Publication des corrections caméra, contacts, SMS et Marketplace, avec présentations FR/EN à jour.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Construction](https://img.shields.io/badge/Construction-475569?style=flat-square) : Construction depuis une copie neuve du dépôt corrigée.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : L’APK public reste en Preview 17 ; ces corrections sont dans les sources.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Ouverture et changement de capteur fiabilisés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Contacts](https://img.shields.io/badge/Contacts-475569?style=flat-square) : Photos, dates, modifications et choix d’un numéro depuis SMS ou Téléphone corrigés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Galerie](https://img.shields.io/badge/Galerie-475569?style=flat-square) : Sélection des médias corrigée.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Correctifs dans les sources ; APK public inchangé à cette étape.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Numéro et texte transmis correctement à l’application Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Bluetooth](https://img.shields.io/badge/Bluetooth-475569?style=flat-square) : Recherche disponible ; association et usages audio encore à vérifier.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Intégration prévue dans un prochain APK.
### ![Web](https://img.shields.io/badge/Web-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Thème intégré : police Fira, navigation compacte, boutons tactiles et captures défilantes.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Affichage intégré conservé pendant la recherche, les filtres et les changements de langue.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Thème préparé localement, pas encore déployé sur le site.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Affichage](https://img.shields.io/badge/Affichage-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Le bandeau « Application installée » disparaît automatiquement et se remplace correctement.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Affichage et fermeture des bandeaux vérifiés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Correctif préparé pour le prochain APK.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Paramètres](https://img.shields.io/badge/Param%C3%A8tres-475569?style=flat-square) : Ouverture des panneaux rétablie quand une animation ne se termine pas normalement.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Correctifs encore absents de l’APK public.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Réseaux détectés et déconnexions affichés correctement.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Recherche, connexion et accès Internet vérifiés.
<a id="development-2026-10-06-marketplace-views"></a>
## 06/10/2026 : Navigation Marketplace
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Message local avec bouton Réessayer si le catalogue est inaccessible.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Le catalogue reste sous la barre de statut et se masque en revenant à l’accueil ou en le fermant.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Navigation](https://img.shields.io/badge/Navigation-475569?style=flat-square) : Une ancienne erreur ne masque plus une nouvelle tentative.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Navigation, erreurs, retour à l’accueil et fermeture vérifiés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Preview 17 reconstruite localement, sans nouveau test Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : 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
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Applications](https://img.shields.io/badge/Applications-475569?style=flat-square) : Installation, lancement et désinstallation d’applications web empaquetées.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Catalogue Vulpes sur l’accueil, en français et en anglais, avec son icône originale.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Sécurité](https://img.shields.io/badge/S%C3%A9curit%C3%A9-475569?style=flat-square) : Applications installées séparées des applications système.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Compatibilité](https://img.shields.io/badge/Compatibilit%C3%A9-475569?style=flat-square) : Certaines anciennes applications et API Firefox OS ne sont pas encore prises en charge.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : Installation et utilisation de 2048 vérifiées.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : 2048 installé, conservé après redémarrage et désinstallé sur un émulateur Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Marketplace](https://img.shields.io/badge/Marketplace-475569?style=flat-square) : 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
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : Logo Vulpes au démarrage.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Accueil](https://img.shields.io/badge/Accueil-475569?style=flat-square) : Défilement horizontal involontaire supprimé ; applications de test masquées.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Personnalisation](https://img.shields.io/badge/Personnalisation-475569?style=flat-square) : Choix des fonds d’écran et des sons rétabli. Langue appliquée à l’accueil et conservée au redémarrage.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : APK construit, non publié ; image Pixel essayée sans installation permanente.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Barres Android masquées, accessibles par balayage ; mode lanceur vérifié.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Verrouillage](https://img.shields.io/badge/Verrouillage-475569?style=flat-square) : Retour après veille et depuis la caméra du verrouillage corrigé.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Installateur avec vérification du modèle et des fichiers avant écriture.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Installation autonome de Vulpes, sans passer par Droidian.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Paquet non publié ; un seul Pixel testé. L’installation remplace les données du téléphone.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Secours](https://img.shields.io/badge/Secours-475569?style=flat-square) : Sauvegardes vérifiées ; restauration complète encore à essayer.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Préparation adaptée à chaque Pixel, avec sauvegardes propres à l’appareil.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Mise au point parfois trop lente ; problème encore ouvert.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Installation directe depuis Android d’origine pas encore disponible.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Préparation des paquets et kit de secours avec sauvegarde du démarrage.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Secours](https://img.shields.io/badge/Secours-475569?style=flat-square) : Restauration possible sans démarrer l’outil de diagnostic.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Préparation vérifiée localement ; installation sur un autre Pixel encore à finaliser.
### ![Web](https://img.shields.io/badge/Web-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Changelog](https://img.shields.io/badge/Changelog-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Réseau](https://img.shields.io/badge/R%C3%A9seau-475569?style=flat-square) : Récupération du modem sans redémarrer tout le téléphone.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : Polices et initialisation du modem corrigées sur une installation neuve.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stabilité](https://img.shields.io/badge/Stabilit%C3%A9-475569?style=flat-square) : Redémarrages intermittents du modem encore à résoudre ; paquet public incomplet.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Préparation d’une image avec des données vierges.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Polices](https://img.shields.io/badge/Polices-475569?style=flat-square) : Affichage des polices au premier démarrage corrigé.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Image non publiée. Appels, SMS et données mobiles encore à vérifier de bout en bout.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stabilité](https://img.shields.io/badge/Stabilit%C3%A9-475569?style=flat-square) : Redémarrage intermittent du modem identifié.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : 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
![Publié](https://img.shields.io/badge/Publi%C3%A9-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Sources](https://img.shields.io/badge/Sources-475569?style=flat-square) : Première publication des sources PC, Android et Tundra, avec présentation FR/EN.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Arrêt et remplacement du flux caméra fiabilisés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Dates et états d’envoi corrigés dans les conversations.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Applications](https://img.shields.io/badge/Applications-475569?style=flat-square) : Navigation, réglages, gestes et conservation des données vérifiés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Stabilité](https://img.shields.io/badge/Stabilit%C3%A9-475569?style=flat-square) : Mise à jour, Photo, Galerie et retours d’arrière-plan vérifiés sur un émulateur Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Sources incluses ; installation universelle encore indisponible à cette étape.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Libération du capteur et changement de caméra corrigés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Conversations vides corrigées.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Sources communes mises à jour, sans nouvel APK.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Arrêt du vibreur rétabli.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![À suivre](https://img.shields.io/badge/%C3%80%20suivre-475569?style=flat-square) : Dialogue après envoi SMS et fluidité caméra encore à corriger.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Focus et zoom confirmés par l’utilisateur.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Messages en échec conservés et états d’envoi mis à jour.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : APK Preview 14 inchangé à cette étape.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Mise au point au toucher et zoom numérique. Photos jusqu’à 12 Mpx à l’arrière et 8 Mpx à l’avant.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Retour de l’aperçu après une photo corrigé.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Réception par le destinataire non encore confirmée.
<a id="2.7-preview.14"></a>
## 30/09/2026 : 2.7-preview.14
![Publié](https://img.shields.io/badge/Publi%C3%A9-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Polices, informations de version, verrouillage et notifications harmonisés.
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Clavier](https://img.shields.io/badge/Clavier-475569?style=flat-square) : Clavier agrandi, édition de texte et saisie du numéro de destinataire améliorées.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : Démarrage et polices vérifiés sur un émulateur Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Notifications](https://img.shields.io/badge/Notifications-475569?style=flat-square) : Arrêt de la vibration corrigé.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Notifications conservées, ouverture de la conversation et doublons corrigés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Réseaux enregistrés, reconnexion et oubli d’un réseau corrigés.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Audio](https://img.shields.io/badge/Audio-475569?style=flat-square) : 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
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Calendrier](https://img.shields.io/badge/Calendrier-475569?style=flat-square) : Rappels des événements corrigés, y compris après suppression.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Galerie](https://img.shields.io/badge/Galerie-475569?style=flat-square) : Premier affichage et fermeture de l’éditeur corrigés.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![SMS](https://img.shields.io/badge/SMS-475569?style=flat-square) : Filtres des conversations corrigés.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Applications](https://img.shields.io/badge/Applications-475569?style=flat-square) : Photo, vidéo, calendrier et conversations vérifiés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Applications](https://img.shields.io/badge/Applications-475569?style=flat-square) : Caméra, Galerie, Calendrier et reprise après arrière-plan vérifiés.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : 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
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Navigateur](https://img.shields.io/badge/Navigateur-475569?style=flat-square) : Page d’accueil locale, en français ou en anglais selon la langue.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Applications](https://img.shields.io/badge/Applications-475569?style=flat-square) : Conversations SMS et traductions du Calendrier corrigées.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Changement de capteur et zoom de l’interface corrigés ; flash grisé s’il est indisponible.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Icônes plus nettes, appui long sur Accueil et gestes corrigés.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Aperçu plus fluide, autour de 23 images par seconde lors des essais.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : Luminosité plus réactive et connexion Wi-Fi corrigée.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Navigateur](https://img.shields.io/badge/Navigateur-475569?style=flat-square) : Première page d’accueil locale.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Extinction](https://img.shields.io/badge/Extinction-475569?style=flat-square) : Animation d’extinction conservée, avec des ronds bleus.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Textes des listes et de Vidéo corrigés ; barre Accueil transparente et adaptée au paysage.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Téléphone](https://img.shields.io/badge/T%C3%A9l%C3%A9phone-475569?style=flat-square) : En-têtes et icônes du journal d’appel corrigés.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Luminosité](https://img.shields.io/badge/Luminosit%C3%A9-475569?style=flat-square) : Réglage de la luminosité raccordé.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Preview 9 construite avec les corrections communes ; Wi-Fi géré par Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : Recherche des réseaux stabilisée pour éviter les blocages du téléphone.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : Caméra et appels encore incomplets.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![À propos](https://img.shields.io/badge/%C3%80%20propos-475569?style=flat-square) : Remerciements, écran de démarrage et nom du navigateur mis à jour.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Panneaux et styles Gaia d’origine rétablis dans Paramètres, Suivi conso, Radio FM et les imports.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Bouton Accueil dégagé des commandes ; date, batterie et écran de verrouillage rétablis.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Intégration](https://img.shields.io/badge/Int%C3%A9gration-475569?style=flat-square) : Mode lanceur, accès aux paramètres Android et reprise après arrière-plan.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Wi-Fi](https://img.shields.io/badge/Wi--Fi-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Téléphone](https://img.shields.io/badge/T%C3%A9l%C3%A9phone-475569?style=flat-square) : Interfaces Gaia d’appel entrant et sortant, avec journal d’appel.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Polices Fira, contacts et affichage de la batterie corrigés.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Boutons](https://img.shields.io/badge/Boutons-475569?style=flat-square) : Prise en charge des touches Marche/Arrêt et Volume, du menu d’extinction et du réveil de l’écran.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Caméra native encore indisponible.
- ![Test](https://img.shields.io/badge/Test-75658A?style=flat-square) ![Appels](https://img.shields.io/badge/Appels-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Polices Fira et informations de batterie corrigées ; boutons de test retirés des paramètres.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Installation](https://img.shields.io/badge/Installation-475569?style=flat-square) : Conservation des données lors du démarrage installé.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : Plein écran stable, sans gestes ni ouverture automatique d’applications.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Matériel](https://img.shields.io/badge/Mat%C3%A9riel-475569?style=flat-square) : 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
![En développement](https://img.shields.io/badge/En%20d%C3%A9veloppement-334155?style=flat-square)
### ![Toutes les plateformes](https://img.shields.io/badge/Toutes%20les%20plateformes-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Accueil](https://img.shields.io/badge/Accueil-475569?style=flat-square) : Bouton rond, fond transparent sur l’accueil et bleu dans les applications.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : État appuyé du bouton, dégradé, polices et informations système corrigés.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Stabilité](https://img.shields.io/badge/Stabilit%C3%A9-475569?style=flat-square) : Reprise de l’application modifiée dans les sources, pas encore testée sur Android.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : Premiers essais d’une image native avec affichage accéléré et batterie.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Disponibilité](https://img.shields.io/badge/Disponibilit%C3%A9-475569?style=flat-square) : Essais temporaires ; de nombreuses fonctions matérielles restent à raccorder.
<a id="2.7-preview.7"></a>
## 2.7-preview.7
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Desktop + Android](https://img.shields.io/badge/Desktop%20%2B%20Android-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![À propos](https://img.shields.io/badge/%C3%80%20propos-475569?style=flat-square) : Versions de Vulpes, Gaia et Gecko distinguées.
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Gestion des autorisations et messages d’erreur plus précis.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Reprise après la première demande d’autorisation corrigée.
<a id="2.7-preview.6"></a>
## 2.7-preview.6
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Application Photo Gaia reliée à la webcam.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Galerie](https://img.shields.io/badge/Galerie-475569?style=flat-square) : Import de fichiers et photos disponibles dans Galerie.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Changement de capteur selon le matériel ; flash et vidéo indisponibles.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Application Photo Gaia reliée aux caméras Android.
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Galerie](https://img.shields.io/badge/Galerie-475569?style=flat-square) : Import de fichiers et photos disponibles dans Galerie.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Caméra](https://img.shields.io/badge/Cam%C3%A9ra-475569?style=flat-square) : Changement de capteur selon le matériel ; flash et vidéo indisponibles.
<a id="2.7-preview.5"></a>
## 2.7-preview.5
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Desktop](https://img.shields.io/badge/Desktop-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Intégration](https://img.shields.io/badge/Int%C3%A9gration-475569?style=flat-square) : Réglages système, imports de médias et de contacts, notifications et essai webcam.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Interface et numérotation des versions alignées avec Android.
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Interface](https://img.shields.io/badge/Interface-475569?style=flat-square) : Interface et numérotation des versions alignées avec le PC.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Stabilité](https://img.shields.io/badge/Stabilit%C3%A9-475569?style=flat-square) : 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
![APK construit · non publié](https://img.shields.io/badge/APK%20construit%20%C2%B7%20non%20publi%C3%A9-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Intégration](https://img.shields.io/badge/Int%C3%A9gration-475569?style=flat-square) : Réglages Android, son, luminosité, imports et notifications.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Applications](https://img.shields.io/badge/Applications-475569?style=flat-square) : Appels, SMS, horloge et caméra confiés aux applications Android.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Stabilité](https://img.shields.io/badge/Stabilit%C3%A9-475569?style=flat-square) : Écrans blancs et reprise après arrière-plan corrigés.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Version](https://img.shields.io/badge/Version-475569?style=flat-square) : 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
![Publié](https://img.shields.io/badge/Publi%C3%A9-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Identité](https://img.shields.io/badge/Identit%C3%A9-475569?style=flat-square) : Nom « Vulpes OS – Preview » et icône du projet.
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Accueil](https://img.shields.io/badge/Accueil-475569?style=flat-square) : Clignotement du fond d’écran corrigé.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Version](https://img.shields.io/badge/Version-475569?style=flat-square) : 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
![Publié](https://img.shields.io/badge/Publi%C3%A9-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Fix](https://img.shields.io/badge/Fix-52677D?style=flat-square) ![Accueil](https://img.shields.io/badge/Accueil-475569?style=flat-square) : 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
![Publié](https://img.shields.io/badge/Publi%C3%A9-334155?style=flat-square)
### ![Android / APK](https://img.shields.io/badge/Android%20%2F%20APK-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Découverte](https://img.shields.io/badge/D%C3%A9couverte-475569?style=flat-square) : Première Preview Android pour découvrir Vulpes sans flasher le téléphone.
- ![Note](https://img.shields.io/badge/Note-6B7280?style=flat-square) ![Accueil](https://img.shields.io/badge/Accueil-475569?style=flat-square) : 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
### ![Desktop + Sargo](https://img.shields.io/badge/Desktop%20%2B%20Sargo-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Applications](https://img.shields.io/badge/Applications-475569?style=flat-square) : Adaptation des contacts, messages, paramètres, alarmes et du stockage.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Identité](https://img.shields.io/badge/Identit%C3%A9-475569?style=flat-square) : Création de Vulpes : identité visuelle, traductions et informations système.
- ![Change](https://img.shields.io/badge/Change-64748B?style=flat-square) ![Moteur](https://img.shields.io/badge/Moteur-475569?style=flat-square) : Passage des anciens moteurs Firefox OS à un Gecko moderne.
### ![Sargo / Pixel 3a](https://img.shields.io/badge/Sargo%20%2F%20Pixel%203a-2563EB?style=flat-square)
- ![Add](https://img.shields.io/badge/Add-487768?style=flat-square) ![Démarrage](https://img.shields.io/badge/D%C3%A9marrage-475569?style=flat-square) : Premiers démarrages natifs et essais matériels sur le Pixel 3a.

373
LICENSE
View file

@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

24
NOTICE
View file

@ -1,24 +0,0 @@
Vulpes OS — community project by oversu / NNS Production
https://vulpes-os.org
https://nnsprod.com/git/oversu/VulpesOS
Original Vulpes host, adapter and tooling additions are licensed under the
Mozilla Public License 2.0 (see LICENSE). Preserve the applicable copyright,
license and attribution notices when redistributing modified files.
Inherited components retain their original licenses:
- Gaia: Mozilla Foundation and contributors; see gaia/LICENSE (Apache 2.0)
and the notices in individual files and bundled components.
- Firefox / Gecko / GeckoView: Mozilla and contributors; engine versions,
download locations and source archives are pinned in engine-lock.json,
release.json and the Android Gradle configuration. Engines are downloaded
separately; they are not stored in this Git repository.
- Fira fonts and Gaia icons: see gaia/shared/elements/vulpes-fonts/ and the
notices included with each font and application.
- Tundra references AOSP, Debian, Droidian, libhybris, UBports, Capyloon and
other upstream projects. Their original licenses remain applicable.
Publishing the Tundra tooling does not relicense proprietary vendor files.
Vulpes OS is independent of Mozilla. Firefox and other third-party trademarks
belong to their respective owners. This project is not an official Firefox OS
release or an endorsement by those organizations.

View file

@ -1,83 +1,219 @@
<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">
<p align="center"><img src="docs/media/vulpes-logo.png" width="120" alt="Vulpes OS"></p> <a href="https://nnsprod.com/git/oversu/VulpesOS" lang="en" hreflang="en">🇬🇧 English</a> · <a href="https://nnsprod.com/git/oversu/VulpesOS/src/branch/main/README.fr.md" lang="fr" hreflang="fr"><strong>🇫🇷 Français</strong></a>
</p>
<p align="center">
<img src="docs/media/vulpes-logo.png" width="160" alt="Logo Vulpes OS — un renard blanc et bleu">
</p>
<h1 align="center">Vulpes OS</h1> <h1 align="center">Vulpes OS</h1>
<p align="center">Firefox OS fork · Open source</p>
<p align="center"> <p align="center">
<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> <strong>L’esprit Firefox OS. Un moteur Gecko d’aujourd’hui.</strong><br>
<img src="https://img.shields.io/badge/Gaia-2.7-0755be?style=flat-square" alt="Gaia: 2.7"> A community continuation of Firefox OS.
<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>
<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>
[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)
Vulpes OS reprend Firefox OS pour faire fonctionner **Gaia**, son interface et ses applications HTML/CSS/JavaScript, avec un moteur **Gecko** récent.
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"> <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> <img src="https://img.shields.io/badge/Vulpes-2.7-0755be?style=flat-square" alt="Vulpes 2.7">
<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> <img src="https://img.shields.io/badge/Gecko_desktop-156.0.1-0099cc?style=flat-square" alt="Gecko desktop 156.0.1">
<img src="https://img.shields.io/badge/Statut-Preview-f3a712?style=flat-square" alt="Statut : Preview">
<img src="https://img.shields.io/badge/Plateformes-Linux_%C2%B7_Android-334155?style=flat-square" alt="Linux et Android">
</p> </p>
<p align="center"><em>Accueil et paramètres de Gaia · captures de la version pour ordinateur.</em></p>
## Essayer Vulpes <p align="center">
<a href="https://vulpes-os.org"><strong>🌐 Découvrir le projet</strong></a>
&nbsp; · &nbsp;
<a href="#telechargements"><strong>📥 APK Android</strong></a>
&nbsp; · &nbsp;
<a href="#points-forts">Les points forts</a>
&nbsp; · &nbsp;
<a href="#architecture">L’architecture</a>
&nbsp; · &nbsp;
<a href="#suite">La suite</a>
</p>
| 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). |
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. ## 🦊 Le Web comme plateforme d’applications
### Preview 20 · 13 octobre 2026 Et si l’interface de votre téléphone était faite avec les mêmes technologies que vos sites web ?
[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. **Vulpes redonne vie à cette idée de Firefox OS.** L’accueil, les paramètres et les applications Gaia retrouvent un environnement d’exécution sur un Gecko récent. HTML, CSS et JavaScript restent au cœur de l’expérience : une interface que l’on peut lire, comprendre et modifier.
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é. Le projet relie deux générations : **les applications et l’interface historiques de Firefox OS**, et **le moteur web maintenu par Mozilla**. Son ambition est de faire évoluer l’un sans devoir réécrire l’autre à chaque version.
### Linux **Un seul projet, deux façons de l’utiliser : l’émulateur desktop Linux et l’application Android Vulpes OS - Preview (APK).** Ils partagent Gaia et les services Vulpes, avec une intégration Gecko adaptée à chaque plateforme.
Prérequis : Linux x86_64, Git, Python 3 et les bibliothèques système nécessaires à Firefox. > **Disponible :** les trois Preview Android sont téléchargeables dans les [Releases](https://nnsprod.com/git/oversu/VulpesOS/releases). Les sources complètes, la distribution desktop et leurs instructions d’installation sont en préparation. Les captures ci-dessous montrent le prototype desktop.
```sh <p align="center">
git clone https://nnsprod.com/git/oversu/VulpesOS.git <a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-desktop.png"><img src="docs/media/vulpes-desktop.png" width="300" alt="Capture réelle de Vulpes : accueil Gaia en anglais"></a>
cd VulpesOS &nbsp;
./setup.sh <a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-gecko.png"><img src="docs/media/vulpes-gecko.png" width="300" alt="Paramètres Vulpes en anglais, affichant la version réelle du moteur Gecko"></a>
./run.sh </p>
<p align="center"><em>Accueil et informations système — captures réelles du prototype desktop en anglais. Le champ « Platform Version » affiche la version Gecko renvoyée par le moteur exécuté. Cliquer sur une image pour l’agrandir.</em></p>
<a name="telechargements"></a>
## 📥 Tester la Preview Android
**Pour commencer, choisissez [Vulpes OS - Preview 2.7-156.0.1](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)**, la plus récente des trois versions. Chaque page contient l’APK, les notes FR/EN et le fichier de contrôle `SHA256SUMS.txt`.
| Version | Contenu | Usage |
| :--- | :--- | :--- |
| [2.7-156.0.1](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1) | Nom et icône Vulpes OS - Preview ; correctif du fond d’écran inclus | **Preview conseillée** |
| [2.7-preview.2](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2) | Correction du fond d’écran clignotant | Archive |
| [2.7-preview.1](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1) | Première Preview Android | Archive — clignotement du fond d’écran connu |
Téléchargez l’APK et ouvrez-le sur Android pour l’installer. Les versions successives conservent le même identifiant et la même signature : une mise à jour par-dessus la précédente permet de garder les données.
- **Android 8.0 / API 26 minimum déclaré.** Validation sur un émulateur Android 16 / API 36 ; les autres configurations restent à tester.
- **Environ 592 Mio par APK**, avec les architectures ARM64, ARM 32 bits et x86_64 intégrées.
- **Preview de développement :** elle ne remplace pas Android et ne fournit pas les appels/SMS cellulaires. L’import des médias Android reste à porter.
- **Desktop Linux :** environnement de développement distinct, encore sans distribution publique prête à installer. Ce simulateur affiche Gaia ; il n’émule pas tout le matériel d’un téléphone.
Les APK sont joints aux Releases, sans alourdir l’historique Git. Ces tags désignent les APK historiques publiés ; ils ne représentent pas encore leurs sources de compilation complètes.
<a name="points-forts"></a>
## ✨ Ce qui rend Vulpes intéressant
### 🔄 Un moteur qui peut évoluer
Le passage de Gecko 45 à 52 a servi de première étape. Le prototype desktop utilise maintenant **Gecko 156.0.1** avec une architecture qui sépare l’interface, les services et l’intégration au moteur.
Un outil automatise le téléchargement d’une version candidate, le contrôle de son empreinte et les tests de compatibilité. Le basculement reste explicite et un retour arrière est prévu. Les incompatibilités sont détectées et documentées ; elles ne sont pas réparées automatiquement.
### 🧩 Faire vivre les applications Firefox OS
Vulpes adapte les anciennes API dont Gaia a besoin : réglages, registre des applications, contacts locaux, stockage et communications entre applications. Le travail porte sur leur fonctionnement, leurs permissions et leurs données persistantes.
Des essais avec des applications historiques accompagnent le portage. **La compatibilité progresse application par application** : la présence d’une ancienne API ne garantit pas encore le fonctionnement de tout le catalogue Firefox OS.
### 🛠️ Modifier l’interface sans reconstruire Gecko
Une grande partie de l’interface se travaille dans les fichiers HTML, CSS et JavaScript. Les ressources servies et les surcharges peuvent être modifiées puis rechargées, sans recompiler le moteur.
Certaines applications conservent des bundles issus de l’ancien système de construction Gaia. Leur modification demande de vérifier quel fichier est réellement chargé : tous les fichiers des sources historiques ne sont pas encore reliés directement à l’interface exécutée.
### 📱 Deux façons d’explorer le projet
- **Sur Linux :** une fenêtre au format téléphone, avec Gaia, le multitâche et un navigateur utilisant des vues Gecko séparées.
- **Sur Android :** **Vulpes OS – Preview**, une application GeckoView pour découvrir l’expérience sans remplacer le système du téléphone.
La Preview Android est un terrain d’essai. Elle n’est pas une ROM et ne remplace pas les fonctions de téléphonie d’Android.
## 🧪 Où en est le prototype ?
| Fonction | Desktop Linux | Preview Android |
| :--- | :--- | :--- |
| Accueil Gaia, lancement des applications | ✅ Fonctionnel | ✅ Disponible |
| Réglages persistants et interface traduite | ✅ Parcours testés | ✅ Intégrés, validation mobile à poursuivre |
| Navigation web | ✅ Vues Gecko séparées | ✅ Sessions GeckoView séparées |
| Contacts | ✅ Contacts locaux au profil | ✅ Contacts locaux à Vulpes |
| SMS | 🧪 Interface et brouillons locaux | 🧪 Interface locale |
| Galerie et musique | ✅ Import et lecture testés | 🚧 Import des médias Android à porter |
| Alarmes | ✅ Création, déclenchement et persistance testés | 🧪 Liées à la durée de vie du processus Vulpes |
| Appels et SMS sur le réseau mobile | 🚧 Non intégrés | 🚧 Non intégrés |
| Installation comme système de téléphone | 🚧 Futur chantier | 🚧 L’APK ne remplace pas Android |
**Versions de référence — septembre 2026**
| Élément | Version |
| :--- | :--- |
| Produit communautaire | Vulpes OS 2.7 |
| Moteur desktop | Gecko 156.0.1 |
| Application Android | Preview 2.7-156.0.1 |
| Dépendance GeckoView | `156.0.20260921121718` |
| Minimum Android déclaré | Android 8.0 · API 26 |
La version de l’APK, celle du moteur et celle du produit sont distinctes. Le `.1` final de la Preview désigne sa révision ; GeckoView rapporte ici **156.0**. Le minimum Android déclaré ne signifie pas que chaque appareil a été testé.
<a name="architecture"></a>
## ⚙️ Sous le capot
```text
Gaia + applications web
HTML · CSS · JS
│
Compatibilité des anciennes API moz*
Contrats Vulpes versionnés
│
Réglages · Applications · Contacts · Stockage
Messages locaux · Alarmes
│
┌────────────┴────────────┐
│ │
Hôte desktop Hôte Android
Fenêtre privilégiée GeckoView + passerelle
et vues web séparées WebExtension/native
│ │
Firefox / Gecko GeckoView
│ │
Linux Android
``` ```
`setup.sh` télécharge le moteur Gecko prévu pour cette version et vérifie son empreinte. ### Une frontière claire entre les applications et le système
### Compiler l’APK Les applications passent par des services dont les permissions sont contrôlées par l’hôte. Les pages web externes utilisent des contextes de navigation séparés et n’obtiennent pas les API privilégiées de Vulpes.
Depuis la racine du dépôt : Le port desktop actuel s’appuie sur le **binaire officiel de Firefox**, avec un hôte et des adaptateurs Vulpes, sans modification native du moteur. Certaines interfaces utilisées par cet hôte restent internes à Gecko : les tests à chaque changement de version sont donc essentiels.
```sh ### Des mises à jour vérifiées avant activation
cd android
./build-apk.sh ```text
Télécharger → Vérifier l’empreinte → Tester les interfaces
↓
Ancien moteur conservé ← Activer ← Tester Gaia et la persistance
``` ```
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. Les profils de test sont séparés des données utilisateur. L’outil conserve l’ancien moteur et son profil pour permettre un retour arrière. Le pipeline concerne aujourd’hui le desktop ; les mises à jour Android suivent le cycle de construction de l’APK.
## Participer ### Une base de travail testée
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 contrôles automatisés couvrent notamment :
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. - le démarrage de la vraie interface Gaia et le chargement des icônes ;
- les langues, les réglages et la persistance après redémarrage ;
- les contacts, le pavé numérique et le compositeur SMS ;
- les médias importés, les alarmes et la navigation ;
- les refus d’accès lorsque l’origine ou les permissions ne conviennent pas.
## Crédits et licences Un export des fichiers prévus pour publication a également démarré et passé les tests desktop sans les anciens dossiers de développement. Le packaging Android et la compilation de l’APK ont été vérifiés. Cela ne constitue pas une validation exhaustive de Firefox OS ni de tous les téléphones Android.
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. <a name="suite"></a>
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). ## 🗺️ La suite
Vulpes OS est un projet indépendant d’**OverSu / NNS Production**, sans affiliation avec Mozilla. - **Publier une base autonome :** sources, licences, documentation et procédure de démarrage.
- **Consolider la compatibilité :** poursuivre les essais d’applications historiques et les corrections des parcours du quotidien.
- **Améliorer la Preview Android :** accès aux médias, intégration à la plateforme et retours d’usage sur appareils réels.
- **Réduire le coût des mises à jour Gecko :** maintenir des contrats stables et limiter les adaptations propres au moteur.
- **Explorer un véritable port mobile :** matériel, modem, pilotes et intégration système, une fois la base consolidée.
## 🤝 Participer
Les retours utiles décrivent **l’appareil ou la distribution Linux, la version utilisée, les étapes pour reproduire le problème et le résultat attendu**. Une capture et un extrait de journal sans données personnelles aident beaucoup.
Le projet a aussi besoin de contributions en JavaScript, CSS, intégration Gecko/GeckoView, tests, traduction et documentation. Les consignes de contribution accompagneront la publication des sources.
## 📜 Origines et licences
Vulpes est construit à partir du travail de Mozilla et des contributeurs de **Boot to Gecko / Firefox OS**, notamment Gaia.
La licence retenue pour les nouveaux fichiers Vulpes est la **Mozilla Public License 2.0 (`MPL-2.0`)**. Gaia et les autres composants hérités conservent leurs licences, notamment **Apache 2.0** pour la base Gaia, ainsi que leurs mentions d’attribution. Les textes de licence et le détail des composants accompagneront la publication des sources.
---
<p align="center">
<strong>VULPES</strong><br>
Firefox OS Community Project<br><br>
<a href="https://vulpes-os.org">vulpes-os.org</a>
</p>
<p align="center">
<sub>Vulpes est un projet communautaire indépendant, sans affiliation ni approbation de Mozilla.<br>
Vulpes is an independent community project and is not affiliated with or endorsed by Mozilla.</sub>
</p>

241
README.md
View file

@ -1,83 +1,218 @@
<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">
<p align="center"><img src="docs/media/vulpes-logo.png" width="120" alt="Vulpes OS"></p> <a href="https://nnsprod.com/git/oversu/VulpesOS" lang="en" hreflang="en"><strong>🇬🇧 English</strong></a> · <a href="https://nnsprod.com/git/oversu/VulpesOS/src/branch/main/README.fr.md" lang="fr" hreflang="fr">🇫🇷 Français</a>
</p>
<p align="center">
<img src="docs/media/vulpes-logo.png" width="160" alt="Vulpes OS logo — a white and blue fox">
</p>
<h1 align="center">Vulpes OS</h1> <h1 align="center">Vulpes OS</h1>
<p align="center">Firefox OS fork · Open source</p>
<p align="center"> <p align="center">
<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> <strong>The spirit of Firefox OS. Powered by a modern Gecko.</strong><br>
<img src="https://img.shields.io/badge/Gaia-2.7-0755be?style=flat-square" alt="Gaia: 2.7"> A community continuation of Firefox OS.
<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>
<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>
[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)
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.
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"> <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> <img src="https://img.shields.io/badge/Vulpes-2.7-0755be?style=flat-square" alt="Vulpes 2.7">
<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> <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-Preview-f3a712?style=flat-square" alt="Status: Preview">
<img src="https://img.shields.io/badge/Platforms-Linux_%C2%B7_Android-334155?style=flat-square" alt="Linux and Android">
</p> </p>
<p align="center"><em>Gaia home screen and Settings · screenshots from the desktop version.</em></p>
## Try Vulpes <p align="center">
<a href="https://vulpes-os.org"><strong>🌐 Discover the project</strong></a>
&nbsp; · &nbsp;
<a href="#downloads"><strong>📥 Android APK</strong></a>
&nbsp; · &nbsp;
<a href="#highlights">Highlights</a>
&nbsp; · &nbsp;
<a href="#architecture">Architecture</a>
&nbsp; · &nbsp;
<a href="#roadmap">Roadmap</a>
</p>
| 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. |
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. ## 🦊 The Web as an application platform
### Preview 20 · 13 October 2026 What if your phone’s interface were built with the same technologies as your websites?
[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. **Vulpes brings that Firefox OS idea back to life.** The Gaia home screen, settings and applications run on a recent Gecko engine. HTML, CSS and JavaScript remain at the heart of the experience: an interface you can read, understand and modify.
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. The project connects two generations: **the original Firefox OS interface and applications**, and **the web engine maintained by Mozilla**. The aim is to let one evolve without having to rewrite the other for every release.
### Linux **One project, two ways to run it: the Linux desktop emulator and the Vulpes OS - Preview Android application (APK).** They share Gaia and Vulpes services, with Gecko integration adapted to each platform.
You’ll need Linux x86_64, Git, Python 3 and the system libraries required by Firefox. > **Available now:** all three Android Previews can be downloaded from [Releases](https://nnsprod.com/git/oversu/VulpesOS/releases). Full sources, the desktop distribution and their installation instructions are being prepared. The screenshots below show the desktop prototype.
```sh <p align="center">
git clone https://nnsprod.com/git/oversu/VulpesOS.git <a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-desktop.png"><img src="docs/media/vulpes-desktop.png" width="300" alt="Actual Vulpes screenshot: the Gaia home screen in English"></a>
cd VulpesOS &nbsp;
./setup.sh <a href="https://nnsprod.com/git/oversu/VulpesOS/media/branch/main/docs/media/vulpes-gecko.png"><img src="docs/media/vulpes-gecko.png" width="300" alt="Vulpes Settings in English, showing the actual Gecko engine version"></a>
./run.sh </p>
<p align="center"><em>Home screen and device information — actual desktop captures in English. The “Platform Version” field shows the Gecko version reported by the running engine. Click either image to enlarge it.</em></p>
<a name="downloads"></a>
## 📥 Try the Android Preview
**Start with [Vulpes OS - Preview 2.7-156.0.1](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1)**, the latest of the three builds. Each release page includes the APK, English/French notes and a `SHA256SUMS.txt` checksum file.
| Version | Changes | Use |
| :--- | :--- | :--- |
| [2.7-156.0.1](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-156.0.1) | Vulpes OS - Preview name and icon; wallpaper fix included | **Recommended Preview** |
| [2.7-preview.2](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.2) | Fixes wallpaper flashing | Archive |
| [2.7-preview.1](https://nnsprod.com/git/oversu/VulpesOS/releases/tag/android-2.7-preview.1) | First Android Preview | Archive — known wallpaper flashing |
Download the APK and open it on Android to install it. Successive versions share the same package ID and signature: installing the newer build over the previous one preserves application data.
- **Declared minimum: Android 8.0 / API 26.** Validated on an Android 16 / API 36 emulator; other configurations still need testing.
- **Approximately 592 MiB per APK**, including ARM64, 32-bit ARM and x86_64 architectures.
- **Development Preview:** it does not replace Android or provide cellular calls/SMS. Android media import remains to be ported.
- **Linux desktop:** a separate development environment, with no ready-to-install public distribution yet. This simulator runs Gaia; it does not emulate all phone hardware.
APKs are attached to Releases, keeping binaries out of Git history. These tags identify the historical APK publications; they do not yet contain their complete build sources.
<a name="highlights"></a>
## ✨ What makes Vulpes interesting
### 🔄 An engine that can keep evolving
Moving from Gecko 45 to 52 was the first step. The desktop prototype now uses **Gecko 156.0.1**, with an architecture that separates the interface, services and engine integration.
An update tool automates downloading a candidate version, checking its checksum and running compatibility tests. Switching versions remains an explicit action, with rollback available. Incompatibilities are detected and documented; they are not repaired automatically.
### 🧩 Keeping Firefox OS applications alive
Vulpes adapts the legacy APIs Gaia needs: settings, the application registry, local contacts, storage and communication between applications. This work covers their behavior, permissions and persistent data.
Tests with archived applications accompany the porting effort. **Compatibility improves one application at a time**: the presence of a legacy API does not yet guarantee that the entire Firefox OS catalog will work.
### 🛠️ Edit the interface without rebuilding Gecko
Much of the interface can be developed directly in HTML, CSS and JavaScript files. Served resources and overrides can be edited and reloaded without recompiling the engine.
Some applications still use bundles produced by the old Gaia build system. Editing them means checking which file is actually loaded: not every file in the historical sources is directly connected to the running interface yet.
### 📱 Two ways to explore the project
- **On Linux:** a phone-sized window with Gaia, multitasking and a browser using separate Gecko views.
- **On Android:** **Vulpes OS – Preview**, a GeckoView application for exploring the experience without replacing your phone’s operating system.
The Android Preview is a testing environment. It is not a ROM and does not replace Android’s telephony features.
## 🧪 Prototype status
| Feature | Linux desktop | Android Preview |
| :--- | :--- | :--- |
| Gaia home screen and application launching | ✅ Working | ✅ Available |
| Persistent settings and translated interface | ✅ Tested workflows | ✅ Integrated; further mobile validation needed |
| Web browsing | ✅ Separate Gecko views | ✅ Separate GeckoView sessions |
| Contacts | ✅ Local to the profile | ✅ Local to Vulpes |
| SMS | 🧪 Interface and local drafts | 🧪 Local interface |
| Gallery and music | ✅ Import and playback tested | 🚧 Android media import still to be ported |
| Alarms | ✅ Creation, delivery and persistence tested | 🧪 Tied to the Vulpes process lifetime |
| Cellular calls and SMS | 🚧 Not integrated | 🚧 Not integrated |
| Installation as a phone operating system | 🚧 Future work | 🚧 The APK does not replace Android |
**Reference versions — September 2026**
| Component | Version |
| :--- | :--- |
| Community product | Vulpes OS 2.7 |
| Desktop engine | Gecko 156.0.1 |
| Android application | Preview 2.7-156.0.1 |
| GeckoView dependency | `156.0.20260921121718` |
| Declared Android minimum | Android 8.0 · API 26 |
The APK, engine and product have separate version numbers. The final `.1` in the Preview version identifies its revision; GeckoView reports **156.0** here. The declared Android minimum does not mean every device has been tested.
<a name="architecture"></a>
## ⚙️ Under the hood
```text
Gaia + web applications
HTML · CSS · JS
│
Legacy moz* API compatibility
Versioned Vulpes contracts
│
Settings · Applications · Contacts · Storage
Local messages · Alarms
│
┌─────────────┴─────────────┐
│ │
Desktop host Android host
Privileged window GeckoView + bridge
and separate web views WebExtension/native
│ │
Firefox / Gecko GeckoView
│ │
Linux Android
``` ```
`setup.sh` downloads the Gecko engine selected for this version and verifies its checksum. ### A clear boundary between applications and the system
### Build the APK Applications use services whose permissions are checked by the host. External web pages use separate browsing contexts and do not receive Vulpes’s privileged APIs.
From the repository root: The current desktop port uses the **official Firefox binary**, with a Vulpes host and adapters, without native engine patches. Some interfaces used by this host remain internal to Gecko, making tests essential whenever the engine version changes.
```sh ### Updates checked before activation
cd android
./build-apk.sh ```text
Download → Verify checksum → Test engine interfaces
↓
Keep old engine ← Activate ← Test Gaia and persistence
``` ```
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. Test profiles are separate from user data. The tool keeps the previous engine and its profile to allow rollback. This pipeline currently covers the desktop; Android updates follow the APK build cycle.
## Contribute ### A tested development base
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. Automated checks cover, among other things:
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. - booting the actual Gaia interface and loading application icons;
- languages, settings and persistence across restarts;
- contacts, the dial pad and the SMS composer;
- imported media, alarms and web browsing;
- rejecting access when the origin or permissions do not match.
## Credits and licenses An export of the files planned for publication also booted and passed the desktop tests without the old development directories. Android asset packaging and APK compilation have been verified. This is not exhaustive validation of Firefox OS or every Android phone.
Thanks to Mozilla, Gaia contributors, Capyloon, Droidian, UBports, libhybris, Debian and AOSP for the code, tools and work that have helped this project. <a name="roadmap"></a>
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). ## 🗺️ What’s next
Vulpes OS is an independent project by **OverSu / NNS Production**, with no affiliation with Mozilla. - **Publish a self-contained base:** source code, licenses, documentation and startup instructions.
- **Strengthen compatibility:** continue testing historical applications and improving everyday workflows.
- **Improve the Android Preview:** media access, platform integration and feedback from real devices.
- **Reduce Gecko update maintenance:** keep service contracts stable and limit engine-specific adaptations.
- **Explore a full mobile port:** hardware, modem, drivers and system integration once the foundation is ready.
## 🤝 Get involved
Useful reports include **the device or Linux distribution, the version in use, steps to reproduce the issue and the expected result**. A screenshot and a log excerpt without personal data help a lot.
The project also welcomes work on JavaScript, CSS, Gecko/GeckoView integration, testing, translation and documentation. Contribution guidelines will accompany the source release.
## 📜 Origins and licenses
Vulpes builds on the work of Mozilla and the contributors to **Boot to Gecko / Firefox OS**, including Gaia.
The license selected for new Vulpes files is the **Mozilla Public License 2.0 (`MPL-2.0`)**. Gaia and other inherited components retain their own licenses, including **Apache 2.0** for the Gaia base, along with their attribution notices. License texts and component details will accompany the source release.
---
<p align="center">
<strong>VULPES</strong><br>
Firefox OS Community Project<br><br>
<a href="https://vulpes-os.org">vulpes-os.org</a>
</p>
<p align="center">
<sub>Vulpes is an independent community project and is not affiliated with or endorsed by Mozilla.</sub>
</p>

View file

@ -1,72 +0,0 @@
export class DataStoresIndexedDB {
constructor(factory, keyRange, uuid) {
Object.assign(this, { factory, keyRange, uuid });
}
open() {
return (this.database ||= new Promise((resolve, reject) => {
const request = this.factory.open('vulpes-datastores-v1', 1);
request.onupgradeneeded = () => {
request.result.createObjectStore('values');
request.result.createObjectStore('revisions');
};
request.onerror = () => reject(request.error);
request.onsuccess = () => {
request.result.onversionchange = () => request.result.close();
resolve(request.result);
};
}));
}
async run(key, operation, args) {
const db = await this.open();
return new Promise((resolve, reject) => {
const write = ['add', 'put', 'remove', 'clear'].includes(operation);
const tx = db.transaction(['values', 'revisions'], write ? 'readwrite' : 'readonly');
const store = tx.objectStore('values'),
revisions = tx.objectStore('revisions');
const range = this.keyRange.bound([key], [key, []]);
let value,
revisionId = '',
entries = [];
tx.onabort = () => reject(tx.error || new Error('TRANSACTION_ABORTED'));
tx.onerror = () => {};
tx.oncomplete = () => resolve({ value, revisionId });
if (operation === 'get') {
value = [];
args.forEach((id, index) => {
store.get([key, id]).onsuccess = (event) => {
if (args.length === 1) value = event.target.result;
else value[index] = event.target.result;
};
});
} else if (operation === 'snapshot') {
revisions.get(key).onsuccess = (event) => {
revisionId = event.target.result || '';
};
store.openCursor(range).onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
entries.push({ id: cursor.key[1], data: cursor.value });
cursor.continue();
} else value = { revisionId, entries };
};
} else if (operation === 'length') {
store.count(range).onsuccess = (event) => {
value = event.target.result;
};
} else if (write) {
revisionId = this.uuid();
revisions.put(revisionId, key);
if (operation === 'clear') store.delete(range);
else if (operation === 'remove') {
store.count([key, args[0]]).onsuccess = (event) => {
value = event.target.result > 0;
};
store.delete([key, args[0]]);
} else {
value = args[1] === undefined ? this.uuid() : args[1];
store[operation === 'add' ? 'add' : 'put'](args[0], [key, value]);
}
} else tx.abort();
});
}
}

View file

@ -1,39 +0,0 @@
// Web-standard storage adapter. Schema version is independent of Gecko version.
export class IndexedDBStorage {
constructor(database) {
this.database = database;
}
static open(name = 'vulpes-services-v1') {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, 1);
req.onupgradeneeded = () => req.result.createObjectStore('settings');
req.onsuccess = () => {
const db = req.result;
db.onversionchange = () => db.close();
resolve(new IndexedDBStorage(db));
};
req.onerror = () => reject(req.error);
req.onblocked = () => reject(new Error('DATABASE_BLOCKED'));
});
}
get(key) {
return new Promise((resolve, reject) => {
const tx = this.database.transaction('settings', 'readonly');
const req = tx.objectStore('settings').get(key);
tx.oncomplete = () => resolve(req.result);
tx.onabort = () => reject(tx.error);
});
}
setMany(entries) {
return new Promise((resolve, reject) => {
const tx = this.database.transaction('settings', 'readwrite');
tx.oncomplete = () => resolve();
tx.onabort = () => reject(tx.error);
const store = tx.objectStore('settings');
for (const [key, value] of entries) store.put(value, key);
});
}
close() {
this.database.close();
}
}

9
android/.gitignore vendored
View file

@ -1,9 +0,0 @@
/toolchain/
/.gradle/
/build/
/app/build/
/app/src/main/assets/
/local.properties
/dist/
/keys/
/logs/

View file

@ -1,20 +0,0 @@
plugins { id 'com.android.application' }
def vulpesRelease = new groovy.json.JsonSlurper().parse(rootProject.file('../release.json'))
def productVersion = vulpesRelease.version ?: "${vulpesRelease.gaia}-${vulpesRelease.channel}.${vulpesRelease.revision}"
android {
namespace 'org.vulpes_os.preview'
compileSdk { version = release(37) { minorApiLevel = 1 } }
buildToolsVersion '37.0.0'
defaultConfig {
applicationId 'org.vulpes_os.preview'
minSdk 26
targetSdk 37
versionCode vulpesRelease.androidVersionCode as int
versionName productVersion
ndk { abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86_64' }
}
signingConfigs { debug { storeFile rootProject.file('keys/preview.keystore'); storePassword 'android'; keyAlias 'androiddebugkey'; keyPassword 'android' } }
compileOptions { sourceCompatibility JavaVersion.VERSION_17; targetCompatibility JavaVersion.VERSION_17 }
packaging { jniLibs { useLegacyPackaging = false } }
}
dependencies { implementation "org.mozilla.geckoview:geckoview:${vulpesRelease.geckoview}" }

View file

@ -1,35 +0,0 @@
<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" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<application android:name=".VulpesApplication" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher" android:theme="@android:style/Theme.Material.Light.NoActionBar"
android:allowBackup="false" android:supportsRtl="true" android:usesCleartextTraffic="true">
<activity android:name=".MainActivity" android:exported="true"
android:launchMode="singleTop"
android:configChanges="orientation|screenSize|keyboardHidden|smallestScreenSize|uiMode"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.HOME" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<provider android:name=".CaptureProvider" android:authorities="${applicationId}.capture"
android:exported="false" android:grantUriPermissions="true" />
</application>
</manifest>

View file

@ -1,161 +0,0 @@
package org.vulpes_os.preview;
import android.app.*;
import android.content.Intent;
import android.content.SharedPreferences;
import java.util.HashMap;
import org.json.*;
import org.mozilla.geckoview.*;
final class AndroidNotifications implements WebNotificationDelegate {
private final VulpesApplication app;
private final NotificationManager manager;
private final HashMap<String, WebNotification> live = new HashMap<>();
private final HashMap<String, Integer> ids = new HashMap<>();
private int nextId = 1;
AndroidNotifications(VulpesApplication app) {
this.app = app;
manager = app.getSystemService(NotificationManager.class);
manager.createNotificationChannel(
new NotificationChannel("vulpes", "Vulpes OS", NotificationManager.IMPORTANCE_DEFAULT)
);
}
private String key(WebNotification n) {
return n.origin + "|" + (n.tag == null || n.tag.isEmpty() ? System.identityHashCode(n) : n.tag);
}
public void onShowNotification(WebNotification n) {
if (!manager.areNotificationsEnabled()) {
n.dismiss();
return;
}
String key = key(n);
int id = ids.computeIfAbsent(key, k -> nextId++);
live.put(key, n);
Intent open = new Intent(app, MainActivity.class)
.setAction("org.vulpes_os.preview.NOTIFICATION")
.putExtra("notificationKey", key)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent action = PendingIntent.getActivity(
app,
id,
open,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
Notification notification = new Notification.Builder(app, "vulpes")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(n.title)
.setContentText(n.text)
.setStyle(new Notification.BigTextStyle().bigText(n.text))
.setContentIntent(action)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.build();
try {
manager.notify(id, notification);
n.show();
} catch (SecurityException e) {
live.remove(key);
n.dismiss();
}
}
public void onCloseNotification(WebNotification n) {
String key = key(n);
Integer id = ids.remove(key);
live.remove(key);
if (id != null) manager.cancel(id);
}
Object local(JSONObject p) throws JSONException {
String owner = p.getString("owner"),
operation = p.getString("operation");
SharedPreferences store = app.getSharedPreferences("notifications", 0);
if (operation.equals("query")) return manager.areNotificationsEnabled() ? "granted" : "denied";
if (operation.equals("list")) {
JSONArray rows = new JSONArray();
for (android.service.notification.StatusBarNotification item : manager.getActiveNotifications()) {
String raw = store.getString("local." + item.getId(), null);
if (raw == null) continue;
JSONObject data = new JSONObject(raw);
if (data.optString("owner").equals(owner)) rows.put(data);
}
return rows;
}
if (operation.equals("close")) {
int id = p.getInt("id");
JSONObject data = new JSONObject(store.getString("local." + id, "{}"));
if (!data.optString("owner").equals(owner)) throw new SecurityException("PERMISSION_DENIED");
manager.cancel(id);
store.edit().remove("local." + id).apply();
return null;
}
if (!operation.equals("show")) throw new IllegalArgumentException(
"UNKNOWN_NOTIFICATION_OPERATION"
);
if (!manager.areNotificationsEnabled()) throw new SecurityException("NOTIFICATIONS_DENIED");
String tag = p.optString("tag");
int id = 0;
if (
!tag.isEmpty()
) for (android.service.notification.StatusBarNotification item : manager.getActiveNotifications()) {
JSONObject saved = new JSONObject(store.getString("local." + item.getId(), "{}"));
if (owner.equals(saved.optString("owner")) && tag.equals(saved.optString("tag"))) id =
item.getId();
}
if (id == 0) {
id = store.getInt("next", 10000);
store.edit().putInt("next", id + 1).apply();
}
JSONObject data = new JSONObject(p.toString()).put("id", id);
store.edit().putString("local." + id, data.toString()).apply();
Intent open = new Intent(app, MainActivity.class)
.setAction("org.vulpes_os.preview.LOCAL_NOTIFICATION")
.putExtra("localNotification", id)
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent action = PendingIntent.getActivity(
app,
id,
open,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
);
Notification n = new Notification.Builder(app, "vulpes")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle(p.optString("title", "Vulpes OS"))
.setContentText(p.optString("body"))
.setStyle(new Notification.BigTextStyle().bigText(p.optString("body")))
.setContentIntent(action)
.setAutoCancel(true)
.setOnlyAlertOnce(true)
.build();
manager.notify(id, n);
return id;
}
void click(Intent intent) {
if (intent != null && "org.vulpes_os.preview.LOCAL_NOTIFICATION".equals(intent.getAction())) {
int id = intent.getIntExtra("localNotification", -1);
SharedPreferences store = app.getSharedPreferences("notifications", 0);
String raw = store.getString("local." + id, null);
if (raw != null && app.port != null) try {
JSONObject data = new JSONObject(raw);
app.port.postMessage(new JSONObject().put("type", "notification-click").put("data", data));
store.edit().remove("local." + id).apply();
intent.removeExtra("localNotification");
} catch (Exception e) {
android.util.Log.w("Vulpes", "Notification click", e);
}
return;
}
if (intent == null || !"org.vulpes_os.preview.NOTIFICATION".equals(intent.getAction())) return;
String key = intent.getStringExtra("notificationKey");
WebNotification n = live.remove(key);
Integer id = ids.remove(key);
if (id != null) manager.cancel(id);
if (n != null) n.click();
intent.removeExtra("notificationKey");
}
}

View file

@ -1,172 +0,0 @@
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");
}
}

View file

@ -1,555 +0,0 @@
package org.vulpes_os.preview;
import android.app.*;
import android.content.*;
import android.database.Cursor;
import android.media.AudioManager;
import android.net.Uri;
import android.os.*;
import android.provider.*;
import android.util.Base64;
import android.webkit.MimeTypeMap;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import org.json.*;
import org.mozilla.geckoview.GeckoResult;
/** Android operations reachable only from the bundled extension's authorized RPC. */
final class AndroidServices {
static final int PICK = 410,
CAPTURE = 411,
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;
String pendingType;
AndroidServices(VulpesApplication app) {
this.app = app;
personal = new AndroidPersonalData(app);
}
GeckoResult<Object> request(JSONObject message) {
String op = message.optString("type");
GeckoResult<Object> result = new GeckoResult<>();
try {
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")) {
activity().notificationPermission(granted ->
result.complete(granted ? "granted" : "denied")
);
} else result.complete(app.notifications.local(request));
break;
case "android.settings":
openSettings(p.getString("panel"));
result.complete(null);
break;
case "android.snapshot":
result.complete(snapshot());
break;
case "android.set":
set(p);
result.complete(snapshot());
break;
case "android.pick":
pick(p.getString("type"), result);
break;
case "android.capture":
capture(result);
break;
case "android.alarms":
activity().startActivity(new Intent(AlarmClock.ACTION_SHOW_ALARMS));
result.complete(null);
break;
case "android.contact":
if (pending != null) throw new IllegalStateException("PICKER_BUSY");
activity().startActivityForResult(
new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI),
CONTACT
);
pending = result;
pendingType = "contact";
break;
case "android.sms":
String numbers = p.optString("number"),
text = p.optString("body");
if (
!numbers.matches("[+0-9*# ,;()-]{0,200}") || text.length() > 10000
) throw new IllegalArgumentException("INVALID_MESSAGE");
activity().startActivity(
new Intent(Intent.ACTION_SENDTO, Uri.fromParts("smsto", numbers, null)).putExtra(
"sms_body",
text
)
);
result.complete(null);
break;
case "android.dial":
String number = p.getString("number");
if (!number.matches("[+0-9*# ,;()-]{1,80}")) throw new IllegalArgumentException(
"INVALID_NUMBER"
);
activity().startActivity(
new Intent(Intent.ACTION_DIAL, Uri.fromParts("tel", number, null))
);
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(() -> {
try {
Object value = media(params);
main.post(() -> result.complete(value));
} catch (Exception e) {
fail(result, e);
}
});
break;
default:
throw new IllegalArgumentException("METHOD_NOT_SUPPORTED");
}
} catch (Exception e) {
fail(result, e);
}
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 {
result.complete(
new JSONObject().put(
"error",
e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()
)
);
} catch (JSONException ignored) {}
});
}
private MainActivity activity() {
if (app.activity == null || app.activity.isFinishing()) throw new IllegalStateException(
"ACTIVITY_UNAVAILABLE"
);
return app.activity;
}
void openSettings(String panel) {
Map<String, String> actions = new HashMap<>();
actions.put("wifi", Settings.ACTION_WIFI_SETTINGS);
actions.put("bluetooth", Settings.ACTION_BLUETOOTH_SETTINGS);
actions.put("data", Settings.ACTION_WIRELESS_SETTINGS);
actions.put("airplane-mode", Settings.ACTION_AIRPLANE_MODE_SETTINGS);
actions.put("geolocation", Settings.ACTION_LOCATION_SOURCE_SETTINGS);
actions.put("display", Settings.ACTION_DISPLAY_SETTINGS);
actions.put("sound", Settings.ACTION_SOUND_SETTINGS);
actions.put("dateTime", Settings.ACTION_DATE_SETTINGS);
actions.put("screenLock", Settings.ACTION_SECURITY_SETTINGS);
actions.put("battery", Settings.ACTION_BATTERY_SAVER_SETTINGS);
actions.put("home", Settings.ACTION_HOME_SETTINGS);
actions.put("storage", Settings.ACTION_INTERNAL_STORAGE_SETTINGS);
actions.put("notifications", Settings.ACTION_APP_NOTIFICATION_SETTINGS);
actions.put("apps", Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
actions.put("root", Settings.ACTION_SETTINGS);
if (!actions.containsKey(panel)) throw new IllegalArgumentException("UNKNOWN_PANEL");
Intent intent = new Intent(actions.get(panel));
if (panel.equals("notifications")) intent.putExtra(
Settings.EXTRA_APP_PACKAGE,
app.getPackageName()
);
if (panel.equals("apps")) intent.setData(Uri.parse("package:" + app.getPackageName()));
activity().startActivity(intent);
}
JSONObject snapshot() throws Exception {
AudioManager audio = app.getSystemService(AudioManager.class);
JSONObject data = new JSONObject();
Intent battery = app.registerReceiver(null, new android.content.IntentFilter(Intent.ACTION_BATTERY_CHANGED));
if (battery != null) {
int level = battery.getIntExtra(android.os.BatteryManager.EXTRA_LEVEL, -1);
int scale = battery.getIntExtra(android.os.BatteryManager.EXTRA_SCALE, -1);
int status = battery.getIntExtra(android.os.BatteryManager.EXTRA_STATUS, -1);
if (level >= 0 && scale > 0) data.put("battery", new JSONObject()
.put("level", (double) level / scale)
.put("charging", status == android.os.BatteryManager.BATTERY_STATUS_CHARGING ||
status == android.os.BatteryManager.BATTERY_STATUS_FULL));
}
data.put(
"screen.brightness",
app
.getSharedPreferences("android", 0)
.getFloat(
"brightness",
Settings.System.getInt(app.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 128) /
255f
)
);
data.put(
"audio.volume.content",
Math.round(
(15f * audio.getStreamVolume(AudioManager.STREAM_MUSIC)) /
audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
)
);
data.put("audio.volume.content.max", audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC));
data.put(
"airplaneMode.enabled",
Settings.Global.getInt(app.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0) != 0
);
data.put(
"geolocation.enabled",
app
.getSystemService(android.location.LocationManager.class)
.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER)
);
data.put("wifi.enabled", Settings.Global.getInt(app.getContentResolver(), "wifi_on", 0) != 0);
data.put(
"bluetooth.enabled",
Settings.Global.getInt(app.getContentResolver(), "bluetooth_on", 0) != 0
);
data.put("deviceinfo.product_model", Build.MANUFACTURER + " " + Build.MODEL);
data.put("deviceinfo.hardware", Build.HARDWARE);
data.put("deviceinfo.firmware_revision", "Android " + Build.VERSION.RELEASE);
data.put("deviceinfo.software", "Vulpes OS " + app.getPackageManager().getPackageInfo(app.getPackageName(), 0).versionName);
data.put(
"android.notifications.enabled",
app.getSystemService(NotificationManager.class).areNotificationsEnabled()
);
return data;
}
void set(JSONObject p) throws JSONException {
String key = p.getString("key");
if (key.equals("screen.brightness")) {
float value = (float) p.getDouble("value");
if (!Float.isFinite(value) || value < 0 || value > 1) throw new IllegalArgumentException(
"INVALID_VALUE"
);
android.view.WindowManager.LayoutParams attrs = activity().getWindow().getAttributes();
attrs.screenBrightness = Math.max(.02f, value);
activity().getWindow().setAttributes(attrs);
app.getSharedPreferences("android", 0).edit().putFloat("brightness", value).apply();
} else if (key.equals("audio.volume.content")) {
AudioManager audio = app.getSystemService(AudioManager.class);
int value = p.getInt("value");
if (value < 0 || value > 15) throw new IllegalArgumentException("INVALID_VALUE");
audio.setStreamVolume(
AudioManager.STREAM_MUSIC,
Math.round((value * audio.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) / 15f),
0
);
} else throw new IllegalArgumentException("SETTING_REQUIRES_ANDROID_PANEL");
}
void pick(String type, GeckoResult<Object> result) {
checkType(type);
if (pending != null) throw new IllegalStateException("PICKER_BUSY");
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType(prefix(type) + "*")
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
activity().startActivityForResult(intent, PICK);
pending = result;
pendingType = type;
app
.getSharedPreferences("pending-media", 0)
.edit()
.putString("type", type)
.putInt("code", PICK)
.commit();
}
void capture(GeckoResult<Object> result) throws IOException {
if (pending != null) throw new IllegalStateException("PICKER_BUSY");
Files.deleteIfExists(new File(app.getCacheDir(), "capture.jpg").toPath());
Uri uri = Uri.parse("content://" + app.getPackageName() + ".capture/capture.jpg");
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra(MediaStore.EXTRA_OUTPUT, uri)
.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setClipData(ClipData.newRawUri("Vulpes photo", uri));
activity().startActivityForResult(intent, CAPTURE);
pending = result;
pendingType = "pictures";
app
.getSharedPreferences("pending-media", 0)
.edit()
.putString("type", "pictures")
.putInt("code", CAPTURE)
.commit();
}
void onResult(int code, int status, Intent intent) {
if (code == CONTACT) {
GeckoResult<Object> result = pending;
pending = null;
pendingType = null;
if (result == null) return;
if (status != Activity.RESULT_OK || intent == null || intent.getData() == null) {
fail(result, new IOException("AbortError"));
return;
}
Uri uri = intent.getData();
io.execute(() -> {
try (
Cursor c = app
.getContentResolver()
.query(
uri,
new String[] {
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
},
null,
null,
null
)
) {
if (c == null || !c.moveToFirst()) throw new IOException("NotFoundError");
JSONObject contact = new JSONObject()
.put("name", new JSONArray().put(c.getString(0)))
.put("givenName", new JSONArray().put(c.getString(0)))
.put(
"tel",
new JSONArray().put(
new JSONObject()
.put("value", c.getString(1))
.put("type", new JSONArray().put("mobile"))
)
);
main.post(() -> result.complete(contact));
} catch (Exception e) {
fail(result, e);
}
});
return;
}
if (code != PICK && code != CAPTURE) return;
SharedPreferences saved = app.getSharedPreferences("pending-media", 0);
if (pending == null && saved.getInt("code", 0) != code) return;
GeckoResult<Object> result = pending == null ? new GeckoResult<>() : pending;
String type = pendingType == null ? saved.getString("type", "pictures") : pendingType;
pending = null;
pendingType = null;
saved.edit().clear().commit();
if (status != Activity.RESULT_OK) {
fail(result, new IOException("AbortError"));
return;
}
ArrayList<Uri> uris = new ArrayList<>();
if (code == CAPTURE) uris.add(Uri.fromFile(new File(app.getCacheDir(), "capture.jpg")));
else if (intent != null && intent.getClipData() != null) {
for (int i = 0; i < intent.getClipData().getItemCount(); i++) uris.add(
intent.getClipData().getItemAt(i).getUri()
);
} else if (intent != null && intent.getData() != null) uris.add(intent.getData());
io.execute(() -> {
try {
JSONArray names = new JSONArray();
for (Uri uri : uris) {
String mime = code == CAPTURE ? "image/jpeg" : app.getContentResolver().getType(uri);
String name = code == CAPTURE
? "Photo-" + System.currentTimeMillis() + ".jpg"
: displayName(uri);
if (mime == null) mime = mime(name);
if (!mime.startsWith(prefix(type))) throw new IOException("TypeMismatchError");
name = UUID.randomUUID() + "-" + name.replaceAll("[^a-zA-Z0-9._-]", "_");
File target = path(type, name);
try (
InputStream in = code == CAPTURE
? new FileInputStream(new File(uri.getPath()))
: app.getContentResolver().openInputStream(uri)
) {
copy(in, target);
}
names.put("/sdcard/" + name);
}
main.post(() -> result.complete(names));
} catch (Exception e) {
fail(result, e);
} finally {
if (code == CAPTURE) new File(app.getCacheDir(), "capture.jpg").delete();
}
});
}
String displayName(Uri uri) {
try (
Cursor c = app
.getContentResolver()
.query(uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null)
) {
if (c != null && c.moveToFirst()) return c.getString(0);
} catch (Exception ignored) {}
return "media";
}
static void checkType(String type) {
if (
!Arrays.asList("pictures", "videos", "music").contains(type)
) throw new IllegalArgumentException("INVALID_MEDIA_TYPE");
}
static String prefix(String type) {
checkType(type);
return type.equals("pictures") ? "image/" : type.equals("videos") ? "video/" : "audio/";
}
static String mime(String name) {
String ext = name.substring(name.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
String type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
return type == null ? "application/octet-stream" : type;
}
File path(String type, String name) throws IOException {
checkType(type);
if (name.startsWith("/sdcard/")) name = name.substring(8);
File root = new File(app.getFilesDir(), "media/" + type);
root.mkdirs();
File file = new File(root, name);
if (
!file.getCanonicalPath().startsWith(root.getCanonicalPath() + "/") ||
name.contains("..") ||
name.startsWith("/")
) throw new IOException("SecurityError");
return file;
}
void copy(InputStream in, File file) throws IOException {
if (in == null) throw new IOException("NotFoundError");
file.getParentFile().mkdirs();
File temp = new File(file.getPath() + ".tmp");
try (OutputStream out = new FileOutputStream(temp)) {
byte[] buf = new byte[32768];
int n;
long total = 0;
while ((n = in.read(buf)) != -1) {
total += n;
if (total > MAX_MEDIA) throw new IOException("MEDIA_TOO_LARGE_32_MIB");
out.write(buf, 0, n);
}
Files.move(temp.toPath(), file.toPath());
} finally {
temp.delete();
}
}
JSONObject info(File root, File f) throws JSONException {
return new JSONObject()
.put("name", "/sdcard/" + root.toPath().relativize(f.toPath()).toString())
.put("size", f.length())
.put("lastModified", f.lastModified())
.put("type", mime(f.getName()));
}
Object media(JSONObject p) throws Exception {
String type = p.getString("type"),
op = p.getString("operation");
checkType(type);
File root = path(type, "dummy").getParentFile();
if (op.equals("available")) return "available";
if (op.equals("freeSpace")) return root.getUsableSpace();
if (op.equals("enumerate") || op.equals("usedSpace")) {
JSONArray rows = new JSONArray();
long used = 0;
try (java.util.stream.Stream<Path> paths = Files.walk(root.toPath())) {
for (Path path : (Iterable<Path>) paths.filter(Files::isRegularFile)::iterator) {
File f = path.toFile();
if (f.getName().endsWith(".tmp")) continue;
used += f.length();
String name = root.toPath().relativize(path).toString();
if (
name.startsWith(p.optString("name", "").replaceFirst("^/sdcard/", "")) &&
f.lastModified() >= p.optLong("since", 0)
) rows.put(info(root, f));
}
}
return op.equals("usedSpace") ? used : rows;
}
File f = path(type, p.getString("name"));
if (op.equals("get")) {
if (!f.isFile()) throw new IOException("NotFoundError");
if (f.length() > MAX_MEDIA) throw new IOException("MEDIA_TOO_LARGE_32_MIB");
return info(root, f).put(
"blob",
new JSONObject()
.put("__vulpesBlobV1", true)
.put("type", mime(f.getName()))
.put("base64", Base64.encodeToString(Files.readAllBytes(f.toPath()), Base64.NO_WRAP))
);
}
if (op.equals("delete")) {
if (!f.isFile() || !f.delete()) throw new IOException("NotFoundError");
return "/sdcard/" + root.toPath().relativize(f.toPath());
}
if (op.equals("add")) {
JSONObject blob = p.getJSONObject("blob");
if (
!blob.optBoolean("__vulpesBlobV1") || !blob.getString("type").startsWith(prefix(type))
) throw new IOException("TypeMismatchError");
if (f.exists()) throw new IOException("NoModificationAllowedError");
String encoded = blob.getString("base64");
if (encoded.length() > (MAX_MEDIA * 4) / 3 + 4) throw new IOException(
"MEDIA_TOO_LARGE_32_MIB"
);
copy(new ByteArrayInputStream(Base64.decode(encoded, Base64.DEFAULT)), f);
return "/sdcard/" + root.toPath().relativize(f.toPath());
}
throw new IllegalArgumentException("NotSupportedError");
}
}

View file

@ -1,61 +0,0 @@
package org.vulpes_os.preview;
import android.content.*;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import java.io.*;
/** Grants the selected camera access to one temporary output, never the profile. */
public final class CaptureProvider extends ContentProvider {
public boolean onCreate() {
return true;
}
private File file(Uri uri) throws FileNotFoundException {
if (!"/capture.jpg".equals(uri.getPath())) throw new FileNotFoundException();
return new File(getContext().getCacheDir(), "capture.jpg");
}
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
return ParcelFileDescriptor.open(file(uri), ParcelFileDescriptor.parseMode(mode));
}
public String getType(Uri uri) {
return "image/jpeg";
}
public Cursor query(Uri uri, String[] columns, String selection, String[] args, String order) {
try {
File f = file(uri);
if (columns == null) columns = new String[] {
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE,
};
MatrixCursor cursor = new MatrixCursor(columns);
Object[] row = new Object[columns.length];
for (int i = 0; i < columns.length; i++) row[i] = OpenableColumns.SIZE.equals(columns[i])
? f.length()
: f.getName();
cursor.addRow(row);
return cursor;
} catch (IOException e) {
return null;
}
}
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException();
}
public int delete(Uri uri, String selection, String[] args) {
throw new UnsupportedOperationException();
}
public int update(Uri uri, ContentValues values, String selection, String[] args) {
throw new UnsupportedOperationException();
}
}

View file

@ -1,173 +0,0 @@
package org.vulpes_os.preview;
import android.content.Context;
import android.webkit.MimeTypeMap;
import java.io.*;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.zip.*;
/** Read-only APK assets, loopback only. Never serves profile data or native RPC. */
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");
try (
InputStream in = context.getAssets().open("gaia.zip");
OutputStream out = new FileOutputStream(temp)
) {
copy(in, out);
}
Files.move(temp.toPath(), archive.toPath(), StandardCopyOption.REPLACE_EXISTING);
zip = new ZipFile(archive);
socket = new ServerSocket();
socket.setReuseAddress(true);
socket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), PORT));
Thread accept = new Thread(
() -> {
while (!socket.isClosed()) try {
Socket client = socket.accept();
workers.submit(() -> serve(client));
} catch (IOException e) {
if (!socket.isClosed()) android.util.Log.e("Vulpes", "HTTP accept", e);
}
},
"Vulpes-assets"
);
accept.setDaemon(true);
accept.start();
}
private static void copy(InputStream in, OutputStream out) throws IOException {
byte[] buf = new byte[32768];
int n;
while ((n = in.read(buf)) != -1) out.write(buf, 0, n);
}
private static String line(InputStream in) throws IOException {
ByteArrayOutputStream b = new ByteArrayOutputStream();
int c;
while ((c = in.read()) != -1 && c != '\n') {
if (b.size() > 8192) throw new IOException("Header too long");
if (c != '\r') b.write(c);
}
return b.toString("UTF-8");
}
private void serve(Socket client) {
try (Socket c = client) {
c.setSoTimeout(5000);
InputStream in = c.getInputStream();
OutputStream out = c.getOutputStream();
String[] start = line(in).split(" ");
String host = "",
origin = "";
int total = 0;
for (String h; (h = line(in)).length() > 0; ) {
total += h.length();
if (total > 32768) throw new IOException("Headers too large");
int i = h.indexOf(':');
if (i > 0) {
String key = h.substring(0, i);
String value = h.substring(i + 1).trim();
if (key.equalsIgnoreCase("Host")) host = value.toLowerCase(Locale.ROOT);
if (key.equalsIgnoreCase("Origin")) origin = value;
}
}
if (
start.length < 2 ||
!start[0].equals("GET") ||
!host.matches("[a-z0-9_-]+\\.localhost:" + PORT)
) {
error(out, 403);
return;
}
String app = host.substring(0, host.indexOf('.'));
if (app.equals("theme")) app = "default_theme";
if (!InstalledApps.valid(app) && zip.getEntry(app + "/manifest.webapp") == null) {
error(out, 404);
return;
}
String path = URLDecoder.decode(start[1].split("\\?", 2)[0].replace("+", "%2B"), "UTF-8");
if (
!path.startsWith("/") ||
path.contains("\\") ||
path.indexOf('\0') >= 0 ||
Arrays.asList(path.split("/")).contains("..")
) {
error(out, 400);
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(
"_shared/" + path.substring(8)
);
if (entry == null || entry.isDirectory()) {
error(out, 404);
return;
}
String ext = path.substring(path.lastIndexOf('.') + 1).toLowerCase(Locale.ROOT);
String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
if (ext.equals("js") || ext.equals("mjs")) mime = "text/javascript";
if (ext.equals("webapp") || ext.equals("json")) mime = "application/json";
if (ext.equals("properties")) mime = "text/plain";
if (mime == null) mime = "application/octet-stream";
String headers =
"HTTP/1.1 200 OK\r\nContent-Type: " +
mime +
"\r\nContent-Length: " +
entry.getSize() +
"\r\nConnection: close\r\nX-Content-Type-Options: nosniff\r\nCache-Control: no-cache\r\nContent-Security-Policy: script-src 'self' 'unsafe-eval'; object-src 'none'; base-uri 'self'; worker-src 'self' blob:; frame-ancestors http://*.localhost:" +
PORT +
"\r\n";
if (
origin.matches("http://[a-z0-9_-]+\\.localhost:" + PORT) &&
zip.getEntry(new URI(origin).getHost().split("\\.")[0] + "/manifest.webapp") != null
) headers += "Access-Control-Allow-Origin: " + origin + "\r\nVary: Origin\r\n";
out.write((headers + "\r\n").getBytes(StandardCharsets.US_ASCII));
try (InputStream body = zip.getInputStream(entry)) {
copy(body, out);
}
} catch (Exception e) {
android.util.Log.d("Vulpes", "HTTP closed: " + e.getMessage());
}
}
private static void error(OutputStream out, int code) throws IOException {
out.write(
("HTTP/1.1 " + code + " Error\r\nContent-Length: 0\r\nConnection: close\r\n\r\n").getBytes(
StandardCharsets.US_ASCII
)
);
}
public void close() throws IOException {
socket.close();
workers.shutdownNow();
zip.close();
}
}

View file

@ -1,37 +0,0 @@
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);
}
}

View file

@ -1,680 +0,0 @@
package org.vulpes_os.preview;
import android.app.*;
import android.content.*;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.*;
import android.text.InputType;
import android.view.*;
import android.widget.*;
import java.net.URI;
import java.util.ArrayList;
import org.mozilla.geckoview.*;
public final class MainActivity extends Activity {
private VulpesApplication app;
private GeckoSession gaia, web;
private GeckoView gaiaView, webView;
private LinearLayout root, webPanel;
private TextView notice;
private EditText address;
private Button home;
private boolean webBack;
private boolean resumed, recoverGaia, recoverWeb;
private long lastCrash;
private int crashCount;
private final java.util.Map<Integer, java.util.function.Consumer<Boolean>> permissionResults =
new java.util.HashMap<>();
private final java.util.List<java.util.function.Consumer<Boolean>> resumedCameraPermissions =
new java.util.ArrayList<>();
private int nextPermission = 500;
private GeckoResult<GeckoSession.PromptDelegate.PromptResponse> fileResult;
private GeckoSession.PromptDelegate.FilePrompt filePrompt;
@Override
public void onCreate(Bundle state) {
super.onCreate(state);
app = (VulpesApplication) getApplication();
app.activity = this;
root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setBackgroundColor(Color.rgb(18, 35, 50));
root.setOnApplyWindowInsetsListener((v, insets) -> {
if (Build.VERSION.SDK_INT >= 30) {
android.graphics.Insets bars = insets.getInsets(
WindowInsets.Type.ime() |
WindowInsets.Type.displayCutout()
);
v.setPadding(bars.left, bars.top, bars.right, bars.bottom);
} else v.setPadding(
insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
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) {
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.screenBrightness = Math.max(.02f, brightness);
getWindow().setAttributes(attrs);
}
notice = new TextView(this);
notice.setText("VULPES\nChargement de Firefox OS…");
notice.setTextColor(Color.WHITE);
notice.setGravity(Gravity.CENTER);
root.addView(notice, new LinearLayout.LayoutParams(-1, dp(90)));
gaiaView = new GeckoView(this);
root.addView(gaiaView, new LinearLayout.LayoutParams(-1, 0, 1));
webPanel = new LinearLayout(this);
webPanel.setOrientation(LinearLayout.VERTICAL);
webPanel.setBackgroundColor(Color.WHITE);
webPanel.setVisibility(View.GONE);
root.addView(webPanel, new LinearLayout.LayoutParams(-1, 0, 1));
LinearLayout nav = new LinearLayout(this);
webPanel.addView(nav, new LinearLayout.LayoutParams(-1, dp(52)));
Button back = new Button(this);
back.setText("‹");
back.setContentDescription("Retour");
back.setOnClickListener(v -> back());
nav.addView(back, new LinearLayout.LayoutParams(dp(55), -1));
address = new EditText(this);
address.setSingleLine(true);
address.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
address.setTextSize(14);
nav.addView(address, new LinearLayout.LayoutParams(0, -1, 1));
address.setOnEditorActionListener((v, id, event) -> {
String url = address.getText().toString().trim();
openBrowser(url.contains("://") ? url : "https://" + url);
((android.view.inputmethod.InputMethodManager) getSystemService(
INPUT_METHOD_SERVICE
)).hideSoftInputFromWindow(address.getWindowToken(), 0);
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("◯");
home.setTextColor(Color.WHITE);
home.setBackgroundColor(Color.rgb(18, 35, 50));
home.setContentDescription("Accueil Vulpes. Appui long : applications ouvertes.");
root.addView(home, new LinearLayout.LayoutParams(-1, dp(44)));
home.setOnClickListener(v -> {
closeBrowser();
app.home(false);
});
home.setOnLongClickListener(v -> {
closeBrowser();
app.home(true);
return true;
});
setHomeSurface(app.homeVisible);
if (Build.VERSION.SDK_INT >= 33) getOnBackInvokedDispatcher().registerOnBackInvokedCallback(
0,
this::back
);
try {
app.prepare();
boolean retained = app.gaiaSession != null;
gaia = retained
? app.gaiaSession
: new GeckoSession(
new GeckoSessionSettings.Builder()
.displayMode(GeckoSessionSettings.DISPLAY_MODE_STANDALONE)
.build()
);
app.gaiaSession = gaia;
gaia.setContentDelegate(content);
gaia.setPromptDelegate(prompts);
gaia.setPermissionDelegate(permissions);
gaia.setNavigationDelegate(
new GeckoSession.NavigationDelegate() {
@Override
public GeckoResult<AllowOrDeny> onLoadRequest(GeckoSession s, LoadRequest r) {
if (r.uri.startsWith("http://") && isGaia(r.uri)) return GeckoResult.fromValue(
AllowOrDeny.ALLOW
);
if (r.uri.equals("about:blank")) return GeckoResult.fromValue(AllowOrDeny.ALLOW);
openBrowser(r.uri);
return GeckoResult.fromValue(AllowOrDeny.DENY);
}
}
);
if (!gaia.isOpen()) gaia.open(app.runtime);
gaiaView.setSession(gaia);
app.extension.accept(
ext -> {
notice.setVisibility(View.GONE);
if (isDestroyed()) return;
if (!retained) gaia.loadUri(gaiaUrl());
app.notifications.click(getIntent());
},
e -> {
notice.setText("Impossible de démarrer Vulpes : " + e.getMessage());
}
);
} catch (Exception e) {
notice.setText("Impossible de démarrer Vulpes : " + e.getMessage());
android.util.Log.e("Vulpes", "Startup", e);
}
}
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);
}
private boolean isGaia(String url) {
try {
URI u = new URI(url);
return (
u.getHost() != null && u.getHost().endsWith(".localhost") && u.getPort() == GaiaServer.PORT
);
} catch (Exception e) {
return false;
}
}
void openBrowser(String url) {
try {
URI uri = new URI(url);
if (
!("https".equals(uri.getScheme()) || "http".equals(uri.getScheme())) || isGaia(url)
) return;
} catch (Exception e) {
return;
}
if (web == null) {
web = new GeckoSession();
web.setContentDelegate(content);
web.setPromptDelegate(prompts);
web.setPermissionDelegate(permissions);
web.setNavigationDelegate(
new GeckoSession.NavigationDelegate() {
@Override
public void onCanGoBack(GeckoSession s, boolean can) {
webBack = can;
}
@Override
public void onLocationChange(
GeckoSession s,
String url,
java.util.List<GeckoSession.PermissionDelegate.ContentPermission> permissions,
Boolean gesture
) {
address.setText(url);
}
@Override
public GeckoResult<AllowOrDeny> onLoadRequest(GeckoSession s, LoadRequest r) {
return GeckoResult.fromValue(
(r.uri.startsWith("https://") || r.uri.startsWith("http://")) && !isGaia(r.uri)
? AllowOrDeny.ALLOW
: AllowOrDeny.DENY
);
}
@Override
public GeckoResult<GeckoSession> onNewSession(GeckoSession s, String uri) {
openBrowser(uri);
return GeckoResult.fromValue(null);
}
}
);
web.open(app.runtime);
webView.setSession(web);
}
gaiaView.setVisibility(View.GONE);
webPanel.setVisibility(View.VISIBLE);
setHomeSurface(false);
address.setText(url);
web.loadUri(url);
syncActive();
}
void setHomeSurface(boolean visible) {
runOnUiThread(() -> {
boolean overlay = visible && webPanel.getVisibility() != View.VISIBLE;
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) home.getLayoutParams();
boolean landscape = getResources().getConfiguration().orientation ==
android.content.res.Configuration.ORIENTATION_LANDSCAPE;
root.setOrientation(landscape ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
gaiaView.setLayoutParams(landscape ? new LinearLayout.LayoutParams(0, -1, 1) :
new LinearLayout.LayoutParams(-1, 0, 1));
webPanel.setLayoutParams(landscape ? new LinearLayout.LayoutParams(0, -1, 1) :
new LinearLayout.LayoutParams(-1, 0, 1));
params.width = landscape ? dp(44) : -1;
params.height = landscape ? -1 : dp(44);
// Only Home overlays Gaia wallpaper. Apps keep their full usable area.
params.topMargin = !landscape && overlay ? -dp(44) : 0;
params.leftMargin = landscape && overlay ? -dp(44) : 0;
home.setLayoutParams(params);
if (overlay && app.homeAtBottom) home.setBackgroundColor(Color.TRANSPARENT);
else if (overlay) home.setBackground(
new android.graphics.drawable.GradientDrawable(
landscape ? android.graphics.drawable.GradientDrawable.Orientation.RIGHT_LEFT :
android.graphics.drawable.GradientDrawable.Orientation.BOTTOM_TOP,
new int[] { Color.argb(204, 0, 0, 0), Color.TRANSPARENT }
)
);
else home.setBackgroundColor(Color.rgb(18, 35, 50));
});
}
@Override
public void onConfigurationChanged(android.content.res.Configuration config) {
super.onConfigurationChanged(config);
setHomeSurface(app.homeVisible);
}
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() {
if (webPanel.getVisibility() == View.VISIBLE) {
if (webBack) web.goBack();
else closeBrowser();
} else app.home(false);
}
@Override
public void onBackPressed() {
back();
}
private String gaiaUrl() {
return "http://system.localhost:" + GaiaServer.PORT + "/index.html";
}
private void syncActive() {
boolean browsing = webPanel != null && webPanel.getVisibility() == View.VISIBLE;
if (gaia != null && gaia.isOpen()) gaia.setActive(resumed && !browsing);
if (web != null && web.isOpen()) web.setActive(resumed && browsing);
}
private void recover(GeckoSession session, boolean crashed) {
if (isDestroyed()) return;
if (crashed) {
long now = SystemClock.elapsedRealtime();
crashCount = now - lastCrash < 30000 ? crashCount + 1 : 1;
lastCrash = now;
if (crashCount > 2) {
notice.setText("Vulpes s’est arrêté. Touchez ici pour réessayer.");
notice.setVisibility(View.VISIBLE);
notice.setOnClickListener(v -> {
crashCount = 0;
recover(session, false);
});
return;
}
}
if (!resumed) {
if (session == gaia) recoverGaia = true;
else recoverWeb = true;
return;
}
try {
if (!session.isOpen()) session.open(app.runtime);
session.loadUri(session == gaia ? gaiaUrl() : address.getText().toString());
syncActive();
android.util.Log.i(
"Vulpes",
"Recovered " + (session == gaia ? "Gaia" : "browser") + " session"
);
} catch (Exception e) {
notice.setText("Reprise impossible : " + e.getMessage());
notice.setVisibility(View.VISIBLE);
}
}
private final GeckoSession.ContentDelegate content = new GeckoSession.ContentDelegate() {
public void onCrash(GeckoSession session) {
recover(session, true);
}
public void onKill(GeckoSession session) {
recover(session, false);
}
public void onFirstComposite(GeckoSession session) {
if (session == gaia) notice.setVisibility(View.GONE);
}
};
@Override
public void onResume() {
super.onResume();
resumed = true;
immersive();
app.activity = this;
if (recoverGaia) {
recoverGaia = false;
recover(gaia, false);
}
if (recoverWeb && web != null) {
recoverWeb = false;
recover(web, false);
}
syncActive();
// Android delivers permission results before onResume. Start the sensor only
// after GeckoView has been made active again.
java.util.List<java.util.function.Consumer<Boolean>> grants = new java.util.ArrayList<>(
resumedCameraPermissions
);
resumedCameraPermissions.clear();
for (java.util.function.Consumer<Boolean> grant : grants) grant.accept(true);
gaiaView.requestLayout();
app.resumed();
}
@Override
public void onPause() {
resumed = false;
syncActive();
super.onPause();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
if (intent.hasCategory(Intent.CATEGORY_HOME)) { closeBrowser(); app.home(false); }
if ("org.vulpes_os.preview.LOCAL_NOTIFICATION".equals(intent.getAction())) closeBrowser();
if (app.notifications != null) app.notifications.click(intent);
}
@Override
public void onDestroy() {
gaiaView.releaseSession();
webView.releaseSession();
if (gaia != null) {
gaia.setContentDelegate(null);
gaia.setNavigationDelegate(null);
gaia.setPromptDelegate(null);
gaia.setPermissionDelegate(null);
if (isFinishing()) {
gaia.close();
app.gaiaSession = null;
}
}
if (web != null) web.close();
if (fileResult != null) {
fileResult.complete(filePrompt.dismiss());
fileResult = null;
}
for (java.util.function.Consumer<Boolean> callback : permissionResults.values())
callback.accept(false);
permissionResults.clear();
for (java.util.function.Consumer<Boolean> grant : resumedCameraPermissions) grant.accept(false);
resumedCameraPermissions.clear();
if (app.activity == this) app.activity = null;
super.onDestroy();
}
void notificationPermission(java.util.function.Consumer<Boolean> callback) {
if (!resumed) {
callback.accept(false);
return;
}
permission(
Build.VERSION.SDK_INT >= 33
? new String[] { android.Manifest.permission.POST_NOTIFICATIONS }
: new String[0],
callback
);
}
private void finishPermission(
String[] names,
boolean granted,
java.util.function.Consumer<Boolean> callback
) {
if (
granted &&
!resumed &&
java.util.Arrays.asList(names).contains(android.Manifest.permission.CAMERA)
) resumedCameraPermissions.add(callback);
else callback.accept(granted);
}
void permission(String[] names, java.util.function.Consumer<Boolean> callback) {
boolean granted = true;
for (String name : names)
granted &= checkSelfPermission(name) == PackageManager.PERMISSION_GRANTED;
if (granted) {
finishPermission(names, true, callback);
return;
}
int code = nextPermission++;
permissionResults.put(code, callback);
requestPermissions(names, code);
}
@Override
public void onRequestPermissionsResult(int code, String[] names, int[] grants) {
super.onRequestPermissionsResult(code, names, grants);
java.util.function.Consumer<Boolean> callback = permissionResults.remove(code);
if (callback != null) {
boolean granted = grants.length > 0;
// Approximate location is a valid choice; do not require fine location as well.
if (
java.util.Arrays.asList(names).contains(android.Manifest.permission.ACCESS_COARSE_LOCATION)
) granted =
checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED;
else for (int grant : grants) granted &= grant == PackageManager.PERMISSION_GRANTED;
finishPermission(names, granted, callback);
}
}
@Override
protected void onActivityResult(int code, int status, Intent intent) {
super.onActivityResult(code, status, intent);
if (code == 420 && fileResult != null) {
GeckoResult<GeckoSession.PromptDelegate.PromptResponse> result = fileResult;
fileResult = null;
ArrayList<Uri> uris = new ArrayList<>();
if (status == RESULT_OK && intent != null) {
if (intent.getClipData() != null) for (
int i = 0;
i < intent.getClipData().getItemCount();
i++
) uris.add(intent.getClipData().getItemAt(i).getUri());
else if (intent.getData() != null) uris.add(intent.getData());
}
result.complete(
uris.isEmpty() ? filePrompt.dismiss() : filePrompt.confirm(this, uris.toArray(new Uri[0]))
);
filePrompt = null;
} else if (app.services != null) app.services.onResult(code, status, intent);
}
private final GeckoSession.PermissionDelegate permissions =
new GeckoSession.PermissionDelegate() {
@Override
public void onAndroidPermissionsRequest(
GeckoSession session,
String[] names,
Callback callback
) {
permission(names, granted -> {
if (granted) callback.grant();
else callback.reject();
});
}
@Override
public void onMediaPermissionRequest(
GeckoSession session,
String uri,
MediaSource[] video,
MediaSource[] audio,
MediaCallback callback
) {
Uri origin = Uri.parse(uri);
boolean camera =
session == gaia &&
"http".equals(origin.getScheme()) &&
"camera.localhost".equals(origin.getHost()) &&
origin.getPort() == 18765;
boolean hasVideo = video != null && video.length > 0;
boolean hasAudio = audio != null && audio.length > 0;
if (!camera || (!hasVideo && !hasAudio)) {
callback.reject();
return;
}
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();
});
}
@Override
public GeckoResult<Integer> onContentPermissionRequest(
GeckoSession session,
ContentPermission permission
) {
boolean local = session == gaia && isGaia(permission.uri);
if (
permission.permission == PERMISSION_DESKTOP_NOTIFICATION ||
permission.permission == PERMISSION_GEOLOCATION
) {
GeckoResult<Integer> result = new GeckoResult<>();
Runnable ask = () -> {
String[] names = permission.permission == PERMISSION_GEOLOCATION
? new String[] {
android.Manifest.permission.ACCESS_COARSE_LOCATION,
android.Manifest.permission.ACCESS_FINE_LOCATION,
}
: Build.VERSION.SDK_INT >= 33
? new String[] { android.Manifest.permission.POST_NOTIFICATIONS }
: new String[0];
permission(names, granted ->
result.complete(
granted ? ContentPermission.VALUE_ALLOW : ContentPermission.VALUE_DENY
)
);
};
new AlertDialog.Builder(MainActivity.this)
.setTitle("Autorisation Vulpes")
.setMessage(
permission.uri +
(permission.permission == PERMISSION_GEOLOCATION
? "\nAccéder à votre position ?"
: "\nAfficher des notifications Android ?")
)
.setPositiveButton("Autoriser", (d, w) -> ask.run())
.setNegativeButton("Refuser", (d, w) -> result.complete(ContentPermission.VALUE_DENY))
.setOnCancelListener(d -> result.complete(ContentPermission.VALUE_DENY))
.show();
return result;
}
boolean allow =
local &&
(permission.permission == PERMISSION_PERSISTENT_STORAGE ||
permission.permission == PERMISSION_AUTOPLAY_INAUDIBLE ||
permission.permission == PERMISSION_AUTOPLAY_AUDIBLE);
return GeckoResult.fromValue(
allow ? ContentPermission.VALUE_ALLOW : ContentPermission.VALUE_DENY
);
}
};
private final GeckoSession.PromptDelegate prompts = new GeckoSession.PromptDelegate() {
@Override
public GeckoResult<PromptResponse> onFilePrompt(GeckoSession session, FilePrompt prompt) {
if (fileResult != null) return GeckoResult.fromValue(prompt.dismiss());
GeckoResult<PromptResponse> result = new GeckoResult<>();
Intent pick = new Intent(Intent.ACTION_OPEN_DOCUMENT)
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("*/*")
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, prompt.type == FilePrompt.Type.MULTIPLE);
if (prompt.mimeTypes != null && prompt.mimeTypes.length > 0) pick.putExtra(
Intent.EXTRA_MIME_TYPES,
prompt.mimeTypes
);
try {
startActivityForResult(pick, 420);
fileResult = result;
filePrompt = prompt;
} catch (ActivityNotFoundException e) {
result.complete(prompt.dismiss());
}
return result;
}
@Override
public GeckoResult<PromptResponse> onAlertPrompt(GeckoSession s, AlertPrompt p) {
GeckoResult<PromptResponse> result = new GeckoResult<>();
new AlertDialog.Builder(MainActivity.this)
.setTitle(p.title)
.setMessage(p.message)
.setPositiveButton("OK", (d, w) -> result.complete(p.dismiss()))
.setOnCancelListener(d -> result.complete(p.dismiss()))
.show();
return result;
}
@Override
public GeckoResult<PromptResponse> onChoicePrompt(GeckoSession s, ChoicePrompt p) {
ArrayList<ChoicePrompt.Choice> choices = new ArrayList<>();
flatten(p.choices, choices);
String[] labels = new String[choices.size()];
for (int i = 0; i < labels.length; i++) labels[i] = choices.get(i).label;
GeckoResult<PromptResponse> result = new GeckoResult<>();
new AlertDialog.Builder(MainActivity.this)
.setTitle(p.title)
.setItems(labels, (d, n) -> result.complete(p.confirm(choices.get(n))))
.setOnCancelListener(d -> result.complete(p.dismiss()))
.show();
return result;
}
private void flatten(ChoicePrompt.Choice[] items, ArrayList<ChoicePrompt.Choice> out) {
for (ChoicePrompt.Choice c : items) {
if (c.items != null) flatten(c.items, out);
else if (!c.disabled && !c.separator) out.add(c);
}
}
};
}

View file

@ -1,181 +0,0 @@
package org.vulpes_os.preview;
import android.app.Application;
import android.util.Log;
import java.io.*;
import java.nio.charset.StandardCharsets;
import org.json.JSONObject;
import org.mozilla.geckoview.*;
public final class VulpesApplication extends Application {
GeckoRuntime runtime;
GaiaServer server;
WebExtension.Port port;
MainActivity activity;
GeckoResult<WebExtension> extension;
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);
File config = new File(getFilesDir(), "geckoview.yaml");
try (FileOutputStream out = new FileOutputStream(config)) {
out.write(
("prefs:\n datareporting.policy.dataSubmissionEnabled: false\n toolkit.telemetry.enabled: false\n browser.tabs.warnOnClose: false\n" +
(android.os.Build.HARDWARE.equals("ranchu")
? " gfx.webrender.software: true\n"
: "")).getBytes(StandardCharsets.UTF_8)
);
}
runtime = GeckoRuntime.create(
this,
new GeckoRuntimeSettings.Builder()
.configFilePath(config.getAbsolutePath())
.consoleOutput(true)
.remoteDebuggingEnabled(
(getApplicationInfo().flags & android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
)
.locales(new String[] { "fr", "en-US" })
.automaticFontSizeAdjustment(false)
.fontInflation(false)
.build()
);
services = new AndroidServices(this);
notifications = new AndroidNotifications(this);
runtime.setWebNotificationDelegate(notifications);
extension = runtime
.getWebExtensionController()
.ensureBuiltIn("resource://android/assets/bridge/", "android-host@vulpes-os.org");
extension.accept(
ext ->
ext.setMessageDelegate(
new WebExtension.MessageDelegate() {
@Override
public void onConnect(WebExtension.Port p) {
port = p;
sendScreenLock();
if (activity != null) notifications.click(activity.getIntent());
p.setDelegate(
new WebExtension.PortDelegate() {
@Override
public void onDisconnect(WebExtension.Port p) {
if (port == p) port = null;
}
}
);
}
@Override
public GeckoResult<Object> onMessage(
String app,
Object message,
WebExtension.MessageSender sender
) {
if (message instanceof JSONObject) {
if (sender.environmentType != WebExtension.MessageSender.ENV_TYPE_EXTENSION) {
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);
return GeckoResult.fromValue(null);
}
if (json.optString("type").equals("navigation")) {
homeVisible = json.optBoolean("home");
if (activity != null) activity.setHomeSurface(homeVisible);
return GeckoResult.fromValue(null);
}
if (json.optString("type").startsWith("android.")) return services
.request(json)
.map(value -> {
try {
return new JSONObject()
.put("result", value == null ? JSONObject.NULL : value)
.toString();
} catch (org.json.JSONException e) {
throw new IllegalArgumentException(e);
}
});
if (json.optString("type").equals("open") && activity != null) activity.openBrowser(
json.optString("url")
);
}
return GeckoResult.fromValue(null);
}
},
"vulpes"
),
error -> Log.e("Vulpes", "Extension failed", error)
);
}
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 {
port.postMessage(new JSONObject().put("type", held ? "holdhome" : "home"));
} catch (Exception e) {
Log.e("Vulpes", "Home", e);
}
}
void resumed() {
if (services != null) services.personal.observe();
sendScreenLock();
if (port == null) return;
try {
port.postMessage(new JSONObject().put("type", "android-resume"));
} catch (Exception e) {
Log.w("Vulpes", "Resume notification", e);
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View file

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/launcher_background" />
<foreground>
<inset android:inset="20%">
<bitmap android:src="@drawable/vulpes_logo" android:gravity="fill" android:filter="true" />
</inset>
</foreground>
</adaptive-icon>

View file

@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Vulpes OS - Preview</string>
<color name="launcher_background">#10243A</color>
</resources>

View file

@ -1,26 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd -- "$(dirname -- "${BASH_SOURCE[0]}")"
python3 tools/prepare-toolchain.py
export JAVA_HOME="$PWD/toolchain/jdk-21.0.12.1+1"
export ANDROID_HOME="$PWD/toolchain/sdk"
export GRADLE_USER_HOME="$PWD/toolchain/gradle-cache"
export PATH="$JAVA_HOME/bin:$PATH"
mkdir -p keys
if [[ ! -f keys/preview.keystore ]]; then
keytool -genkeypair -keystore keys/preview.keystore -storepass android -keypass android -alias androiddebugkey -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Vulpes Preview,O=Vulpes,C=FR"
chmod 600 keys/preview.keystore
fi
python3 tools/pack-assets.py "$@"
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
./toolchain/gradle-9.5.0/bin/gradle --no-daemon :app:assembleDebug
mkdir -p dist
vulpes_version=$(python3 -c 'import sys; sys.path.insert(0,"../tools"); from project import release_info; print(release_info()["version"])')
vulpes_apk="dist/vulpes-os-preview-${vulpes_version}.apk"
"$ANDROID_HOME/build-tools/37.0.0/zipalign" -f -P 16 4 app/build/outputs/apk/debug/app-debug.apk "$vulpes_apk"
"$ANDROID_HOME/build-tools/37.0.0/apksigner" sign --ks keys/preview.keystore --ks-pass pass:android --key-pass pass:android --ks-key-alias androiddebugkey "$vulpes_apk"
"$ANDROID_HOME/build-tools/37.0.0/apksigner" verify --verbose --print-certs "$vulpes_apk" | tee "dist/signature-${vulpes_version}.txt"
"$ANDROID_HOME/build-tools/37.0.0/zipalign" -c -P 16 -v 4 "$vulpes_apk" > dist/alignment.txt
(cd dist && sha256sum "vulpes-os-preview-${vulpes_version}.apk" > SHA256SUMS)
python3 ../tools/check-parity.py
printf '\nAPK : %s/%s\n' "$PWD" "$vulpes_apk"

View file

@ -1 +0,0 @@
plugins { id 'com.android.application' version '9.3.3' apply false }

View file

@ -1 +0,0 @@
<!doctype html><meta charset="utf-8"><script type="module" src="background.js"></script>

View file

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

View file

@ -1,147 +0,0 @@
/* Extension world: the browser provides origin/tab/frame identity, never Gaia. */
(() => {
if (location.port !== '18765' || location.hostname.startsWith('user-')) return;
const win = window.wrappedJSObject;
console.info('VULPES Android bridge', location.origin);
let port;
let reconnect;
function connect() {
port = browser.runtime.connect({ name: 'gaia' });
port.onMessage.addListener(onMessage);
port.onDisconnect.addListener(onDisconnect);
}
const pending = new Map();
let seq = 0;
function request(data) {
return new window.Promise((resolve, reject) => {
const id = ++seq;
pending.set(id, { resolve, reject });
try {
port.postMessage({ id, request: JSON.parse(JSON.stringify(data)) });
} catch (error) {
pending.delete(id);
reject(new window.Error('VULPES_HOST_CLOSED'));
}
});
}
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) }),
);
return;
}
const callback = pending.get(message.id);
if (!callback) return;
pending.delete(message.id);
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();
clearTimeout(reconnect);
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();
let nextFrame = 0;
exportFunction(
(frame, data) => {
if (frame.ownerDocument !== window.document || frame.localName !== 'iframe')
throw new window.Error('INVALID_VIEW');
let id = [...frames].find(([, f]) => f === frame)?.[0];
if (!id) {
id = ++nextFrame;
frames.set(id, frame);
}
const params = JSON.parse(JSON.stringify(data));
params.contextId = id;
return request({ version: 1, id: 0, method: 'views.request', params });
},
win.navigator,
{ defineAs: 'vulpesFrameRequest', allowCrossOriginArguments: true },
);
exportFunction((id) => frames.get(id) || null, win.navigator, {
defineAs: 'vulpesFrameForContext',
});
})();

View file

@ -1,46 +0,0 @@
{
"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"
}
]
}

View file

@ -1 +0,0 @@
installMarketplacePage(window,url=>browser.runtime.sendMessage({marketplaceDownload:url}).catch(console.error));

View file

@ -1,88 +0,0 @@
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);
}
}

View file

@ -1,3 +0,0 @@
org.gradle.jvmargs=-Xmx3g -Dfile.encoding=UTF-8
org.gradle.workers.max=4
android.useAndroidX=true

View file

@ -1,7 +0,0 @@
pluginManagement { repositories { google(); mavenCentral(); gradlePluginPortal() } }
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { google(); mavenCentral(); maven { url = uri('https://maven.mozilla.org/maven2/') } }
}
rootProject.name = 'VulpesPreview'
include ':app'

View file

@ -1,67 +0,0 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import vm from 'node:vm';
import { SettingsService } from '../../services/settings.mjs';
const context = vm.createContext({ window: {}, Blob, Uint8Array, btoa, atob });
vm.runInContext(
await readFile(new URL('../../host/settings-codec.js', import.meta.url), 'utf8'),
context,
);
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);
const image = new Blob([pixels], { type: 'image/png' });
let stored;
const backend = {
get: async (key) => stored[key],
setMany: async (entries) => {
stored = JSON.parse(JSON.stringify(Object.fromEntries(entries)));
},
};
const payload = JSON.parse(
JSON.stringify(await encode({ 'wallpaper.image': image, 'wallpaper.image.valid': true })),
);
const request = { version: 1, id: 1, method: 'settings.set', params: { values: payload } };
const service = new SettingsService(backend, { maxBytes: 32 * 1024 * 1024 });
const session = service.bindSession('system', ['settings.read', 'settings.write']);
assert.equal((await session(request)).result.committed, true);
const reply = await session({
version: 1,
id: 2,
method: 'settings.get',
params: { key: 'wallpaper.image' },
});
for (const value of [
decode(reply.result).value,
decode(stored)['wallpaper.image'],
decode(stored['wallpaper.image']),
]) {
assert.ok(value instanceof Blob);
assert.equal(value.type, image.type);
assert.equal(value.size, image.size); // Gaia's savedBlobSize loop guard now works.
assert.deepEqual(new Uint8Array(await value.arrayBuffer()), pixels);
}
assert.equal(
(await service.bindSession('unprivileged', [])(request)).error.code,
'PERMISSION_DENIED',
);
assert.equal(
(await new SettingsService(backend).bindSession('desktop', ['settings.write'])(request)).error
.code,
'VALUE_TOO_LARGE',
);
assert.equal(
JSON.stringify(
decode(await encode({ language: 'fr', enabled: true, unset: null, list: [1, 'x'] })),
),
JSON.stringify({ language: 'fr', enabled: true, unset: null, list: [1, 'x'] }),
);
await assert.rejects(
encode(new Blob([new Uint8Array(16 * 1024 * 1024 + 1)])),
/SETTING_BLOB_TOO_LARGE/,
);
console.log(
'PASS: binary settings survive JSON persistence, reads and observers; limits and permissions retained.',
);

View file

@ -1,29 +0,0 @@
{
"jdk": "Temurin 21.0.12.1+1",
"gradle": "9.5.0",
"androidGradlePlugin": "9.3.3",
"geckoview": "156.0.20260921121718",
"sdk": [
"platforms;android-37.1",
"build-tools;37.0.0"
],
"archives": {
"jdk.tar.gz": {
"sha256": "ce79869e1307ed8ee1e2baa86a412b1eb5b75d10a01006d788a6f968bcfaee94",
"url": "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.12.1%2B1/OpenJDK21U-jdk_x64_linux_hotspot_21.0.12.1_1.tar.gz"
},
"gradle-9.5.zip": {
"sha256": "553c78f50dafcd54d65b9a444649057857469edf836431389695608536d6b746",
"url": "https://services.gradle.org/distributions/gradle-9.5.0-bin.zip"
},
"geckoview.aar": {
"sha256": "42f80fcccbbe72c17f67bc7be3044cae097e4cb61a9084b406ec935f66e8edca",
"url": "https://maven.mozilla.org/maven2/org/mozilla/geckoview/geckoview/156.0.20260921121718/geckoview-156.0.20260921121718.aar"
},
"commandlinetools.zip": {
"url": "https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip",
"sha1": "5fdcc763663eefb86a5b8879697aa6088b041e70",
"checksumSource": "https://dl.google.com/android/repository/repository2-1.xml"
}
}
}

View file

@ -1,110 +0,0 @@
"""Exercise alarms and Android permission boundaries in the packaged bridge."""
import functools
import json
import os
import threading
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from playwright.sync_api import sync_playwright
# This harness mocks extension storage/native messaging, not the Gaia service code.
root = Path(__file__).resolve().parents[2]
class Handler(SimpleHTTPRequestHandler):
def log_message(self, *args):
pass
def do_GET(self):
if self.path == "/test.html":
body = b'<!doctype html><html><head><script type="module" src="/background.js"></script></head><body>Bridge test</body></html>'
self.send_response(200)
self.send_header("Content-Type", "text/html")
self.end_headers()
self.wfile.write(body)
else:
super().do_GET()
server = ThreadingHTTPServer(
("127.0.0.1", 0),
functools.partial(Handler, directory=str(root / "android/app/src/main/assets/bridge")),
)
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
with sync_playwright() as p:
b = p.chromium.launch(executable_path=os.environ.get("PLAYWRIGHT_CHROMIUM_EXECUTABLE"))
page = b.new_page()
messages = []
errors = []
page.on("console", lambda msg: messages.append(msg.text))
page.on("pageerror", lambda e: errors.append(str(e)))
page.add_init_script(
"""
const values={};window.nativeCalls=[];window.events=[];
window.browser={storage:{local:{get:async key=>({[key]:structuredClone(values[key])}),set:async data=>Object.assign(values,structuredClone(data))}},runtime:{
connectNative:()=>({onMessage:{addListener(fn){window.nativeEvent=fn;}},onDisconnect:{addListener(){}},postMessage(){}}),
sendNativeMessage:async(app,message)=>{nativeCalls.push(message);return message.type==='android.snapshot'?{}:null;},onConnect:{addListener(fn){window.connectPort=fn;}}
}};
window.makePort=url=>{
let receive,seq=0;const waiting=new Map();
connectPort({name:'gaia',sender:{url},onMessage:{addListener(fn){receive=fn;}},onDisconnect:{addListener(){}},postMessage(msg){if(msg.event)events.push({url,...msg.event});if(waiting.has(msg.id)){waiting.get(msg.id)(msg.reply);waiting.delete(msg.id);}}});
return (method,params)=>new Promise(resolve=>{const id=++seq;waiting.set(id,resolve);receive({id,request:{version:1,id,method,params}});});
};
"""
)
page.goto(f"http://127.0.0.1:{server.server_port}/test.html")
page.wait_for_function('typeof connectPort === "function"')
results = page.evaluate(
"""async()=>{
const clock=makePort('http://clock.localhost:18765/onring.html');
const before=await clock('alarms.request',{operation:'getAll'});
if(before.error)throw Error(JSON.stringify(before));
const add=await clock('alarms.request',{operation:'add',date:Date.now()+60000,respectTimezone:'honorTimezone',local:[2026,8,23,9,0,0,0],data:{label:'test'}});
if(add.error)throw Error(JSON.stringify(add));
const list=await clock('alarms.request',{operation:'getAll'});
await clock('alarms.request',{operation:'remove',id:add.result});
const empty=await clock('alarms.request',{operation:'getAll'});
const gallery=makePort('http://gallery.localhost:18765/index.html');
const denied=await gallery('android.settings',{panel:'wifi'});
const cameraDenied=await gallery('android.capture',{});
const settingsDenied=await gallery('settings.set',{values:{'screen.brightness':.4}});
const settings=makePort('http://settings.localhost:18765/index.html');
nativeCalls.length=0;
const malformed=await settings('settings.set',{values:{'screen.brightness':.5,'':true}});
const noSideEffect=malformed.error?.code==='INVALID_KEY' && nativeCalls.length===0;
const notificationDenied=await gallery('android.notification',{operation:'show',owner:'settings',title:'Spoof'});
const smsDenied=await clock('android.sms',{number:'123',body:'test'});
const foreign=makePort('https://example.com/');
const foreignDenied=await foreign('android.snapshot',{});
const system=makePort('http://system.localhost:18765/index.html');
events.length=0;
await nativeEvent({type:'notification-click',data:{owner:'settings',id:1}});
const queued=!events.some(e=>e.type==='launch');
const forbiddenReady=await gallery('android.systemReady',{});
await system('android.systemReady',{});
const replayed=events.some(e=>e.type==='launch' && e.data.manifestURL==='http://settings.localhost:18765/manifest.webapp');
return {coldNotificationWaitsForGaia:queued && replayed,systemReadinessPermission:forbiddenReady.error?.code==='PERMISSION_DENIED',invalidSettingNoNativeSideEffect:noSideEffect,notificationPermissions:notificationDenied.error?.code==='PERMISSION_DENIED',smsPermissions:smsDenied.error?.code==='PERMISSION_DENIED',started:true,stored:list.result.length===1,removed:empty.result.length===0,
hardwarePermissions:denied.error?.code==='PERMISSION_DENIED' && cameraDenied.error?.code==='PERMISSION_DENIED',
settingsPermissions:settingsDenied.error?.code==='PERMISSION_DENIED',
externalOriginDenied:foreignDenied.error?.code==='PERMISSION_DENIED'};
}"""
)
assert all(results.values()), results
assert not errors, errors
(root / "logs/repository-preparation").mkdir(parents=True, exist_ok=True)
(root / "logs/repository-preparation/android-bridge-test.json").write_text(
json.dumps(
{
"results": results,
"scope": "Packaged extension in a browser; native messaging and extension storage mocked. Not an Android device test.",
},
indent=2,
)
)
print(results)
b.close()
finally:
server.shutdown()
server.server_close()

View file

@ -1,56 +0,0 @@
#!/usr/bin/env python3
"""Reproduce first camera consent on the test emulator, without restarting afterward."""
import importlib.util
import json
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
ADB = str(ROOT / "android/toolchain/sdk/platform-tools/adb")
spec = importlib.util.spec_from_file_location("probe", Path(__file__).with_name("probe-android.py"))
probe = importlib.util.module_from_spec(spec)
spec.loader.exec_module(probe)
def adb(*args):
return subprocess.check_output([ADB, "-s", "emulator-5554", *args], text=True).strip()
assert adb("shell", "getprop", "ro.kernel.qemu") == "1"
adb("shell", "pm", "revoke", "org.vulpes_os.preview", "android.permission.CAMERA")
adb("shell", "am", "start", "-n", "org.vulpes_os.preview/.MainActivity")
adb("forward", "tcp:6088", "localabstract:org.vulpes_os.preview/firefox-debugger-socket")
for _ in range(60):
if probe.run("system", 'document.body?.getAttribute("ready-state")==="fullyLoaded"').get(
"value"
):
break
time.sleep(0.3)
else:
raise TimeoutError("Gaia boot")
probe.run(
"system",
'VulpesCompat.call("apps.launch",{manifestURL:"http://camera.localhost:18765/manifest.webapp"})',
)
subprocess.run(
["python3", str(ROOT / "android/tools/ui-android.py"), "While using the app"],
check=True,
timeout=30,
)
for _ in range(60):
state = probe.run(
"camera",
'({live:!!app.camera.mozCamera && app.camera.mozCamera.getVideoTracks().every(t=>t.readyState==="live"),loaded:!!app.loaded,hidden:document.hidden,busy:app.camera.isBusy,width:document.querySelector(".viewfinder video")?.videoWidth,text:document.body.innerText})',
).get("value", {})
if state.get("live") and state.get("width") and state.get("loaded") and not state.get("busy"):
break
time.sleep(0.3)
else:
raise AssertionError(state)
result = {"first_consent_without_restart": state, "scope": "Android 16 x86_64 emulator"}
(ROOT / "android/logs/camera-first-permission.json").write_text(
json.dumps(result, indent=2, ensure_ascii=False) + "\n"
)
print(json.dumps(result, indent=2, ensure_ascii=False))

View file

@ -1,141 +0,0 @@
#!/usr/bin/env python3
"""Exercise Gaia Photo/Gallery on an Android emulator with its virtual sensor."""
import importlib.util
import json
import subprocess
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
spec = importlib.util.spec_from_file_location("probe", Path(__file__).with_name("probe-android.py"))
probe = importlib.util.module_from_spec(spec)
spec.loader.exec_module(probe)
ADB = str(ROOT / "android/toolchain/sdk/platform-tools/adb")
def adb(*args):
return subprocess.check_output([ADB, "-s", "emulator-5554", *args], text=True).strip()
assert (
adb("shell", "getprop", "ro.kernel.qemu") == "1"
), "Use a virtual sensor, not a personal camera."
def ev(app, code):
reply = probe.run(app, code)
if "error" in reply:
raise AssertionError(reply)
return reply.get("value")
def wait(app, code):
for _ in range(60):
value = ev(app, code)
if value:
return value
time.sleep(0.3)
raise TimeoutError(app + ": " + code)
def launch(app):
ev(
"system",
'VulpesCompat.call("apps.launch",{manifestURL:"http://'
+ app
+ '.localhost:18765/manifest.webapp"})',
)
wait(app, 'document.readyState==="complete" && !document.hidden')
result = {"scope": "Android 16 x86_64 emulator, virtual camera; physical phones untested."}
wait("system", 'document.body?.getAttribute("ready-state")==="fullyLoaded"')
ev("system", "(ScreenManager.turnScreenOn(true),true)")
time.sleep(.5)
ev("system", "(lockScreen.unlock(true),true)")
wait("system", "!lockScreen.locked")
time.sleep(.5)
launch("camera")
wait(
"camera",
'!!app.loaded && !!app.camera.mozCamera && !app.camera.isBusy && !!document.querySelector(".viewfinder video")?.videoWidth',
)
result["preview"] = ev(
"camera",
'(()=>{const v=document.querySelector(".viewfinder video");return {width:v.videoWidth,height:v.videoHeight,cameras:navigator.mozCameras.getListOfCameras()}})()',
)
result["original_gaia_ui"] = ev(
"camera",
'!!document.querySelector(".js-capture") && !document.querySelector(".vulpes-capability")',
)
assert result["original_gaia_ui"]
ev(
"camera",
'(async()=>{window.__beforeCapture=(await VulpesCompat.call("media.request",{type:"pictures",operation:"enumerate"})).map(f=>f.name);window.__streamBefore=app.camera.mozCamera;return true})()',
)
point = ev(
"camera",
'(()=>{const r=document.querySelector(".js-capture").getBoundingClientRect();return {x:(mozInnerScreenX+r.x+r.width/2)*devicePixelRatio,y:(mozInnerScreenY+r.y+r.height/2)*devicePixelRatio}})()',
)
adb("shell", "input", "tap", str(round(point["x"])), str(round(point["y"])))
photo = wait(
"camera",
'(async()=>(await VulpesCompat.call("media.request",{type:"pictures",operation:"enumerate"})).find(f=>!__beforeCapture.includes(f.name)))()',
)
assert photo["size"] > 0 and photo["type"] == "image/jpeg"
result["photo"] = photo
launch("gallery")
result["camera_stops_when_hidden"] = wait(
"camera", '__streamBefore.getTracks().every(t=>t.readyState==="ended")'
)
name = json.dumps(photo["name"])
result["gallery_indexes_photo"] = wait(
"gallery",
'typeof files!=="undefined" && files.some(f=>f.name==='
+ name
+ " && f.metadata.width>0 && f.metadata.thumbnail)",
)
ev(
"gallery",
'(()=>{const e=[...document.querySelectorAll(".thumbnailImage")].find(e=>e.dataset.filename==='
+ name
+ ");e.click();return true})()",
)
result["gallery_opens_photo"] = wait(
"gallery", 'document.body.classList.contains("fullscreenView")'
)
result["gallery_import_button"] = ev(
"gallery", '!!document.querySelector("gaia-header [data-vulpes-import]") && !document.querySelector(".vulpes-import")'
)
assert result["gallery_import_button"]
ev("gallery", "(launchCameraApp(),true)")
wait("camera", "!document.hidden")
result["camera_resumes"] = wait(
"camera",
'!!app.camera.mozCamera && app.camera.mozCamera!==__streamBefore && app.camera.mozCamera.getVideoTracks().every(t=>t.readyState==="live") && !app.camera.isBusy',
)
ev("camera", "(window.__streamBefore=app.camera.mozCamera,true)")
adb("shell", "input", "keyevent", "KEYCODE_HOME")
result["camera_stops_in_android_background"] = wait(
"camera", '__streamBefore.getTracks().every(t=>t.readyState==="ended")'
)
adb("shell", "am", "start", "-n", "org.vulpes_os.preview/.MainActivity")
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["capability_controls"] = ev(
"camera",
'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["capability_controls"]
result["errors"] = ev(
"camera", '__vulpesDiagnostics.filter(d=>d.kind==="error" || d.kind==="rejection")'
)
assert not result["errors"], result
launch("gallery")
(ROOT / "android/logs/camera-results.json").write_text(
json.dumps(result, indent=2, ensure_ascii=False) + "\n"
)
print(json.dumps(result, indent=2, ensure_ascii=False))

View file

@ -1,50 +0,0 @@
#!/usr/bin/env python3
"""Exercise Android background/resume and renderer termination on a test device."""
import importlib.util,json,subprocess,sys,time
from pathlib import Path
ROOT=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(ROOT/'android/tools'))
spec=importlib.util.spec_from_file_location('probe',ROOT/'android/tools/probe-android.py');probe=importlib.util.module_from_spec(spec);spec.loader.exec_module(probe)
from rdp import RDP
ADB=str(ROOT/'android/toolchain/sdk/platform-tools/adb')
def adb(*args):return subprocess.check_output([ADB,*args],text=True).strip()
def wait_app():
end=time.monotonic()+30
while time.monotonic()<end:
try:
result=probe.run('system','({ready:document.readyState,marker:window.__lifecycleMarker,frames:[...document.querySelectorAll("iframe")].map(f=>f.src)})')
if result.get('value',{}).get('ready')=='complete':return result['value']
except (Exception,):pass
time.sleep(.5)
raise TimeoutError('Gaia did not recover')
report={}
adb('shell','am','start','-n','org.vulpes_os.preview/.MainActivity')
wait_app();probe.run('system','window.__lifecycleMarker="lifecycle-20260924"')
cycles=[]
for i in range(10):
adb('shell','input','keyevent','KEYCODE_HOME')
adb('shell','am','start','-a','android.settings.SETTINGS')
time.sleep(.5)
adb('shell','am','start','-n','org.vulpes_os.preview/.MainActivity')
state=wait_app();cycles.append(state['marker']=='lifecycle-20260924')
print('Resume cycle',i+1,cycles[-1],flush=True)
assert all(cycles),cycles
report['ten_switches_preserve_document']=cycles
r=RDP();tab=next(t for t in r.call('root','listTabs')['tabs'] if 'system.localhost' in t.get('url',''));target=r.call(tab['actor'],'getTarget')['frame'];r.s.close()
pid=str(target['processID']);name=adb('shell','ps','-p',pid,'-o','NAME')
assert 'org.vulpes_os.preview:tab' in name,name
adb('shell','input','keyevent','KEYCODE_HOME');adb('shell','run-as','org.vulpes_os.preview','kill','-9',pid)
time.sleep(.5);adb('shell','am','start','-n','org.vulpes_os.preview/.MainActivity')
state=wait_app();assert state.get('marker') is None,state
for _ in range(30):
try:
home=probe.run('homescreen','({icons:document.querySelectorAll("gaia-app-icon").length,ready:document.readyState})')['value']
if home['icons']>10:break
except Exception:pass
time.sleep(.5)
assert home['icons']>10,home
report['renderer_killed_in_background_recovers']=home
report['snapshot']=probe.run('system','VulpesCompat.call("android.snapshot",{})')['value']
report['scope']='Android 16 x86_64 emulator; not a physical-device validation.'
(ROOT/'android/logs/integration/lifecycle.json').write_text(json.dumps(report,indent=2))
print(json.dumps(report,indent=2),flush=True)

View file

@ -1,45 +0,0 @@
#!/usr/bin/env python3
"""Observe real Gaia wallpaper events over an ADB-forwarded debug connection."""
import json
import sys
import time
from rdp import RDP
def evaluate(source):
r = RDP()
try:
result = r.eval(source)
if result.get("hasException"):
raise RuntimeError(result)
return result.get("result")
finally:
r.s.close()
duration = int(sys.argv[1]) if len(sys.argv) > 1 else 30
evaluate(
"""window.__wallpaperProbe={changes:0,settings:0,start:Date.now()};
addEventListener('wallpaperchange',()=>__wallpaperProbe.changes++);
navigator.mozSettings.addObserver('wallpaper.image',()=>__wallpaperProbe.settings++);
navigator.mozSettings.createLock().get('wallpaper.image').onsuccess=e=>{
const v=e.target.result['wallpaper.image'];
__wallpaperProbe.image={blob:v instanceof Blob,size:v.size,type:v.type};
}; 'probe-started';"""
)
time.sleep(duration)
result = json.loads(
evaluate(
"""JSON.stringify({...__wallpaperProbe,
elapsed:Date.now()-__wallpaperProbe.start,ready:applications.ready,
background:document.getElementById('screen').style.backgroundImage})"""
)
)
print(json.dumps(result, indent=2))
assert result["ready"], "Gaia not ready"
assert result.get("image", {}).get("blob"), "Wallpaper was not restored as a Blob"
assert result["image"]["size"] > 0, "Empty wallpaper"
assert (
result["changes"] == 0 and result["settings"] == 0
), "Wallpaper is still changing without user action"
assert "blob:" in result["background"], "Wallpaper missing from the screen"

View file

@ -1,19 +0,0 @@
import sys
import json
from rdp import RDP
r = RDP()
targets = r.call("root", "listProcesses")["processes"]
for p in targets:
if p["isParent"]:
continue
t = r.call(p["actor"], "getTarget")["process"]
if sys.argv[1] not in t.get("remoteType", ""):
continue
a = r.call(t["consoleActor"], "evaluateJSAsync", text=sys.argv[2])
while True:
q = r.recv()
if q.get("type") == "evaluationResult":
print(json.dumps(q, indent=2))
break
r.s.close()

View file

@ -1,184 +0,0 @@
#!/usr/bin/env python3
"""Snapshot the same Gaia assets/overlays as the desktop, without changing them."""
import hashlib
import json
import re
import shutil
import subprocess
import zipfile
import sys
import base64
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "tools"))
from project import SOURCE_OVERRIDES, build_info, validate_assets, release_info, application_manifest
validate_assets()
OUT = ROOT / "android/app/src/main/assets"
OUT.mkdir(parents=True, exist_ok=True)
PORT = 18765
pattern = re.compile(
r"app://([a-zA-Z0-9_-]+)\.gaiamobile\.org|http://([a-zA-Z0-9_-]+)\.localhost:8765"
)
def translate(text):
return pattern.sub(lambda m: f"http://{m[1] or m[2]}.localhost:{PORT}", text)
APPS = ROOT / "assets/webapps"
registry = []
for manifest in sorted(APPS.glob("*/manifest.webapp")):
name = manifest.parent.name.removesuffix(".gaiamobile.org")
if not re.fullmatch("[a-z0-9_-]+", name):
continue
origin = f"http://{name}.localhost:{PORT}"
registry.append(
dict(
id=name,
origin=origin,
manifestURL=origin + "/manifest.webapp",
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"])
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-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 = {}
def put(name, data):
z.writestr(name, data)
hashes[name] = hashlib.sha256(data).hexdigest()
for app in registry:
folder = APPS / (app["id"] + ".gaiamobile.org")
files = {p.relative_to(folder).as_posix(): p for p in folder.rglob("*") if p.is_file()}
common = ROOT / "overrides/shared"
if common.is_dir():
for source in common.rglob("*"):
if source.is_file():
files["shared/" + source.relative_to(common).as_posix()] = source
overlay = ROOT / "overrides" / folder.name
if overlay.exists():
files.update(
{
p.relative_to(overlay).as_posix(): p
for p in overlay.rglob("*")
if p.is_file() and p.name != "manifest.webapp"
}
)
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()
if p.suffix in textsuffix:
text = translate(data.decode("utf8"))
if p.suffix in {".html", ".css", ".js"}:
for a, b in [
("-moz-calc(", "calc("),
("-moz-box-sizing:", "box-sizing:"),
("-moz-plaintext", "plaintext"),
(":-moz-dir(", ":dir("),
("offset-inline-start:", "inset-inline-start:"),
("offset-inline-end:", "inset-inline-end:"),
]:
text = text.replace(a, b)
# Clearing a media URL with null requests the literal /null path.
if app["id"] == "music" and p.suffix == ".js":
text = text.replace("audio.src=null;", "audio.removeAttribute('src');")
if name == "resources/gaia_commit.txt":
text = revision + "\n" + commit_time + "\n"
data = text.encode()
if p.suffix == ".html" and not (app["id"] == "search" and name.startswith("home/")):
data = data.replace(b"<head>", head, 1)
if app["id"] == "communications" and name.startswith("dialer/"):
data = data.replace(
b"<head>",
b'<head><script src="/shared/js/accessibility_helper.js"></script>',
1,
)
put(app["id"] + "/" + name, data)
for p in (ROOT / "gaia/shared").rglob("*"):
if p.is_file():
data = p.read_bytes()
if p.suffix in textsuffix:
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-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 / "host/settings-codec.js").read_bytes())
put("_vulpes/android-ui.js", (ROOT / "android/ui/android-ui.js").read_bytes())
put(
"_vulpes/android.css",
b'html,body{overscroll-behavior:none} html[data-vulpes-app="system"] #keyboard-frame{display:none!important} html[data-vulpes-app="system"] #screen{height:100%!important} input,textarea{touch-action:manipulation}',
)
(OUT / "payload-checksums.json").write_text(json.dumps(hashes, sort_keys=True))
ext = OUT / "bridge"
shutil.copytree(ROOT / "android/extension", ext, dirs_exist_ok=True)
for folder in ["services", "adapters"]:
shutil.copytree(ROOT / folder, ext / folder, dirs_exist_ok=True)
for name in ["Connections", "Contacts", "Messages"]:
text = (
(ROOT / "host" / f"{name}.sys.mjs")
.read_text()
.replace("Services.uuid.generateUUID().toString()", "crypto.randomUUID()")
)
(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()))
wallpaper = ROOT / "gaia/apps/default_theme/wallpapers/Illus_Flow2_Blue@2x.jpg"
if not wallpaper.is_file():
raise RuntimeError("Default wallpaper is missing: " + str(wallpaper))
settings["wallpaper.image"] = (
"data:image/jpeg;base64," + base64.b64encode(wallpaper.read_bytes()).decode()
)
settings.update(
{
"ftu.manifestURL": "",
"lockscreen.enabled": True,
"screen.timeout": 0,
"language.current": "fr",
"software-button.enabled": False,
"metrics.selectedMetrics.level": "None",
"ftu.pingEnabled": False,
"search.suggestions.enabled": False,
"search.marketplace.url": "",
"gaia.system.checkForUpdates": False,
"deviceinfo.os": release["gaia"],
"deviceinfo.software": "Vulpes OS " + release["version"],
"deviceinfo.product_model": "Vulpes Android Preview",
"deviceinfo.product_device": "android",
"deviceinfo.platform_version": release["androidEngine"],
"deviceinfo.platform_build_id": release["androidEngineBuildID"],
}
)
(ext / "defaults.json").write_text(json.dumps(settings, ensure_ascii=False))
(OUT / "build-info.json").write_text(
json.dumps(
{
"revision": revision,
"geckoview": release["geckoview"],
"version": release["version"],
"minSdk": 26,
"targetSdk": 37,
}
)
)
print(
f'Packaged {len(registry)} Gaia apps, {round((OUT/"gaia.zip").stat().st_size/1024/1024,1)} MiB'
)

View file

@ -1,108 +0,0 @@
#!/usr/bin/env python3
"""Install the locked Linux x86_64 Android build tools in the ignored toolchain/."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import tarfile
import tempfile
import urllib.request
import zipfile
ROOT = Path(__file__).resolve().parents[1]
TOOLS = ROOT / "toolchain"
def fetch(name, entry):
path = TOOLS / "downloads" / name
path.parent.mkdir(parents=True, exist_ok=True)
algorithm = "sha256" if "sha256" in entry else "sha1"
if not path.exists():
partial = path.with_suffix(path.suffix + ".partial")
with urllib.request.urlopen(entry["url"], timeout=60) as source, partial.open("wb") as out:
shutil.copyfileobj(source, out)
partial.replace(path)
with path.open("rb") as stream:
digest = hashlib.file_digest(stream, algorithm).hexdigest()
if digest != entry[algorithm]:
raise RuntimeError(f"Checksum mismatch; archive left unextracted: {path}")
return path
def extract(archive, member_root, target):
if target.exists():
raise RuntimeError(f"Incomplete installation left untouched: {target}")
target.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".install-", dir=TOOLS) as temporary:
stage = Path(temporary)
if archive.name.endswith(".tar.gz"):
with tarfile.open(archive) as package:
package.extractall(stage, filter="data")
else:
with zipfile.ZipFile(archive) as package:
for info in package.infolist():
path = stage / info.filename
if not path.resolve().is_relative_to(stage) or "\\" in info.filename:
raise RuntimeError("Unsafe toolchain archive entry")
if (info.external_attr >> 16) & 0o170000 == 0o120000:
raise RuntimeError("Symlink in toolchain ZIP archive")
package.extract(info, stage)
mode = (info.external_attr >> 16) & 0o777
if mode and path.is_file():
path.chmod(mode)
(stage / member_root).rename(target)
def main():
parser = argparse.ArgumentParser()
parser.add_argument(
"--check", action="store_true", help="Check installed tools without downloading"
)
args = parser.parse_args()
if platform.system() != "Linux" or platform.machine() not in ("x86_64", "amd64"):
raise RuntimeError("This bootstrap currently supports Linux x86_64 only")
lock = json.loads((ROOT / "toolchain-lock.json").read_text())
jdk = TOOLS / "jdk-21.0.12.1+1"
gradle = TOOLS / "gradle-9.5.0"
sdk = TOOLS / "sdk"
installs = [
("jdk.tar.gz", "jdk-21.0.12.1+1", jdk, jdk / "bin/java"),
("gradle-9.5.zip", "gradle-9.5.0", gradle, gradle / "bin/gradle"),
(
"commandlinetools.zip",
"cmdline-tools",
sdk / "cmdline-tools/latest",
sdk / "cmdline-tools/latest/bin/sdkmanager",
),
]
missing = [
str(executable.relative_to(ROOT))
for _, _, _, executable in installs
if not executable.is_file()
]
packages = [sdk / "platforms/android-37.1/android.jar", sdk / "build-tools/37.0.0/apksigner"]
if args.check:
missing += [str(path.relative_to(ROOT)) for path in packages if not path.is_file()]
if missing:
raise RuntimeError("Missing tools: " + ", ".join(missing))
else:
TOOLS.mkdir(exist_ok=True)
for name, member, target, executable in installs:
if not executable.is_file():
extract(fetch(name, lock["archives"][name]), member, target)
if not all(path.is_file() for path in packages):
env = {**os.environ, "JAVA_HOME": str(jdk)}
manager = sdk / "cmdline-tools/latest/bin/sdkmanager"
# sdkmanager asks the user to review any licenses not already accepted.
subprocess.run(
[str(manager), "--sdk_root=" + str(sdk), *lock["sdk"]], env=env, check=True
)
print("Android build tools available in", TOOLS)
if __name__ == "__main__":
main()

View file

@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""Evaluate a diagnostic in one Gaia app through the ADB-only debugger."""
import json,sys,time
from rdp import 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 && (!%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:
value=r.recv()
if value.get('type')=='evaluationResult' and value.get('resultID')==response['resultID']:
if value.get('hasException'): raise RuntimeError(value.get('exceptionMessage',str(value)))
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:
if connection is None:r.s.close()
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"',connection)
break
except RuntimeError as e:
if time.monotonic()>=start: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"',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))

View file

@ -1,62 +0,0 @@
#!/usr/bin/env python3
"""ADB-forwarded Firefox debugger helper for local development APKs."""
import socket
import json
import sys
class RDP:
def __init__(self, port=6088):
self.events = []
self.s = socket.create_connection(("127.0.0.1", port), timeout=20)
self.recv()
def recv(self):
n = b""
while not n.endswith(b":"):
c = self.s.recv(1)
if not c:
raise EOFError()
n += c
data = b""
while len(data) < int(n[:-1]):
data += self.s.recv(int(n[:-1]) - len(data))
return json.loads(data)
def call(self, to, kind, **args):
data = json.dumps(dict(to=to, type=kind, **args)).encode()
self.s.sendall(str(len(data)).encode() + b":" + data)
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
def eval(self, text):
tabs = self.call("root", "listTabs")["tabs"]
tab = next(t for t in tabs if "system.localhost" in t.get("url", ""))
frame = self.call(tab["actor"], "getTarget")["frame"]
reply = self.call(frame["consoleActor"], "evaluateJSAsync", text=text)
if "resultID" not in reply:
return reply
while True:
result = self.recv()
if (
result.get("type") == "evaluationResult"
and result.get("resultID") == reply["resultID"]
):
return result
if __name__ == "__main__":
c = RDP()
print(
json.dumps(
c.eval(sys.argv[1]) if len(sys.argv) > 1 else c.call("root", "listTabs"),
ensure_ascii=False,
indent=2,
)
)
c.s.close()

View file

@ -1,17 +0,0 @@
#!/usr/bin/env python3
"""Read or tap a labeled Android accessibility node on the connected test device."""
import re,sys,subprocess,xml.etree.ElementTree as E
from pathlib import Path
adb=str(Path(__file__).resolve().parents[1]/'toolchain/sdk/platform-tools/adb')
subprocess.run([adb,'shell','uiautomator','dump','/sdcard/vulpes-ui.xml'],stdout=subprocess.DEVNULL,check=True)
nodes=list(E.fromstring(subprocess.check_output([adb,'shell','cat','/sdcard/vulpes-ui.xml'],text=True)).iter('node'))
if len(sys.argv)>1:
needle=sys.argv[1]
for n in nodes:
if needle in (n.get('text'),n.get('content-desc'),n.get('resource-id')):
x1,y1,x2,y2=map(int,re.findall(r'\d+',n.get('bounds')))
subprocess.run([adb,'shell','input','tap',str((x1+x2)//2),str((y1+y2)//2)],check=True);print('Tapped:',needle);break
else:raise SystemExit('Node not found: '+needle)
else:
for n in nodes:
if n.get('text') or n.get('clickable')=='true':print(n.get('text'),n.get('content-desc'),n.get('resource-id'),n.get('bounds'))

View file

@ -1,202 +0,0 @@
// 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();
})();

View file

@ -1,40 +0,0 @@
// Android's extension storage/RPC is JSON. Preserve binary Gaia settings across
// both boundaries and reconstruct Blobs in the application's own JS realm.
(() => {
const marker = '__vulpesBlobV1';
async function encode(value) {
if (value instanceof Blob) {
if (value.size > 16 * 1024 * 1024) throw new Error('SETTING_BLOB_TOO_LARGE');
const bytes = new Uint8Array(await value.arrayBuffer());
const chunks = [];
for (let i = 0; i < bytes.length; i += 16384) {
chunks.push(String.fromCharCode(...bytes.subarray(i, i + 16384)));
}
return { [marker]: true, type: value.type, base64: btoa(chunks.join('')) };
}
if (Array.isArray(value)) return Promise.all(value.map(encode));
if (value && typeof value === 'object') {
return Object.fromEntries(
await Promise.all(
Object.entries(value).map(async ([key, item]) => [key, await encode(item)]),
),
);
}
return value;
}
function decode(value) {
if (!value || typeof value !== 'object') return value;
if (
value[marker] === true &&
typeof value.type === 'string' &&
typeof value.base64 === 'string' &&
Object.keys(value).length === 3
) {
const bytes = Uint8Array.from(atob(value.base64), (c) => c.charCodeAt(0));
return new Blob([bytes], { type: value.type });
}
if (Array.isArray(value)) return value.map(decode);
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, decode(item)]));
}
window.VulpesAndroidSettingsCodec = Object.freeze({ encode, decode });
})();

File diff suppressed because one or more lines are too long

View file

@ -1,7 +0,0 @@
;var Pairview={_debug:false,_remoteDeviceName:null,_pairMethod:null,_options:null,_passkey:'',pairview:document.getElementById('pair-view'),nameLabel:document.getElementById('label-name'),pairDescription:document.getElementById('pair-description'),pairButton:document.getElementById('button-pair'),closeButton:document.getElementById('button-close'),comfirmationItem:document.getElementById('confirmation-method'),pinInputItem:document.getElementById('pin-input-method'),passkey:document.getElementById('passkey'),pinInput:document.getElementById('pin-input'),get isFullAttentionMode(){return(window.innerHeight>200);},show:function pv_show(){if(!this.isFullAttentionMode){this.close();return;}
var _=navigator.mozL10n.get;this.pairButton.addEventListener('click',this);this.closeButton.addEventListener('click',this);window.addEventListener('resize',this);this._remoteDeviceName=this._remoteDeviceName||_('unnamed-device');var truncatedDeviceName=getTruncated(this._remoteDeviceName,{node:this.nameLabel,maxLine:2,ellipsisIndex:3});this.nameLabel.textContent=truncatedDeviceName;this.pairview.hidden=false;var stringName='passive-pair-'+this._pairMethod;this.pairDescription.textContent=_(stringName,{device:truncatedDeviceName});switch(this._pairMethod){case'displaypasskey':case'confirmation':this.passkey.textContent=this._passkey;this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;case'enterpincode':this.pinInputItem.hidden=false;this.comfirmationItem.hidden=true;this.pinInput.focus();break;case'consent':this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;}},init:function pv_init(method,options){this._pairMethod=method;this._options=options;this._remoteDeviceName=options.deviceName;if(options.handle&&options.handle.passkey){var passkey=options.handle.passkey;var len=passkey.toString().length;var zeros=(len<6)?(new Array((6-len)+1)).join('0'):'';this._passkey=zeros+passkey;}
navigator.mozL10n.once(Pairview.show.bind(Pairview));},close:function pv_close(){this._options.handle.reject().then(()=>{this.debug('Resolved in reject '+this._pairMethod+' request.');window.close();},(aReason)=>{this.debug('Rejected in reject '+this._pairMethod+' request with reason: '+aReason);window.close();});},closeInput:function pv_closeInput(){if(!this.pinInputItem.hidden){this.pinInput.blur();}},handleEvent:function pv_handleEvent(evt){var _=navigator.mozL10n.get;if(!evt.target){return;}
switch(evt.type){case'click':evt.preventDefault();switch(evt.target.id){case'button-pair':this.pairDescription.textContent=_('device-status-waiting');this.pairButton.disabled=true;this.closeButton.disabled=true;switch(this._pairMethod){case'displaypasskey':window.close();break;case'enterpincode':var pinCode=this.pinInput.value;this._options.handle.setPinCode(pinCode).then(()=>{this.debug('Resolved setPinCode operation');window.close();},(aReason)=>{this.debug('Rejected setPinCode with reason: '+aReason);window.close();});break;case'confirmation':case'consent':this._options.handle.accept().then(()=>{this.debug('Resolved in '+this._pairMethod+' request');window.close();},(aReason)=>{this.debug('Rejected in '+this._pairMethod+' request '+' with reason: '+aReason);window.close();});break;}
break;case'button-close':this.close();break;}
break;case'resize':if(!this.isFullAttentionMode){this.close();}
break;default:break;}},debug:function(msg){if(this._debug){console.log('Pairview(): '+msg);}}};;function getTruncated(t,e){function i(t,e){return t!==e}var n=e.maxLine||2,l=e.node,o=e.ellipsisIndex||3,r=e.ellipsisCharacter||"...";if(null===l)return t;var s,a,c,u,d,y,h="",v=!0,p=[],f=t.slice(-o),g=l.style.visibility,x=l.style.wordBreak;l.style.visibility="hidden",l.style.wordBreak="break-all",l.textContent=".",c=l.clientHeight,l.textContent="";var C=t.split("").some(function(t,e){if(p.push(t),y=p.join(""),s=l.clientHeight,l.textContent=y+r+f,a=l.clientHeight,0===e&&(u=Math.floor(a/c)),i(s,a)&&0!==e){var o;l.textContent=y,o=l.clientHeight,i(s,o)?(v=!0,u+=1):v&&(d=e,v=!1)}return u>n?(0===e&&console.log("Your string is in a overflowed situation, please check your options"),p.pop(),l.textContent="",!0):void 0});return l.style.visibility=g,l.style.wordBreak=x,C?(h+=p.join("").slice(0,d),h+=r,h+=f):h=t,h}

File diff suppressed because one or more lines are too long

View file

@ -1,373 +0,0 @@
;window.COMPONENTS_BASE_URL="/shared/elements/";;(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GaiaHeader=f()}})(function(){var define,module,exports;return(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){;(function(define){define(function(require,exports,module){'use strict';var debug=0?console.log.bind(console):function(){};var cache={};var MIN=16;var MAX=24;var BUFFER=3;module.exports=function(config){debug('font fit',config);var space=config.space-BUFFER;var min=config.min||MIN;var max=config.max||MAX;var text=trim(config.text);var fontSize=max;var textWidth;var font;do{font=config.font.replace(/\d+px/,fontSize+'px');textWidth=getTextWidth(text,font);}while(textWidth>space&&fontSize!==min&&fontSize--);return{textWidth:textWidth,fontSize:fontSize,overflowing:textWidth>space};};function getTextWidth(text,font){var ctx=getCanvasContext(font);var width=ctx.measureText(text).width;debug('got text width',width);return width;}
function getCanvasContext(font){debug('get canvas context',font);var cached=cache[font];if(cached){return cached;}
var canvas=document.createElement('canvas');canvas.setAttribute('moz-opaque','true');canvas.setAttribute('width','1px');canvas.setAttribute('height','1px');debug('created canvas',canvas);var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.font=font;return cache[font]=ctx;}
function trim(text){return text.replace(/\s+/g,' ').trim();}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('font-fit',this));},{}],2:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var textContent=Object.getOwnPropertyDescriptor(Node.prototype,'textContent');var innerHTML=Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');var removeAttribute=Element.prototype.removeAttribute;var setAttribute=Element.prototype.setAttribute;var noop=function(){};exports.register=function(name,props){var baseProto=getBaseProto(props.extends);var template=props.template||baseProto.templateString;var extensible=props.extensible=props.hasOwnProperty('extensible')?props.extensible:true;delete props.extends;if(template){if(extensible&&props.template){props.templateString=props.template;}
var output=processCss(template,name);props.template=document.createElement('template');props.template.innerHTML=output.template;props.lightCss=output.lightCss;props.globalCss=props.globalCss||'';props.globalCss+=output.globalCss;}
injectGlobalCss(props.globalCss);delete props.globalCss;var descriptors=mixin(props.attrs||{},base.descriptors);props._attrs=props.attrs;delete props.attrs;var proto=createProto(baseProto,props);Object.defineProperties(proto,descriptors);try{return document.registerElement(name,{prototype:proto});}catch(e){if(e.name!=='NotSupportedError'){throw e;}}};var base={properties:{GaiaComponent:true,attributeChanged:noop,attached:noop,detached:noop,created:noop,createdCallback:function(){if(this.dirObserver){addDirObserver();}
injectLightCss(this);this.created();},attributeChangedCallback:function(name,from,to){var prop=toCamelCase(name);if(this._attrs&&this._attrs[prop]){this[prop]=to;}
this.attributeChanged(name,from,to);},attachedCallback:function(){if(this.dirObserver){this.setInnerDirAttributes=setInnerDirAttributes.bind(null,this);document.addEventListener('dirchanged',this.setInnerDirAttributes);}
this.attached();},detachedCallback:function(){if(this.dirObserver){document.removeEventListener('dirchanged',this.setInnerDirAttributes);}
this.detached();},setupShadowRoot:function(){if(!this.template){return;}
var node=document.importNode(this.template.content,true);this.createShadowRoot().appendChild(node);if(this.dirObserver){setInnerDirAttributes(this);}
return this.shadowRoot;},setAttr:function(name,value){var internal=this.shadowRoot.firstElementChild;setAttribute.call(internal,name,value);setAttribute.call(this,name,value);},removeAttr:function(name){var internal=this.shadowRoot.firstElementChild;removeAttribute.call(internal,name);removeAttribute.call(this,name);}},descriptors:{textContent:{set:function(value){textContent.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:function(){return textContent.get();}},innerHTML:{set:function(value){innerHTML.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:innerHTML.get}}};var defaultPrototype=createProto(HTMLElement.prototype,base.properties);function getBaseProto(proto){if(!proto){return defaultPrototype;}
proto=proto.prototype||proto;return!proto.GaiaComponent?createProto(proto,base.properties):proto;}
function createProto(proto,props){return mixin(Object.create(proto),props);}
var hasShadowCSS=(function(){var div=document.createElement('div');try{div.querySelector(':host');div.querySelector('::content');return true;}
catch(e){return false;}})();var regex={shadowCss:/(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,':host':/(?:\:host)/g,':host()':/\:host\((.+)\)(?: \:\:content)?/g,':host-context':/\:host-context\((.+)\)([^{,]+)?/g,'::content':/(?:\:\:content)/g};function processCss(template,name){var globalCss='';var lightCss='';if(!hasShadowCSS){template=template.replace(regex.shadowCss,function(match){var hostContext=regex[':host-context'].exec(match);if(hostContext){globalCss+=match.replace(regex['::content'],'').replace(regex[':host-context'],'$1 '+name+'$2').replace(/ +/g,' ');}else{lightCss+=match.replace(regex[':host()'],name+'$1').replace(regex[':host'],name).replace(regex['::content'],name);}
return'';});}
return{template:template,lightCss:lightCss,globalCss:globalCss};}
function injectGlobalCss(css){if(!css){return;}
var style=document.createElement('style');style.innerHTML=css.trim();headReady().then(function(){document.head.appendChild(style);});}
function headReady(){return new Promise(function(resolve){if(document.head){return resolve();}
window.addEventListener('load',function fn(){window.removeEventListener('load',fn);resolve();});});}
function injectLightCss(el){if(hasShadowCSS){return;}
var stylesheet=el.querySelector('style');if(!stylesheet){stylesheet=document.createElement('style');stylesheet.setAttribute('scoped','');stylesheet.appendChild(document.createTextNode(el.lightCss));el.appendChild(stylesheet);}
el.lightStyle=stylesheet;}
function toCamelCase(string){return string.replace(/-(.)/g,function replacer(string,p1){return p1.toUpperCase();});}
var dirObserver;function setInnerDirAttributes(component){var dir=component.dir||document.dir;Array.from(component.shadowRoot.children).forEach(element=>{if(element.nodeName!=='STYLE'){element.dir=dir;}});}
function addDirObserver(){if(dirObserver){return;}
dirObserver=new MutationObserver(onChanged);dirObserver.observe(document.documentElement,{attributeFilter:['dir'],attributes:true});function onChanged(mutations){document.dispatchEvent(new Event('dirchanged'));}}
function mixin(target,source){for(var key in source){target[key]=source[key];}
return target;}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-component',this));},{}],3:[function(require,module,exports){(function(define){'use strict';define(function(require,exports,module){var base=window.GAIA_ICONS_BASE_URL||window.COMPONENTS_BASE_URL||'bower_components/';if(!document.documentElement){window.addEventListener('load',load);}else{load();}
function load(){if(isLoaded()){return;}
var link=document.createElement('link');link.rel='stylesheet';link.type='text/css';link.href=base+'gaia-icons/gaia-icons.css';document.head.appendChild(link);exports.loaded=true;}
function isLoaded(){return exports.loaded||document.querySelector('link[href*=gaia-icons]')||document.documentElement.classList.contains('gaia-icons-loaded');}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-icons',this));},{}],4:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var component=require('gaia-component');var fontFit=require('font-fit');require('gaia-icons');var debug=0?console.log.bind(console):function(){};const KNOWN_ACTIONS={menu:'menu',back:'back',close:'close'};const TITLE_FONT='italic 300 24px FiraSans';const TITLE_PADDING=10;const MINIMUM_FONT_SIZE_CENTERED=20;const MINIMUM_FONT_SIZE_UNCENTERED=16;const MAXIMUM_FONT_SIZE=23;module.exports=component.register('gaia-header',{extensible:false,dirObserver:true,created:function(){debug('created');this.setupShadowRoot();this.els={actionButton:this.shadowRoot.querySelector('.action-button'),titles:this.getElementsByTagName('h1')};this.els.actionButton.addEventListener('click',e=>this.onActionButtonClick(e));this.observer=new MutationObserver(this.onMutation.bind(this));this.ignoreDir=this.hasAttribute('ignore-dir');this.titleEnd=this.getAttribute('title-end');this.titleStart=this.getAttribute('title-start');this.noFontFit=this.getAttribute('no-font-fit');this.notFlush=this.hasAttribute('not-flush');this.action=this.getAttribute('action');this.unresolved={};this.pending={};this._resizeThrottlingId=null;this.onResize=this.onResize.bind(this);},attached:function(){debug('attached');this.runFontFitSoon();this.observerStart();window.addEventListener('resize',this.onResize);},detached:function(){debug('detached');window.removeEventListener('resize',this.onResize);this.observerStop();this.clearPending();},clearPending:function(){for(var key in this.pending){this.pending[key].clear();delete this.pending[key];}
window.cancelAnimationFrame(this._resizeThrottlingId);this._resizeThrottlingId=null;},runFontFit:function(){debug('run font-fit');if(this.noFontFit){return Promise.resolve();}
var titles=this.els.titles;var space=this.getTitleSpace();var styles=[].map.call(titles,el=>this.getTitleStyle(el,space));return this.setTitleStylesSoon(styles);},runFontFitSoon:function(){debug('run font-fit soon');if(this.pending.runFontFitSoon){return;}
this.pending.runFontFitSoon=this.nextTick(()=>{delete this.pending.runFontFitSoon;this.runFontFit();});},getTitleStyle:function(el,space){debug('get el style',el,space);var text=el.textContent;var styleId=space.start+text+space.end+'#'+space.value;if(!text||!text.trim()){return debug('exit: no text');}
if(getStyleId(el)===styleId){return debug('exit: no change');}
var marginStart=this.getTitleMarginStart();var textSpace=space.value-Math.abs(marginStart);var fontFitResult=this.fontFit(text,textSpace,{min:MINIMUM_FONT_SIZE_CENTERED});var overflowing=fontFitResult.overflowing;var padding={start:0,end:0};if(overflowing){debug('title overflowing');padding.start=!space.start?TITLE_PADDING:0;padding.end=!space.end?TITLE_PADDING:0;textSpace=space.value-padding.start-padding.end;fontFitResult=this.fontFit(text,textSpace);marginStart=0;}
return{id:styleId,fontSize:fontFitResult.fontSize,marginStart:marginStart,overflowing:overflowing,padding:padding};},setTitleStylesSoon:function(styles){debug('set title styles soon',styles);var key='setStyleTitlesSoon';this._titleStyles=styles;if(this.unresolved[key]){return this.unresolved[key];}
this.unresolved[key]=new Promise((resolve)=>{this.pending[key]=this.nextTick(()=>{var styles=this._titleStyles;var els=this.els.titles;[].forEach.call(els,(el,i)=>{if(!styles[i]){return debug('exit');}
this.setTitleStyle(el,styles[i]);});delete this._titleStyles;delete this.unresolved[key];delete this.pending[key];resolve();});});},setTitleStyle:function(el,style){debug('set title style',style);this.observerStop();if(this.ignoreDir){el.style.marginLeft=style.marginStart+'px';el.style.paddingLeft=style.padding.start+'px';el.style.paddingRight=style.padding.end+'px';}else{el.style.marginInlineStart=style.marginStart+'px';el.style.paddingInlineStart=style.padding.start+'px';el.style.paddingInlineEnd=style.padding.end+'px';}
el.style.fontSize=style.fontSize+'px';setStyleId(el,style.id);this.observerStart();},fontFit:function(text,space,opts={}){debug('font fit:',text,space,opts);var fontFitArgs={font:TITLE_FONT,min:opts.min||MINIMUM_FONT_SIZE_UNCENTERED,max:MAXIMUM_FONT_SIZE,text:text,space:space};return fontFit(fontFitArgs);},observerStart:function(){if(this.observing){return;}
this.observer.observe(this,{childList:true,attributes:true,subtree:true});this.observing=true;debug('observer started');},observerStop:function(){if(!this.observing){return;}
this.observer.disconnect();this.observing=false;debug('observer stopped');},onResize:function(e){debug('onResize',this._resizeThrottlingId);if(this._resizeThrottlingId!==null){return;}
this._resizeThrottlingId=window.requestAnimationFrame(()=>{this._resizeThrottlingId=null;this.runFontFitSoon();});},onMutation:function(mutations){debug('on mutation',mutations);if(!this.pending.runFontFitSoon){this.runFontFit();}},getTitleSpace:function(){var start=this.titleStart;var end=this.titleEnd;var space=this.getWidth()-start-end;var result={value:space,start:start,end:end};debug('get title space',result);return result;},getWidth:function(){var value=this.notFlush?this.clientWidth:window.innerWidth;debug('get width',value);return value;},triggerAction:function(){if(this.action){this.els.actionButton.click();}},onActionButtonClick:function(){debug('action button click');var config={detail:{type:this.action}};var e=new CustomEvent('action',config);setTimeout(()=>this.dispatchEvent(e));},getTitleMarginStart:function(){var start=this.titleStart;var end=this.titleEnd;var marginStart=end-start;debug('get title margin start',marginStart);return marginStart;},getButtonsBeforeTitle:function(){var children=this.children;var l=children.length;var els=[];for(var i=0;i<l;i++){var el=children[i];if(el.tagName==='H1'){break;}
if(!contributesToLayout(el)){continue;}
els.push(el);}
if(this.action){els.push(this.els.actionButton);}
return els;},getButtonsAfterTitle:function(){var children=this.children;var els=[];for(var i=children.length-1;i>=0;i--){var el=children[i];if(el.tagName==='H1'){break;}
if(!contributesToLayout(el)){continue;}
els.push(el);}
return els;},sumButtonWidths:function(buttons){var defaultWidth=50;var sum=buttons.reduce((prev,button)=>{var isStandardButton=button===this.els.actionButton;var width=isStandardButton?defaultWidth:button.clientWidth;return prev+width;},0);debug('sum button widths',buttons,sum);return sum;},attrs:{action:{get:function(){return this._action;},set:function(value){var action=KNOWN_ACTIONS[value];if(action===this._action){return;}
this.setAttr('action',action);this._action=action;}},titleStart:{get:function(){debug('get title-start');if('_titleStart'in this){return this._titleStart;}
var buttons=this.getButtonsBeforeTitle();var value=this.sumButtonWidths(buttons);debug('get title-start',buttons,value);return value;},set:function(value){debug('set title-start',value);value=parseInt(value,10);if(value===this._titleStart||isNaN(value)){return;}
this.setAttr('title-start',value);this._titleStart=value;debug('set');}},titleEnd:{get:function(){debug('get title-end');if('_titleEnd'in this){return this._titleEnd;}
var buttons=this.getButtonsAfterTitle();return this.sumButtonWidths(buttons);},set:function(value){debug('set title-end',value);value=parseInt(value,10);if(value===this._titleEnd||isNaN(value)){return;}
this.setAttr('title-end',value);this._titleEnd=value;}},noFontFit:{get:function(){return this._noFontFit||false;},set:function(value){debug('set no-font-fit',value);value=!!(value||value==='');if(value===this.noFontFit){return;}
this._noFontFit=value;if(value){this.setAttr('no-font-fit','');}
else{this.removeAttr('no-font-fit');}}},ignoreDir:{get:function(){return this._ignoreDir||false;},set:function(value){debug('set ignore-dir',value);value=!!(value||value==='');if(value===this.ignoreDir){return;}
this._ignoreDir=value;this.dirObserver=!value;if(value){this.setAttr('ignore-dir','');}
else{this.removeAttr('ignore-dir');}}}},template:`<div class="inner">
<button class="action-button">
<content select="[l10n-action]"></content>
</button>
<content></content>
</div>
<style>
:host {
display: block;
-moz-user-select: none;
--gaia-header-button-color:
var(--header-button-color,
var(--header-color,
var(--link-color,
inherit)));
}
/**
* [hidden]
*/
:host[hidden] {
display: none;
}
/** Reset
---------------------------------------------------------*/
::-moz-focus-inner { border: 0; }
/** Inner
---------------------------------------------------------*/
.inner {
display: flex;
min-height: 50px;
-moz-user-select: none;
background:
var(--header-background,
var(--background,
#fff));
}
/** Action Button
---------------------------------------------------------*/
/**
* 1. Hidden by default
*/
.action-button {
position: relative;
display: none; /* 1 */
width: 50px;
font-size: 30px;
margin: 0;
padding: 0;
border: 0;
outline: 0;
align-items: center;
background: none;
cursor: pointer;
transition: opacity 200ms 280ms;
color:
var(--header-action-button-color,
var(--header-icon-color,
var(--gaia-header-button-color)));
}
/**
* [action=back]
* [action=menu]
* [action=close]
*
* 1. For icon vertical-alignment
*/
[action=back] .action-button,
[action=menu] .action-button,
[action=close] .action-button {
display: flex; /* 1 */
}
/**
* :active
*/
.action-button:active {
transition: none;
opacity: 0.2;
}
/** Action Button Icon
---------------------------------------------------------*/
.action-button:before {
font-family: 'gaia-icons';
font-style: normal;
text-rendering: optimizeLegibility;
font-weight: 500;
}
[action=close] .action-button:before { content: 'close' }
[action=menu] .action-button:before { content: 'menu' }
[action=back]:-moz-dir(ltr) .action-button:before { content: 'left' }
[action=back]:-moz-dir(rtl) .action-button:before { content: 'right' }
/** Action Button Icon
---------------------------------------------------------*/
/**
* 1. To enable vertical alignment.
*/
.action-button:before {
display: block;
}
/** Action Button Text
---------------------------------------------------------*/
/**
* To provide custom localized content for
* the action-button, we allow the user
* to provide an element with the class
* .l10n-action. This node is then
* pulled inside the real action-button.
*
* Example:
*
* <gaia-header action="back">
* <span l10n-action aria-label="Back">Localized text</span>
* <h1>title</h1>
* </gaia-header>
*/
::content [l10n-action] {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 0;
}
/** Title
---------------------------------------------------------*/
/**
* 1. Vertically center text. We can't use flexbox
* here as it breaks text-overflow ellipsis
* without an inner div.
*/
::content h1 {
flex: 1;
margin: 0;
padding: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
line-height: 50px; /* 1 */
font-weight: 300;
font-style: italic;
font-size: 24px;
color:
var(--header-title-color,
var(--header-color,
var(--title-color,
var(--text-color,
inherit))));
}
/**
* [ignore-dir]
*
* When the <gaia-header> component has an [ignore-dir] attribute, header
* direction is forced to LTR but we still want the <h1> text to be reversed
* so that strings like '1 selected' become 'selected 1'.
*
* When we're happy for <gaia-header> to be fully RTL responsive we won't need
* these rules anymore, but this depends on all Gaia apps being ready.
*
* This should be safe to remove when bug 1179459 lands.
*/
:host[ignore-dir] {
direction: ltr;
}
:host[ignore-dir]:-moz-dir(rtl) h1 {
direction: rtl;
}
/** Buttons
---------------------------------------------------------*/
::content a,
::content button {
position: relative;
z-index: 1;
box-sizing: border-box;
display: flex;
width: auto;
height: auto;
min-width: 50px;
margin: 0;
padding: 0 10px;
outline: 0;
border: 0;
font-size: 14px;
line-height: 1;
align-items: center;
justify-content: center;
text-decoration: none;
text-align: center;
background: none;
border-radius: 0;
font-style: italic;
cursor: pointer;
transition: opacity 200ms 280ms;
color: var(--gaia-header-button-color);
}
/**
* :active
*/
::content a:active,
::content button:active {
transition: none;
opacity: 0.2;
}
/**
* [hidden]
*/
::content a[hidden],
::content button[hidden] {
display: none;
}
/**
* [disabled]
*/
::content a[disabled],
::content button[disabled] {
pointer-events: none;
color: var(--header-disabled-button-color);
}
/** Icon Buttons
---------------------------------------------------------*/
/**
* Icons are a different color to text
*/
::content .icon,
::content [data-icon] {
color:
var(--header-icon-color,
var(--gaia-header-button-color));
}
/**
* If users want their action button
* to be in the component's light-dom
* they can add an .action class
* to make it look like the
* shadow action button.
*/
::content .action {
color:
var(--header-action-button-color,
var(--header-icon-color,
var(--gaia-header-button-color)));
}
/**
* [data-icon]:empty
*
* Icon buttons with no textContent,
* should always be 50px.
*
* This is to prevent buttons being
* larger than they should be before
* icon-font has loaded.
*/
::content [data-icon]:empty {
width: 50px;
}
</style>`,nextTick:nextTick});function contributesToLayout(el){return el.localName!=='style'&&!el.hasAttribute('l10n-action');}
function setStyleId(el,id){el._styleId=id;}
function getStyleId(el){return el._styleId;}
function nextTick(fn){var cleared;Promise.resolve().then(()=>{if(!cleared){fn();}});return{clear:function(){cleared=true;}};}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this));},{"font-fit":1,"gaia-component":2,"gaia-icons":3}]},{},[4])(4)});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
require.config({baseUrl:"/js",paths:{modules:"modules",views:"views",shared:"../shared/js"},shim:{"shared/async_storage":{exports:"asyncStorage"}}});

View file

@ -1 +0,0 @@
var gDeviceList=null;navigator.mozL10n.once(function(){function e(e){o&&console.log("[Bluetooth APP Device List]: "+e)}var t=navigator.mozL10n.get,n=window.navigator.mozSettings,i=window.navigator.mozBluetooth,d=null,o=!1;n&&i&&(gDeviceList=function(){function n(e,n){var i=document.createElement("span"),d=""===e.name?t("unnamed-device"):e.name,o=""===e.name?"unnamed-device":"";i.textContent=d,i.dataset.l10nId=o;var a=document.createElement("small");a.textContent=""===n?"":t(n),a.dataset.l10nId=n;var s=document.createElement("progress");s.classList.add("overlapping-icon"),s.classList.add("hidden");var l=document.createElement("a");l.appendChild(i),l.appendChild(a);var r=document.createElement("li");return r.classList.add("bluetooth-device"),r.classList.add("bluetooth-type-"+e.icon),r.appendChild(l),r.appendChild(s),r}function o(e){p.hidden=!e,e?(q.show(!0),x.hidden=!1):(q.show(!1),S.show(!1),x.hidden=!0,C=null,E=null,b=null,clearTimeout(I),I=null)}function a(e,t,n){f.hidden=!1,y.addEventListener("action",m),d=e,d.onpairedstatuschanged=function(e){c(e.status,"Authentication Failed",e.address)},d.ondiscoverystatechanged=function(e){e.discovering?(g.disabled=!0,x.hidden=!1):(g.disabled=!1,x.hidden=!0,clearTimeout(I),I=null)},L=t,B=n,l(),u()}function s(){f.hidden=!0,d=null,y.removeEventListener("action",m)}function l(){if(i.enabled&&d){var e=d.getPairedDevices();e.onsuccess=function(){function i(){return function(e){var i=e.address===b?"device-status-connected":"",d=n(e,i);d.onclick=function(){if(!D&&!C){v();var n=d.querySelector("small");n.textContent=t("device-status-connecting"),n.dataset.l10nId="device-status-connecting",h(e)}},S.list.appendChild(d),S.index[e.address]=[e,d]}}var d=e.result.slice(),o=d.length;if(0===o)return void S.show(!1);S.clear(),d.sort(function(e,t){return e.name>t.name});for(var a=0;o>a;a++)i()(d[a]);S.show(!0)}}}function r(i){var o=i.device,a=q.index[o.address]||S.index[o.address];if(a){var s=a[1];if(o.name&&s){var l=s.querySelector("a > span");l&&(l.dataset.l10nId="",l.textContent=o.name)}}else{var r=n(o,"device-status-tap-connect");r.onclick=function(){if(v(),!D&&!C){var n=r.querySelector("small");n.textContent=t("device-status-pairing"),n.dataset.l10nId="device-status-pairing",r.classList.add("icon-hidden");var i=r.querySelector("progress");i.classList.remove("hidden");var a=d.pair(o.address);C=o.address;var s="pairing with address = "+C;e(s),a.onerror=function(e){c(!1,a.error.name)}}},q.list.appendChild(r),q.index[o.address]=[o,r]}}function c(n,i,d){if(C){var o=C;if(C=null,n){if(q.index[o]){var a=q.index[o][0],s=q.index[o][1];q.list.removeChild(s),delete q.index[o],E=o,h(a)}}else{var r=t("error-pair-title");if("Repeated Attempts"===i?r=r+"\n"+t("error-pair-toofast"):"Authentication Failed"===i&&(r=r+"\n"+t("error-pair-pincode")),e(r),window.alert(r),q.index[o]){var c=q.index[o][1].querySelector("small");c.textContent=t("device-status-tap-connect"),c.dataset.l10nId="device-status-tap-connect";var u=q.index[o][1];u.classList.remove("icon-hidden");var v=q.index[o][1].querySelector("progress");v.classList.add("hidden")}}l()}else if(l(),d&&n&&q.index[d]){var m=q.index[d][1];q.list.removeChild(m),delete q.index[d]}}function u(){if(i.enabled&&d&&!I){var e=d.startDiscovery();e.onsuccess=function(){I||(I=setTimeout(v,w))},e.onerror=function(){console.error("Can not discover nearby device")}}}function v(){if(i.enabled&&d&&I){var e=d.stopDiscovery();e.onerror=function(){console.error("Failed to stop discovery of nearby devices")},clearTimeout(I),I=null}}function h(e){D=!0,L&&L(e)}function m(){B&&B()}var f=document.getElementById("devices-list-view"),p=document.getElementById("bluetooth-search"),g=document.getElementById("search-device"),x=document.getElementById("bluetooth-searching"),y=document.getElementById("devices-list-header"),C=null,E=null,b=null,w=6e4,I=null,L=null,B=null,D=!1,S={title:document.getElementById("bluetooth-paired-title"),list:document.getElementById("bluetooth-paired-devices"),index:[],clear:function(){for(;this.list.hasChildNodes();)this.list.removeChild(this.list.lastChild);this.index=[]},show:function(e){e||this.clear(),this.title.hidden=!e,this.list.hidden=!e}},q={title:document.getElementById("bluetooth-found-title"),list:document.getElementById("bluetooth-devices"),index:[],clear:function(){for(;this.list.hasChildNodes();)this.list.removeChild(this.list.lastChild);this.index=[]},show:function(e){e||this.clear(),this.title.hidden=!e,this.list.hidden=!e}};return g.onclick=function(){D||C||(o(!0),q.clear(),u())},{update:o,initWithAdapter:a,uninit:s,startDiscovery:u,onDeviceFound:r}}())});

View file

@ -1,111 +0,0 @@
/**
* BluetoothAdapterManager:
* - BluetoothAdapterManager is an Observable that wraps the platform
* Bluetooth object for construct default adapter.
* - It has only one observable properties: defaultAdapter
* BluetoothAdapterManager only update state and does not involve in any UI
* logic.
*
* @module BluetoothAdapterManager
*/
define(['require','modules/navigator/mozBluetooth','modules/mvvm/observable'],function(require) {
'use strict';
var NavigatorBluetooth = require('modules/navigator/mozBluetooth');
var Observable = require('modules/mvvm/observable');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btam_debug(msg) {
console.log('--> [BluetoothAdapterManager]: ' + msg);
};
}
/**
* @alias module:modules/bluetooth/BluetoothAdapterManager
* @requires module:modules/navigator/mozBluetooth
* @requires module:modules/mvvm/observable
* @return {BluetoothAdapterManager}
*/
var BluetoothAdapterManager = {
/**
* The default adapter used to connect to the remote bluetooth devices.
*
* @readonly
* @memberOf BluetoothAdapterManager
* @type {Object}
*/
defaultAdapter: null,
/**
* Init Bluetooth module.
*
* @access private
* @memberOf BluetoothAdapterManager
*/
_init: function btam__init() {
// Early return while there is no navigator.mozBluetooth module.
if (!NavigatorBluetooth) {
return;
}
// watch BluetoothManager event
this._watchMozBluetoothAttributechanged();
// init default adapter
this._initDefaultAdapter();
},
/**
* Watch 'attributechanged' event from mozBluetooth for updating default
* adapter information.
*
* 'attributechanged' event description:
* A handler to trigger when bluetooth manager's only property
* defaultAdapter has changed.
*
* @access private
* @memberOf BluetoothAdapterManager
*/
_watchMozBluetoothAttributechanged:
function btam__watchMozBluetoothAttributechanged() {
NavigatorBluetooth.addEventListener('attributechanged', (evt) => {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
case 'defaultAdapter':
// Default adapter attribute change.
// Usually, it means that we reach new default adapter.
this.defaultAdapter = NavigatorBluetooth.defaultAdapter;
debug('_watchMozBluetoothAttributechanged(): ' +
'this.defaultAdapter = ' + this.defaultAdapter);
break;
default:
break;
}
}
});
},
/* Reach default adapter from platform Bluetooth.
*
* If the property is accessed on platform in the first time, it will
* trigger Gecko::Bluetooth to get default adapter from the chipset.
* So the value would be null.
* Then we will receive 'attributechanged' event later.
*
* @access private
* @memberOf BluetoothAdapterManager
*/
_initDefaultAdapter: function btam__initDefaultAdapter() {
this.defaultAdapter = NavigatorBluetooth.defaultAdapter;
debug('_initDefaultAdapter(): this.defaultAdapter = ' +
this.defaultAdapter);
}
};
// Create the observable object using the prototype.
var bluetoothAdapterManager = Observable(BluetoothAdapterManager);
bluetoothAdapterManager._init();
return bluetoothAdapterManager;
});

View file

@ -1 +0,0 @@
define([],function(){"use strict";var e=!1,o=function(){};e&&(o=function(e){console.log("--> [BluetoothCODMapper]: "+e)});var i={Computer:{0:"computer",1:"computer",2:"computer",3:"computer",4:"pda",5:"pda",6:"computer",7:"computer",reserved:"computer"},Phone:{0:"phone",1:"phone",2:"phone",3:"phone",4:"modem",5:"phone",reserved:"phone"},AudioVideo:{0:"audio-card",1:"audio-card",2:"audio-card",4:"audio-input-microphone",5:"audio-card",6:"audio-card",7:"audio-card",8:"audio-card",9:"audio-card",10:"audio-card",11:"camera-video",12:"camera-video",13:"camera-video",14:"video-display",15:"video-display",16:"video-display",18:"audio-card",reserved:"audio-card"},Peripheral:{1:"input-gaming",2:"input-gaming",16:"input-keyboard",17:"input-keyboard",18:"input-keyboard",19:"input-keyboard",20:"input-keyboard",21:"input-keyboard",22:"input-keyboard",23:"input-keyboard",24:"input-keyboard",25:"input-keyboard",26:"input-keyboard",27:"input-keyboard",28:"input-keyboard",29:"input-keyboard",30:"input-keyboard",31:"input-keyboard",32:"input-mouse",33:"input-mouse",34:"input-mouse",35:"input-mouse",36:"input-mouse",37:"input-tablet",38:"input-mouse",39:"input-mouse",40:"input-mouse",41:"input-mouse",42:"input-mouse",43:"input-mouse",44:"input-mouse",45:"input-mouse",46:"input-mouse",47:"input-mouse"},Imaging:{4:"video-display",8:"camera-photo",16:"scanner",32:"printer"}},r={1:i.Computer,2:i.Phone,3:"network-wireless",4:i.AudioVideo,5:i.Peripheral,6:i.Imaging},a={getDeviceType:function(e){o("cod.majorDeviceClass = "+e.majorDeviceClass),o("cod.majorServiceClass = "+e.majorServiceClass),o("cod.minorDeviceClass = "+e.minorDeviceClass);var i="",a=r[e.majorDeviceClass];return o("majorDeviceClass = "+a),"object"==typeof a?(i=a[e.minorDeviceClass]||a.reserved||"",o("return in minor class, type = "+i),i):"string"==typeof a?(o("return in major class, type = network-wireless"),a):256&e.majorServiceClass?(i="audio-card",o("return in major service class, type = audio-card"),i):(o("not mapping in any class, return type = "+i),i)}};return a});

View file

@ -1,953 +0,0 @@
/**
* BluetoothConnectionManager:
* BluetoothConnectionManager only update state and does not involve in any UI
* logic.
*
* @module BluetoothConnectionManager
*/
define(['require','modules/bluetooth/bluetooth_adapter_manager','shared/async_storage'],function(require) {
'use strict';
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
var AsyncStorage = require('shared/async_storage');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btam_debug(msg) {
console.log('--> [BluetoothConnectionManager]: ' + msg);
};
}
/**
* @alias module:modules/bluetooth/BluetoothConnectionManager
* @requires module:modules/bluetooth/bluetooth_adapter_manager
* @requires module:shared/async_storage
* @return {BluetoothConnectionManager}
*/
var BluetoothConnectionManager = {
/**
* The profiles of connected device that we are defined here.
*
* @public
* @memberOf BluetoothConnectionManager
* @type {Object}
*/
Profiles: {
'hfp': 0x111E, // Handsfree
'a2dp': 0x110D // Advanced Audio Distribution Devices
},
/**
* The address of device that we are trying to connect.
*
* @public
* @memberOf BluetoothConnectionManager
* @type {String}
*/
connectingAddress: null,
/**
* A object that we cache the connected devices information(address, device,
* connectedProfiles). It will be inited while default adapter is ready.
* And these information is coming from profile events.
* Each connected device is hashed by device address.
*
* EX:
* _connectedDevicesInfo = {
* 'AA:BB:CC:00:11:22': {
* 'device': DeviceObject,
* 'connectedProfiles': {
* 'hfp': true,
* 'a2dp': false
* }
* }
* };
*
* @private
* @memberOf BluetoothConnectionManager
* @type {Object}
*/
_connectedDevicesInfo: {},
/**
* An instance to maintain that we have created a promise to get connected
* devices.
*
* @access public
* @memberOf BluetoothConnectionManager
* @return {Promise}
*/
_getConnectedDevicesPromise: null,
/**
* The object maintains listeners' callback per property name.
* Each listener would be called as following definition.
* 'connecting' - be called when device is connecting.
* 'connected': - be called when device is connected.
* 'disconnected': - be called when device is disconnected.
* 'profileChanged': - be called when profile is changed.
*
* @memberOf BluetoothConnectionManager
* @type {Object}
*/
_listeners: {
'connecting': [],
'connected': [],
'disconnected': [],
'profileChanged': []
},
/**
* Default adapter of Bluetooth.
*
* @access private
* @memberOf BluetoothConnectionManager
* @type {Object} BluetoothAdapter
*/
_defaultAdapter: null,
/**
* Init BluetoothConnectionManager module.
*
* @access private
* @memberOf BluetoothConnectionManager
*/
_init: function btcm__init() {
// Observe 'defaultAdapter' property for reaching default adapter.
AdapterManager.observe('defaultAdapter',
this._onDefaultAdapterChanged.bind(this));
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
},
/**
* 'defaultAdapter' change event handler from adapter manager for
* updating adapter immediately.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter newAdapter
* @param {Object} BluetoothAdapter oldAdapter
*/
_onDefaultAdapterChanged:
function btcm__onDefaultAdapterChanged(newAdapter, oldAdapter) {
debug('_onDefaultAdapterChanged(): newAdapter = ' + newAdapter);
debug('_onDefaultAdapterChanged(): oldAdapter = ' + oldAdapter);
// save default adapter
this._defaultAdapter = newAdapter;
if (oldAdapter) {
// unwatch event since the old adapter is no longer usefull
this._unwatchProfilesStatuschanged(oldAdapter);
this._unwatchDefaultAdapterOnattributechanged(oldAdapter);
}
if (newAdapter) {
// watch event since the new adapter is ready to access
this._watchProfilesStatuschanged(newAdapter);
this._watchDefaultAdapterOnattributechanged(newAdapter);
// restore connection
if (newAdapter.state === 'enabled') {
this._restoreConnection();
}
} else {
// reset properties only
this._resetConnectionInfo();
}
},
/**
* Return the cache of connected devices in ConnectionManager.
*
* @access public
* @memberOf BluetoothConnectionManager
* @return {Promise}
*/
getConnectedDevices: function btcm_getConnectedDevices() {
if (!this._getConnectedDevicesPromise) {
this._getConnectedDevicesPromise =
this._initConnectedDevicesInfo().then(() => {
debug('getConnectedDevices(): resolved with latest cache = ' +
JSON.stringify(this._connectedDevicesInfo));
return this._connectedDevicesInfo;
}, (reason) => {
debug('getConnectedDevices(): rejected with reason = ' + reason);
this._getConnectedDevicesPromise = null;
});
}
return this._getConnectedDevicesPromise;
},
/**
* Init cache of connected device and save it in cache.
*
* @access private
* @memberOf BluetoothConnectionManager
* @return {Promise}
*/
_initConnectedDevicesInfo: function btcm__initConnectedDevicesInfo() {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
// Init connection status and profile from platform.
// Then, save these connected device information in cache.
return this._getConnectedDevicesFromPlatform().then(
(connectedDevicesByProfile) => {
this._constructDeviceItemsMap(connectedDevicesByProfile);
}, (reason) => {
debug('_initConnectedDevicesInfo(): rejected in ' +
'_getConnectedDevicesFromPlatform');
return Promise.reject(reason);
});
},
/**
* The method will update each device item in maintaining map.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} connectedDevices
*/
_constructDeviceItemsMap:
function btcm__constructDeviceItemsMap(connectedDevices) {
debug('_constructDeviceItemsMap(): connectedDevices = ' +
JSON.stringify(connectedDevices));
if (!connectedDevices) {
// Return empty object while there is no any connected devices.
debug('_constructDeviceItemsMap(): early return with empty object');
return;
}
Object.keys(this.Profiles).map((profileID) => {
connectedDevices[profileID].forEach((connectedDevice) => {
var connectionDeviceInfo = {
address: connectedDevice.address,
connected: true,
profileID: profileID,
device: connectedDevice
};
debug('_constructDeviceItemsMap(): connectionDeviceInfo = ' +
JSON.stringify(connectionDeviceInfo));
this._initConnectedDevicesCache(connectionDeviceInfo);
});
});
},
/**
* Init the cache which is maintained for connection devices.
* And the input is gotten from platform API adapter.getConnectedDevices().
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.address - address of the device
* @param {Boolean} options.connected - is connected or not
* @param {Object} options.profileID - profile ID of the connection type
* @param {Object} options.device - connect device, Bluetooth Object
*/
_initConnectedDevicesCache:
function btcm__initConnectedDevicesCache(options) {
debug('_initConnectedDevicesCache(): options = ' +
JSON.stringify(options));
// hash by device address
var info = this._connectedDevicesInfo[options.address];
if (info) {
// Already have profiles, update it for other profile.
info.connectedProfiles[options.profileID] = options.connected;
} else {
// Not have profiles yet, create for it.
// If options.device is existed, save the connected device.
// Otherwise, given null in this property.
var connectedDevice = (options.device) ? options.device : null;
info = {
'device': connectedDevice,
'connectedProfiles': {}
};
info.connectedProfiles[options.profileID] = options.connected;
}
// Save the device/profile in map.
this._connectedDevicesInfo[options.address] = info;
// If there is no profile connected,
// remove the device item from cache since it is already disconnected.
var dataToCheckConnectedProfile = {
address: options.address,
connectedDevices: this._connectedDevicesInfo
};
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
delete this._connectedDevicesInfo[options.address];
}
// Return the latest cache which is just updated here.
debug('_initConnectedDevicesCache(): this._connectedDevicesInfo = ' +
JSON.stringify(this._connectedDevicesInfo));
return this._connectedDevicesInfo;
},
/**
* Update the cache which is maintained for connection devices.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.address - address of the device
* @param {Boolean} options.connected - is connected or not
* @param {Object} options.profileID - profile ID of the connection type
* @param {Object} options.device - connect device, Bluetooth Object
*/
_updateConnectedDevices:
function btcm__updateConnectedDevices(options) {
return this.getConnectedDevices().then((connectedDevicesInfo) => {
debug('_updateConnectedDevices(): connectedDevicesInfo = ' +
JSON.stringify(connectedDevicesInfo));
// hash by device address
var info =
(connectedDevicesInfo) ? connectedDevicesInfo[options.address] : null;
if (info) {
// Already have profiles, update it for other profile.
info.connectedProfiles[options.profileID] = options.connected;
} else {
// Not have profiles yet, create for it.
// If options.device is existed, save the connected device.
// Otherwise, given null in this property.
var connectedDevice = (options.device) ? options.device : null;
info = {
'device': connectedDevice,
'connectedProfiles': {}
};
info.connectedProfiles[options.profileID] = options.connected;
}
// Save the device/profile in map.
this._connectedDevicesInfo[options.address] = info;
// If there is no profile connected,
// remove the device item from cache since it is already disconnected.
var dataToCheckConnectedProfile = {
address: options.address,
connectedDevices: this._connectedDevicesInfo
};
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
delete this._connectedDevicesInfo[options.address];
}
// Return the latest cache which is just updated here.
debug('_updateConnectedDevices(): this._connectedDevicesInfo = ' +
JSON.stringify(this._connectedDevicesInfo));
return Promise.resolve(this._connectedDevicesInfo);
}, () => {
debug('_updateConnectedDevices(): rejected with some exception');
return Promise.reject('rejected with some exception');
});
},
/**
* Only reset properties since there is no available default adapter.
*
* @access private
* @memberOf BluetoothConnectionManager
*/
_resetConnectionInfo: function btcm__resetConnectionInfo() {
// Reset connection status.
this.connectingAddress = null;
// Clean up the instance to get connected devices
// while new adapter is ready.
this._getConnectedDevicesPromise = null;
},
/**
* Watch 'onattributechanged' event from default adapter for watching
* adapter enabled/disabled status.
*
* Description of 'onattributechanged' event:
* A handler to trigger when one of the local bluetooth adapter's properties
* has changed. Note access to the changed property in this event handler
* would get the updated value.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_watchDefaultAdapterOnattributechanged:
function btcm__watchDefaultAdapterOnattributechanged(adapter) {
adapter.addEventListener('attributechanged',
this._onAdapterAttributeChanged.bind(this, adapter));
},
/**
* Unwatch 'onattributechanged' event from default adapter since adapter is
* removed.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_unwatchDefaultAdapterOnattributechanged:
function btcm__unwatchDefaultAdapterOnattributechanged(adapter) {
adapter.removeEventListener('attributechanged',
this._onAdapterAttributeChanged);
},
/**
* 'onattributechanged' event handler from default adapter for reaching
* adapter enabled/disabled status.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
* @param {event} evt
*/
_onAdapterAttributeChanged:
function btcm__onAdapterAttributeChanged(adapter, evt) {
for (var i in evt.attrs) {
debug('_onAdapterAttributeChanged(): ' + evt.attrs[i]);
switch (evt.attrs[i]) {
case 'state':
if (adapter.state === 'enabled') {
// Restore connection while default adapter state is enabled.
this._restoreConnection();
}
break;
default:
break;
}
}
},
/**
* Watch every of profile events('onhfpstatuschanged','ona2dpstatuschanged')
* from default adapter for updating device connected status immediately.
*
* Description of 'onhfpstatuschanged' event:
* Specifies an event listener to receive hfpstatuschanged events.
* Those events occur when an HFP connection status changes.
*
* Description of 'ona2dpstatuschanged' event:
* Specifies an event listener to receive a2dpstatuschanged events.
* Those events occur when an A2DP connection status changes.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_watchProfilesStatuschanged:
function btcm__watchProfilesStatuschanged(adapter) {
var eventName;
for (var profileID in this.Profiles) {
eventName = 'on' + profileID + 'statuschanged';
adapter[eventName] =
this._onProfileStatuschangeHandler.bind(this, profileID);
}
},
/**
* Unwatch every of profile events('onhfpstatuschanged',
* 'ona2dpstatuschanged') from default adapter since adapter is removed.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_unwatchProfilesStatuschanged:
function btc__unwatchProfilesStatuschanged(adapter) {
var eventName;
for (var profileID in this.Profiles) {
eventName = 'on' + profileID + 'statuschanged';
adapter[eventName] = null;
}
},
/**
* 'onhfpstatuschanged', 'ona2dpstatuschanged' events handler from
* default adapter for updating device connected status.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} profileID
* @param {event} evt
*/
_onProfileStatuschangeHandler:
function btcm__onProfileStatuschangeHandler(profileID, evt) {
debug('_onProfileStatuschangeHandler(): profileID = ' + profileID +
', evt.address = ' + evt.address + ', evt.status = ' + evt.status);
var options = {
address: evt.address,
connected: evt.status,
profileID: profileID
};
// Update connection status.
this._updateConnectionStatus(options);
},
/**
* Query async storage to restore connection.
*
* @access private
* @memberOf BluetoothConnectionManager
*/
_restoreConnection: function btcm__restoreConnection() {
if (!this._defaultAdapter) {
return;
}
// Reconnect the one kept in the async storage.
AsyncStorage.getItem('device.connected', (address) => {
if (!address) {
return;
}
// Make sure the restore device is already connected or not.
this.getConnectedDevices().then((connectedDevices) => {
if (connectedDevices && connectedDevices[address]) {
// Do an early return since the restore device is connected already.
debug('_restoreConnection(): early return cause connected already');
return;
}
// Get the device which is wanted to connect from paired devices.
var restoreDevice = this._getPairedDeviceByAddress(address);
if (restoreDevice) {
this.connectingAddress = restoreDevice.address;
// Fire 'connecting' event.
var event = {
type: 'connecting',
detail: {
address: restoreDevice.address
}
};
this._emitEvent(event);
this._connect(restoreDevice).then(() => {
debug('_restoreConnection(): restore connection successfully');
}, (reason) => {
debug('_restoreConnection(): restore connection failed, ' +
'reason = ' + reason);
// No available profiles are connected. Reset connecting address.
this.connectingAddress = null;
// Then, fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: restoreDevice.address
}
};
this._emitEvent(event);
});
}
});
});
},
/**
* Record connected device so if Bluetooth is turned off and then on
* we can restore the connection.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} action - to set or remove item for recording connection
* @param {String} address - the address of connected device
*/
_recordConnection: function btcm__recordConnection(action, address) {
if (action === 'set') {
// record connected device so if Bluetooth is turned off and then on
// we can restore the connection
AsyncStorage.setItem('device.connected', address);
debug('_recordConnection(): set item');
} else if ((action === 'remove') &&
(this._defaultAdapter.state === 'enabled')) {
// Clean up the connected device from async storage
// which is recorded before.
// Only remove the record while Bluetooth state is enabled.
// Because the request also comes while Bluetooth is turned off.
AsyncStorage.removeItem('device.connected');
debug('_recordConnection(): remove item');
}
},
/**
* Update connection status.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.address - address of the device
* @param {Boolean} options.connected - is connected or not
* @param {Object} options.profileID - profile ID of the connection type
*/
_updateConnectionStatus: function btcm___updateConnectionStatus(options) {
debug('_updateConnectionStatus(): address = ' + options.address +
', connected = ' + options.connected +
', profileID = ' + options.profileID);
this.connectingAddress = null;
// Update the profile in the cache of connected device info.
this._updateConnectedDevices(options).then((connectedDevicesInfo) => {
debug('_updateConnectionStatus(): _updateConnectedDevices() ' +
'resolved with connectedDevicesInfo = ' +
JSON.stringify(connectedDevicesInfo));
// Prepare latest data to check connected profile.
var dataToCheckConnectedProfile = {
address: options.address,
connectedDevices: connectedDevicesInfo
};
// Fire 'connected'/'disconnected' event according to
// the connection profile. Then, record connection.
var event;
if (options.connected) {
// Fire 'connected' event.
event = {
type: 'connected',
detail: {
address: options.address
}
};
this._emitEvent(event);
// Record connection.
this._recordConnection('set', options.address);
} else {
// If there is no profile connected,
// we have to remove the record connection.
// And fire 'disconnected' event for outer modules.
if (!this._hasConnectedProfileByAddress(
dataToCheckConnectedProfile)) {
// Record connection. Only remove the record while Bluetooth state
// is enabled. Because the event also comes while Bluetooth is
// turned off.
if (this._defaultAdapter.state === 'enabled') {
this._recordConnection('remove');
}
// Fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: options.address
}
};
this._emitEvent(event);
}
}
// Fire 'profileChanged' event.
var newProfiles;
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
newProfiles = null;
} else {
newProfiles = connectedDevicesInfo[options.address].connectedProfiles;
}
event = {
type: 'profileChanged',
detail: {
address: options.address,
profiles: newProfiles
}
};
this._emitEvent(event);
}, (reason) => {
debug('_updateConnectionStatus(): miss to update in rejected case, ' +
'reason = ' + reason);
});
},
/**
* It provides a convenient function for panel to connect with a device.
* And the panel no need to care about connected device currently.
* The method will disconnect current connected device first.
* Then, it will start connecting a paired device with the device's adapter.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {BluetoothDevice} device
* @return {Promise}
*/
connect: function btcm_connect(device) {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
// Disconnect current connected device first.
debug('connect(): Want to connect device address = ' + device.address +
', name = ' + device.name);
var connectedDevices = [];
return this.getConnectedDevices().then((connectedDevicesInfo) => {
for (var address in connectedDevicesInfo) {
if (connectedDevicesInfo[address].device !== null) {
connectedDevices.push(connectedDevicesInfo[address].device);
debug('connect(): push device cache in queue = ' +
JSON.stringify(connectedDevices));
} else {
var regetPairedDevice = this._getPairedDeviceByAddress(address);
connectedDevices.push(regetPairedDevice);
debug('connect(): push _getPairedDeviceByAddress in queue = ' +
JSON.stringify(regetPairedDevice));
}
}
debug('connect(): Will disconnect these connected devices = ' +
JSON.stringify(connectedDevices));
// Disconnect these connected device before
// service to connect with new device.
return Promise.all(connectedDevices.map((connectedDevice) => {
return this.disconnect(connectedDevice);
})).then(() => {
// All connected devices is disconnected.
// We can start to connect the new request.
debug('connect(): Start to connect with wanted device address = ' +
device.address);
return this._connect(device).then(() => {
debug('connect(): Resolved');
}, (reason) => {
debug('connect(): reason = ' + reason);
return Promise.reject(reason);
});
});
});
},
/**
* The method will connect the input device with the device's adapter.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {BluetoothDevice} device
* @returns {Promise}
*/
_connect: function btcm__connect(device) {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
// Save the connecting address
this.connectingAddress = device.address;
// Fire 'connecting' event.
var event = {
type: 'connecting',
detail: {
address: device.address
}
};
this._emitEvent(event);
debug('_connect(): before start to connect, stop discovery first');
// Note on Bluedroid stack, discovery has to be stopped before connect
// (i.e., call stopDiscovery() before connect())
// otherwise stack callbacks with connect failure.
return this._defaultAdapter.stopDiscovery().then(() => {
debug('_connect(): start connecting device, ' +
'address = ' + device.address);
return this._defaultAdapter.connect(device).then(() => {
debug('_connect(): resolve, already connected with address = ' +
device.address);
}, () => {
// No available profiles are connected. Reset connecting address.
this.connectingAddress = null;
// Fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: device.address
}
};
this._emitEvent(event);
debug('_connect(): reject with connection failed');
return Promise.reject('connection failed');
});
}, () => {
// Cannot connect with the device since stopDiscovery failed.
this.connectingAddress = null;
// Fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: device.address
}
};
this._emitEvent(event);
debug('_connect(): reject with stop discovery failed');
return Promise.reject('stop discovery failed');
});
},
/**
* The method will disconnect the input device with the device's adapter.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {BluetoothDevice} device
* @returns {Promise}
*/
disconnect: function btcm_disconnect(device) {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
return this._defaultAdapter.disconnect(device).then(() => {
debug('disconnect(): onsuccess(): resolve');
}, () => {
debug('disconnect(): onerror(): reject with disconnect failed');
return Promise.reject('disconnect failed');
});
},
/**
* The method will get all connected devices profiles
* which we are interested in. Profile: HFP, A2DP.
*
* @access private
* @memberOf BluetoothConnectionManager
* @returns {Promise} resolve: connectedDevices
* @returns {Promise} reject: reason
*/
_getConnectedDevicesFromPlatform:
function btcm__getConnectedDevicesFromPlatform() {
// Get connected device via profiles HFP, A2DP
return Promise.all(Object.keys(this.Profiles).map((profile) => {
return this._getConnectedDevicesByProfile(this.Profiles[profile]);
})).then((connectedDevices) => {
// Update each connected devices in map.
var collectedConnectedDevicesByProfile = {};
Object.keys(this.Profiles).forEach((profile, index) => {
collectedConnectedDevicesByProfile[profile] = connectedDevices[index];
});
debug('_getConnectedDevicesFromPlatform(): ' +
'collectedConnectedDevicesByProfile = ' +
JSON.stringify(collectedConnectedDevicesByProfile));
return Promise.resolve(collectedConnectedDevicesByProfile);
});
},
/**
* The method will get connected device by inputed profile.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} profileID
* @returns {Promise} resolve: the connected devices in array
* @returns {Promise} reject: reason
*/
_getConnectedDevicesByProfile:
function btcm__getConnectedDevicesByProfile(profileID) {
if (!this._defaultAdapter) {
debug('_getConnectedDevicesByProfile(): reject with no adapter');
return Promise.reject('default adapter is not existed!!');
}
if (this._defaultAdapter.state === 'disabled') {
debug('_getConnectedDevicesByProfile(): resolve with empty array ' +
'since it is impossible to connect with any device');
return Promise.reject('getConnectedDevices in disabled state');
}
return this._defaultAdapter.getConnectedDevices(profileID).then(
(connectedDevice) => {
debug('_getConnectedDevicesByProfile(): resolved with ' +
'connectedDevice = ' + JSON.stringify(connectedDevice));
return Promise.resolve(connectedDevice || []);
}, (reason) => {
debug('_getConnectedDevicesByProfile(): rejected with ' +
'reason = ' + reason);
return Promise.reject(reason);
});
},
/**
* Get device from paired devices by address.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} address
*/
_getPairedDeviceByAddress:
function btcm__getPairedDeviceByAddress(address) {
if (!this._defaultAdapter) {
return null;
}
var pairedDevices = this._defaultAdapter.getPairedDevices();
if (pairedDevices.length === 0) {
return null;
} else {
for (var i in pairedDevices) {
if (pairedDevices[i].address === address) {
return pairedDevices[i];
}
}
return null;
}
},
/**
* Find out there is any profile still connected.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.connectedDevices - connected devices info
* @param {String} options.address - the address of device
*/
_hasConnectedProfileByAddress:
function btcm__hasConnectedProfileByAddress(options) {
if (!options.connectedDevices[options.address]) {
return false;
}
var hasConnectedProfile = false;
for (var profileID in this.Profiles) {
var connectedProfiles =
options.connectedDevices[options.address].connectedProfiles;
if (connectedProfiles && (connectedProfiles[profileID] === true)) {
hasConnectedProfile = true;
}
}
return hasConnectedProfile;
},
/**
* A function to emit event to each registered listener by the input type.
*
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.type - type of event name
* @param {Object} options.detail - the object pass to the listener
*/
_emitEvent: function btcm__emitEvent(options) {
this._listeners[options.type].forEach(function(listener) {
listener(options);
});
},
/**
* The method will provide event listener for outer modules to regist.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {String} eventName
* @param {Function} callback
*/
addEventListener: function btcm_addEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
this._listeners[eventName].push(callback);
}
},
/**
* The method will provide event listener for outer modules to un-regist.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {String} eventName
* @param {Function} callback
*/
removeEventListener:
function btcm_removeEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
var index = this._listeners[eventName].indexOf(callback);
if (index >= 0) {
this._listeners[eventName].splice(index, 1);
}
}
}
};
BluetoothConnectionManager._init();
return BluetoothConnectionManager;
});

View file

@ -1 +0,0 @@
define(["require","modules/bluetooth/bluetooth_cod_mapper","modules/mvvm/observable"],function(e){"use strict";var t=e("modules/bluetooth/bluetooth_cod_mapper"),i=e("modules/mvvm/observable"),n=!1,o=function(){};n&&(o=function(e){console.log("--> [BluetoothDevice]: "+e)});var s=function(e){o("updateConnectionInfo(): this.address = "+this.address+", this.name = "+this.name+", options = "+JSON.stringify(e)),e.connectionStatus&&(this.connectionStatus=e.connectionStatus),e.profiles&&(this.profiles=Object.create(e.profiles)),o("updateConnectionInfo(): this.connectionStatus = "+this.connectionStatus+", this.profiles = "+JSON.stringify(this.profiles))},c=function(){if(o("updateDescriptionText():"),this.paired===!1)this.descriptionText="tapToConnect";else if("pairing"===this.paired)this.descriptionText="pairing";else if(this.paired===!0)if(this.descriptionText="paired","connecting"===this.connectionStatus)this.descriptionText="connecting";else if("connected"===this.connectionStatus)if(this.profiles){var e=this.profiles.hfp,t=this.profiles.a2dp;e&&t?this.descriptionText="connectedWithDeviceMedia":e?this.descriptionText="connectedWithDevice":t?this.descriptionText="connectedWithMedia":this.descriptionText="connectedWithNoProfileInfo"}else this.descriptionText="connectedWithNoProfileInfo";else"disconnected"===this.connectionStatus&&(this.descriptionText="disconnected");o("updateDescriptionText(): this.descriptionText = "+this.descriptionText)};return function(e){var n=t.getDeviceType(e.cod),r=e.connectionStatus||"disconnected",d=i({name:e.name,paired:e.paired,address:e.address,type:n,connectionStatus:r,profiles:null,descriptionText:"",get data(){return e},updateConnectionInfo:s,updateDescriptionText:c});return d._init=function(){this.observe("paired",this.updateDescriptionText.bind(this)),this.observe("connectionStatus",this.updateDescriptionText.bind(this)),this.observe("profiles",this.updateDescriptionText.bind(this))},e.onattributechanged=function(i){for(var n in i.attrs)switch(o("onDeviceAttributeChanged(): "+i.attrs[n]),i.attrs[n]){case"name":d.name=e.name;break;case"paired":o("onDeviceAttributeChanged(): device.paired = "+e.paired),d.paired=e.paired;break;case"cod":o("onDeviceAttributeChanged(): device.cod = "+e.cod),d.type=t.getDeviceType(e.cod)}},d._init(),d}});

View file

@ -1 +0,0 @@
define(["require","modules/mvvm/observable_array"],function(e){var n=new WeakMap,r=e("modules/mvvm/observable_array"),t=function(e,t,i){function o(e){var n=u.get(e);if(n&&"object"==typeof n){var r=u.get(n);r&&(r.forEach(function(e){n.unobserve(e)}),u["delete"](n),u["delete"](e))}}function l(e,n){var r=u.get(e)||[];u.has(e)&&(r=u.get(e)),r=Object.keys(n).map(function(r){return e.observe(r,n[r]),n[r](),n[r]}),u.set(e,r)}var a=null,s=e,c=i,f=!0,u=new WeakMap,v={observeAndCall:l},d=function(e){if(f){var n=e.data;switch(e.type){case"insert":b(n.index,n.items);break;case"remove":h(n.index,n.count);break;case"replace":m(n.index,n.newValue);break;case"reset":g(n.items||[])}}},b=function(e,n){if(!(n.length<=0))for(var r=s.children[e],t=n.length-1;t>=0;t--){var i=c(n[t],void 0,v);u.set(i,n[t]),s.insertBefore(i,r),r=i}},h=function(e,n){if(0!==n)if(n>=s.childElementCount)for(;s.firstElementChild;)o(s.firstElementChild),s.removeChild(s.firstElementChild);else for(var r=s.children[e],t=0;n>t&&r;t++){var i=r.nextElementSibling;o(r),s.removeChild(r),r=i}},m=function(e,n){var r=s.children[e];if(r){o(r);var t=c(n,r,v);u.set(r,n),t!==r&&(s.insertBefore(t,r),s.removeChild(r))}},g=function(e){var n=e.length,r=s.childElementCount;if(0===n)h(0,r);else if(r>=n)e.forEach(function(e,n){m(n,e)}),h(n,r-n);else{var t=e.slice(0,r),i=e.slice(r);t.forEach(function(e,n){m(n,e)}),b(r,i)}},y=function(){f&&g(a.array)},C={set:function(e){return a&&a.unobserve(d),e?(Array.isArray(e)&&(e=r(e)),a=e,a.observe("insert",d),a.observe("remove",d),a.observe("replace",d),a.observe("reset",d),void(this.enabled&&g(a.array))):void(a&&(h(0,a.length),a=null))},destroy:function(){a&&a.unobserve(d),h(0,a.length),n["delete"](s),s=null,a=null,c=null,f=!1},get element(){return s},set enabled(e){f!==e&&(f=e,y())},get enabled(){return f}};return n.has(s)&&n.get(s).destroy(),n.set(s,C),s.innerHTML="",C.set(t),C};return t});

View file

@ -1 +0,0 @@
define([],function(){function n(n,e,r){function t(e,r){var t=n[r];if(t){var i=t.indexOf(e);i>=0&&t.splice(i,1)}}"function"==typeof e?Object.keys(n).forEach(t.bind(null,e)):r?t(r,e):e in n&&(n[e]=[])}function e(e){var r={},t={observe:function(n,e){var t=r[n];t&&t.push(e)},unobserve:n.bind(null,r)},i=function(n){return function(){return t["_"+n]}},f=function(n){return function(e){var i=t["_"+n];if(i!==e){t["_"+n]=e;var f=r[n];f.forEach(function(n){n(e,i)})}}};for(var u in e)"function"!=typeof e[u]?(r[u]=[],Object.defineProperty(t,"_"+u,{value:e[u],writable:!0}),Object.defineProperty(t,u,{enumerable:!0,get:i(u),set:f(u)})):t[u]=e[u];return t}return e});

View file

@ -1 +0,0 @@
define([],function(){function n(n,e,t){function r(e,t){var r=n[t];if(r){var i=r.indexOf(e);i>=0&&r.splice(i,1)}}"function"==typeof e?Object.keys(n).forEach(r.bind(null,e)):t?r(t,e):e in n&&(n[e]=[])}function e(e){var t=e||[],r={insert:[],remove:[],replace:[],reset:[]},i=function(n,e){var t=r[n];t.forEach(function(t){t({type:n,data:e})})};return{get length(){return t.length},get array(){return t},forEach:function(n){t.forEach(n)},observe:function(n,e){var t=r[n];t&&t.push(e)},unobserve:n.bind(null,r),push:function(n){t.push(n),i("insert",{index:t.length-1,count:1,items:[n]})},pop:function(){if(t.length){var n=t.pop();return i("remove",{index:t.length,count:1}),n}},splice:function(n,e){if(!(arguments.length<2)){var r=Array.prototype.slice.call(arguments,2);t.splice.apply(t,arguments),e&&i("remove",{index:n,count:e}),r.length&&i("insert",{index:n,count:r.length,items:r})}},set:function(n,e){if(!(0>n||n>=t.length)){var r=t[n];t[n]=e,i("replace",{index:n,oldValue:r,newValue:e})}},get:function(n){return t[n]},reset:function(n){t=n,i("reset",{items:t})}}}return e});

View file

@ -1 +0,0 @@
define([],function(){"use strict";return navigator.mozBluetooth?navigator.mozBluetooth:null});

View file

@ -1,430 +0,0 @@
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global NotificationHelper */
define(['require','modules/bluetooth/bluetooth_adapter_manager','modules/bluetooth/bluetooth_context','views/pair_expired_dialog'],function(require) {
'use strict';
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
var BtContext = require('modules/bluetooth/bluetooth_context');
var PairExpiredDialog = require('views/pair_expired_dialog');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btam_debug(msg) {
console.log('--> [PairManager]: ' + msg);
};
}
/*
* PairManager is responsible for:
* 1. Handling system message 'bluetooth-pairing-request' while there is an
* incoming/outgoing pairing request.
* 2. Handling dom event from BluetoothPairingListener while there is an
* incoming/outgoing pairing request.
* 3. Handling dom event 'ondeviceunpaired' while some remote devices
* request for canceling an overdue pairing request. The reason could be
* authentication fails, remote device down, and internal error happens.
*/
var PairManager = {
/**
* Default adapter of Bluetooth.
*
* @access private
* @memberOf PairManger
* @type {Object} BluetoothAdapter
*/
_defaultAdapter: null,
/**
* Indicate if the pair request is expired
* @type {Boolean}
*/
_isExpired: false,
init: function() {
// Observe 'defaultAdapter' property for reaching default adapter.
AdapterManager.observe('defaultAdapter',
this._onDefaultAdapterChanged.bind(this));
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
// Watch pairing events.
this._watchOndisplaypasskeyreq();
this._watchOnenterpincodereq();
this._watchOnpairingconfirmationreq();
this._watchOnpairingconsentreq();
this._watchOnpairingaborted();
navigator.mozSetMessageHandler('bluetooth-pairing-request',
this._onRequestPairingFromSystemMessage.bind(this)
);
// Observe screen lockscreen locked/unlocked state for show pending
// pairing request immediately.
navigator.mozSettings.addObserver('lockscreen.locked',
function gotScreenLockedChanged(event) {
this.showPendingPairing(event.settingValue);
}.bind(this));
// Observe Bluetooth 'enabled' property from hardware side.
// Then, close pairing dialog immediately.
BtContext.observe('enabled', (enabled) => {
if (!enabled) {
this.onBluetoothDisabled();
}
});
},
/**
* 'defaultAdapter' change event handler from adapter manager for
* updating it immediately.
*
* @access private
* @memberOf PairManager
* @param {Object} BluetoothAdapter newAdapter
*/
_onDefaultAdapterChanged: function(newAdapter) {
// save default adapter
this._defaultAdapter = newAdapter;
},
/**
* Watch 'ondisplaypasskeyreq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests to display
* passkey on the screen during pairing process.
*
* @access private
* @memberOf PairManager
*/
_watchOndisplaypasskeyreq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOndisplaypasskeyreq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener('displaypasskeyreq',
(evt) => {
debug('_watchOndisplaypasskeyreq()');
this._handlePairingRequest({
method: 'displaypasskey',
evt: evt
});
});
},
/**
* Watch 'onenterpincodereq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests user enter
* PIN code during pairing process.
*
* @access private
* @memberOf PairManager
*/
_watchOnenterpincodereq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOnenterpincodereq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener('enterpincodereq',
(evt) => {
debug('_watchOnenterpincodereq()');
this._handlePairingRequest({
method: 'enterpincode',
evt: evt
});
});
},
/**
* Watch 'onpairingconfirmationreq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests user
* confirm passkey during pairing process. Applications may prompt passkey
* to user for confirmation, or confirm the passkey for user proactively.
*
* @access private
* @memberOf PairManager
*/
_watchOnpairingconfirmationreq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOnpairingconfirmationreq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener(
'pairingconfirmationreq', (evt) => {
debug('_watchOnpairingconfirmationreq: evt = ' + JSON.stringify(evt));
this._handlePairingRequest({
method: 'confirmation',
evt: evt
});
});
},
/**
* Watch 'onpairingconsentreq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests user
* confirm pairing during pairing process. Applications may prompt user
* for confirmation or confirm for user proactively.
*
* @access private
* @memberOf PairManager
*/
_watchOnpairingconsentreq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOnpairingconsentreq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener('pairingconsentreq',
(evt) => {
debug('_watchOnpairingconsentreq: evt = ' + JSON.stringify(evt));
this._handlePairingRequest({
method: 'consent',
evt: evt
});
});
},
/**
* Watch 'onpairingaborted' dom event for pairing aborted.
* A handler to trigger when pairing fails due to one of
* following conditions:
* - authentication fails
* - remote device down (bluetooth ACL becomes disconnected)
* - internal error happens
*
* @access private
* @memberOf PairManager
*/
_watchOnpairingaborted: function() {
if (!this._defaultAdapter) {
return;
}
this._defaultAdapter.addEventListener('pairingaborted',
this._onPairingAborted.bind(this));
},
/**
* A handler to handle 'onpairingaborted' event while it's coming.
*
* @access private
* @memberOf PairManager
*/
_onPairingAborted: function(evt) {
debug('_onPairingAborted(): evt = ' + JSON.stringify(evt));
// if the attention screen still open, close it
if (this.childWindow) {
this.childWindow.Pairview.closeInput();
this.childWindow.close();
}
// If "onpairingaborted" event is coming, and there is a pending pairing
// request, we have reset the object. Because the callback is useless
// since the older pairing request is expired.
// The moment is fit to notify user that the later pairing request
// is timeout or canceled. According to user story, do nothing here.
// Clear up the pending pairing request only.
if (this.pendingPairing) {
this.pendingPairing = null;
}
// Close pairing expired dialog if it is showing.
if (PairExpiredDialog.isVisible) {
PairExpiredDialog.close();
// Have to close Bluetooth app after the dialog is closed.
window.close();
}
},
/**
* Receive the system message event for launch Bluetooth app here.
*
* @access private
* @memberOf PairManager
*/
_onRequestPairingFromSystemMessage: function() {
debug('onRequestPairingFromSystemMessage():');
},
/**
* It is used to handle each pairing request from different pairing methods.
*
* @memberOf PairManager
* @access private
* @param {Object} pairingInfo
* @param {Object} pairingInfo.method - method of this pairing request
* @param {Object} pairingInfo.evt - DOM evt of this pairing request
*/
_handlePairingRequest: function(pairingInfo) {
debug('_onRequestPairing():' +
' pairingInfo.method = ' + pairingInfo.method +
' pairingInfo.evt = ' + pairingInfo.evt);
var req = navigator.mozSettings.createLock().get('lockscreen.locked');
req.onsuccess = () => {
if (req.result['lockscreen.locked']) {
// notify user that we are receiving pairing request
this.fireNotification(pairingInfo);
} else {
// We have clear up pending one before show the new pairing requst.
// Becasue the pending pairing request is no longer usefull.
this.cleanPendingPairing();
// show pair view directly while lock screen is unlocked
this.showPairview(pairingInfo);
}
};
req.onerror = () => {
// We have clear up pending one before show the new pairing requst.
// Becasue the pending pairing request is no longer usefull.
this.cleanPendingPairing();
// fallback to default value 'unlocked'
this.showPairview(pairingInfo);
};
},
fireNotification: function(pairingInfo) {
// Once we received a pairing request in screen locked mode,
// overwrite the object with the latest pairing request. Because the
// older pairing request might be timeout and useless now.
this.pendingPairing = {
showPairviewCallback: this.showPairview.bind(this, pairingInfo)
};
// Prepare notification toast.
var titleL10n = 'bluetooth-pairing-request-now-title';
var bodyL10n = pairingInfo.evt.deviceName ?
{ raw: pairingInfo.evt.deviceName } :
'unnamed-device';
var iconUrl =
'app://bluetooth.gaiamobile.org/style/images/icon_bluetooth.png';
// We always use tag "pairing-request" to manage these notifications.
var notificationId = 'pairing-request';
return NotificationHelper.send(titleL10n, {
bodyL10n: bodyL10n,
icon: iconUrl,
tag: notificationId,
closeOnClick: false
}).then(notification => {
// set onclick handler for the notification
notification.onclick =
this.pairingRequestExpiredNotificationHandler.bind(this,
notification);
});
},
// According to user story, it won't notify user again
// while the pending pairing request is just timeout or canceled.
// So we have to set onclick handler in this moment.
// The handler will pop out a pairing request expired prompt only.
pairingRequestExpiredNotificationHandler: function(notification) {
var req = navigator.mozSettings.createLock().get('lockscreen.locked');
req.onsuccess = () => {
// Avoid to do nothting while the notification toast is showing
// and a user is able to trigger onclick event. Make sure screen
// is unlocked, then show the prompt in bluetooth app.
if (!req.result['lockscreen.locked']) {
// Clean the pairing request notficiation which is expired.
notification.close();
navigator.mozApps.getSelf().onsuccess = (evt) => {
var app = evt.target.result;
// launch bluetooth app to foreground for showing the prompt
if (PairExpiredDialog.isVisible) {
app.launch();
}
// show an alert with the overdue message
if (!PairExpiredDialog.isVisible && this._isExpired) {
debug('show expired dialog');
PairExpiredDialog.showConfirm(function() {
// Have to close Bluetooth app after the dialog is closed.
window.close();
});
}
};
}
};
},
// If there is a pending pairing request while a user just unlocks screen,
// we will show pair view immediately. Then, we clear up the notification.
// If pendingPairing object is not exist, it means pair request is expired.
showPendingPairing: function(screenLocked) {
debug('showPendingPairing when screenLocked:' + screenLocked);
if (!screenLocked) {
if (this.pendingPairing) {
this._isExpired = false;
// show pair view from the callback function
if (this.pendingPairing.showPairviewCallback) {
this.pendingPairing.showPairviewCallback();
}
this.cleanPendingPairing();
} else {
this._isExpired = true;
}
}
},
cleanPendingPairing: function() {
debug('cleanPendingPairing(): has pendingPairing = ' +
(this.pendingPairing));
// Clear up the pending pairing request
if (this.pendingPairing) {
this.pendingPairing = null;
}
// Clear up the pairing request from notification.
this.cleanNotifications();
},
// Clean all notifications which are fired with tag 'pairing-request'.
cleanNotifications: function() {
Notification.get().then(function(notifications) {
if (notifications) {
notifications.forEach(function(notification) {
// Compare tags, as the tag is based on the "pairing-request" and
// we only have one notification for the request. Plus, there
// is no "id" field on the notification.
if (notification.tag === 'pairing-request' &&
notification.close) {
notification.close();
}
});
}
});
},
showPairview: function(pairingInfo) {
debug('showPairview(): pairingInfo = ' + pairingInfo);
var protocol = window.location.protocol;
var host = window.location.host;
this.childWindow = window.open(protocol + '//' + host + '/onpair.html',
'pair_screen', 'attention');
this.childWindow.onload = () => {
this.childWindow.Pairview.init(pairingInfo.method, pairingInfo.evt);
};
},
onBluetoothDisabled: function() {
debug('onBluetoothDisabled():');
// if the attention screen still open, close it
if (this.childWindow) {
this.childWindow.Pairview.closeInput();
this.childWindow.close();
}
// Since Bluetooth is off, close itself.
window.close();
}
};
return PairManager;
});

View file

@ -1,349 +0,0 @@
/*
* TransferManager is responsible for:
* - Handling system message 'activity' while there is an activity request
* incoming. Then, it will post result/error to requesting app.
* - Watching Bluetooth enabled/disabled state, then show/hide turn Bluetooth
* on dialog.
* - Watching default adapter existed or not. And will post error if we cannot
* get default adapter.
* - Watching 'devicePicked', 'cancelSelection' events from DevicePickerPanel
* module. Then, do corrected activity service flow regarding user decision.
*
* @module BluetoothContext
*/
define(['require','modules/bluetooth/bluetooth_adapter_manager','modules/bluetooth/bluetooth_context','views/cannot_transfer_dialog','views/device_picker_panel','views/turn_bluetooth_on_dialog'],function(require) {
'use strict';
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
var BtContext = require('modules/bluetooth/bluetooth_context');
var CannotTransferDialog = require('views/cannot_transfer_dialog');
var DevicePickerPanel = require('views/device_picker_panel');
var TurnBluetoothOnDialog = require('views/turn_bluetooth_on_dialog');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function bttm_debug(msg) {
console.log('--> [TransferManager]: ' + msg);
};
}
var TransferManager = {
/**
* An instance to access activity source.
*
* @access private
* @memberOf TransferManager
* @type {Object}
*/
_activity: null,
/**
* The init function only watches the 'activity' event from system message.
*
* @access private
* @memberOf TransferManager
*/
init: function bttm_init() {
// Watch 'activity' event from system message.
this._watchActivityRequest();
},
/**
* Watch 'activity' event with callback function from system message.
*
* @access private
* @memberOf TransferManager
*/
_watchActivityRequest: function bttm__watchActivityRequest() {
navigator.mozSetMessageHandler('activity',
this._activityHandler.bind(this));
},
/**
* Handle 'activity' request. Then make decision to service the request or
* reject it.
*
* @access private
* @memberOf TransferManager
* @param {Object} activityRequest
*/
_activityHandler: function bttm__activityHandler(activityRequest) {
debug('_activityHandler(): activityRequest = ' +
JSON.stringify(activityRequest));
if (activityRequest.source.data.blobs &&
activityRequest.source.data.blobs.length > 0) {
this._activity = activityRequest;
// Source info is enough to servie file transfer.
// Start to init and observe Bluetooth relative modules.
this._observeDefaultAdapter();
this._observeBluetoothEnabled();
this._watchEventFromDevicePicker();
DevicePickerPanel.visible = true;
} else {
debug('_activityHandler(): Cannot transfer without blobs data!');
this._cannotTransfer();
}
},
/**
* Show alert dialog. End the activity service while a user confirm to close
* the dialog.
*
* @access private
* @memberOf TransferManager
*/
_cannotTransfer: function bttm__cannotTransfer() {
debug('_cannotTransfer(): show alert dialog!!');
// show an alert with the overdue message
if (!CannotTransferDialog.isVisible) {
CannotTransferDialog.showConfirm().then(() => {
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
});
}
},
/**
* Post result/error to end the activity service.
*
* @access private
* @memberOf TransferManager
*/
_endTransferWithReason: function bttm__endTransferWithReason(reason) {
debug('_endTransferWithReason(): reason = ' + reason);
if (reason === 'transferred') {
this._activity.postResult(reason);
} else if (reason === 'cancelled') {
this._activity.postError(reason);
}
this._activity = null;
},
/**
* Observe and get default adapter from bluetooth adapter manager.
*
* @access private
* @memberOf TransferManager
*/
_observeDefaultAdapter: function bttm__observeDefaultAdapter() {
// Observe 'defaultAdapter' property for reaching default adapter.
AdapterManager.observe('defaultAdapter',
this._onDefaultAdapterChanged.bind(this));
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
},
/**
* Observe and get 'enabled' property from bluetooth context.
*
* @access private
* @memberOf TransferManager
*/
_observeBluetoothEnabled: function bttm__observeBluetoothEnabled() {
// Observe Bluetooth 'enabled' property from hardware side.
BtContext.observe('enabled', this._onBluetoothEnabledChanged.bind(this));
this._onBluetoothEnabledChanged(BtContext.enabled);
},
/**
* 'defaultAdapter' change event handler from adapter manager for
* updating it immediately.
*
* @access private
* @memberOf TransferManager
* @param {Object} BluetoothAdapter newAdapter
* @param {Object} BluetoothAdapter oldAdapter
*/
_onDefaultAdapterChanged:
function bttm__onDefaultAdapterChanged(newAdapter, oldAdapter) {
if (oldAdapter && (newAdapter === null)) {
debug('_onDefaultAdapterChanged(): Can not get bluetooth adapter!');
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
}
},
/**
* 'enabled' change event handler from bluetooth context for
* updating UI flow immediately.
*
* @access private
* @memberOf TransferManager
* @param {Boolean} enabled
*/
_onBluetoothEnabledChanged:
function bttm__onBluetoothEnabledChanged(enabled) {
debug('_onBluetoothEnabledChanged(): enabled = ' + enabled);
switch(enabled) {
case undefined:
break;
case true:
// Close the confirmation dialog
TurnBluetoothOnDialog.close();
break;
default:
TurnBluetoothOnDialog.showConfirm().then((result) => {
if (result === 'confirm') {
// Turn Bluetooth on.
BtContext.setEnabled(true);
} else if (result === 'cancel') {
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
}
});
break;
}
},
/**
* Watch 'devicePicked', 'cancelSelection' event from device picker panel.
*
* @access private
* @memberOf TransferManager
*/
_watchEventFromDevicePicker: function bttm__watchEventFromDevicePicker() {
debug('_watchEventFromDevicePicker():');
// Watch 'devicePicked' event for reaching device picked from user.
DevicePickerPanel.addEventListener('devicePicked',
this._onDevicePicked.bind(this));
// Watch 'cancelSelection' event for reaching cancel selection from user.
DevicePickerPanel.addEventListener('cancelSelection',
this._onCancelSelection.bind(this));
},
/**
* While the device is picked from user, let's send file(s) to the device.
*
* @access private
* @memberOf TransferManager
* @param {Object} event
* @param {String} event.type - type of event name
* @param {Object} event.detail - device info in this object
* @param {Object} event.detail.address - address of device
* @returns {Promise}
*/
_onDevicePicked: function bttm__onDevicePicked(event) {
// Send file to the device via address
debug('_onDevicePicked(): address of target device = ' +
event.detail.address);
var targetDeviceAddress = event.detail.address;
var blobs = this._activity.source.data.blobs;
// Produce sending files schedule.
// Then, post message to system app for sending files in queue.
var schedule = this._produceSendingFilesSchedule(blobs.length);
this._postMessageToSystemApp(schedule);
// Send each file via Bluetooth sendFile API
return Promise.all(blobs.map((blob, index) => {
/**
* Check blob.name because sendFile() API needs a "file"
* object and a filaname before sending it.
* If there is no filename in the blob, Bluetooth API will give a
* default name "Unknown.jpeg".
* So Bluetooth app has to find out the name via device storage.
*/
if (blob.name) {
// The blob has name, send the blob directly.
debug('blob is sending with name...');
return BtContext.sendFile(targetDeviceAddress, blob);
} else if (this._activity.source.data.filepaths) {
// The blob does not have name,
// browse the file via filepath from storage again.
var filepath = this._activity.source.data.filepaths[index];
var storage = navigator.getDeviceStorage('sdcard');
return storage.get(filepath).then((file) => {
debug('getFile succeed & file is sending... file = ' + file);
return BtContext.sendFile(targetDeviceAddress, file);
}, (error) => {
debug('getFile failed so that blob is sending without filename ' +
error && error.name);
return BtContext.sendFile(targetDeviceAddress, blob);
});
} else {
// The blob does not have name and filepath,
// pass the blob without name to send file directly.
debug('no filepath to get from device storage ' +
'so that blob is sending without filename');
return BtContext.sendFile(targetDeviceAddress, blob);
}
})).then(() => {
debug('all files are already sent out..');
// Post result with reason then end the activity service.
this._endTransferWithReason('transferred');
return Promise.resolve();
});
},
/**
* While a user cancel selection, let's post result then end the activity
* service.
*
* @access private
* @memberOf TransferManager
* @param {Object} event
* @param {String} event.type - type of event name
*/
_onCancelSelection: function bttm__onCancelSelection(event) {
debug('_onCancelSelection(): cancel from user decision');
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
},
/**
* A function is a role producer.
* Bluetooth app produce one message for each sending file request.
*
* @access private
* @memberOf TransferManager
* @param {Number} numberOfTasks - number of tasks
* @returns {Object}
*/
_produceSendingFilesSchedule:
function bttm__produceSendingFilesSchedule(numberOfTasks) {
// Construct a object to contain info of sending files schedule.
// And the info will be posted message to system app.
// The result of send files will be displayed on notification.
var sendingFilesSchedule = {
numberOfFiles: numberOfTasks,
numSuccessful: 0,
numUnsuccessful: 0
};
return sendingFilesSchedule;
},
/**
* A function to do inner app communcation.
*
* @access private
* @memberOf TransferManager
* @param {Object} message - the info of sending files schedule
*/
_postMessageToSystemApp:
function bttm__postMessageToSystemApp(message) {
// Set up Inter-App Communications
var getRequest = navigator.mozApps.getSelf();
getRequest.onsuccess = (evt) => {
var app = evt.target.result;
// If IAC doesn't exist, just bail out.
if (!app.connect) {
debug('_postMessageToSystemApp(): Cannot post message since no ' +
'connect function, message = ' + JSON.stringify(message));
return;
}
// Post message object to system app.
app.connect('bluetoothTransfercomms').then((ports) => {
ports.forEach((port) => {
port.postMessage(message);
debug('_postMessageToSystemApp(): sending files schedule = ' +
JSON.stringify(message));
});
});
};
}
};
return TransferManager;
});

File diff suppressed because one or more lines are too long

View file

@ -1,16 +0,0 @@
require(['config/require'], function() {
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btsp_debug(msg) {
console.log('--> [startup_pair]: ' + msg);
};
}
require(['modules/pair_manager'], function(PairManager) {
debug('Load pair manager with API version 2. Loaded!!');
navigator.mozL10n.once(() => PairManager.init());
});
});

View file

@ -1,16 +0,0 @@
require(['config/require'], function() {
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btst_debug(msg) {
console.log('--> [startup_transfer]: ' + msg);
};
}
require(['modules/transfer_manager'], function(TransferManager) {
debug('Load transfer manager with API version 2. Loaded!!');
navigator.mozL10n.once(() => TransferManager.init());
});
});

View file

@ -1 +0,0 @@
;function getTruncated(t,e){function i(t,e){return t!==e}var n=e.maxLine||2,l=e.node,o=e.ellipsisIndex||3,r=e.ellipsisCharacter||"...";if(null===l)return t;var s,a,c,u,d,y,h="",v=!0,p=[],f=t.slice(-o),g=l.style.visibility,x=l.style.wordBreak;l.style.visibility="hidden",l.style.wordBreak="break-all",l.textContent=".",c=l.clientHeight,l.textContent="";var C=t.split("").some(function(t,e){if(p.push(t),y=p.join(""),s=l.clientHeight,l.textContent=y+r+f,a=l.clientHeight,0===e&&(u=Math.floor(a/c)),i(s,a)&&0!==e){var o;l.textContent=y,o=l.clientHeight,i(s,o)?(v=!0,u+=1):v&&(d=e,v=!1)}return u>n?(0===e&&console.log("Your string is in a overflowed situation, please check your options"),p.pop(),l.textContent="",!0):void 0});return l.style.visibility=g,l.style.wordBreak=x,C?(h+=p.join("").slice(0,d),h+=r,h+=f):h=t,h}

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
define([],function(){"use strict";function e(e,a,n){var c=n,r=document.createElement("span");t(r,c.name);var s=document.createElement("small");"remote"===e&&s.setAttribute("data-l10n-id","device-status-tap-connect");var o=document.createElement("li"),u=document.createElement("a");if(o.classList.add("bluetooth-device"),d("device.type = "+c.type),o.classList.add("bluetooth-type-"+c.type),i(o,s,c.descriptionText,e),u.appendChild(r),u.appendChild(s),o.appendChild(u),"remote"===e){var l=document.createElement("progress");l.classList.add("overlapping-icon"),l.classList.add("hidden"),o.appendChild(l)}return"function"==typeof a&&(o.onclick=function(){a(n)}),c.observe("name",function(e){t(r,e)}),c.observe("descriptionText",function(t){i(o,s,t,e)}),o}function t(e,t){""!==t?(e.removeAttribute("data-l10n-id"),e.textContent=t):e.setAttribute("data-l10n-id","unnamed-device")}function i(e,t,i,a){switch(d("_updateItemDescriptionText(): descriptionText = "+i),i){case"tapToConnect":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-tap-connect"),"remote"===a&&(e.querySelector("progress").classList.add("hidden"),e.classList.remove("icon-hidden"));break;case"pairing":e.setAttribute("aria-disabled",!0),t.setAttribute("data-l10n-id","device-status-pairing"),"remote"===a&&(e.classList.add("icon-hidden"),e.querySelector("progress").classList.remove("hidden"));break;case"paired":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-paired"),"remote"===a&&(e.querySelector("progress").classList.add("hidden"),e.classList.remove("icon-hidden"));break;case"connecting":e.setAttribute("aria-disabled",!0),t.setAttribute("data-l10n-id","device-status-connecting");break;case"connectedWithDeviceMedia":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected-device-media");break;case"connectedWithDevice":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected-device");break;case"connectedWithMedia":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected-media");break;case"connectedWithNoProfileInfo":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected");break;case"disconnected":e.removeAttribute("aria-disabled"),t.removeAttribute("data-l10n-id"),t.textContent=""}}var a=!1,d=function(){};return a&&(d=function(e){console.log("--> [BluetoothTemplateFactory]: "+e)}),function(t,i){return e.bind(null,t,i)}});

View file

@ -1,37 +0,0 @@
/*
* CannotTransferDialog is responsible for:
* Alert a user that the activity request is not able to transfer via
* Bluetooth with some reason. There is an alert handling via here.
*/
define([],function() {
'use strict';
var CannotTransferDialog = {
init: function() {
this.dialog = document.getElementById('alert-view');
this.confirmBtn =
document.getElementById('alert-button-ok');
},
showConfirm: function() {
return new Promise((resolve) => {
this.confirmBtn.onclick = () => {
this.close();
resolve();
};
this.dialog.hidden = false;
});
},
get isVisible() {
return (!this.dialog.hidden);
},
close : function() {
this.dialog.hidden = true;
}
};
CannotTransferDialog.init();
return CannotTransferDialog;
});

View file

@ -1,413 +0,0 @@
/*
* DevicePickerPanel is responsible for:
* - Render paired/found devices list in this panel.
* - Emit 'devicePicked' event with device address while a user click on a
* paired device.
* - Pair with the picked device while a user click on a found device.
* Emit 'devicePicked' event with device address until the picked device
* is pared.
* - Emit 'cancelSelection' event while a user click close button on the
* header for leaving.
*
* @module DevicePickerPanel
*/
define(['require','modules/bluetooth/bluetooth_context','views/bt_template_factory','modules/mvvm/list_view'],function(require) {
'use strict';
var BtContext = require('modules/bluetooth/bluetooth_context');
var BtTemplateFactory = require('views/bt_template_factory');
var ListView = require('modules/mvvm/list_view');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btdpp_debug(msg) {
console.log('--> [DevicePickerPanel]: ' + msg);
};
}
var DevicePickerPanel = {
/**
* A instance to maintain paired devices list view.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object}
*/
_pairedDevicesListView: null,
/**
* A instance to maintain found devices list view.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object}
*/
_foundDevicesListView: null,
/**
* Construct elements.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object}
*/
_elements: {
view: {
viewDevicePicker: document.querySelector('.devices-list-view')
},
header: {
headerActionClose: document.querySelector('.devices-list-header')
},
paired: {
pairedDevicesHeader: document.querySelector('.bluetooth-paired-title'),
pairedDevicesList: document.querySelector('.bluetooth-paired-devices')
},
found: {
foundDevicesHeader: document.querySelector('.bluetooth-found-title'),
foundDevicesList: document.querySelector('.bluetooth-devices')
},
search: {
searchingItem: document.querySelector('.bluetooth-searching'),
searchItem: document.querySelector('.bluetooth-search'),
searchButton: document.querySelector('.search-device')
}
},
/**
* The object maintains listeners' callback per property name.
* Each listener would be called as following definition.
* 'devicePicked' - be called when device is picked and paired successfully.
* 'cancelSelection': - be called when user click on close button on header.
*
* @memberOf DevicePickerPanel
* @type {Object}
*/
_listeners: {
'devicePicked': [],
'cancelSelection': []
},
/**
* Default adapter of Bluetooth.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object} BluetoothAdapter
*/
_defaultAdapter: null,
/**
* The function init the paired/found devices list and search button.
*
* @access private
* @memberOf DevicePickerPanel
*/
_init: function btdpp__init() {
// Bind click event for header.
this._elements.header.headerActionClose.addEventListener('action',
this._onHeaderActionCloseClick.bind(this));
// Bind click event for search button.
this._elements.search.searchButton.addEventListener('click',
this._onSearchButtonClick.bind(this));
// Bind click event for paired device item.
var pairedDeviceTemplate =
BtTemplateFactory('paired', this._onPairedDeviceItemClick.bind(this));
// Create paired devices list view.
this._pairedDevicesListView =
ListView(this._elements.paired.pairedDevicesList,
BtContext.getPairedDevices(),
pairedDeviceTemplate);
// Bind click event for found device item.
var foundDeviceTemplate =
BtTemplateFactory('remote', this._onFoundDeviceItemClick.bind(this));
// Create found devices list view.
this._foundDevicesListView =
ListView(this._elements.found.foundDevicesList,
BtContext.getRemoteDevices(),
foundDeviceTemplate);
// Init items state.
this._initItemsState();
// search devices automatically while the panel is inited.
BtContext.startDiscovery().then(() => {
debug('_init(): startDiscovery successfully');
}, (reason) => {
debug('_init(): startDiscovery failed, ' +
'reason = ' + reason);
});
},
/**
* The function init these elements which are displayed in this panel.
*
* @access private
* @memberOf DevicePickerPanel
*/
_initItemsState: function btdpp__initItemsState() {
// paired devices header
BtContext.observe('hasPairedDevice',
this._updatePairedDevicesHeader.bind(this));
this._updatePairedDevicesHeader(BtContext.hasPairedDevice);
// paired devices list
BtContext.observe('state', this._updatePairedDevicesList.bind(this));
this._updatePairedDevicesList(BtContext.state);
// found devices list
BtContext.observe('state', this._updateFoundDevicesList.bind(this));
this._updateFoundDevicesList(BtContext.state);
this._pairedDevicesListView.enabled = true;
this._foundDevicesListView.enabled = true;
// search
BtContext.observe('state', this._updateSearchItem.bind(this));
this._updateSearchItem(BtContext.state);
BtContext.observe('discovering', this._updateSearchingItem.bind(this));
this._updateSearchingItem(BtContext.discovering);
},
/**
* Start discovery devices while the search button is clicked.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onHeaderActionCloseClick: function btdpp__onHeaderActionCloseClick() {
debug('_onHeaderActionCloseClick():');
// Emit 'cancelSelection' event to outer modules.
var event = {
type: 'cancelSelection'
};
this._emitEvent(event);
},
/**
* Start discovery devices while the search button is clicked.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onSearchButtonClick: function btdpp__onSearchButtonClick() {
debug('_onSearchButtonClick():');
BtContext.startDiscovery().then(() => {
debug('_onSearchButtonClick(): startDiscovery successfully');
}, (reason) => {
debug('_onSearchButtonClick(): startDiscovery failed, ' +
'reason = ' + reason);
});
},
/**
* While the paired device is clicked, emit event to outer modules with
* the device item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onPairedDeviceItemClick:
function btdpp__onPairedDeviceItemClick(deviceItem) {
debug('_onPairedDeviceItemClick(): deviceItem.address = ' +
deviceItem.address);
debug('_onPairedDeviceItemClick(): deviceItem.paired = ' +
deviceItem.paired);
// Emit 'devicePicked' event to outer modules with the device item.
var event = {
type: 'devicePicked',
detail: {
address: deviceItem.address
}
};
this._emitEvent(event);
},
/**
* Pair with the clicked device. While the device is paired successfully,
* emit event to outer modules with the device item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onFoundDeviceItemClick:
function btdpp__onFoundDeviceItemClick(deviceItem) {
debug('_onFoundDeviceItemClick(): deviceItem.address = ' +
deviceItem.address);
// Update device pairing status first.
deviceItem.paired = 'pairing';
// Pair with the remote device.
BtContext.pair(deviceItem.address).then(() => {
debug('_onFoundDeviceItemClick(): pair successfully');
// Emit 'devicePicked' event to outer modules with the
// paired device item.
var event = {
type: 'devicePicked',
detail: {
address: deviceItem.address
}
};
this._emitEvent(event);
}, (reason) => {
debug('_onFoundDeviceItemClick(): pair failed, ' +
'reason = ' + reason);
// Reset the paired status back to false,
// since the 'pairing' status is given in Gaia side.
deviceItem.paired = false;
// Show alert message while pair device failed.
this._alertPairFailedErrorMessage(reason);
});
},
/**
* Show an alert dialog since the device is failed in pairing process.
*
* @access private
* @memberOf DevicePickerPanel
*/
_alertPairFailedErrorMessage:
function btdpp__alertPairFailedErrorMessage(errorReason) {
var _ = navigator.mozL10n.get;
var msg = _('error-pair-title'); // This is a default message.
if (errorReason === 'Repeated Attempts') {
msg = msg + '\n' + _('error-pair-toofast');
} else if (errorReason === 'Authentication Failed') {
msg = msg + '\n' + _('error-pair-pincode');
}
window.alert(msg);
},
/**
* A function to show/hide paired devices header.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updatePairedDevicesHeader:
function btdpp__updatePairedDevicesHeader(hasPairedDevice) {
debug('_updatePairedDevicesHeader(): ' +
'callback from observe "hasPairedDevice" = ' + hasPairedDevice);
this._elements.paired.pairedDevicesHeader.hidden =
!(hasPairedDevice && (BtContext.state === 'enabled'));
},
/**
* A function to show/hide paired devices list.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updatePairedDevicesList: function btdpp__updatePairedDevicesList(state) {
debug('_updatePairedDevicesList(): ' +
'callback from observe "state" = ' + state);
this._elements.paired.pairedDevicesHeader.hidden =
!((state === 'enabled') && (BtContext.hasPairedDevice));
this._elements.paired.pairedDevicesList.hidden = (state !== 'enabled');
},
/**
* A function to show/hide found devices list.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updateFoundDevicesList: function btdpp__updateFoundDevicesList(state) {
debug('_updateFoundDevicesList(): ' +
'callback from observe "state" = ' + state);
this._elements.found.foundDevicesHeader.hidden = (state !== 'enabled');
this._elements.found.foundDevicesList.hidden = (state !== 'enabled');
},
/**
* A function to show/hide search item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updateSearchItem: function btdpp__updateSearchItem(state) {
debug('_updateSearchItem(): ' +
'callback from observe "state" = ' + state);
this._elements.search.searchItem.hidden = (state !== 'enabled');
},
/**
* A function to show/hide searching description item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updateSearchingItem: function btdpp__updateSearchingItem(discovering) {
debug('_updateSearchingItem(): ' +
'callback from observe "discovering" = ' + discovering);
this._elements.search.searchingItem.hidden = !discovering;
this._elements.search.searchButton.disabled = discovering;
},
/**
* A function to emit event to each registered listener by the input type.
*
* @memberOf DevicePickerPanel
* @param {Object} options
* @param {String} options.type - type of event name
* @param {Object} options.detail - the object pass to the listener
*/
_emitEvent: function btdpp__emitEvent(options) {
this._listeners[options.type].forEach(function(listener) {
listener(options);
});
},
/**
* The method will provide event listener for outer modules to regist.
*
* @access public
* @memberOf DevicePickerPanel
* @param {String} eventName
* @param {Function} callback
*/
addEventListener: function btdpp_addEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
this._listeners[eventName].push(callback);
}
},
/**
* The method will provide event listener for outer modules to un-regist.
*
* @access public
* @memberOf DevicePickerPanel
* @param {String} eventName
* @param {Function} callback
*/
removeEventListener:
function btdpp_removeEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
var index = this._listeners[eventName].indexOf(callback);
if (index >= 0) {
this._listeners[eventName].splice(index, 1);
}
}
},
/**
* The setter will show/hide the device picker view for outer modules.
*
* @access public
* @memberOf DevicePickerPanel
* @param {Boolean} enabled
*/
set visible(enabled) {
// display device picker view
this._elements.view.viewDevicePicker.hidden = !enabled;
}
};
DevicePickerPanel._init();
return DevicePickerPanel;
});

View file

@ -1 +0,0 @@
define(["require"],function(i){"use strict";var n={init:function(){this.dialog=document.getElementById("pairing-request-timeout"),this.confirmBtn=document.getElementById("incoming-pairing-timeout-confirm")},showConfirm:function(i){var n=this;this.confirmBtn.onclick=function(){n.close(),i&&i()},this.dialog.hidden=!1},get isVisible(){return!this.dialog.hidden},close:function(){this.dialog.hidden=!0}};return n.init(),n});

View file

@ -1,7 +0,0 @@
;var Pairview={_debug:false,_remoteDeviceName:null,_pairMethod:null,_options:null,_passkey:'',pairview:document.getElementById('pair-view'),nameLabel:document.getElementById('label-name'),pairDescription:document.getElementById('pair-description'),pairButton:document.getElementById('button-pair'),closeButton:document.getElementById('button-close'),comfirmationItem:document.getElementById('confirmation-method'),pinInputItem:document.getElementById('pin-input-method'),passkey:document.getElementById('passkey'),pinInput:document.getElementById('pin-input'),get isFullAttentionMode(){return(window.innerHeight>200);},show:function pv_show(){if(!this.isFullAttentionMode){this.close();return;}
var _=navigator.mozL10n.get;this.pairButton.addEventListener('click',this);this.closeButton.addEventListener('click',this);window.addEventListener('resize',this);this._remoteDeviceName=this._remoteDeviceName||_('unnamed-device');var truncatedDeviceName=getTruncated(this._remoteDeviceName,{node:this.nameLabel,maxLine:2,ellipsisIndex:3});this.nameLabel.textContent=truncatedDeviceName;this.pairview.hidden=false;var stringName='passive-pair-'+this._pairMethod;this.pairDescription.textContent=_(stringName,{device:truncatedDeviceName});switch(this._pairMethod){case'displaypasskey':case'confirmation':this.passkey.textContent=this._passkey;this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;case'enterpincode':this.pinInputItem.hidden=false;this.comfirmationItem.hidden=true;this.pinInput.focus();break;case'consent':this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;}},init:function pv_init(method,options){this._pairMethod=method;this._options=options;this._remoteDeviceName=options.deviceName;if(options.handle&&options.handle.passkey){var passkey=options.handle.passkey;var len=passkey.toString().length;var zeros=(len<6)?(new Array((6-len)+1)).join('0'):'';this._passkey=zeros+passkey;}
navigator.mozL10n.once(Pairview.show.bind(Pairview));},close:function pv_close(){this._options.handle.reject().then(()=>{this.debug('Resolved in reject '+this._pairMethod+' request.');window.close();},(aReason)=>{this.debug('Rejected in reject '+this._pairMethod+' request with reason: '+aReason);window.close();});},closeInput:function pv_closeInput(){if(!this.pinInputItem.hidden){this.pinInput.blur();}},handleEvent:function pv_handleEvent(evt){var _=navigator.mozL10n.get;if(!evt.target){return;}
switch(evt.type){case'click':evt.preventDefault();switch(evt.target.id){case'button-pair':this.pairDescription.textContent=_('device-status-waiting');this.pairButton.disabled=true;this.closeButton.disabled=true;switch(this._pairMethod){case'displaypasskey':window.close();break;case'enterpincode':var pinCode=this.pinInput.value;this._options.handle.setPinCode(pinCode).then(()=>{this.debug('Resolved setPinCode operation');window.close();},(aReason)=>{this.debug('Rejected setPinCode with reason: '+aReason);window.close();});break;case'confirmation':case'consent':this._options.handle.accept().then(()=>{this.debug('Resolved in '+this._pairMethod+' request');window.close();},(aReason)=>{this.debug('Rejected in '+this._pairMethod+' request '+' with reason: '+aReason);window.close();});break;}
break;case'button-close':this.close();break;}
break;case'resize':if(!this.isFullAttentionMode){this.close();}
break;default:break;}},debug:function(msg){if(this._debug){console.log('Pairview(): '+msg);}}};

View file

@ -1,51 +0,0 @@
/*
* TurnBluetoothOnDialog is responsible for:
* A confirmation dialog to ask user to turn Bluetooth on or not.
* There is a confirmation handling via here.
*/
define([],function() {
'use strict';
var TurnBluetoothOnDialog = {
init: function() {
this.dialog = document.getElementById('enable-bluetooth-view');
this.cancelBtn =
document.getElementById('enable-bluetooth-button-cancel');
this.confirmBtn =
document.getElementById('enable-bluetooth-button-turn-on');
},
showConfirm: function() {
return new Promise((resolve) => {
this.cancelBtn.onclick = (evt) => {
if (evt) {
evt.preventDefault();
}
this.close();
resolve('cancel');
};
this.confirmBtn.onclick = (evt) => {
if (evt) {
evt.preventDefault();
}
this.close();
resolve('confirm');
};
this.dialog.hidden = false;
});
},
get isVisible() {
return (!this.dialog.hidden);
},
close: function() {
this.dialog.hidden = true;
}
};
TurnBluetoothOnDialog.init();
return TurnBluetoothOnDialog;
});

View file

@ -1,18 +0,0 @@
{
"name": "bluetooth",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["apps/bluetooth/js/**/*.js"]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/bluetooth"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more